reach.rs
7.0 kB · rust · 238 lines
1use super::models::Cell3d;2use mrlycore::errors::{value_error, Result};3use mrlycore::rng::Rng;4use std::collections::HashSet;56const DIRS: [[isize; 3]; 6] = [7 [1, 0, 0],8 [-1, 0, 0],9 [0, 1, 0],10 [0, -1, 0],11 [0, 0, 1],12 [0, 0, -1],13];1415/// The tally of one diffusion run over a cube's wall faces.16#[derive(Clone, Debug, PartialEq)]17pub struct Reach {18 /// The count of wall faces between an inner void and a fill.19 pub faces: usize,20 /// The count of wall faces some walker reacted on.21 pub used: usize,22 /// The used share of the wall faces.23 pub utilisation: f64,24 /// The share of walkers that reacted before exiting or stalling.25 pub reacted: f64,26 /// The count of open entry sites on the first face.27 pub entries: usize,28}2930fn shape_of(cell: &Cell3d) -> [usize; 3] {31 let grid = cell.types();32 [grid.shape[0], grid.shape[1], grid.shape[2]]33}3435fn shift(site: [usize; 3], step: &[isize; 3], shape: &[usize; 3]) -> Option<[usize; 3]> {36 let mut next = [0usize; 3];37 for axis in 0..3 {38 let moved = site[axis] as isize + step[axis];39 if moved < 0 || moved >= shape[axis] as isize {40 return None;41 }42 next[axis] = moved as usize;43 }44 Some(next)45}4647/// Returns every wall face as a void site and the direction index of its filled neighbor.48pub fn walls(cell: &Cell3d) -> Vec<[usize; 4]> {49 let grid = cell.types();50 let shape = shape_of(cell);51 let mut out = Vec::new();52 for x in 0..shape[0] {53 for y in 0..shape[1] {54 for z in 0..shape[2] {55 if grid.get(&[x, y, z]) != 0 {56 continue;57 }58 for (d, step) in DIRS.iter().enumerate() {59 if let Some(site) = shift([x, y, z], step, &shape) {60 if grid.get(&site) != 0 {61 out.push([x, y, z, d]);62 }63 }64 }65 }66 }67 }68 out69}7071fn doors(cell: &Cell3d) -> Vec<[usize; 3]> {72 let grid = cell.types();73 let shape = shape_of(cell);74 let mut out = Vec::new();75 for y in 0..shape[1] {76 for z in 0..shape[2] {77 if grid.get(&[0, y, z]) == 0 {78 out.push([0, y, z]);79 }80 }81 }82 out83}8485/// Walks seeded reactant in from the open first face and tallies the wall faces it reaches.86///87/// Each walker enters at a random void site of the first face and steps at random. A step88/// back out through that face exits, any other step off the cube reflects, and a step into89/// a fill reacts with the given probability, marking the wall face, or reflects otherwise.90pub fn explore(91 cell: &Cell3d,92 p_react: f64,93 walkers: usize,94 max_steps: usize,95 seed: u64,96) -> Result<Reach> {97 if walkers == 0 {98 return value_error("the walk needs at least one walker.");99 }100 let grid = cell.types();101 let shape = shape_of(cell);102 let faces = walls(cell);103 if faces.is_empty() {104 return value_error("the design offers no wall faces.");105 }106 let entry = doors(cell);107 if entry.is_empty() {108 return value_error("the first face offers no entry site.");109 }110 let mut rng = Rng::new(seed);111 let mut hit: HashSet<[usize; 4]> = HashSet::new();112 let mut reacted = 0usize;113 for _ in 0..walkers {114 let mut site = entry[rng.below(entry.len())];115 for _ in 0..max_steps {116 let d = rng.below(6);117 let step = &DIRS[d];118 let Some(next) = shift(site, step, &shape) else {119 if site[0] as isize + step[0] < 0 {120 break;121 }122 continue;123 };124 if grid.get(&next) != 0 {125 if rng.chance(p_react) {126 hit.insert([site[0], site[1], site[2], d]);127 reacted += 1;128 break;129 }130 continue;131 }132 site = next;133 }134 }135 Ok(Reach {136 faces: faces.len(),137 used: hit.len(),138 utilisation: hit.len() as f64 / faces.len() as f64,139 reacted: reacted as f64 / walkers as f64,140 entries: entry.len(),141 })142}143144/// Sweeps the reaction probability and pairs each value with its utilisation, same seed each run.145pub fn sweep(146 cell: &Cell3d,147 probabilities: &[f64],148 walkers: usize,149 max_steps: usize,150 seed: u64,151) -> Result<Vec<(f64, f64)>> {152 probabilities153 .iter()154 .map(|&p| Ok((p, explore(cell, p, walkers, max_steps, seed)?.utilisation)))155 .collect()156}157158#[cfg(test)]159mod tests {160 use super::*;161 use crate::three::{carpet, census, net, ones, void};162163 fn hull(cell: &Cell3d) -> u128 {164 let grid = cell.types();165 let shape = shape_of(cell);166 let mut count = 0u128;167 for x in 0..shape[0] {168 for y in 0..shape[1] {169 for z in 0..shape[2] {170 if grid.get(&[x, y, z]) == 0 {171 continue;172 }173 for (site, size) in [(x, shape[0]), (y, shape[1]), (z, shape[2])] {174 if site == 0 {175 count += 1;176 }177 if site + 1 == size {178 count += 1;179 }180 }181 }182 }183 }184 count185 }186187 #[test]188 fn walls_complete_the_censused_surface() {189 for cell in [190 carpet(3, 1).unwrap(),191 carpet(3, 2).unwrap(),192 net(3, 1).unwrap(),193 void(2, 1).unwrap(),194 void(2, 2).unwrap(),195 ] {196 let inner = walls(&cell).len() as u128;197 assert_eq!(inner + hull(&cell), census::surface(&cell));198 }199 }200201 #[test]202 fn a_solid_block_of_eight_shows_only_its_hull() {203 let cell = ones(2, 1).unwrap();204 assert_eq!(census::fills(&cell), 8);205 assert_eq!(census::surface(&cell), 24);206 assert!(walls(&cell).is_empty());207 assert!(explore(&cell, 0.5, 8, 8, 0).is_err());208 }209210 #[test]211 fn the_carpet_cube_offers_twenty_four_walls() {212 let cell = carpet(3, 1).unwrap();213 assert_eq!(walls(&cell).len(), 24);214 assert_eq!(census::surface(&cell), 72);215 assert_eq!(explore(&cell, 0.5, 16, 32, 0).unwrap().entries, 1);216 }217218 #[test]219 fn a_fixed_seed_repeats_exactly() {220 let cell = carpet(3, 2).unwrap();221 let a = explore(&cell, 0.5, 64, 128, 9).unwrap();222 let b = explore(&cell, 0.5, 64, 128, 9).unwrap();223 assert_eq!(a, b);224 assert!(a.used <= a.faces);225 assert!((0.0..=1.0).contains(&a.utilisation));226 assert!((0.0..=1.0).contains(&a.reacted));227 }228229 #[test]230 fn faster_reactions_screen_the_interior() {231 let cell = carpet(3, 1).unwrap();232 let curve = sweep(&cell, &[0.05, 0.5, 0.9], 800, 400, 0).unwrap();233 for pair in curve.windows(2) {234 assert!(pair[0].1 >= pair[1].1, "{curve:?}");235 }236 assert!(curve[0].1 > curve[2].1, "{curve:?}");237 }238}