run.rs

12.0 kB · rust · 380 lines

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