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}