surface.rs
11.7 kB · rust · 310 lines
1use crate::bang::factory;2use crate::bang::universe::Code;3use crate::formulas::counting::{fill_from_corners, positions};4use mrlycore::errors::{value_error, Result};5use mrlycore::tensor::Tensor;6use mrlynum::census::exposed;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.bytes().iter().filter(|&&v| v != 0).count() 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 bytes = tile.bytes();26 let strides = strides(shape);27 (0..shape.len())28 .map(|axis| {29 let (stride, side) = (strides[axis], shape[axis]);30 let (mut adjacent, mut spanning) = (0u128, 0u128);31 for (flat, &cell) in bytes.iter().enumerate() {32 if cell == 0 {33 continue;34 }35 let position = flat / stride % side;36 if position == 0 && bytes[flat + (side - 1) * stride] != 0 {37 spanning += 1;38 }39 if position + 1 < side && bytes[flat + stride] != 0 {40 adjacent += 1;41 }42 }43 (adjacent, spanning)44 })45 .collect()46}4748/// 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.49///50/// With `occ` filled cells, `V(1)` exposed faces and per axis `P` adjacent pairs and `S` spanning51/// positions, `V(L + 1) = occ V(L) - 2 sum P S^L`: the perimeter in the plane, the surface in space.52#[derive(Clone, Debug, PartialEq, Eq)]53pub struct Exposure {54 /// The filled cells of the tile.55 pub occupancy: u128,56 /// The exposed faces of the tile.57 pub exposed: u128,58 /// Per axis, the adjacent filled pairs and the spanning positions.59 pub axes: Vec<(u128, u128)>,60}6162impl Exposure {63 /// Reads the counts off a rendered tile.64 pub fn of_tile(tile: &Tensor) -> Exposure {65 Exposure {66 occupancy: occupancy(tile),67 exposed: exposed(tile),68 axes: pairs(tile),69 }70 }71 /// Folds the counts from the filled residue corners at a side number, without rendering the tile.72 ///73 /// An adjacent pair sits at positions `i, i + 1` whose residues are `r, r + 1 mod q`, and a74 /// spanning position pairs residue 0 with the residue of `n - 1`.75 pub fn from_corners(76 filled: &[Vec<u8>],77 number: usize,78 dimension: usize,79 base: usize,80 ) -> Exposure {81 let set: HashSet<&Vec<u8>> = filled.iter().collect();82 let across = |corner: &[u8], axis: usize| -> u128 {83 (0..dimension)84 .filter(|&b| b != axis)85 .map(|b| positions(corner[b] as usize, number, base))86 .product()87 };88 let occupancy = fill_from_corners(filled, number, dimension, 1, base);89 let axes: Vec<(u128, u128)> = (0..dimension)90 .map(|axis| {91 let (mut adjacent, mut spanning) = (0u128, 0u128);92 for corner in filled {93 let mut next = corner.clone();94 next[axis] = ((corner[axis] as usize + 1) % base) as u8;95 if set.contains(&next) {96 adjacent +=97 positions(corner[axis] as usize, number.saturating_sub(1), base)98 * across(corner, axis);99 }100 if corner[axis] == 0 && number > 0 {101 let mut far = corner.clone();102 far[axis] = ((number - 1) % base) as u8;103 if set.contains(&far) {104 spanning += across(corner, axis);105 }106 }107 }108 (adjacent, spanning)109 })110 .collect();111 let buried: u128 = axes.iter().map(|&(adjacent, _)| adjacent).sum();112 Exposure {113 occupancy,114 exposed: 2 * dimension as u128 * occupancy - 2 * buried,115 axes,116 }117 }118 /// Returns the exposed faces of the level-fold Kronecker power, or none past a u128.119 pub fn at(&self, level: u32) -> Option<u128> {120 let mut value = self.exposed;121 for step in 1..level {122 let buried = self123 .axes124 .iter()125 .try_fold(0u128, |sum, &(adjacent, spanning)| {126 sum.checked_add(adjacent.checked_mul(spanning.checked_pow(step)?)?)127 })?;128 value = self129 .occupancy130 .checked_mul(value)?131 .checked_sub(buried.checked_mul(2)?)?;132 }133 Some(value)134 }135 /// Returns the coefficients `c` of the recurrence `a(L) = c[0] a(L-1) + c[1] a(L-2) + ...` the exposure obeys.136 ///137 /// The roots are `occ` and the distinct nonzero spanning counts, `occ` doubled where a138 /// spanning count equals it.139 pub fn recurrence(&self) -> Vec<i128> {140 let mut roots: Vec<u128> = self141 .axes142 .iter()143 .map(|&(_, spanning)| spanning)144 .filter(|&spanning| spanning != 0)145 .collect();146 roots.sort_unstable();147 roots.dedup();148 roots.insert(0, self.occupancy);149 let mut poly: Vec<i128> = vec![1];150 for root in roots {151 let mut next = vec![0i128; poly.len() + 1];152 for (power, &coefficient) in poly.iter().enumerate() {153 next[power] += coefficient;154 next[power + 1] -= root as i128 * coefficient;155 }156 poly = next;157 }158 poly[1..].iter().map(|&coefficient| -coefficient).collect()159 }160}161162/// Returns the exposed face count of the tile's level-fold Kronecker power in closed form, or none past a u128.163///164/// ```165/// let carpet = mrlymath::bang::factory::create(7, 3, 2, 2, 1).unwrap();166/// let perimeter: Vec<u128> = (1..5).map(|level| mrlymath::formulas::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/// let sponge = mrlymath::bang::factory::create(23, 3, 3, 2, 1).unwrap();177/// assert_eq!(mrlymath::formulas::exposure_recurrence(&sponge), [28, -160]);178/// ```179pub fn exposure_recurrence(tile: &Tensor) -> Vec<i128> {180 Exposure::of_tile(tile).recurrence()181}182183/// Returns the exposed face count of the code's fractal in any dimension at the given level, folded from its corners, or an error past a u128.184pub fn exposure(185 code: Code,186 number: usize,187 dimension: usize,188 level: u32,189 base: usize,190) -> Result<u128> {191 let filled = factory::code_to_corners(code, dimension, base)?;192 match Exposure::from_corners(&filled, number, dimension, base).at(level) {193 Some(value) => Ok(value),194 None => value_error("the exposure passes a hundred and twenty-eight bits."),195 }196}197198/// Returns the exposed face count of the code's 3D fractal at the given level.199pub fn surface(code: Code, number: usize, level: u32, base: usize) -> Result<u128> {200 exposure(code, number, 3, level, base)201}202203#[cfg(test)]204mod tests {205 use super::*;206 use mrlycore::atoms;207 #[test]208 fn prediction_matches_census_on_every_cube_code() {209 for code in 0..256u128 {210 for level in 1..4u32 {211 let direct = factory::create(code, 3, 3, 2, level as usize).unwrap();212 assert_eq!(213 surface(code, 3, level, 2).unwrap(),214 exposed(&direct),215 "code={code} l={level}"216 );217 }218 }219 }220 #[test]221 fn prediction_matches_census_in_the_plane_and_beyond() {222 for code in 0..16u128 {223 for number in [2usize, 3, 4, 5] {224 for level in 1..4u32 {225 let direct = factory::create(code, number, 2, 2, level as usize).unwrap();226 assert_eq!(227 exposure(code, number, 2, level, 2).unwrap(),228 exposed(&direct),229 "code={code} n={number} l={level}"230 );231 }232 }233 }234 for code in [1u128, 23, 255, 4369, 65535, 32767] {235 for level in 1..3u32 {236 let direct = factory::create(code, 3, 4, 2, level as usize).unwrap();237 assert_eq!(exposure(code, 3, 4, level, 2).unwrap(), exposed(&direct));238 }239 }240 for code in [7u128, 100, 511] {241 let direct = factory::create(code, 3, 2, 3, 3).unwrap();242 assert_eq!(exposure(code, 3, 2, 3, 3).unwrap(), exposed(&direct));243 }244 }245 #[test]246 fn the_recurrence_holds_on_every_cube_code() {247 for code in 0..256u128 {248 let tile = factory::create(code, 3, 3, 2, 1).unwrap();249 let rule = exposure_recurrence(&tile);250 let terms: Vec<i128> = (1..8u32)251 .map(|level| exposure_of_tile(&tile, level).unwrap() as i128)252 .collect();253 for at in rule.len()..terms.len() {254 let predicted: i128 = rule255 .iter()256 .enumerate()257 .map(|(back, &c)| c * terms[at - back - 1])258 .sum();259 assert_eq!(predicted, terms[at], "code={code} at={at} rule={rule:?}");260 }261 }262 }263 #[test]264 fn the_corners_fold_what_the_tile_shows() {265 for code in 0..256u128 {266 for number in [1usize, 2, 3, 4, 5, 7] {267 let filled = factory::code_to_corners(code, 3, 2).unwrap();268 let tile = factory::create(code, number, 3, 2, 1).unwrap();269 assert_eq!(270 Exposure::from_corners(&filled, number, 3, 2),271 Exposure::of_tile(&tile),272 "code={code} n={number}"273 );274 }275 }276 for (code, dimension, base) in [277 (7u128, 2usize, 3usize),278 (100, 2, 3),279 (511, 2, 3),280 (4369, 4, 2),281 (32767, 4, 2),282 (1, 1, 2),283 (2, 1, 3),284 ] {285 for number in [2usize, 3, 4, 5, 6, 9] {286 let filled = factory::code_to_corners(code, dimension, base).unwrap();287 let tile = factory::create(code, number, dimension, base, 1).unwrap();288 assert_eq!(289 Exposure::from_corners(&filled, number, dimension, base),290 Exposure::of_tile(&tile),291 "code={code} d={dimension} q={base} n={number}"292 );293 }294 }295 }296 #[test]297 fn the_classics_close() {298 let sponge: Vec<u128> = (1..4).map(|l| surface(23, 3, l, 2).unwrap()).collect();299 assert_eq!(sponge, [72, 1056, 18048]);300 let carpet: Vec<u128> = (1..5).map(|l| exposure(7, 3, 2, l, 2).unwrap()).collect();301 assert_eq!(carpet, [16, 80, 496, 3536]);302 assert_eq!(303 exposure_recurrence(&factory::create(7, 3, 2, 2, 1).unwrap()),304 [11, -24]305 );306 assert_eq!(exposure_of_tile(&atoms::ones_3d(2), 3), Some(384));307 assert_eq!(exposure_of_tile(&atoms::ones_3d(1), 5), Some(6));308 assert!(exposure(23, 3, 3, 120, 2).is_err());309 }310}