Skip to main content

rill_core_model/plate/
mod.rs

1//! 2D plate/membrane model — waveguide mesh with FDTD.
2//!
3//! Solves the 2D wave equation ∂²z/∂t² = c²(∂²z/∂x² + ∂²z/∂y²)
4//! using finite-difference time-domain (FDTD) on a rectangular grid.
5//! Single-sample excitation at a configurable position; output is
6//! read at the same position.
7
8mod params;
9
10pub use params::PlateParams;
11
12use rill_core::traits::algorithm::{
13    Algorithm, AlgorithmCategory, AlgorithmMetadata, ParameterizedAlgorithm,
14};
15use rill_core::traits::ParamValue;
16use rill_core::Transcendental;
17
18/// 2D plate model using FDTD waveguide mesh.
19///
20/// Pre-allocates two full grids (`grid_prev`, `grid_curr`) plus a scratch
21/// buffer. All grid updates happen inside `process()` — RT-safe, no allocation.
22#[derive(Debug, Clone)]
23pub struct PlateModel<T: Transcendental> {
24    params: PlateParams<T>,
25    grid_prev: Vec<T>,
26    grid_curr: Vec<T>,
27    grid_next: Vec<T>,
28    exc_x: usize,
29    exc_y: usize,
30    input_energy: T,
31}
32
33impl<T: Transcendental> PlateModel<T> {
34    /// Create a plate model with the given grid dimensions.
35    ///
36    /// The grid is pre-allocated. `wave_speed` must be ≤ 0.25 for stability.
37    pub fn new(params: PlateParams<T>) -> Self {
38        let size = params.grid_width * params.grid_height;
39        let exc_x = ((params.excitation_x.to_f64() * (params.grid_width - 1) as f64).round()
40            as usize)
41            .min(params.grid_width - 1);
42        let exc_y = ((params.excitation_y.to_f64() * (params.grid_height - 1) as f64).round()
43            as usize)
44            .min(params.grid_height - 1);
45        Self {
46            params,
47            grid_prev: vec![T::ZERO; size],
48            grid_curr: vec![T::ZERO; size],
49            grid_next: vec![T::ZERO; size],
50            exc_x,
51            exc_y,
52            input_energy: T::ZERO,
53        }
54    }
55
56    /// Excite the plate at the configured excitation position.
57    pub fn strike(&mut self, strength: T) {
58        let idx = self.exc_y * self.params.grid_width + self.exc_x;
59        self.grid_curr[idx] = strength;
60    }
61
62    fn idx(&self, x: usize, y: usize) -> usize {
63        y * self.params.grid_width + x
64    }
65
66    fn process_sample(&mut self, input: T) -> T {
67        let w = self.params.grid_width;
68        let h = self.params.grid_height;
69        let c2 = self.params.wave_speed * self.params.wave_speed;
70        let two = T::from_f32(2.0);
71
72        // FDTD update for interior points
73        for y in 1..(h - 1) {
74            for x in 1..(w - 1) {
75                let i = self.idx(x, y);
76                let lap = self.grid_curr[self.idx(x, y - 1)]
77                    + self.grid_curr[self.idx(x, y + 1)]
78                    + self.grid_curr[self.idx(x - 1, y)]
79                    + self.grid_curr[self.idx(x + 1, y)]
80                    - self.grid_curr[i] * T::from_f32(4.0);
81                self.grid_next[i] = c2 * lap + two * self.grid_curr[i] - self.grid_prev[i];
82                self.grid_next[i] *= self.params.decay;
83            }
84        }
85
86        // Boundary damping (simplified clamped edge)
87        let bound = self.params.boundary;
88        for x in 0..w {
89            let top = self.idx(x, 0);
90            let bot = self.idx(x, h - 1);
91            self.grid_next[top] *= bound;
92            self.grid_next[bot] *= bound;
93        }
94        for y in 1..(h - 1) {
95            let left = self.idx(0, y);
96            let right = self.idx(w - 1, y);
97            self.grid_next[left] *= bound;
98            self.grid_next[right] *= bound;
99        }
100
101        // Excitation injection
102        let exc_idx = self.idx(self.exc_x, self.exc_y);
103        self.grid_next[exc_idx] += input;
104
105        let output = self.grid_next[exc_idx];
106
107        // Rotate grids
108        std::mem::swap(&mut self.grid_prev, &mut self.grid_curr);
109        std::mem::swap(&mut self.grid_curr, &mut self.grid_next);
110
111        output
112    }
113}
114
115impl<T: Transcendental> Algorithm<T> for PlateModel<T> {
116    fn process(
117        &mut self,
118        input: Option<&[T]>,
119        output: &mut [T],
120    ) -> rill_core::traits::ProcessResult<()> {
121        for (i, out) in output.iter_mut().enumerate() {
122            let inp = input
123                .map(|s| s.get(i).copied().unwrap_or(T::ZERO))
124                .unwrap_or(T::ZERO);
125            *out = self.process_sample(inp);
126        }
127        Ok(())
128    }
129
130    fn reset(&mut self) {
131        self.grid_prev.fill(T::ZERO);
132        self.grid_curr.fill(T::ZERO);
133        self.grid_next.fill(T::ZERO);
134        self.input_energy = T::ZERO;
135    }
136
137    fn metadata(&self) -> AlgorithmMetadata {
138        AlgorithmMetadata {
139            name: "Plate Model",
140            category: AlgorithmCategory::Generator,
141            description: "2D FDTD waveguide mesh for plate and membrane simulation",
142            author: "Rill",
143            version: "0.5",
144        }
145    }
146}
147
148impl<T: Transcendental> ParameterizedAlgorithm<T> for PlateModel<T> {
149    type Params = PlateParams<T>;
150
151    fn params(&self) -> &Self::Params {
152        &self.params
153    }
154
155    fn set_params(&mut self, params: Self::Params) {
156        let size_changed = params.grid_width != self.params.grid_width
157            || params.grid_height != self.params.grid_height;
158        self.params = params;
159        if size_changed {
160            let size = self.params.grid_width * self.params.grid_height;
161            self.grid_prev = vec![T::ZERO; size];
162            self.grid_curr = vec![T::ZERO; size];
163            self.grid_next = vec![T::ZERO; size];
164        }
165        self.exc_x = ((self.params.excitation_x.to_f64() * (self.params.grid_width - 1) as f64)
166            .round() as usize)
167            .min(self.params.grid_width - 1);
168        self.exc_y = ((self.params.excitation_y.to_f64() * (self.params.grid_height - 1) as f64)
169            .round() as usize)
170            .min(self.params.grid_height - 1);
171    }
172
173    fn set_parameter(&mut self, name: &str, value: ParamValue) -> Result<(), &'static str> {
174        match name {
175            "wave_speed" => {
176                let mut p = self.params.clone();
177                p.wave_speed = T::from_f64(value.as_f32().map(|v| v as f64).unwrap_or(0.25));
178                self.set_params(p);
179                Ok(())
180            }
181            "decay" => {
182                let mut p = self.params.clone();
183                p.decay = T::from_f64(value.as_f32().map(|v| v as f64).unwrap_or(0.999));
184                self.set_params(p);
185                Ok(())
186            }
187            "boundary" => {
188                let mut p = self.params.clone();
189                p.boundary = T::from_f64(value.as_f32().map(|v| v as f64).unwrap_or(0.5));
190                self.set_params(p);
191                Ok(())
192            }
193            _ => Err("Unknown parameter"),
194        }
195    }
196}
197
198#[cfg(test)]
199mod tests {
200    use super::*;
201
202    #[test]
203    fn test_plate_creation() {
204        let params = PlateParams::<f64>::default();
205        let model = PlateModel::<f64>::new(params);
206        assert_eq!(model.grid_prev.len(), 16 * 16);
207    }
208
209    #[test]
210    fn test_plate_algorithm_process() {
211        let params = PlateParams::<f64>::default();
212        let mut model = PlateModel::<f64>::new(params);
213        model.strike(1.0.into());
214        let mut output = [0.0f64; 64];
215        model.process(None, &mut output).unwrap();
216        let max_abs = output.iter().map(|x| x.abs()).fold(0.0, f64::max);
217        assert!(max_abs > 0.0);
218    }
219
220    #[test]
221    fn test_plate_boundary() {
222        let mut params = PlateParams::<f64>::default();
223        params.boundary = 0.0.into();
224        params.decay = 0.95.into();
225        params.grid_width = 8;
226        params.grid_height = 8;
227        params.excitation_x = 0.5.into();
228        params.excitation_y = 0.5.into();
229        let mut model = PlateModel::<f64>::new(params);
230        model.strike(1.0.into());
231        for _ in 0..200 {
232            let mut out = [0.0f64; 1];
233            model.process(None, &mut out).unwrap();
234        }
235        let mut out = [0.0f64; 1];
236        model.process(None, &mut out).unwrap();
237        assert!(out[0].abs() < 0.01, "expected near-zero, got {}", out[0]);
238    }
239
240    #[test]
241    fn test_plate_params() {
242        let params = PlateParams::<f64>::default();
243        let mut model = PlateModel::<f64>::new(params);
244        let new_params = PlateParams {
245            wave_speed: 0.125.into(),
246            ..PlateParams::default()
247        };
248        model.set_params(new_params);
249        let ws = model.params.wave_speed.to_f64();
250        assert!((ws - 0.125).abs() < 1e-10, "expected 0.125, got {}", ws);
251    }
252
253    #[test]
254    fn test_plate_reset() {
255        let params = PlateParams::<f64>::default();
256        let mut model = PlateModel::<f64>::new(params);
257        model.strike(1.0.into());
258        model.reset();
259        let mut out = [0.0f64; 1];
260        model.process(None, &mut out).unwrap();
261        assert!((out[0].abs() - 0.0).abs() < 1e-15);
262    }
263}