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}