chladni.rs

5.8 kB · rust · 203 lines

1#![allow(clippy::too_many_arguments)]23use crate::life::life_noise;4use crate::{code_of, Fault, Grid};5use mrlycore::json;6use mrlymath::life;7use mrlynum::fft::{8    convolve_with, embed_kernel, log_spectrum, peak_ring, radial_profile, transform,9};10use wasm_bindgen::prelude::*;1112fn checked_size(size: usize) -> Result<(), Fault> {13    if size == 0 || !size.is_power_of_two() {14        return Err(Fault::new("the field size must be a power of two."));15    }16    Ok(())17}1819fn field_of(types: &[u8], size: usize) -> Result<Vec<f64>, Fault> {20    checked_size(size)?;21    if types.len() != size * size {22        return Err(Fault::new("the grid bytes do not match size squared."));23    }24    Ok(types25        .iter()26        .map(|&t| if t != 0 { 1.0 } else { 0.0 })27        .collect())28}2930struct Mask {31    span: usize,32    cells: Vec<u8>,33    budget: usize,34}3536fn mask_of(code: &str, side: usize, level: usize, size: usize) -> Result<Mask, Fault> {37    checked_size(size)?;38    let mask = life::design_mask(2, code_of(code)?, side, level)?;39    let span = mask.shape[0];40    if span > size {41        return Err(Fault::new("the mask does not fit the field."));42    }43    let budget = mask.sum() as usize;44    if budget == 0 {45        return Err(Fault::new("the mask is empty."));46    }47    Ok(Mask {48        span,49        cells: mask.bytes().to_vec(),50        budget,51    })52}5354fn window(lo: f64, hi: f64, budget: usize) -> Vec<bool> {55    (0..=budget)56        .map(|k| {57            let share = k as f64 / budget as f64;58            lo <= share && share <= hi59        })60        .collect()61}6263struct Engine {64    size: usize,65    budget: usize,66    kernel_re: Vec<f64>,67    kernel_im: Vec<f64>,68    born: Vec<bool>,69    kept: Vec<bool>,70    field: Vec<f64>,71}7273impl Engine {74    fn new(75        code: &str,76        side: usize,77        level: usize,78        b_lo: f64,79        b_hi: f64,80        s_lo: f64,81        s_hi: f64,82        size: usize,83    ) -> Result<Engine, Fault> {84        let mask = mask_of(code, side, level, size)?;85        let (kernel_re, kernel_im) = transform(&embed_kernel(&mask.cells, mask.span, size), size);86        Ok(Engine {87            size,88            budget: mask.budget,89            kernel_re,90            kernel_im,91            born: window(b_lo, b_hi, mask.budget),92            kept: window(s_lo, s_hi, mask.budget),93            field: vec![0.0; size * size],94        })95    }96    fn step(&mut self, types: &mut [u8]) {97        for (slot, &t) in self.field.iter_mut().zip(types.iter()) {98            *slot = if t != 0 { 1.0 } else { 0.0 };99        }100        let counts = convolve_with(&self.field, &self.kernel_re, &self.kernel_im, self.size);101        for (slot, &count) in types.iter_mut().zip(&counts) {102            let n = (count.round().max(0.0) as usize).min(self.budget);103            let lives = if *slot != 0 {104                self.kept[n]105            } else {106                self.born[n]107            };108            *slot = u8::from(lives);109        }110    }111}112113/// Runs a seeded soup on a design mask for the given steps on a wrapped power-of-two torus: a dead cell is born and a live one kept when its neighbour count over the mask budget lies in the closed birth or survive window, the count read by FFT convolution.114#[wasm_bindgen]115pub fn chladni_run(116    code: &str,117    side: usize,118    level: usize,119    b_lo: f64,120    b_hi: f64,121    s_lo: f64,122    s_hi: f64,123    size: usize,124    steps: usize,125    density: f64,126    seed: u32,127) -> Result<Grid, Fault> {128    let mut engine = Engine::new(code, side, level, b_lo, b_hi, s_lo, s_hi, size)?;129    let mut types = life_noise(size, size, density, seed);130    for _ in 0..steps {131        engine.step(&mut types);132    }133    Ok(Grid {134        width: size as u32,135        height: size as u32,136        types,137    })138}139140/// Advances a size-square grid one generation on a design mask under the closed birth and survive windows, wrapping the edges.141#[wasm_bindgen]142pub fn chladni_next(143    types: &[u8],144    size: usize,145    code: &str,146    side: usize,147    level: usize,148    b_lo: f64,149    b_hi: f64,150    s_lo: f64,151    s_hi: f64,152) -> Result<Vec<u8>, Fault> {153    field_of(types, size)?;154    let mut engine = Engine::new(code, side, level, b_lo, b_hi, s_lo, s_hi, size)?;155    let mut next = types.to_vec();156    engine.step(&mut next);157    Ok(next)158}159160/// Draws a design mask centred in a size-square picture, the mask centre at size over two.161#[wasm_bindgen]162pub fn chladni_kernel(code: &str, side: usize, level: usize, size: usize) -> Result<Grid, Fault> {163    let mask = mask_of(code, side, level, size)?;164    let start = size / 2 - mask.span / 2;165    let mut types = vec![0u8; size * size];166    for r in 0..mask.span {167        for c in 0..mask.span {168            types[(start + r) * size + start + c] = mask.cells[r * mask.span + c];169        }170    }171    Ok(Grid {172        width: size as u32,173        height: size as u32,174        types,175    })176}177178/// Reads the centred log magnitude spectrum of a size-square 0/1 field, scaled to at most one by its peak.179#[wasm_bindgen]180pub fn chladni_spectrum(types: &[u8], size: usize) -> Result<Vec<f32>, Fault> {181    let spectrum = log_spectrum(&field_of(types, size)?, size);182    let top = spectrum.iter().cloned().fold(0.0f64, f64::max);183    let scale = if top > 0.0 { 1.0 / top } else { 0.0 };184    Ok(spectrum.iter().map(|&v| (v * scale) as f32).collect())185}186187/// Reads the ring means of a size-square 0/1 field's log spectrum, the peak ring past the centre and its wavelength in cells, as JSON.188#[wasm_bindgen]189pub fn chladni_profile(types: &[u8], size: usize) -> Result<String, Fault> {190    let profile = radial_profile(&log_spectrum(&field_of(types, size)?, size), size);191    let ring = peak_ring(&profile);192    let wavelength = if ring == 0 {193        0.0194    } else {195        size as f64 / ring as f64196    };197    Ok(json!({198        "profile": profile,199        "peak_ring": ring,200        "wavelength": wavelength,201    })202    .to_string())203}