chladni.rs

5.9 kB · rust · 205 lines

1#![allow(clippy::too_many_arguments)]23use crate::life::life_noise;4use crate::{code_of, Fault, Grid};5use mrlyrs::core::json;6use mrlyrs::life;7use mrlyrs::math::bang::Code;8use mrlyrs::num::fft::{9    convolve_with, embed_kernel, log_spectrum, peak_ring, radial_profile, transform,10};11use wasm_bindgen::prelude::*;1213fn checked_size(size: usize) -> Result<(), Fault> {14    if size == 0 || !size.is_power_of_two() {15        return Err(Fault::new("the field size must be a power of two."));16    }17    Ok(())18}1920fn field_of(types: &[u8], size: usize) -> Result<Vec<f64>, Fault> {21    checked_size(size)?;22    if types.len() != size * size {23        return Err(Fault::new("the grid bytes do not match size squared."));24    }25    Ok(types26        .iter()27        .map(|&t| if t != 0 { 1.0 } else { 0.0 })28        .collect())29}3031struct Mask {32    span: usize,33    cells: Vec<u8>,34    budget: usize,35}3637fn mask_of(code: &str, side: usize, level: usize, size: usize) -> Result<Mask, Fault> {38    checked_size(size)?;39    let mask = life::design_mask(2, Code::from(code_of(code)?), side, level)?;40    let span = mask.shape[0];41    if span > size {42        return Err(Fault::new("the mask does not fit the field."));43    }44    let budget = mask.sum() as usize;45    if budget == 0 {46        return Err(Fault::new("the mask is empty."));47    }48    Ok(Mask {49        span,50        cells: mask.bytes()?.to_vec(),51        budget,52    })53}5455fn window(lo: f64, hi: f64, budget: usize) -> Vec<bool> {56    (0..=budget)57        .map(|k| {58            let share = k as f64 / budget as f64;59            lo <= share && share <= hi60        })61        .collect()62}6364struct Engine {65    size: usize,66    budget: usize,67    kernel_re: Vec<f64>,68    kernel_im: Vec<f64>,69    born: Vec<bool>,70    kept: Vec<bool>,71    field: Vec<f64>,72}7374impl Engine {75    fn new(76        code: &str,77        side: usize,78        level: usize,79        b_lo: f64,80        b_hi: f64,81        s_lo: f64,82        s_hi: f64,83        size: usize,84    ) -> Result<Engine, Fault> {85        let mask = mask_of(code, side, level, size)?;86        let (kernel_re, kernel_im) = transform(&embed_kernel(&mask.cells, mask.span, size)?, size)?;87        Ok(Engine {88            size,89            budget: mask.budget,90            kernel_re,91            kernel_im,92            born: window(b_lo, b_hi, mask.budget),93            kept: window(s_lo, s_hi, mask.budget),94            field: vec![0.0; size * size],95        })96    }97    fn step(&mut self, types: &mut [u8]) -> Result<(), Fault> {98        for (slot, &t) in self.field.iter_mut().zip(types.iter()) {99            *slot = if t != 0 { 1.0 } else { 0.0 };100        }101        let counts = convolve_with(&self.field, &self.kernel_re, &self.kernel_im, self.size)?;102        for (slot, &count) in types.iter_mut().zip(&counts) {103            let n = (count.round().max(0.0) as usize).min(self.budget);104            let lives = if *slot != 0 {105                self.kept[n]106            } else {107                self.born[n]108            };109            *slot = u8::from(lives);110        }111        Ok(())112    }113}114115/// 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.116#[wasm_bindgen]117pub fn chladni_run(118    code: &str,119    side: usize,120    level: usize,121    b_lo: f64,122    b_hi: f64,123    s_lo: f64,124    s_hi: f64,125    size: usize,126    steps: usize,127    density: f64,128    seed: u32,129) -> Result<Grid, Fault> {130    let mut engine = Engine::new(code, side, level, b_lo, b_hi, s_lo, s_hi, size)?;131    let mut types = life_noise(size, size, density, seed);132    for _ in 0..steps {133        engine.step(&mut types)?;134    }135    Ok(Grid {136        width: size as u32,137        height: size as u32,138        types,139    })140}141142/// Advances a size-square grid one generation on a design mask under the closed birth and survive windows, wrapping the edges.143#[wasm_bindgen]144pub fn chladni_next(145    types: &[u8],146    size: usize,147    code: &str,148    side: usize,149    level: usize,150    b_lo: f64,151    b_hi: f64,152    s_lo: f64,153    s_hi: f64,154) -> Result<Vec<u8>, Fault> {155    field_of(types, size)?;156    let mut engine = Engine::new(code, side, level, b_lo, b_hi, s_lo, s_hi, size)?;157    let mut next = types.to_vec();158    engine.step(&mut next)?;159    Ok(next)160}161162/// Draws a design mask centred in a size-square picture, the mask centre at size over two.163#[wasm_bindgen]164pub fn chladni_kernel(code: &str, side: usize, level: usize, size: usize) -> Result<Grid, Fault> {165    let mask = mask_of(code, side, level, size)?;166    let start = size / 2 - mask.span / 2;167    let mut types = vec![0u8; size * size];168    for r in 0..mask.span {169        for c in 0..mask.span {170            types[(start + r) * size + start + c] = mask.cells[r * mask.span + c];171        }172    }173    Ok(Grid {174        width: size as u32,175        height: size as u32,176        types,177    })178}179180/// Reads the centred log magnitude spectrum of a size-square 0/1 field, scaled to at most one by its peak.181#[wasm_bindgen]182pub fn chladni_spectrum(types: &[u8], size: usize) -> Result<Vec<f32>, Fault> {183    let spectrum = log_spectrum(&field_of(types, size)?, size)?;184    let top = spectrum.iter().cloned().fold(0.0f64, f64::max);185    let scale = if top > 0.0 { 1.0 / top } else { 0.0 };186    Ok(spectrum.iter().map(|&v| (v * scale) as f32).collect())187}188189/// 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.190#[wasm_bindgen]191pub fn chladni_profile(types: &[u8], size: usize) -> Result<String, Fault> {192    let profile = radial_profile(&log_spectrum(&field_of(types, size)?, size)?, size)?;193    let ring = peak_ring(&profile);194    let wavelength = if ring == 0 {195        0.0196    } else {197        size as f64 / ring as f64198    };199    Ok(json!({200        "profile": profile,201        "peak_ring": ring,202        "wavelength": wavelength,203    })204    .to_string())205}