weights.rs
10.4 kB · rust · 311 lines
1use crate::{checked, rgba, theme, Fault, Pixels};2use mrlymath::two;3use wasm_bindgen::prelude::*;45const REACH: usize = 1024;6const SPREAD: f64 = 30.0;78// OBJECT910struct Weighted {11 number: usize,12 corners: Vec<(usize, usize)>,13 weights: Vec<f64>,14 logs: Vec<f64>,15 rung: f64,16}1718fn corners_of(code: &str, number: usize, base: usize) -> Result<Vec<(usize, usize)>, Fault> {19 if number < 2 {20 return Err(Fault::new("the side must be at least two."));21 }22 let tile = two::create(checked(code, 2, base)?, number, 1, 0, base)?;23 let types = tile.types().bytes().to_vec();24 let corners: Vec<(usize, usize)> = (0..number * number)25 .filter(|&at| types[at] != 0)26 .map(|at| (at / number, at % number))27 .collect();28 if corners.is_empty() {29 return Err(Fault::new("the empty design carries no mass."));30 }31 Ok(corners)32}3334fn ramp(t: f64) -> [u8; 4] {35 let ink = theme();36 let stops = [rgba(ink.ground), rgba(ink.blue), rgba(ink.yellow)];37 let reach = t.clamp(0.0, 1.0) * (stops.len() - 1) as f64;38 let low = (reach.floor() as usize).min(stops.len() - 2);39 let fade = reach - low as f64;40 let mix = |a: u8, b: u8| (f64::from(a) + (f64::from(b) - f64::from(a)) * fade).round() as u8;41 [42 mix(stops[low][0], stops[low + 1][0]),43 mix(stops[low][1], stops[low + 1][1]),44 mix(stops[low][2], stops[low + 1][2]),45 255,46 ]47}4849impl Weighted {50 fn read(code: &str, number: usize, base: usize, weights: &[f64]) -> Result<Weighted, Fault> {51 let corners = corners_of(code, number, base)?;52 if weights.len() != corners.len() {53 return Err(Fault::new(format!(54 "the design fills {} corners and {} weights arrived.",55 corners.len(),56 weights.len()57 )));58 }59 if weights.iter().any(|w| !w.is_finite() || *w <= 0.0) {60 return Err(Fault::new("every weight must be finite and above zero."));61 }62 let total: f64 = weights.iter().sum();63 let weights: Vec<f64> = weights.iter().map(|w| w / total).collect();64 let logs = weights.iter().map(|w| w.ln()).collect();65 Ok(Weighted {66 number,67 corners,68 weights,69 logs,70 rung: (number as f64).ln(),71 })72 }7374 fn tilt(&self, s: f64) -> (f64, f64) {75 let peak = self76 .logs77 .iter()78 .map(|l| s * l)79 .fold(f64::NEG_INFINITY, f64::max);80 let (mut sum, mut drift) = (0.0, 0.0);81 for l in &self.logs {82 let share = (s * l - peak).exp();83 sum += share;84 drift += share * l;85 }86 ((peak + sum.ln()) / self.rung, -drift / sum / self.rung)87 }8889 fn span(&self, level: usize) -> Result<usize, Fault> {90 if level < 1 {91 return Err(Fault::new("the level must be at least one."));92 }93 self.number94 .checked_pow(level as u32)95 .filter(|span| *span <= REACH)96 .ok_or_else(|| {97 Fault::new(format!(98 "side {} at level {level} passes the {REACH} cells a side the grid allows.",99 self.number100 ))101 })102 }103104 fn mass(&self, level: usize) -> Result<(usize, Vec<f64>), Fault> {105 let span = self.span(level)?;106 let mut wide = 1usize;107 let mut field = vec![1.0f64];108 for _ in 0..level {109 let next = wide * self.number;110 let mut grown = vec![0.0f64; next * next];111 for row in 0..wide {112 for col in 0..wide {113 let held = field[row * wide + col];114 if held == 0.0 {115 continue;116 }117 for (at, &(a, b)) in self.corners.iter().enumerate() {118 grown[(row * self.number + a) * next + col * self.number + b] =119 held * self.weights[at];120 }121 }122 }123 field = grown;124 wide = next;125 }126 Ok((span, field))127 }128}129130// EXPORTS131132/// Lists the filled corners of the design's level-one tile as row and column pairs, two numbers a corner, the order every weight vector is read in.133///134/// The corner count `k` is half the length: a weighted design puts a probability vector on these135/// `k` cells, and the level-`L` cell of the digit word `f_1 ... f_L` carries the product of their136/// weights.137#[wasm_bindgen]138pub fn weights_corners(code: &str, number: usize, base: usize) -> Result<Vec<usize>, Fault> {139 Ok(corners_of(code, number, base)?140 .into_iter()141 .flat_map(|(row, col)| [row, col])142 .collect())143}144145/// Reads the four local dimensions of a weighted design: `alpha_min`, `alpha_max`, the information exponent `alpha(1)` and `tau(0)`.146///147/// `alpha_min = -log_q max_f w_f` and `alpha_max = -log_q min_f w_f` are the ends of the spectrum's148/// support, `alpha(1) = -sum_f w_f log_q w_f` is the exponent almost every point carries, and149/// `tau(0) = log_q k` is the box dimension of the support, which no weighting moves. The weights150/// are given per filled corner in the corner order and normalised to sum to one inside.151#[wasm_bindgen]152pub fn weights_dims(153 code: &str,154 number: usize,155 base: usize,156 weights: &[f64],157) -> Result<Vec<f64>, Fault> {158 let read = Weighted::read(code, number, base, weights)?;159 let peak = read.logs.iter().copied().fold(f64::MIN, f64::max);160 let least = read.logs.iter().copied().fold(f64::MAX, f64::min);161 let held: f64 = read162 .weights163 .iter()164 .zip(&read.logs)165 .map(|(w, l)| w * l)166 .sum();167 Ok(vec![168 -peak / read.rung,169 -least / read.rung,170 -held / read.rung,171 (read.corners.len() as f64).ln() / read.rung,172 ])173}174175/// Builds the level-`L` mass field of a weighted design row-major on the `q^L` by `q^L` grid, the masses summing to one and the empty cells zero.176///177/// The support is the unweighted design's own: only the contraction ratios enter it, and they stay178/// `1/q` at every weighting. The side is refused past 1024 cells.179#[wasm_bindgen]180pub fn weights_mass(181 code: &str,182 number: usize,183 level: usize,184 base: usize,185 weights: &[f64],186) -> Result<Vec<f32>, Fault> {187 let read = Weighted::read(code, number, base, weights)?;188 Ok(read189 .mass(level)?190 .1191 .into_iter()192 .map(|mass| mass as f32)193 .collect())194}195196/// Paints the level-`L` mass field through the ground-blue-gold ramp, every cell read at `(mass / peak)^gamma`, the empty cells left on the ground.197///198/// The gamma is display alone: it lifts the light cells against the heavy ones without touching a199/// number the page prints.200#[wasm_bindgen]201pub fn weights_pixels(202 code: &str,203 number: usize,204 level: usize,205 base: usize,206 weights: &[f64],207 gamma: f64,208) -> Result<Pixels, Fault> {209 if !(gamma.is_finite() && gamma > 0.0) {210 return Err(Fault::new("the gamma must be finite and above zero."));211 }212 let read = Weighted::read(code, number, base, weights)?;213 let (span, field) = read.mass(level)?;214 let peak = field215 .iter()216 .copied()217 .fold(f64::MIN, f64::max)218 .max(f64::MIN_POSITIVE);219 let ground = rgba(theme().ground);220 let colors = field221 .iter()222 .map(|mass| {223 if *mass <= 0.0 {224 ground225 } else {226 ramp((mass / peak).powf(gamma))227 }228 })229 .collect();230 Ok(Pixels::of(span, span, colors))231}232233/// Walks the pressure `tau(s) = log_q sum_f w_f^s` across the range, the pairs `s` and `tau(s)`, two numbers a sample.234///235/// The closed form holds under the open set condition, which every design satisfies with the unit236/// cell. `tau(0) = log_q k` and `tau(1) = 0` at every probability vector.237#[wasm_bindgen]238pub fn weights_pressure(239 code: &str,240 number: usize,241 base: usize,242 weights: &[f64],243 s_lo: f64,244 s_hi: f64,245 samples: usize,246) -> Result<Vec<f32>, Fault> {247 if samples < 2 {248 return Err(Fault::new("the pressure needs at least two samples."));249 }250 if !(s_lo.is_finite() && s_hi.is_finite() && s_lo < s_hi) {251 return Err(Fault::new(252 "the range must run from a low s to a higher one.",253 ));254 }255 let read = Weighted::read(code, number, base, weights)?;256 let mut out = Vec::with_capacity(2 * samples);257 for step in 0..samples {258 let s = s_lo + (s_hi - s_lo) * step as f64 / (samples - 1) as f64;259 out.push(s as f32);260 out.push(read.tilt(s).0 as f32);261 }262 Ok(out)263}264265/// Reads the pressure at one `s` as the four numbers `s`, `tau(s)`, `alpha(s)` and `f(alpha(s))`.266///267/// `alpha(s) = -tau'(s) = -(sum_f w_f^s log_q w_f)/(sum_f w_f^s)` in closed form, and the Legendre268/// transform is attained there, `f(alpha(s)) = alpha(s) s + tau(s)`. At `s = 1` this is the269/// information exponent and its own spectrum value, since `tau(1) = 0`.270#[wasm_bindgen]271pub fn weights_point(272 code: &str,273 number: usize,274 base: usize,275 weights: &[f64],276 s: f64,277) -> Result<Vec<f64>, Fault> {278 if !s.is_finite() {279 return Err(Fault::new("the moment s must be finite."));280 }281 let (tau, alpha) = Weighted::read(code, number, base, weights)?.tilt(s);282 Ok(vec![s, tau, alpha, alpha * s + tau])283}284285/// Walks the multifractal spectrum, the pairs `alpha(s)` and `f(alpha(s))` over the whole range `[alpha_min, alpha_max]`, two numbers a sample.286///287/// The moment is swept on a cubic warp of `s` in minus thirty to thirty, dense where the spectrum288/// turns and sparse on its two flat tails, so the samples land evenly along the curve. Equal289/// weights collapse the range to the single point `(log_q k, log_q k)`.290#[wasm_bindgen]291pub fn weights_spectrum(292 code: &str,293 number: usize,294 base: usize,295 weights: &[f64],296 samples: usize,297) -> Result<Vec<f32>, Fault> {298 if samples < 2 {299 return Err(Fault::new("the spectrum needs at least two samples."));300 }301 let read = Weighted::read(code, number, base, weights)?;302 let mut out = Vec::with_capacity(2 * samples);303 for step in 0..samples {304 let walk = 2.0 * step as f64 / (samples - 1) as f64 - 1.0;305 let s = SPREAD * walk * walk * walk;306 let (tau, alpha) = read.tilt(s);307 out.push(alpha as f32);308 out.push((alpha * s + tau) as f32);309 }310 Ok(out)311}