tube.rs
8.5 kB · rust · 253 lines
1use crate::{checked, Fault};2use mrlymath::two;3use wasm_bindgen::prelude::*;45const REACH: usize = 729;6const FLOOR: f64 = 3.0;78// TRANSFORM910fn lower(f: &[f64], d: &mut [f64], hull: &mut [usize], edge: &mut [f64]) {11 let n = f.len();12 let mut k = 0;13 hull[0] = 0;14 edge[0] = f64::NEG_INFINITY;15 edge[1] = f64::INFINITY;16 for q in 1..n {17 let mut cut;18 loop {19 let p = hull[k];20 cut = ((f[q] + (q * q) as f64) - (f[p] + (p * p) as f64)) / (2 * (q - p)) as f64;21 if k > 0 && cut <= edge[k] {22 k -= 1;23 } else {24 break;25 }26 }27 k += 1;28 hull[k] = q;29 edge[k] = cut;30 edge[k + 1] = f64::INFINITY;31 }32 k = 0;33 for (q, slot) in d.iter_mut().enumerate() {34 while edge[k + 1] < q as f64 {35 k += 1;36 }37 let gap = q as f64 - hull[k] as f64;38 *slot = gap * gap + f[hull[k]];39 }40}4142fn transform(types: &[u8], side: usize) -> Vec<f64> {43 let far = (4 * side * side) as f64;44 let mut square: Vec<f64> = types45 .iter()46 .map(|&b| if b != 0 { 0.0 } else { far })47 .collect();48 let mut lane = vec![0.0; side];49 let mut out = vec![0.0; side];50 let mut hull = vec![0usize; side];51 let mut edge = vec![0.0; side + 1];52 for col in 0..side {53 for row in 0..side {54 lane[row] = square[row * side + col];55 }56 lower(&lane, &mut out, &mut hull, &mut edge);57 for row in 0..side {58 square[row * side + col] = out[row];59 }60 }61 for row in 0..side {62 lane.copy_from_slice(&square[row * side..(row + 1) * side]);63 lower(&lane, &mut out, &mut hull, &mut edge);64 square[row * side..(row + 1) * side].copy_from_slice(&out);65 }66 square.iter().map(|v| v.sqrt()).collect()67}6869// FIELD7071struct Field {72 side: usize,73 number: usize,74 dimension: f64,75 dist: Vec<f64>,76}7778impl Field {79 fn read(code: &str, number: usize, level: usize, base: usize) -> Result<Field, Fault> {80 if number < 2 {81 return Err(Fault::new("the side must be at least two."));82 }83 if level < 1 {84 return Err(Fault::new("the level must be at least one."));85 }86 let side = number87 .checked_pow(level as u32)88 .filter(|&side| side <= REACH)89 .ok_or_else(|| {90 Fault::new(format!(91 "side {number} at level {level} passes the {REACH} cells a side the tube allows."92 ))93 })?;94 let code = checked(code, 2, base)?;95 let tile = two::create(code, number, 1, 0, base)?;96 let digits = tile.types().bytes().iter().filter(|&&b| b != 0).count();97 if digits == 0 {98 return Err(Fault::new("the empty design has no tube."));99 }100 let cell = two::create(code, number, level, 0, base)?;101 if cell.width() != side || cell.height() != side {102 return Err(Fault::new("the design is not a square grid."));103 }104 let dist = transform(cell.types().bytes(), side);105 Ok(Field {106 side,107 number,108 dimension: (digits as f64).ln() / (number as f64).ln(),109 dist,110 })111 }112113 fn volume(&self, eps: f64) -> f64 {114 self.dist115 .iter()116 .map(|&d| (eps - d + 1.0).clamp(0.0, 1.0))117 .sum()118 }119}120121// EXPORTS122123/// Reads the distance field of the design's level-L grid: the Euclidean distance in cell widths from every cell centre to the nearest filled cell centre, row-major, zero on the filled cells.124///125/// The transform is exact, the two-pass lower envelope of parabolas, so a cell inside a square126/// hole of side `s` reads the whole number of cells to the hole's wall. The grid is refused past127/// 729 cells a side.128#[wasm_bindgen]129pub fn tube_distance(130 code: &str,131 number: usize,132 level: usize,133 base: usize,134) -> Result<Vec<f32>, Fault> {135 let field = Field::read(code, number, level, base)?;136 Ok(field.dist.iter().map(|&v| v as f32).collect())137}138139/// Measures the inner tube at radius `eps_cells`: the area within that distance of the design, in cell widths squared, the filled cells counted whole.140///141/// A cell carries the share of itself the radius reaches, `eps - dist + 1` clamped to the unit142/// interval, which is exact at whole radii on a design whose holes are squares: the carpet at143/// level 6 and radius 21 cells reads `5912/6561` of the unit square.144#[wasm_bindgen]145pub fn tube_volume(146 code: &str,147 number: usize,148 level: usize,149 base: usize,150 eps_cells: f64,151) -> Result<f64, Fault> {152 Ok(Field::read(code, number, level, base)?.volume(eps_cells))153}154155/// Walks the Minkowski profile across the radii the grid resolves: the pairs `u = ln(1/eps)` and `M = eps^(d-2) V(eps)` in unit-square units, flat, two numbers a sample.156///157/// The radius runs from `1/q` down to three cells, evenly in `u`, and the pairs come back empty158/// when the grid is too coarse to hold that range. `d` is `log_q k`, the design's fractal159/// dimension, so `M` is the Minkowski content reading whose limit the closed profile is.160#[wasm_bindgen]161pub fn tube_profile(162 code: &str,163 number: usize,164 level: usize,165 base: usize,166 samples: usize,167) -> Result<Vec<f32>, Fault> {168 let field = Field::read(code, number, level, base)?;169 let side = field.side as f64;170 let top = side / field.number as f64;171 if top <= FLOOR || samples < 2 {172 return Ok(Vec::new());173 }174 let (low, high) = ((side / top).ln(), (side / FLOOR).ln());175 let mut pairs = Vec::with_capacity(2 * samples);176 for step in 0..samples {177 let u = low + (high - low) * step as f64 / (samples - 1) as f64;178 let cells = side * (-u).exp();179 let eps = cells / side;180 let volume = field.volume(cells) / (side * side);181 pairs.push(u as f32);182 pairs.push((eps.powf(field.dimension - 2.0) * volume) as f32);183 }184 Ok(pairs)185}186187/// Reads the closed limit profile `G(t)` of the interior-hole class at base `q` with `k` filled cells, the hole sum in closed form.188///189/// Every hole of such a design is an isolated open square whose boundary lies in the design, so190/// the inner tube is the exact sum over the levels, `V(eps) = sum k^(m-1) h(q^-m, eps)`, and191/// `M(eps) = eps^(d-2) V(eps)` runs onto `G`, log-periodic with period `ln q`. The carpet is192/// `q = 3`, `k = 8`, where `G(1/3) = G(1) = 379/280` and the swing above zero is the whole proof193/// that the design is not Minkowski measurable.194#[wasm_bindgen]195pub fn tube_closed(q: usize, k: usize, t: f64) -> Result<f64, Fault> {196 if q < 2 || k <= q || k >= q * q {197 return Err(Fault::new(format!(198 "the class needs {q} < k < {}, not {k}.",199 q * q200 )));201 }202 if !(t > 0.0 && t <= 1.0) {203 return Err(Fault::new("the phase must lie in zero to one."));204 }205 let (base, mass) = (q as f64, k as f64);206 let mut step = 0i32;207 while t * base.powi(step) < 0.5 && step < 512 {208 step += 1;209 }210 while step > -512 && t * base.powi(step - 1) >= 0.5 {211 step -= 1;212 }213 let tail = (mass / (base * base)).powi(step - 1);214 let phase = t * base.powi(step) / base;215 let dimension = mass.ln() / base.ln();216 let sum = 1.0 / (base * base - mass) + 4.0 * phase / (mass - base)217 - 4.0 * phase * phase / (mass - 1.0);218 Ok(t.powf(dimension - 2.0) * tail * sum)219}220221/// Decides whether the design's holes are the isolated interior squares the closed profile needs: every empty cell of the level-one tile off the border, and no two of them touching, even at a corner.222#[wasm_bindgen]223pub fn tube_class(code: &str, number: usize, base: usize) -> Result<bool, Fault> {224 if number < 3 {225 return Ok(false);226 }227 let tile = two::create(checked(code, 2, base)?, number, 1, 0, base)?;228 let types = tile.types().bytes().to_vec();229 if types.len() != number * number {230 return Ok(false);231 }232 let holes: Vec<(usize, usize)> = (0..types.len())233 .filter(|&at| types[at] == 0)234 .map(|at| (at / number, at % number))235 .collect();236 if holes.is_empty() {237 return Ok(false);238 }239 if holes240 .iter()241 .any(|&(row, col)| row == 0 || col == 0 || row + 1 == number || col + 1 == number)242 {243 return Ok(false);244 }245 for (at, &(row, col)) in holes.iter().enumerate() {246 for &(other, next) in &holes[at + 1..] {247 if row.abs_diff(other) <= 1 && col.abs_diff(next) <= 1 {248 return Ok(false);249 }250 }251 }252 Ok(true)253}