star.rs

9.5 kB · rust · 275 lines

1use crate::{code_of, Fault, Grid};2use mrlycore::json;3use mrlymath::six::star::{arm_law, chi8, constant, decay, width_law, Branch, Star};4use wasm_bindgen::prelude::*;56const LAYER_CAP: usize = 800;7const FIELD_CAP: usize = 128;8const SIZE_CAP: usize = 384;9const HALF_CAP: usize = 64;1011// GUARDS1213fn star_of(code: &str) -> Result<Star, Fault> {14    Ok(Star::new(code_of(code)?)?)15}1617fn layers_of(layers: usize, cap: usize) -> Result<usize, Fault> {18    if !(2..=cap).contains(&layers) {19        return Err(Fault::new(format!(20            "the layer count must be between 2 and {cap}."21        )));22    }23    Ok(layers)24}2526fn half_of(half: usize) -> Result<usize, Fault> {27    if half > HALF_CAP {28        return Err(Fault::new(format!(29            "the band half-width must be at most {HALF_CAP} cells."30        )));31    }32    Ok(half)33}3435fn size_of(size: usize) -> Result<usize, Fault> {36    if !(16..=SIZE_CAP).contains(&size) {37        return Err(Fault::new(format!(38            "the raster size must be between 16 and {SIZE_CAP}."39        )));40    }41    Ok(size)42}4344fn columns(size: usize, n: i64) -> Vec<i64> {45    (0..size)46        .map(|step| {47            let point = (step as f64 + 0.5) / size as f64;48            ((point * 4.0 * n as f64).floor() as i64).min(4 * n - 1)49        })50        .collect()51}5253fn rows(size: usize, n: i64) -> Vec<i64> {54    (0..size)55        .map(|step| {56            let point = (step as f64 + 0.5) / size as f64;57            (2 * (point * 2.0 * n as f64).floor() as i64).min(4 * n - 2)58        })59        .collect()60}6162// EXPORTS6364/// Stacks the first `L` odd cut layers onto one square raster and hands back the mean ink at every sample, row-major, `NaN` outside the hexagon every layer shares.65///66/// This is the ideal frame: each layer is resampled onto the common grid, the row of a sample being67/// `z = 2 floor(2 n Z)` and its column `x = floor(4 n X)`, so the half-cell displacement between68/// layers is averaged away. The three bright lines through the centre are the ghost star, and they69/// fade as the layers pile up, since the star and the background both walk to one half.70#[wasm_bindgen]71pub fn star_field(code: &str, layers: usize, size: usize) -> Result<Vec<f32>, Fault> {72    let star = star_of(code)?;73    let layers = layers_of(layers, FIELD_CAP)?;74    let size = size_of(size)?;75    let mut total = vec![0f32; size * size];76    let mut whole = vec![true; size * size];77    for step in 0..layers {78        let number = 2 * step + 1;79        let n = number as i64;80        let across = columns(size, n);81        let down = rows(size, n);82        for (line, z) in down.iter().enumerate() {83            let base = line * size;84            for (slot, x) in across.iter().enumerate() {85                match star.cell(number, *x, *z) {86                    Some(fill) => total[base + slot] += f32::from(u8::from(fill)),87                    None => whole[base + slot] = false,88                }89            }90        }91    }92    Ok(total93        .iter()94        .zip(&whole)95        .map(96            |(sum, keep)| {97                if *keep {98                    sum / layers as f3299                } else {100                    f32::NAN101                }102            },103        )104        .collect())105}106107/// Marks the same raster with the band the star is measured on: `0` the star, `1` the hexagon background, `2` outside.108///109/// The band is `|x - y| <= W` cells of the deepest layer, the widest and last of the `L` stacked, so110/// widening `W` widens the arms of the cross on the picture exactly as it widens the reading.111#[wasm_bindgen]112pub fn star_band(code: &str, layers: usize, size: usize, half: usize) -> Result<Grid, Fault> {113    let star = star_of(code)?;114    let layers = layers_of(layers, FIELD_CAP)?;115    let size = size_of(size)?;116    let half = half_of(half)? as i64;117    let mut types = vec![2u8; size * size];118    let deepest = 2 * layers - 1;119    let n = deepest as i64;120    let across = columns(size, n);121    let down = rows(size, n);122    for step in 0..layers {123        let number = 2 * step + 1;124        let inner = columns(size, number as i64);125        let heights = rows(size, number as i64);126        for (line, z) in heights.iter().enumerate() {127            let base = line * size;128            for (slot, x) in inner.iter().enumerate() {129                if star.cell(number, *x, *z).is_none() {130                    types[base + slot] = 3;131                }132            }133        }134    }135    for (line, z) in down.iter().enumerate() {136        let base = line * size;137        let y = |x: i64| 6 * n - 2 - x - z;138        for (slot, x) in across.iter().enumerate() {139            if types[base + slot] == 3 {140                continue;141            }142            let (x, y, z) = (*x, y(*x), *z);143            let near = (x - y).abs() <= half || (y - z).abs() <= half || (z - x).abs() <= half;144            types[base + slot] = u8::from(!near);145        }146    }147    for slot in types.iter_mut() {148        if *slot == 3 {149            *slot = 2;150        }151    }152    Ok(Grid {153        width: size as u32,154        height: size as u32,155        types,156    })157}158159/// Reads one row per odd layer: the band's exact ink as a fraction, the closed form `1/2 + chi_8(n)/(2n)` beside it, the hexagon's own ink and the excess of one over the other.160///161/// At `W = 0` the band is the arm `x = y` itself, a diameter of `2n` cells with no width to choose,162/// and `exact` says the counted fraction is the closed form cell for cell. `W = 1` is the same point163/// set, since `x - y` is even on the cut, so it is exact too and an odd width is never a new band.164/// The background is the whole hexagon's ink at that layer, so `excess` is the per-layer ghost the165/// stack averages.166#[wasm_bindgen]167pub fn star_layers(code: &str, layers: usize, half: usize) -> Result<String, Fault> {168    let star = star_of(code)?;169    let layers = layers_of(layers, LAYER_CAP)?;170    let half = half_of(half)?;171    let mut out = Vec::with_capacity(layers);172    let mut exact = 0;173    for step in 0..layers {174        let number = 2 * step + 1;175        let band = star.arm(number, half)?;176        let hexagon = star.hexagon(number)?;177        let law = arm_law(number)?;178        let (numer, denom) = band.reduced();179        let (top, bottom) = law.reduced();180        let held = band == law;181        exact += usize::from(held);182        out.push(json!({183            "n": number,184            "inked": band.inked,185            "cells": band.cells,186            "numer": numer,187            "denom": denom,188            "ink": band.value(),189            "lawNumer": top,190            "lawDenom": bottom,191            "law": law.value(),192            "chi": chi8(number),193            "hex": hexagon.value(),194            "excess": band.value() - hexagon.value(),195            "exact": held,196        }));197    }198    Ok(json!({199        "layers": layers,200        "half": half,201        "exact": exact,202        "rows": out,203    })204    .to_string())205}206207/// Reads the decay in the cell frame: the `L`-layer excess scaled by `L`, the `(ln L)/4` it hides, the constant it settles on and the slope against `ln L` the coefficient is.208///209/// Nothing is resampled here: the star is the exact arm band of each layer and the background that210/// layer's own hexagon, so the reading is a ratio of cell counts. At `W = 0` and even `L` the law is211/// `L * excess_L = -(ln L)/4 + C + O(1/L^2)` with `C` in closed form, the slope settles on `-1/4`,212/// and the `1/L^2` term reads `L` mod 4: `-23/192` at `L = 0 mod 4` and `+25/192` at `L = 2 mod 4`.213/// Odd `L` shifts the constant by `1/8` and leaves `O(1/L)` behind. A wider band walks the slope off214/// `-1/4` onto `-(K + b)/(4(2K + 1))`, which is why the star has no frame-free coefficient. The215/// slope is absent unless `L` is divisible by four, the one window that cancels the character term216/// at both of its ends.217#[wasm_bindgen]218pub fn star_decay(code: &str, layers: usize, half: usize) -> Result<String, Fault> {219    let star = star_of(code)?;220    let layers = layers_of(layers, LAYER_CAP)?;221    let half = half_of(half)?;222    let excesses = star.excesses(layers, half)?;223    let read = decay(&excesses, layers)?;224    let branch = Branch::of(layers);225    let mut rungs = vec![layers];226    while rungs[rungs.len() - 1] % 4 == 0 && rungs[rungs.len() - 1] / 2 >= 4 {227        rungs.push(rungs[rungs.len() - 1] / 2);228    }229    let mut ladder = Vec::new();230    for count in rungs {231        let rung = decay(&excesses, count)?;232        let class = Branch::of(count);233        ladder.push(json!({234            "layers": count,235            "excess": rung.excess,236            "scaled": rung.scaled,237            "logged": rung.logged,238            "miss": rung.miss,239            "linear": rung.linear,240            "residual": rung.residual,241            "slope": rung.slope,242            "branch": class.name(),243            "predicted": class.residual(),244        }));245    }246    ladder.reverse();247    let mut walk = Vec::new();248    let mut count = 4;249    while count <= layers {250        let rung = decay(&excesses, count)?;251        walk.push(json!([count, rung.scaled]));252        count += 2;253    }254    Ok(json!({255        "layers": layers,256        "half": half,257        "deepest": 2 * layers - 1,258        "excess": read.excess,259        "scaled": read.scaled,260        "logged": read.logged,261        "miss": read.miss,262        "linear": read.linear,263        "residual": read.residual,264        "slope": read.slope,265        "target": width_law(half),266        "arm": half == 0,267        "constant": constant(),268        "branch": branch.name(),269        "branchConstant": branch.constant(),270        "predicted": branch.residual(),271        "rows": ladder,272        "walk": walk,273    })274    .to_string())275}