tube.rs

8.6 kB · rust · 254 lines

1use crate::{checked, Fault};2use mrlyrs::math::bang::Code;3use mrlyrs::math::two;4use wasm_bindgen::prelude::*;56const REACH: usize = 729;7const FLOOR: f64 = 3.0;89// TRANSFORM1011fn lower(f: &[f64], d: &mut [f64], hull: &mut [usize], edge: &mut [f64]) {12    let n = f.len();13    let mut k = 0;14    hull[0] = 0;15    edge[0] = f64::NEG_INFINITY;16    edge[1] = f64::INFINITY;17    for q in 1..n {18        let mut cut;19        loop {20            let p = hull[k];21            cut = ((f[q] + (q * q) as f64) - (f[p] + (p * p) as f64)) / (2 * (q - p)) as f64;22            if k > 0 && cut <= edge[k] {23                k -= 1;24            } else {25                break;26            }27        }28        k += 1;29        hull[k] = q;30        edge[k] = cut;31        edge[k + 1] = f64::INFINITY;32    }33    k = 0;34    for (q, slot) in d.iter_mut().enumerate() {35        while edge[k + 1] < q as f64 {36            k += 1;37        }38        let gap = q as f64 - hull[k] as f64;39        *slot = gap * gap + f[hull[k]];40    }41}4243fn transform(types: &[u8], side: usize) -> Vec<f64> {44    let far = (4 * side * side) as f64;45    let mut square: Vec<f64> = types46        .iter()47        .map(|&b| if b != 0 { 0.0 } else { far })48        .collect();49    let mut lane = vec![0.0; side];50    let mut out = vec![0.0; side];51    let mut hull = vec![0usize; side];52    let mut edge = vec![0.0; side + 1];53    for col in 0..side {54        for row in 0..side {55            lane[row] = square[row * side + col];56        }57        lower(&lane, &mut out, &mut hull, &mut edge);58        for row in 0..side {59            square[row * side + col] = out[row];60        }61    }62    for row in 0..side {63        lane.copy_from_slice(&square[row * side..(row + 1) * side]);64        lower(&lane, &mut out, &mut hull, &mut edge);65        square[row * side..(row + 1) * side].copy_from_slice(&out);66    }67    square.iter().map(|v| v.sqrt()).collect()68}6970// FIELD7172struct Field {73    side: usize,74    number: usize,75    dimension: f64,76    dist: Vec<f64>,77}7879impl Field {80    fn read(code: &str, number: usize, level: usize, base: usize) -> Result<Field, Fault> {81        if number < 2 {82            return Err(Fault::new("the side must be at least two."));83        }84        if level < 1 {85            return Err(Fault::new("the level must be at least one."));86        }87        let side = number88            .checked_pow(level as u32)89            .filter(|&side| side <= REACH)90            .ok_or_else(|| {91                Fault::new(format!(92                    "side {number} at level {level} passes the {REACH} cells a side the tube allows."93                ))94            })?;95        let code = checked(code, 2, base)?;96        let tile = two::create(Code::from(code), number, 1, 0, base)?;97        let digits = tile.types().bytes()?.iter().filter(|&&b| b != 0).count();98        if digits == 0 {99            return Err(Fault::new("the empty design has no tube."));100        }101        let cell = two::create(Code::from(code), number, level, 0, base)?;102        if cell.width() != side || cell.height() != side {103            return Err(Fault::new("the design is not a square grid."));104        }105        let dist = transform(cell.types().bytes()?, side);106        Ok(Field {107            side,108            number,109            dimension: (digits as f64).ln() / (number as f64).ln(),110            dist,111        })112    }113114    fn volume(&self, eps: f64) -> f64 {115        self.dist116            .iter()117            .map(|&d| (eps - d + 1.0).clamp(0.0, 1.0))118            .sum()119    }120}121122// EXPORTS123124/// 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.125///126/// The transform is exact, the two-pass lower envelope of parabolas, so a cell inside a square127/// hole of side `s` reads the whole number of cells to the hole's wall. The grid is refused past128/// 729 cells a side.129#[wasm_bindgen]130pub fn tube_distance(131    code: &str,132    number: usize,133    level: usize,134    base: usize,135) -> Result<Vec<f32>, Fault> {136    let field = Field::read(code, number, level, base)?;137    Ok(field.dist.iter().map(|&v| v as f32).collect())138}139140/// 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.141///142/// A cell carries the share of itself the radius reaches, `eps - dist + 1` clamped to the unit143/// interval, which is exact at whole radii on a design whose holes are squares: the carpet at144/// level 6 and radius 21 cells reads `5912/6561` of the unit square.145#[wasm_bindgen]146pub fn tube_volume(147    code: &str,148    number: usize,149    level: usize,150    base: usize,151    eps_cells: f64,152) -> Result<f64, Fault> {153    Ok(Field::read(code, number, level, base)?.volume(eps_cells))154}155156/// 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.157///158/// The radius runs from `1/q` down to three cells, evenly in `u`, and the pairs come back empty159/// when the grid is too coarse to hold that range. `d` is `log_q k`, the design's fractal160/// dimension, so `M` is the Minkowski content reading whose limit the closed profile is.161#[wasm_bindgen]162pub fn tube_profile(163    code: &str,164    number: usize,165    level: usize,166    base: usize,167    samples: usize,168) -> Result<Vec<f32>, Fault> {169    let field = Field::read(code, number, level, base)?;170    let side = field.side as f64;171    let top = side / field.number as f64;172    if top <= FLOOR || samples < 2 {173        return Ok(Vec::new());174    }175    let (low, high) = ((side / top).ln(), (side / FLOOR).ln());176    let mut pairs = Vec::with_capacity(2 * samples);177    for step in 0..samples {178        let u = low + (high - low) * step as f64 / (samples - 1) as f64;179        let cells = side * (-u).exp();180        let eps = cells / side;181        let volume = field.volume(cells) / (side * side);182        pairs.push(u as f32);183        pairs.push((eps.powf(field.dimension - 2.0) * volume) as f32);184    }185    Ok(pairs)186}187188/// 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.189///190/// Every hole of such a design is an isolated open square whose boundary lies in the design, so191/// the inner tube is the exact sum over the levels, `V(eps) = sum k^(m-1) h(q^-m, eps)`, and192/// `M(eps) = eps^(d-2) V(eps)` runs onto `G`, log-periodic with period `ln q`. The carpet is193/// `q = 3`, `k = 8`, where `G(1/3) = G(1) = 379/280` and the swing above zero is the whole proof194/// that the design is not Minkowski measurable.195#[wasm_bindgen]196pub fn tube_closed(q: usize, k: usize, t: f64) -> Result<f64, Fault> {197    if q < 2 || k <= q || k >= q * q {198        return Err(Fault::new(format!(199            "the class needs {q} < k < {}, not {k}.",200            q * q201        )));202    }203    if !(t > 0.0 && t <= 1.0) {204        return Err(Fault::new("the phase must lie in zero to one."));205    }206    let (base, mass) = (q as f64, k as f64);207    let mut step = 0i32;208    while t * base.powi(step) < 0.5 && step < 512 {209        step += 1;210    }211    while step > -512 && t * base.powi(step - 1) >= 0.5 {212        step -= 1;213    }214    let tail = (mass / (base * base)).powi(step - 1);215    let phase = t * base.powi(step) / base;216    let dimension = mass.ln() / base.ln();217    let sum = 1.0 / (base * base - mass) + 4.0 * phase / (mass - base)218        - 4.0 * phase * phase / (mass - 1.0);219    Ok(t.powf(dimension - 2.0) * tail * sum)220}221222/// 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.223#[wasm_bindgen]224pub fn tube_class(code: &str, number: usize, base: usize) -> Result<bool, Fault> {225    if number < 3 {226        return Ok(false);227    }228    let tile = two::create(Code::from(checked(code, 2, base)?), number, 1, 0, base)?;229    let types = tile.types().bytes()?.to_vec();230    if types.len() != number * number {231        return Ok(false);232    }233    let holes: Vec<(usize, usize)> = (0..types.len())234        .filter(|&at| types[at] == 0)235        .map(|at| (at / number, at % number))236        .collect();237    if holes.is_empty() {238        return Ok(false);239    }240    if holes241        .iter()242        .any(|&(row, col)| row == 0 || col == 0 || row + 1 == number || col + 1 == number)243    {244        return Ok(false);245    }246    for (at, &(row, col)) in holes.iter().enumerate() {247        for &(other, next) in &holes[at + 1..] {248            if row.abs_diff(other) <= 1 && col.abs_diff(next) <= 1 {249                return Ok(false);250            }251        }252    }253    Ok(true)254}