surface.rs
12.9 kB · rust · 347 lines
1use crate::core::error::{value_error, Result};2use crate::core::tensor::Tensor;3use crate::math::bang::factory;4use crate::math::bang::Code;5use crate::math::counts::counting::{fill_from_corners, positions};6use serde::{Deserialize, Serialize};7use std::collections::HashSet;89fn strides(shape: &[usize]) -> Vec<usize> {10 (0..shape.len())11 .map(|axis| shape[axis + 1..].iter().product())12 .collect()13}1415fn occupancy(tile: &Tensor) -> u128 {16 (tile.size() - tile.count(0)) as u12817}1819/// Counts, per axis, the adjacent filled pairs and the cross positions whose two end cells are both filled.20///21/// A level deeper, each adjacent pair buries one face per spanning position of the block, and the22/// spanning positions of the block multiply level by level, so the exposure closes.23pub fn pairs(tile: &Tensor) -> Vec<(u128, u128)> {24 let shape = &tile.shape;25 let strides = strides(shape);26 (0..shape.len())27 .map(|axis| {28 let (stride, side) = (strides[axis], shape[axis]);29 let (mut adjacent, mut spanning) = (0u128, 0u128);30 for flat in 0..tile.size() {31 if tile.at(flat) == 0 {32 continue;33 }34 let position = flat / stride % side;35 if position == 0 && tile.at(flat + (side - 1) * stride) != 0 {36 spanning += 1;37 }38 if position + 1 < side && tile.at(flat + stride) != 0 {39 adjacent += 1;40 }41 }42 (adjacent, spanning)43 })44 .collect()45}4647/// The counts the exposure recurrence runs on: the filled cells and exposed faces of the tile, and per axis its adjacent pairs and spanning positions.48///49/// With `occ` filled cells, `V(1)` exposed faces and per axis `P` adjacent pairs and `S` spanning50/// positions, `V(L + 1) = occ V(L) - 2 sum P S^L`: the perimeter in the plane, the surface in space.51#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]52pub struct Exposure {53 /// The filled cells of the tile.54 pub occupancy: u128,55 /// The exposed faces of the tile.56 pub exposed: u128,57 /// Per axis, the adjacent filled pairs and the spanning positions.58 pub axes: Vec<(u128, u128)>,59}6061impl Exposure {62 /// Reads the counts off a rendered tile.63 pub fn of_tile(tile: &Tensor) -> Exposure {64 Exposure {65 occupancy: occupancy(tile),66 exposed: tile.exposed(),67 axes: pairs(tile),68 }69 }70 /// Folds the counts from the filled residue corners at a side number, without rendering the tile.71 ///72 /// An adjacent pair sits at positions `i, i + 1` whose residues are `r, r + 1 mod q`, and a73 /// spanning position pairs residue 0 with the residue of `n - 1`.74 pub fn from_corners(75 filled: &[Vec<u8>],76 number: usize,77 dimension: usize,78 base: usize,79 ) -> Exposure {80 let set: HashSet<&Vec<u8>> = filled.iter().collect();81 let across = |corner: &[u8], axis: usize| -> u128 {82 (0..dimension)83 .filter(|&b| b != axis)84 .map(|b| positions(corner[b] as usize, number, base))85 .product()86 };87 let occupancy = fill_from_corners(filled, number, dimension, 1, base);88 let axes: Vec<(u128, u128)> = (0..dimension)89 .map(|axis| {90 let (mut adjacent, mut spanning) = (0u128, 0u128);91 for corner in filled {92 let mut next = corner.clone();93 next[axis] = ((corner[axis] as usize + 1) % base) as u8;94 if set.contains(&next) {95 adjacent +=96 positions(corner[axis] as usize, number.saturating_sub(1), base)97 * across(corner, axis);98 }99 if corner[axis] == 0 && number > 0 {100 let mut far = corner.clone();101 far[axis] = ((number - 1) % base) as u8;102 if set.contains(&far) {103 spanning += across(corner, axis);104 }105 }106 }107 (adjacent, spanning)108 })109 .collect();110 let buried: u128 = axes.iter().map(|&(adjacent, _)| adjacent).sum();111 Exposure {112 occupancy,113 exposed: 2 * dimension as u128 * occupancy - 2 * buried,114 axes,115 }116 }117 /// Returns the exposed faces of the level-fold Kronecker power, or none past a u128.118 pub fn at(&self, level: u32) -> Option<u128> {119 let mut value = self.exposed;120 for step in 1..level {121 let buried = self122 .axes123 .iter()124 .try_fold(0u128, |sum, &(adjacent, spanning)| {125 sum.checked_add(adjacent.checked_mul(spanning.checked_pow(step)?)?)126 })?;127 value = self128 .occupancy129 .checked_mul(value)?130 .checked_sub(buried.checked_mul(2)?)?;131 }132 Some(value)133 }134 /// Returns the coefficients `c` of the recurrence `a(L) = c[0] a(L-1) + c[1] a(L-2) + ...` the exposure obeys.135 ///136 /// The roots are `occ` and the distinct nonzero spanning counts, `occ` doubled where a137 /// spanning count equals it.138 pub fn recurrence(&self) -> Vec<i128> {139 let mut roots: Vec<u128> = self140 .axes141 .iter()142 .map(|&(_, spanning)| spanning)143 .filter(|&spanning| spanning != 0)144 .collect();145 roots.sort_unstable();146 roots.dedup();147 roots.insert(0, self.occupancy);148 let mut poly: Vec<i128> = vec![1];149 for root in roots {150 let mut next = vec![0i128; poly.len() + 1];151 for (power, &coefficient) in poly.iter().enumerate() {152 next[power] += coefficient;153 next[power + 1] -= root as i128 * coefficient;154 }155 poly = next;156 }157 poly[1..].iter().map(|&coefficient| -coefficient).collect()158 }159}160161/// Returns the exposed face count of the tile's level-fold Kronecker power in closed form, or none past a u128.162///163/// ```164/// use mrlyrs::math::bang::Code;165/// let carpet = mrlyrs::math::bang::factory::create(Code::from(7u64), 3, 2, 2, 1).unwrap();166/// let perimeter: Vec<u128> = (1..5).map(|level| mrlyrs::math::counts::exposure_of_tile(&carpet, level).unwrap()).collect();167/// assert_eq!(perimeter, [16, 80, 496, 3536]);168/// ```169pub fn exposure_of_tile(tile: &Tensor, level: u32) -> Option<u128> {170 Exposure::of_tile(tile).at(level)171}172173/// Returns the coefficients of the recurrence the tile's exposure obeys.174///175/// ```176/// use mrlyrs::math::bang::Code;177/// let sponge = mrlyrs::math::bang::factory::create(Code::from(23u64), 3, 3, 2, 1).unwrap();178/// assert_eq!(mrlyrs::math::counts::exposure_recurrence(&sponge), [28, -160]);179/// ```180pub fn exposure_recurrence(tile: &Tensor) -> Vec<i128> {181 Exposure::of_tile(tile).recurrence()182}183184/// Returns the exposed face count of the code's fractal in any dimension at the given level, folded from its corners.185///186/// # Errors187///188/// Errors when the code is out of range, or the exposure passes a hundred and twenty-eight bits.189pub fn exposure(190 code: Code,191 number: usize,192 dimension: usize,193 level: u32,194 base: usize,195) -> Result<u128> {196 let filled = factory::code_to_corners(code, dimension, base)?;197 match Exposure::from_corners(&filled, number, dimension, base).at(level) {198 Some(value) => Ok(value),199 None => value_error("the exposure passes a hundred and twenty-eight bits."),200 }201}202203/// Returns the exposed face count of the code's 3D fractal at the given level.204///205/// # Errors206///207/// Errors when the code is out of range, or the exposure passes a hundred and twenty-eight bits.208pub fn surface(code: Code, number: usize, level: u32, base: usize) -> Result<u128> {209 exposure(code, number, 3, level, base)210}211212#[cfg(test)]213mod tests {214 use super::*;215 use crate::math::atoms;216 #[test]217 fn prediction_matches_census_on_every_cube_code() {218 for bits in 0..256u128 {219 let code = Code(bits);220 for level in 1..3u32 {221 let direct = factory::create(code, 3, 3, 2, level as usize).unwrap();222 assert_eq!(223 surface(code, 3, level, 2).unwrap(),224 direct.exposed(),225 "code={code} l={level}"226 );227 }228 }229 }230 #[test]231 #[ignore = "two hundred and fifty-six level-three cubes, 2 s; run it in release"]232 fn prediction_matches_census_on_every_cube_code_at_the_third_level() {233 for bits in 0..256u128 {234 let code = Code(bits);235 let direct = factory::create(code, 3, 3, 2, 3).unwrap();236 assert_eq!(237 surface(code, 3, 3, 2).unwrap(),238 direct.exposed(),239 "code={code}"240 );241 }242 }243 #[test]244 fn prediction_matches_census_in_the_plane_and_beyond() {245 for bits in 0..16u128 {246 let code = Code(bits);247 for number in [2usize, 3, 4, 5] {248 for level in 1..4u32 {249 let direct = factory::create(code, number, 2, 2, level as usize).unwrap();250 assert_eq!(251 exposure(code, number, 2, level, 2).unwrap(),252 direct.exposed(),253 "code={code} n={number} l={level}"254 );255 }256 }257 }258 for code in [259 Code(1),260 Code(23),261 Code(255),262 Code(4369),263 Code(65535),264 Code(32767),265 ] {266 for level in 1..3u32 {267 let direct = factory::create(code, 3, 4, 2, level as usize).unwrap();268 assert_eq!(exposure(code, 3, 4, level, 2).unwrap(), direct.exposed());269 }270 }271 for code in [Code(7), Code(100), Code(511)] {272 let direct = factory::create(code, 3, 2, 3, 3).unwrap();273 assert_eq!(exposure(code, 3, 2, 3, 3).unwrap(), direct.exposed());274 }275 }276 #[test]277 fn the_recurrence_holds_on_every_cube_code() {278 for bits in 0..256u128 {279 let code = Code(bits);280 let tile = factory::create(code, 3, 3, 2, 1).unwrap();281 let rule = exposure_recurrence(&tile);282 let terms: Vec<i128> = (1..8u32)283 .map(|level| exposure_of_tile(&tile, level).unwrap() as i128)284 .collect();285 for at in rule.len()..terms.len() {286 let predicted: i128 = rule287 .iter()288 .enumerate()289 .map(|(back, &c)| c * terms[at - back - 1])290 .sum();291 assert_eq!(predicted, terms[at], "code={code} at={at} rule={rule:?}");292 }293 }294 }295 #[test]296 fn the_corners_fold_what_the_tile_shows() {297 for bits in 0..256u128 {298 let code = Code(bits);299 for number in [1usize, 2, 3, 4, 5, 7] {300 let filled = factory::code_to_corners(code, 3, 2).unwrap();301 let tile = factory::create(code, number, 3, 2, 1).unwrap();302 assert_eq!(303 Exposure::from_corners(&filled, number, 3, 2),304 Exposure::of_tile(&tile),305 "code={code} n={number}"306 );307 }308 }309 for (code, dimension, base) in [310 (Code(7), 2usize, 3usize),311 (Code(100), 2, 3),312 (Code(511), 2, 3),313 (Code(4369), 4, 2),314 (Code(32767), 4, 2),315 (Code(1), 1, 2),316 (Code(2), 1, 3),317 ] {318 for number in [2usize, 3, 4, 5, 6, 9] {319 let filled = factory::code_to_corners(code, dimension, base).unwrap();320 let tile = factory::create(code, number, dimension, base, 1).unwrap();321 assert_eq!(322 Exposure::from_corners(&filled, number, dimension, base),323 Exposure::of_tile(&tile),324 "code={code} d={dimension} q={base} n={number}"325 );326 }327 }328 }329 #[test]330 fn the_classics_close() {331 let sponge: Vec<u128> = (1..4)332 .map(|l| surface(Code(23), 3, l, 2).unwrap())333 .collect();334 assert_eq!(sponge, [72, 1056, 18048]);335 let carpet: Vec<u128> = (1..5)336 .map(|l| exposure(Code(7), 3, 2, l, 2).unwrap())337 .collect();338 assert_eq!(carpet, [16, 80, 496, 3536]);339 assert_eq!(340 exposure_recurrence(&factory::create(Code(7), 3, 2, 2, 1).unwrap()),341 [11, -24]342 );343 assert_eq!(exposure_of_tile(&atoms::ones_3d(2), 3), Some(384));344 assert_eq!(exposure_of_tile(&atoms::ones_3d(1), 5), Some(6));345 assert!(exposure(Code(23), 3, 3, 120, 2).is_err());346 }347}