weights.rs

10.4 kB · rust · 312 lines

1use crate::{checked, rgba, theme, Fault, Pixels};2use mrlyrs::math::bang::Code;3use mrlyrs::math::two;4use wasm_bindgen::prelude::*;56const REACH: usize = 1024;7const SPREAD: f64 = 30.0;89// OBJECT1011struct Weighted {12    number: usize,13    corners: Vec<(usize, usize)>,14    weights: Vec<f64>,15    logs: Vec<f64>,16    rung: f64,17}1819fn corners_of(code: &str, number: usize, base: usize) -> Result<Vec<(usize, usize)>, Fault> {20    if number < 2 {21        return Err(Fault::new("the side must be at least two."));22    }23    let tile = two::create(Code::from(checked(code, 2, base)?), number, 1, 0, base)?;24    let types = tile.types().bytes()?.to_vec();25    let corners: Vec<(usize, usize)> = (0..number * number)26        .filter(|&at| types[at] != 0)27        .map(|at| (at / number, at % number))28        .collect();29    if corners.is_empty() {30        return Err(Fault::new("the empty design carries no mass."));31    }32    Ok(corners)33}3435fn ramp(t: f64) -> [u8; 4] {36    let ink = theme();37    let stops = [rgba(ink.ground), rgba(ink.blue), rgba(ink.yellow)];38    let reach = t.clamp(0.0, 1.0) * (stops.len() - 1) as f64;39    let low = (reach.floor() as usize).min(stops.len() - 2);40    let fade = reach - low as f64;41    let mix = |a: u8, b: u8| (f64::from(a) + (f64::from(b) - f64::from(a)) * fade).round() as u8;42    [43        mix(stops[low][0], stops[low + 1][0]),44        mix(stops[low][1], stops[low + 1][1]),45        mix(stops[low][2], stops[low + 1][2]),46        255,47    ]48}4950impl Weighted {51    fn read(code: &str, number: usize, base: usize, weights: &[f64]) -> Result<Weighted, Fault> {52        let corners = corners_of(code, number, base)?;53        if weights.len() != corners.len() {54            return Err(Fault::new(format!(55                "the design fills {} corners and {} weights arrived.",56                corners.len(),57                weights.len()58            )));59        }60        if weights.iter().any(|w| !w.is_finite() || *w <= 0.0) {61            return Err(Fault::new("every weight must be finite and above zero."));62        }63        let total: f64 = weights.iter().sum();64        let weights: Vec<f64> = weights.iter().map(|w| w / total).collect();65        let logs = weights.iter().map(|w| w.ln()).collect();66        Ok(Weighted {67            number,68            corners,69            weights,70            logs,71            rung: (number as f64).ln(),72        })73    }7475    fn tilt(&self, s: f64) -> (f64, f64) {76        let peak = self77            .logs78            .iter()79            .map(|l| s * l)80            .fold(f64::NEG_INFINITY, f64::max);81        let (mut sum, mut drift) = (0.0, 0.0);82        for l in &self.logs {83            let share = (s * l - peak).exp();84            sum += share;85            drift += share * l;86        }87        ((peak + sum.ln()) / self.rung, -drift / sum / self.rung)88    }8990    fn span(&self, level: usize) -> Result<usize, Fault> {91        if level < 1 {92            return Err(Fault::new("the level must be at least one."));93        }94        self.number95            .checked_pow(level as u32)96            .filter(|span| *span <= REACH)97            .ok_or_else(|| {98                Fault::new(format!(99                    "side {} at level {level} passes the {REACH} cells a side the grid allows.",100                    self.number101                ))102            })103    }104105    fn mass(&self, level: usize) -> Result<(usize, Vec<f64>), Fault> {106        let span = self.span(level)?;107        let mut wide = 1usize;108        let mut field = vec![1.0f64];109        for _ in 0..level {110            let next = wide * self.number;111            let mut grown = vec![0.0f64; next * next];112            for row in 0..wide {113                for col in 0..wide {114                    let held = field[row * wide + col];115                    if held == 0.0 {116                        continue;117                    }118                    for (at, &(a, b)) in self.corners.iter().enumerate() {119                        grown[(row * self.number + a) * next + col * self.number + b] =120                            held * self.weights[at];121                    }122                }123            }124            field = grown;125            wide = next;126        }127        Ok((span, field))128    }129}130131// EXPORTS132133/// 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.134///135/// The corner count `k` is half the length: a weighted design puts a probability vector on these136/// `k` cells, and the level-`L` cell of the digit word `f_1 ... f_L` carries the product of their137/// weights.138#[wasm_bindgen]139pub fn weights_corners(code: &str, number: usize, base: usize) -> Result<Vec<usize>, Fault> {140    Ok(corners_of(code, number, base)?141        .into_iter()142        .flat_map(|(row, col)| [row, col])143        .collect())144}145146/// Reads the four local dimensions of a weighted design: `alpha_min`, `alpha_max`, the information exponent `alpha(1)` and `tau(0)`.147///148/// `alpha_min = -log_q max_f w_f` and `alpha_max = -log_q min_f w_f` are the ends of the spectrum's149/// support, `alpha(1) = -sum_f w_f log_q w_f` is the exponent almost every point carries, and150/// `tau(0) = log_q k` is the box dimension of the support, which no weighting moves. The weights151/// are given per filled corner in the corner order and normalised to sum to one inside.152#[wasm_bindgen]153pub fn weights_dims(154    code: &str,155    number: usize,156    base: usize,157    weights: &[f64],158) -> Result<Vec<f64>, Fault> {159    let read = Weighted::read(code, number, base, weights)?;160    let peak = read.logs.iter().copied().fold(f64::MIN, f64::max);161    let least = read.logs.iter().copied().fold(f64::MAX, f64::min);162    let held: f64 = read163        .weights164        .iter()165        .zip(&read.logs)166        .map(|(w, l)| w * l)167        .sum();168    Ok(vec![169        -peak / read.rung,170        -least / read.rung,171        -held / read.rung,172        (read.corners.len() as f64).ln() / read.rung,173    ])174}175176/// 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.177///178/// The support is the unweighted design's own: only the contraction ratios enter it, and they stay179/// `1/q` at every weighting. The side is refused past 1024 cells.180#[wasm_bindgen]181pub fn weights_mass(182    code: &str,183    number: usize,184    level: usize,185    base: usize,186    weights: &[f64],187) -> Result<Vec<f32>, Fault> {188    let read = Weighted::read(code, number, base, weights)?;189    Ok(read190        .mass(level)?191        .1192        .into_iter()193        .map(|mass| mass as f32)194        .collect())195}196197/// 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.198///199/// The gamma is display alone: it lifts the light cells against the heavy ones without touching a200/// number the page prints.201#[wasm_bindgen]202pub fn weights_pixels(203    code: &str,204    number: usize,205    level: usize,206    base: usize,207    weights: &[f64],208    gamma: f64,209) -> Result<Pixels, Fault> {210    if !(gamma.is_finite() && gamma > 0.0) {211        return Err(Fault::new("the gamma must be finite and above zero."));212    }213    let read = Weighted::read(code, number, base, weights)?;214    let (span, field) = read.mass(level)?;215    let peak = field216        .iter()217        .copied()218        .fold(f64::MIN, f64::max)219        .max(f64::MIN_POSITIVE);220    let ground = rgba(theme().ground);221    let colors = field222        .iter()223        .map(|mass| {224            if *mass <= 0.0 {225                ground226            } else {227                ramp((mass / peak).powf(gamma))228            }229        })230        .collect();231    Ok(Pixels::of(span, span, colors))232}233234/// 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.235///236/// The closed form holds under the open set condition, which every design satisfies with the unit237/// cell. `tau(0) = log_q k` and `tau(1) = 0` at every probability vector.238#[wasm_bindgen]239pub fn weights_pressure(240    code: &str,241    number: usize,242    base: usize,243    weights: &[f64],244    s_lo: f64,245    s_hi: f64,246    samples: usize,247) -> Result<Vec<f32>, Fault> {248    if samples < 2 {249        return Err(Fault::new("the pressure needs at least two samples."));250    }251    if !(s_lo.is_finite() && s_hi.is_finite() && s_lo < s_hi) {252        return Err(Fault::new(253            "the range must run from a low s to a higher one.",254        ));255    }256    let read = Weighted::read(code, number, base, weights)?;257    let mut out = Vec::with_capacity(2 * samples);258    for step in 0..samples {259        let s = s_lo + (s_hi - s_lo) * step as f64 / (samples - 1) as f64;260        out.push(s as f32);261        out.push(read.tilt(s).0 as f32);262    }263    Ok(out)264}265266/// Reads the pressure at one `s` as the four numbers `s`, `tau(s)`, `alpha(s)` and `f(alpha(s))`.267///268/// `alpha(s) = -tau'(s) = -(sum_f w_f^s log_q w_f)/(sum_f w_f^s)` in closed form, and the Legendre269/// transform is attained there, `f(alpha(s)) = alpha(s) s + tau(s)`. At `s = 1` this is the270/// information exponent and its own spectrum value, since `tau(1) = 0`.271#[wasm_bindgen]272pub fn weights_point(273    code: &str,274    number: usize,275    base: usize,276    weights: &[f64],277    s: f64,278) -> Result<Vec<f64>, Fault> {279    if !s.is_finite() {280        return Err(Fault::new("the moment s must be finite."));281    }282    let (tau, alpha) = Weighted::read(code, number, base, weights)?.tilt(s);283    Ok(vec![s, tau, alpha, alpha * s + tau])284}285286/// Walks the multifractal spectrum, the pairs `alpha(s)` and `f(alpha(s))` over the whole range `[alpha_min, alpha_max]`, two numbers a sample.287///288/// The moment is swept on a cubic warp of `s` in minus thirty to thirty, dense where the spectrum289/// turns and sparse on its two flat tails, so the samples land evenly along the curve. Equal290/// weights collapse the range to the single point `(log_q k, log_q k)`.291#[wasm_bindgen]292pub fn weights_spectrum(293    code: &str,294    number: usize,295    base: usize,296    weights: &[f64],297    samples: usize,298) -> Result<Vec<f32>, Fault> {299    if samples < 2 {300        return Err(Fault::new("the spectrum needs at least two samples."));301    }302    let read = Weighted::read(code, number, base, weights)?;303    let mut out = Vec::with_capacity(2 * samples);304    for step in 0..samples {305        let walk = 2.0 * step as f64 / (samples - 1) as f64 - 1.0;306        let s = SPREAD * walk * walk * walk;307        let (tau, alpha) = read.tilt(s);308        out.push(alpha as f32);309        out.push((alpha * s + tau) as f32);310    }311    Ok(out)312}