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}