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}