run.rs

12.1 kB · rust · 384 lines

1use super::presets::{Mask, Preset, Rule, Seed};2use super::readings::{canonical, read, translate_of, Reading};3use mrlyrs::core::error::{value_error, Result};4use mrlyrs::core::rng::Rng;5use mrlyrs::core::tensor::Tensor;6use mrlyrs::life::{animate, churn, design_mask, lattice_index, Boundary, Config, Fate};7use mrlyrs::math::bang::Code;8use mrlyrs::math::two::{create, Cell2d};9use std::collections::{BTreeMap, BTreeSet};1011/// The record of one run.12#[derive(Clone, Debug)]13pub struct Run {14    /// The seed design.15    pub seed: Seed,16    /// The tessellation factor.17    pub tessellation: usize,18    /// The mask.19    pub mask: Mask,20    /// The index of the lattice the mask generates.21    pub mask_index: usize,22    /// The rule.23    pub rule: Rule,24    /// The boundary.25    pub boundary: Boundary,26    /// The fate at the generation cap.27    pub fate: Fate,28    /// The period: 1 for a fixed point, the cycle length for a loop, 0 at timeout.29    pub period: usize,30    /// The first generation of the settled frame or cycle, the cap at timeout.31    pub settled_at: usize,32    /// The log-log slope of the live bounding box side against the generation.33    pub box_growth: f64,34    /// The shift and lag of the last frame as a translate of an earlier one, timeouts only.35    pub mover: Option<(usize, usize, usize)>,36    /// The number of frames recorded.37    pub frames: usize,38    /// The mean churn over the run.39    pub churn: f64,40    /// The settled frame, one frame of the cycle for a loop, none when dead or timed out.41    pub settled: Option<Cell2d>,42    /// The settled frame's reading.43    pub reading: Option<Reading>,44    /// The settled frame's canonical bytes under the dihedral group and torus translation.45    pub key: Option<Vec<u8>>,46    /// The reading of the heatmap's median level set.47    pub heat: Reading,48}4950impl Run {51    /// Returns the run's one-line label.52    pub fn label(&self) -> String {53        format!(54            "{}s{}l{} t{} {}[{}] {} {}",55            self.seed.code,56            self.seed.side,57            self.seed.level,58            self.tessellation,59            self.mask.name(),60            self.mask_index,61            self.rule.name(),62            format!("{:?}", self.boundary).to_lowercase()63        )64    }65}6667/// The census of one preset.68#[derive(Clone, Debug)]69pub struct Census {70    /// The preset run.71    pub preset: Preset,72    /// The runs in sweep order.73    pub runs: Vec<Run>,74    /// The runs dropped because their mask duplicated an earlier one at the same seed and tessellation.75    pub duplicates: usize,76    /// The number of runs the sweep held before any sample.77    pub universe: usize,78    /// The sampling seed when the universe was cut, none when every run ran.79    pub sample: Option<u64>,80    /// The number of distinct settled frames up to the dihedral group and torus translation.81    pub distinct: usize,82}8384fn board(seed: &Seed, tessellation: usize) -> Result<Cell2d> {85    create(Code::from(seed.code), seed.side, seed.level, 0, 2)?.tile(tessellation, tessellation)86}8788/// Resolves a mask against the tessellated board, the centre popped.89pub fn mask_tensor(mask: &Mask, board: &Cell2d) -> Result<Tensor> {90    match mask {91        Mask::Design { code, side, level } => design_mask(2, Code::from(*code), *side, *level),92        Mask::Copy { inverted } => {93            let mut tensor = board.types().clone();94            if *inverted {95                tensor = tensor.invert();96            }97            let centre = (tensor.shape[0] - 1) / 2;98            tensor.set(&[centre, centre], 0)?;99            Ok(tensor)100        }101    }102}103104fn bounding_side(grid: &Tensor) -> usize {105    let n = grid.shape[0];106    let (mut rmin, mut rmax, mut cmin, mut cmax) = (n, 0, n, 0);107    let mut any = false;108    for i in 0..grid.size() {109        if grid.at(i) != 0 {110            let (r, c) = (i / n, i % n);111            rmin = rmin.min(r);112            rmax = rmax.max(r);113            cmin = cmin.min(c);114            cmax = cmax.max(c);115            any = true;116        }117    }118    if !any {119        return 0;120    }121    (rmax - rmin + 1).max(cmax - cmin + 1)122}123124fn growth(grids: &[Cell2d]) -> f64 {125    let points: Vec<(f64, f64)> = grids126        .iter()127        .enumerate()128        .map(|(t, g)| {129            (130                (t as f64 + 1.0).ln(),131                (bounding_side(g.types()).max(1) as f64).ln(),132            )133        })134        .collect();135    if points.len() < 2 {136        return 0.0;137    }138    let n = points.len() as f64;139    let (sx, sy) = points140        .iter()141        .fold((0.0, 0.0), |(a, b), (x, y)| (a + x, b + y));142    let (sxx, sxy) = points143        .iter()144        .fold((0.0, 0.0), |(a, b), (x, y)| (a + x * x, b + x * y));145    let denominator = n * sxx - sx * sx;146    if denominator.abs() < 1e-12 {147        return 0.0;148    }149    (n * sxy - sx * sy) / denominator150}151152/// Returns the median level set of a run's cumulative visit counts as one frame.153pub fn heat_frame(grids: &[Cell2d]) -> mrlyrs::Result<Cell2d> {154    let shape = grids[0].types().shape.clone();155    let mut total = vec![0usize; grids[0].types().size()];156    for grid in grids {157        for (i, slot) in total.iter_mut().enumerate() {158            *slot += grid.types().at(i) as usize;159        }160    }161    let mut positive: Vec<usize> = total.iter().copied().filter(|&v| v > 0).collect();162    positive.sort_unstable();163    let level = positive164        .get(positive.len() / 2)165        .copied()166        .unwrap_or(usize::MAX);167    let mut heat = Tensor::new(shape);168    for (i, &v) in total.iter().enumerate() {169        heat.put(i, i64::from(v >= level));170    }171    Cell2d::new(heat)172}173174fn mover(grids: &[Cell2d]) -> Option<(usize, usize, usize)> {175    let last = grids.last()?.types();176    if last.sum() == 0 {177        return None;178    }179    for (k, earlier) in grids.iter().enumerate().rev().skip(1) {180        if let Some((dr, dc)) = translate_of(earlier.types(), last) {181            if dr != 0 || dc != 0 {182                return Some((dr, dc, grids.len() - 1 - k));183            }184        }185    }186    None187}188189/// Runs one cell of the sweep and records it.190pub fn run_one(191    preset: &Preset,192    seed: &Seed,193    tessellation: usize,194    mask: &Mask,195    rule: &Rule,196    boundary: Boundary,197) -> Result<Run> {198    if preset.dimension != 2 {199        return value_error("the atlas runs plane presets only.");200    }201    if *rule == Rule::Every {202        return value_error("the full outer-totalistic space is declared, not run.");203    }204    let board = board(seed, tessellation)?;205    let side = board.types().shape[0];206    if side > preset.canvas || !(preset.canvas - side).is_multiple_of(2) {207        return value_error("the board must fit the canvas with an even margin.");208    }209    let tensor = mask_tensor(mask, &board)?;210    let mask_index = lattice_index(&tensor);211    let config = Config {212        mask: Cell2d::new(tensor)?,213        birth: rule.birth(),214        survive: rule.survive(),215        boundary,216        max_generations: preset.generations,217        grid_size: 1,218        padding: (preset.canvas - side) / 2,219    };220    let life = animate(&board, &config)?;221    let frames = life.grids.len();222    let (period, settled_at) = match life.fate {223        Fate::Dead | Fate::Alive => (1, frames - 1),224        Fate::Loop => (life.loop_length, frames - life.loop_length),225        Fate::Timeout => (0, preset.generations),226    };227    let settled_index = match life.fate {228        Fate::Alive => Some(frames - 1),229        Fate::Loop => (settled_at..frames).min_by_key(|&i| canonical(life.grids[i].types())),230        Fate::Dead | Fate::Timeout => None,231    };232    let settled = settled_index.map(|i| life.grids[i].clone());233    let reading = match settled_index {234        Some(i) => Some(read(235            &life.grids[i],236            i.checked_sub(1).map(|j| &life.grids[j]),237        )?),238        None => None,239    };240    let key = settled.as_ref().map(|frame| canonical(frame.types()));241    let mover = if life.fate == Fate::Timeout {242        mover(&life.grids)243    } else {244        None245    };246    let heat = read(&heat_frame(&life.grids)?, None)?;247    Ok(Run {248        seed: *seed,249        tessellation,250        mask: *mask,251        mask_index,252        rule: *rule,253        boundary,254        fate: life.fate,255        period,256        settled_at,257        box_growth: growth(&life.grids[..=settled_at.min(frames - 1)]),258        mover,259        frames,260        churn: churn(&life.grids),261        settled,262        reading,263        key,264        heat,265    })266}267268type Cell = (Seed, usize, Mask, Rule, Boundary);269270fn sweep(preset: &Preset) -> Result<(Vec<Cell>, usize)> {271    let mut cells = Vec::new();272    let mut duplicates = 0;273    for seed in &preset.seeds {274        for &t in &preset.tessellations {275            let board = board(seed, t)?;276            let mut seen: BTreeSet<Vec<u8>> = BTreeSet::new();277            for mask in &preset.masks {278                let tensor = mask_tensor(mask, &board)?;279                let mut signature = tensor.shape.iter().map(|&n| n as u8).collect::<Vec<u8>>();280                signature.extend_from_slice(tensor.bytes()?);281                if !seen.insert(signature) {282                    duplicates += preset.rules.len() * preset.boundaries.len();283                    continue;284                }285                for rule in &preset.rules {286                    for &boundary in &preset.boundaries {287                        cells.push((*seed, t, *mask, *rule, boundary));288                    }289                }290            }291        }292    }293    Ok((cells, duplicates))294}295296/// Runs a preset's whole sweep, or a seeded sample of cap runs when the sweep is larger, and dedupes the settled frames.297pub fn census(preset: &Preset, cap: usize, sample_seed: u64) -> Result<Census> {298    let (mut cells, duplicates) = sweep(preset)?;299    let universe = cells.len();300    let sample = if universe > cap {301        let mut rng = Rng::new(sample_seed);302        for i in (1..cells.len()).rev() {303            cells.swap(i, rng.below(i + 1));304        }305        cells.truncate(cap);306        Some(sample_seed)307    } else {308        None309    };310    let mut runs = Vec::with_capacity(cells.len());311    for (seed, t, mask, rule, boundary) in &cells {312        runs.push(run_one(preset, seed, *t, mask, rule, *boundary)?);313    }314    let distinct = runs315        .iter()316        .filter_map(|run| run.key.as_ref())317        .collect::<BTreeSet<_>>()318        .len();319    Ok(Census {320        preset: preset.clone(),321        runs,322        duplicates,323        universe,324        sample,325        distinct,326    })327}328329/// Tallies the fates per rule, in preset order, as (family, rule, [dead, alive, loop, timeout]).330pub fn fate_table(census: &Census) -> Vec<(&'static str, String, [usize; 4])> {331    let mut table: BTreeMap<usize, [usize; 4]> = BTreeMap::new();332    let position = |rule: &Rule| {333        census334            .preset335            .rules336            .iter()337            .position(|r| r == rule)338            .unwrap_or(usize::MAX)339    };340    for run in &census.runs {341        let slot = match run.fate {342            Fate::Dead => 0,343            Fate::Alive => 1,344            Fate::Loop => 2,345            Fate::Timeout => 3,346        };347        table.entry(position(&run.rule)).or_default()[slot] += 1;348    }349    table350        .into_iter()351        .map(|(i, counts)| {352            let rule = &census.preset.rules[i];353            (rule.family(), rule.name(), counts)354        })355        .collect()356}357358#[cfg(test)]359mod tests {360    use super::super::presets::{mini, MOORE};361    use super::*;362    #[test]363    fn a_life_run_of_the_cross_seed_settles_and_the_copy_mask_pops_its_centre() {364        let preset = mini();365        let seed = Seed {366            code: 6,367            side: 3,368            level: 1,369        };370        let mask = Mask::Design {371            code: MOORE,372            side: 3,373            level: 1,374        };375        let run = run_one(&preset, &seed, 1, &mask, &Rule::Life, Boundary::Wrap).unwrap();376        assert_eq!(run.mask_index, 1);377        assert!(run.frames <= preset.generations);378        assert_eq!(run.heat.side, 27);379        let copy = mask_tensor(&Mask::Copy { inverted: true }, &board(&seed, 3).unwrap()).unwrap();380        assert_eq!(copy.shape, vec![9, 9]);381        assert_eq!(copy.get(&[4, 4]).unwrap(), 0);382        assert_eq!(copy.sum(), 9 * 5 - 1);383    }384}