Skip to main content

ringkernel_wavesim/simulation/
grid.rs

1//! Grid management for the acoustic wave simulation.
2//!
3//! Uses Structure of Arrays (SoA) layout for cache-friendly memory access
4//! and SIMD-ready data organization.
5
6use super::AcousticParams;
7use rayon::prelude::*;
8
9/// Cell type for drawing mode.
10///
11/// Determines how a cell interacts with the wave simulation.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
13pub enum CellType {
14    /// Normal cell - wave propagates freely.
15    #[default]
16    Normal,
17    /// Absorber - fully absorbs waves (0% reflection).
18    Absorber,
19    /// Reflector - fully reflects waves (100% reflection, like a wall).
20    Reflector,
21}
22
23/// The main simulation grid containing all cells.
24///
25/// Uses SoA (Structure of Arrays) memory layout for cache-friendly
26/// sequential access and SIMD compatibility. Pressure arrays are
27/// double-buffered for in-place FDTD time-stepping.
28pub struct SimulationGrid {
29    /// Grid width (number of columns).
30    pub width: u32,
31    /// Grid height (number of rows).
32    pub height: u32,
33
34    /// Current pressure values (width * height, row-major order).
35    pressure: Vec<f32>,
36    /// Previous pressure values for time-stepping.
37    pressure_prev: Vec<f32>,
38
39    /// Boundary cell flags (true if cell is on grid boundary).
40    is_boundary: Vec<bool>,
41    /// Reflection coefficients per cell (0 = absorb, 1 = reflect).
42    reflection_coeff: Vec<f32>,
43    /// Cell types for drawing mode (Normal, Absorber, Reflector).
44    cell_types: Vec<CellType>,
45
46    /// Acoustic simulation parameters.
47    pub params: AcousticParams,
48}
49
50impl SimulationGrid {
51    /// Create a new simulation grid.
52    ///
53    /// # Arguments
54    /// * `width` - Number of columns
55    /// * `height` - Number of rows
56    /// * `params` - Acoustic simulation parameters
57    pub fn new(width: u32, height: u32, params: AcousticParams) -> Self {
58        let size = (width * height) as usize;
59
60        let mut grid = Self {
61            width,
62            height,
63            pressure: vec![0.0; size],
64            pressure_prev: vec![0.0; size],
65            is_boundary: vec![false; size],
66            reflection_coeff: vec![1.0; size],
67            cell_types: vec![CellType::Normal; size],
68            params,
69        };
70        grid.initialize_boundaries();
71        grid
72    }
73
74    /// Initialize boundary flags and reflection coefficients.
75    fn initialize_boundaries(&mut self) {
76        let width = self.width as usize;
77        let height = self.height as usize;
78
79        for y in 0..height {
80            for x in 0..width {
81                let idx = y * width + x;
82                let is_boundary = x == 0 || y == 0 || x == width - 1 || y == height - 1;
83                self.is_boundary[idx] = is_boundary;
84                self.reflection_coeff[idx] = if is_boundary { 0.95 } else { 1.0 };
85            }
86        }
87    }
88
89    /// Convert (x, y) coordinates to linear index.
90    #[inline(always)]
91    fn idx(&self, x: u32, y: u32) -> usize {
92        (y as usize) * (self.width as usize) + (x as usize)
93    }
94
95    /// Perform one simulation step using FDTD scheme.
96    ///
97    /// Uses the 2D wave equation: ∂²p/∂t² = c²∇²p
98    ///
99    /// Discretized as:
100    /// p[n+1] = 2*p[n] - p[n-1] + c²*(p_N + p_S + p_E + p_W - 4*p)
101    pub fn step(&mut self) {
102        let width = self.width as usize;
103        let height = self.height as usize;
104        let c2 = self.params.courant_number().powi(2);
105        let damping = 1.0 - self.params.damping;
106
107        // Process interior cells in parallel using rayon
108        self.step_interior_parallel(width, height, c2, damping);
109
110        // Process boundary cells with reflection (sequential, small workload)
111        self.step_boundaries(c2, damping);
112
113        // Swap buffers
114        std::mem::swap(&mut self.pressure, &mut self.pressure_prev);
115    }
116
117    /// Process interior cells - uses parallel or sequential based on grid size.
118    ///
119    /// For small grids, sequential processing is faster due to lower overhead.
120    /// For larger grids (>=512), parallel processing provides benefits.
121    #[inline]
122    fn step_interior_parallel(&mut self, width: usize, height: usize, c2: f32, damping: f32) {
123        // Threshold: only use parallel for grids with enough work per thread
124        // 512x512 = 262K cells, good balance of work vs overhead
125        const PARALLEL_THRESHOLD: usize = 512;
126
127        if width >= PARALLEL_THRESHOLD || height >= PARALLEL_THRESHOLD {
128            self.step_interior_parallel_impl(width, height, c2, damping);
129        } else {
130            self.step_interior_sequential(width, height, c2, damping);
131        }
132    }
133
134    /// Sequential implementation for interior cells (faster for small grids).
135    #[inline]
136    fn step_interior_sequential(&mut self, width: usize, height: usize, c2: f32, damping: f32) {
137        for y in 1..height - 1 {
138            let row_start = y * width;
139
140            #[cfg(feature = "simd")]
141            {
142                Self::fdtd_row_simd_inline(
143                    &self.pressure,
144                    &mut self.pressure_prev[row_start..row_start + width],
145                    width,
146                    row_start,
147                    c2,
148                    damping,
149                );
150            }
151
152            #[cfg(not(feature = "simd"))]
153            {
154                for x in 1..width - 1 {
155                    let idx = row_start + x;
156
157                    let p_curr = self.pressure[idx];
158                    let p_prev = self.pressure_prev[idx];
159                    let p_north = self.pressure[idx - width];
160                    let p_south = self.pressure[idx + width];
161                    let p_west = self.pressure[idx - 1];
162                    let p_east = self.pressure[idx + 1];
163
164                    let laplacian = p_north + p_south + p_east + p_west - 4.0 * p_curr;
165                    let p_new = 2.0 * p_curr - p_prev + c2 * laplacian;
166
167                    self.pressure_prev[idx] = p_new * damping;
168                }
169            }
170        }
171    }
172
173    /// Parallel implementation for interior cells using rayon.
174    ///
175    /// Each row is processed independently in parallel.
176    /// Uses SIMD for intra-row processing when available.
177    #[inline]
178    fn step_interior_parallel_impl(&mut self, width: usize, height: usize, c2: f32, damping: f32) {
179        // Shared reference to pressure (read-only)
180        let pressure = &self.pressure;
181
182        // Split pressure_prev into rows for parallel mutable access
183        // We skip row 0 (boundary) and process interior rows only
184        let interior_start = width; // Skip first row
185        let interior_end = (height - 1) * width; // Skip last row
186
187        // Get mutable slice of interior rows in pressure_prev
188        let pressure_prev_interior = &mut self.pressure_prev[interior_start..interior_end];
189
190        // Process rows in parallel using par_chunks_mut
191        pressure_prev_interior
192            .par_chunks_mut(width)
193            .enumerate()
194            .for_each(|(row_offset, prev_row)| {
195                let y = row_offset + 1; // Actual y coordinate (offset by 1 for skipped boundary)
196                let row_start = y * width;
197
198                #[cfg(feature = "simd")]
199                {
200                    // SIMD version - process 8 cells at a time
201                    Self::fdtd_row_simd_inline(pressure, prev_row, width, row_start, c2, damping);
202                }
203
204                #[cfg(not(feature = "simd"))]
205                {
206                    // Scalar version
207                    for x in 1..width - 1 {
208                        let idx = row_start + x;
209                        let local_idx = x; // Index within prev_row
210
211                        let p_curr = pressure[idx];
212                        let p_prev = prev_row[local_idx];
213                        let p_north = pressure[idx - width];
214                        let p_south = pressure[idx + width];
215                        let p_west = pressure[idx - 1];
216                        let p_east = pressure[idx + 1];
217
218                        let laplacian = p_north + p_south + p_east + p_west - 4.0 * p_curr;
219                        let p_new = 2.0 * p_curr - p_prev + c2 * laplacian;
220
221                        prev_row[local_idx] = p_new * damping;
222                    }
223                }
224            });
225    }
226
227    /// SIMD implementation for a single row (inlined for parallel use).
228    #[cfg(feature = "simd")]
229    #[inline]
230    fn fdtd_row_simd_inline(
231        pressure: &[f32],
232        prev_row: &mut [f32],
233        width: usize,
234        row_start: usize,
235        c2: f32,
236        damping: f32,
237    ) {
238        use std::simd::f32x8;
239
240        let c2_vec = f32x8::splat(c2);
241        let damping_vec = f32x8::splat(damping);
242        let four = f32x8::splat(4.0);
243        let two = f32x8::splat(2.0);
244
245        let mut x = 1;
246        while x + 8 < width {
247            let idx = row_start + x;
248            let local_idx = x;
249
250            // Load vectors
251            let p_curr = f32x8::from_slice(&pressure[idx..idx + 8]);
252            let p_prev = f32x8::from_slice(&prev_row[local_idx..local_idx + 8]);
253            let p_north = f32x8::from_slice(&pressure[idx - width..idx - width + 8]);
254            let p_south = f32x8::from_slice(&pressure[idx + width..idx + width + 8]);
255            let p_west = f32x8::from_slice(&pressure[idx - 1..idx - 1 + 8]);
256            let p_east = f32x8::from_slice(&pressure[idx + 1..idx + 1 + 8]);
257
258            // FDTD computation
259            let laplacian = p_north + p_south + p_east + p_west - four * p_curr;
260            let p_new = two * p_curr - p_prev + c2_vec * laplacian;
261            let p_damped = p_new * damping_vec;
262
263            // Store result
264            prev_row[local_idx..local_idx + 8].copy_from_slice(&p_damped.to_array());
265
266            x += 8;
267        }
268
269        // Scalar fallback for remaining cells
270        while x < width - 1 {
271            let idx = row_start + x;
272            let local_idx = x;
273
274            let p_curr = pressure[idx];
275            let p_prev = prev_row[local_idx];
276            let p_north = pressure[idx - width];
277            let p_south = pressure[idx + width];
278            let p_west = pressure[idx - 1];
279            let p_east = pressure[idx + 1];
280
281            let laplacian = p_north + p_south + p_east + p_west - 4.0 * p_curr;
282            let p_new = 2.0 * p_curr - p_prev + c2 * laplacian;
283
284            prev_row[local_idx] = p_new * damping;
285            x += 1;
286        }
287    }
288
289    /// Process boundary cells with reflection boundary conditions.
290    fn step_boundaries(&mut self, c2: f32, damping: f32) {
291        let width = self.width as usize;
292        let height = self.height as usize;
293
294        // Top and bottom rows
295        for x in 0..width {
296            // Top row (y = 0)
297            let idx = x;
298            let p_curr = self.pressure[idx];
299            let p_prev = self.pressure_prev[idx];
300            let refl = self.reflection_coeff[idx];
301
302            let p_south = self.pressure[idx + width];
303            let p_west = if x > 0 {
304                self.pressure[idx - 1]
305            } else {
306                p_curr * refl
307            };
308            let p_east = if x < width - 1 {
309                self.pressure[idx + 1]
310            } else {
311                p_curr * refl
312            };
313            let p_north = p_curr * refl; // Reflect at boundary
314
315            let laplacian = p_north + p_south + p_east + p_west - 4.0 * p_curr;
316            let p_new = 2.0 * p_curr - p_prev + c2 * laplacian;
317            self.pressure_prev[idx] = p_new * damping * refl;
318
319            // Bottom row (y = height - 1)
320            let idx = (height - 1) * width + x;
321            let p_curr = self.pressure[idx];
322            let p_prev = self.pressure_prev[idx];
323            let refl = self.reflection_coeff[idx];
324
325            let p_north = self.pressure[idx - width];
326            let p_west = if x > 0 {
327                self.pressure[idx - 1]
328            } else {
329                p_curr * refl
330            };
331            let p_east = if x < width - 1 {
332                self.pressure[idx + 1]
333            } else {
334                p_curr * refl
335            };
336            let p_south = p_curr * refl; // Reflect at boundary
337
338            let laplacian = p_north + p_south + p_east + p_west - 4.0 * p_curr;
339            let p_new = 2.0 * p_curr - p_prev + c2 * laplacian;
340            self.pressure_prev[idx] = p_new * damping * refl;
341        }
342
343        // Left and right columns (excluding corners already processed)
344        for y in 1..height - 1 {
345            // Left column (x = 0)
346            let idx = y * width;
347            let p_curr = self.pressure[idx];
348            let p_prev = self.pressure_prev[idx];
349            let refl = self.reflection_coeff[idx];
350
351            let p_north = self.pressure[idx - width];
352            let p_south = self.pressure[idx + width];
353            let p_east = self.pressure[idx + 1];
354            let p_west = p_curr * refl; // Reflect at boundary
355
356            let laplacian = p_north + p_south + p_east + p_west - 4.0 * p_curr;
357            let p_new = 2.0 * p_curr - p_prev + c2 * laplacian;
358            self.pressure_prev[idx] = p_new * damping * refl;
359
360            // Right column (x = width - 1)
361            let idx = y * width + width - 1;
362            let p_curr = self.pressure[idx];
363            let p_prev = self.pressure_prev[idx];
364            let refl = self.reflection_coeff[idx];
365
366            let p_north = self.pressure[idx - width];
367            let p_south = self.pressure[idx + width];
368            let p_west = self.pressure[idx - 1];
369            let p_east = p_curr * refl; // Reflect at boundary
370
371            let laplacian = p_north + p_south + p_east + p_west - 4.0 * p_curr;
372            let p_new = 2.0 * p_curr - p_prev + c2 * laplacian;
373            self.pressure_prev[idx] = p_new * damping * refl;
374        }
375    }
376
377    /// Inject an impulse at the given grid position.
378    pub fn inject_impulse(&mut self, x: u32, y: u32, amplitude: f32) {
379        if x < self.width && y < self.height {
380            let idx = self.idx(x, y);
381            self.pressure[idx] += amplitude;
382        }
383    }
384
385    /// Get the pressure grid as a 2D vector for visualization.
386    ///
387    /// Returns a Vec of rows, where each row is a Vec of pressure values.
388    pub fn get_pressure_grid(&self) -> Vec<Vec<f32>> {
389        let width = self.width as usize;
390        let height = self.height as usize;
391        let mut grid = vec![vec![0.0; width]; height];
392
393        for (y, row) in grid.iter_mut().enumerate().take(height) {
394            for (x, cell) in row.iter_mut().enumerate().take(width) {
395                *cell = self.pressure[y * width + x];
396            }
397        }
398        grid
399    }
400
401    /// Get the pressure buffer as a flat slice (row-major order).
402    ///
403    /// This is more efficient than `get_pressure_grid()` for bulk operations.
404    #[inline]
405    pub fn pressure_slice(&self) -> &[f32] {
406        &self.pressure
407    }
408
409    /// Get the maximum absolute pressure in the grid.
410    pub fn max_pressure(&self) -> f32 {
411        self.pressure.iter().map(|p| p.abs()).fold(0.0, f32::max)
412    }
413
414    /// Get total energy in the system (sum of squared pressures).
415    pub fn total_energy(&self) -> f32 {
416        self.pressure.iter().map(|p| p * p).sum()
417    }
418
419    /// Reset all cells to initial state.
420    pub fn reset(&mut self) {
421        self.pressure.fill(0.0);
422        self.pressure_prev.fill(0.0);
423    }
424
425    /// Resize the grid.
426    pub fn resize(&mut self, new_width: u32, new_height: u32) {
427        let size = (new_width * new_height) as usize;
428        self.width = new_width;
429        self.height = new_height;
430        self.pressure = vec![0.0; size];
431        self.pressure_prev = vec![0.0; size];
432        self.is_boundary = vec![false; size];
433        self.reflection_coeff = vec![1.0; size];
434        self.cell_types = vec![CellType::Normal; size];
435        self.initialize_boundaries();
436    }
437
438    /// Update acoustic parameters.
439    pub fn set_params(&mut self, params: AcousticParams) {
440        self.params = params;
441    }
442
443    /// Update speed of sound.
444    pub fn set_speed_of_sound(&mut self, speed: f32) {
445        self.params.set_speed_of_sound(speed);
446    }
447
448    /// Update cell size in meters.
449    pub fn set_cell_size(&mut self, size: f32) {
450        self.params.set_cell_size(size);
451    }
452
453    /// Get the number of cells in the grid.
454    pub fn cell_count(&self) -> usize {
455        self.pressure.len()
456    }
457
458    /// Get pressure at a specific cell (for compatibility with old API).
459    pub fn get_pressure(&self, x: u32, y: u32) -> Option<f32> {
460        if x < self.width && y < self.height {
461            Some(self.pressure[self.idx(x, y)])
462        } else {
463            None
464        }
465    }
466
467    /// Check if a cell is a boundary.
468    pub fn is_boundary(&self, x: u32, y: u32) -> bool {
469        if x < self.width && y < self.height {
470            self.is_boundary[self.idx(x, y)]
471        } else {
472            false
473        }
474    }
475
476    /// Set the cell type at the given position.
477    ///
478    /// This affects how the cell interacts with waves:
479    /// - Normal: waves propagate freely
480    /// - Absorber: waves are absorbed (0% reflection)
481    /// - Reflector: waves are reflected (100% reflection)
482    pub fn set_cell_type(&mut self, x: u32, y: u32, cell_type: CellType) {
483        if x < self.width && y < self.height {
484            let idx = self.idx(x, y);
485            self.cell_types[idx] = cell_type;
486
487            // Update reflection coefficient based on cell type
488            self.reflection_coeff[idx] = match cell_type {
489                CellType::Normal => {
490                    // Normal cells at boundary get slight absorption, interior gets full reflection
491                    if self.is_boundary[idx] {
492                        0.95
493                    } else {
494                        1.0
495                    }
496                }
497                CellType::Absorber => 0.0,  // Full absorption
498                CellType::Reflector => 1.0, // Full reflection
499            };
500
501            // Mark reflectors/absorbers as boundary-like for FDTD calculation
502            // (they need special handling in the step function)
503            if cell_type != CellType::Normal {
504                self.is_boundary[idx] = true;
505            } else {
506                // Restore original boundary status
507                let is_edge = x == 0 || y == 0 || x == self.width - 1 || y == self.height - 1;
508                self.is_boundary[idx] = is_edge;
509            }
510        }
511    }
512
513    /// Get the cell type at the given position.
514    pub fn get_cell_type(&self, x: u32, y: u32) -> Option<CellType> {
515        if x < self.width && y < self.height {
516            Some(self.cell_types[self.idx(x, y)])
517        } else {
518            None
519        }
520    }
521
522    /// Get all cell types as a flat slice (row-major order).
523    pub fn cell_types_slice(&self) -> &[CellType] {
524        &self.cell_types
525    }
526
527    /// Clear all user-defined cell types (reset to Normal).
528    ///
529    /// This keeps the grid boundaries intact.
530    pub fn clear_cell_types(&mut self) {
531        for i in 0..self.cell_types.len() {
532            self.cell_types[i] = CellType::Normal;
533        }
534        // Re-initialize boundary reflection coefficients
535        self.initialize_boundaries();
536    }
537
538    /// Get mutable access to pressure buffers for educational mode processing.
539    ///
540    /// Returns (pressure, pressure_prev, width, height, c2, damping)
541    pub fn get_buffers_mut(&mut self) -> (&mut Vec<f32>, &mut Vec<f32>, usize, usize, f32, f32) {
542        let width = self.width as usize;
543        let height = self.height as usize;
544        let c2 = self.params.courant_number().powi(2);
545        let damping = 1.0 - self.params.damping;
546        (
547            &mut self.pressure,
548            &mut self.pressure_prev,
549            width,
550            height,
551            c2,
552            damping,
553        )
554    }
555
556    /// Swap the pressure buffers (called when educational step completes).
557    pub fn swap_buffers(&mut self) {
558        std::mem::swap(&mut self.pressure, &mut self.pressure_prev);
559    }
560}
561
562#[cfg(test)]
563mod tests {
564    use super::*;
565
566    #[test]
567    fn test_grid_creation() {
568        let params = AcousticParams::new(343.0, 1.0);
569        let grid = SimulationGrid::new(8, 8, params);
570
571        assert_eq!(grid.width, 8);
572        assert_eq!(grid.height, 8);
573        assert_eq!(grid.cell_count(), 64);
574    }
575
576    #[test]
577    fn test_boundary_cells() {
578        let params = AcousticParams::new(343.0, 1.0);
579        let grid = SimulationGrid::new(4, 4, params);
580
581        // Corner cells should be boundaries
582        assert!(grid.is_boundary(0, 0));
583        assert!(grid.is_boundary(3, 3));
584
585        // Edge cells should be boundaries
586        assert!(grid.is_boundary(1, 0));
587        assert!(grid.is_boundary(0, 1));
588
589        // Interior cells should not be boundaries
590        assert!(!grid.is_boundary(1, 1));
591        assert!(!grid.is_boundary(2, 2));
592    }
593
594    #[test]
595    fn test_impulse_injection() {
596        let params = AcousticParams::new(343.0, 1.0);
597        let mut grid = SimulationGrid::new(8, 8, params);
598
599        grid.inject_impulse(4, 4, 1.0);
600
601        assert_eq!(grid.get_pressure(4, 4), Some(1.0));
602    }
603
604    #[test]
605    fn test_wave_propagation() {
606        let params = AcousticParams::new(343.0, 1.0);
607        let mut grid = SimulationGrid::new(8, 8, params);
608
609        // Inject impulse at center
610        grid.inject_impulse(4, 4, 1.0);
611
612        // Run several steps
613        for _ in 0..10 {
614            grid.step();
615        }
616
617        // Wave should have propagated to neighbors
618        let neighbor_pressure = grid.get_pressure(4, 5).unwrap();
619        assert!(
620            neighbor_pressure.abs() > 0.0,
621            "Wave should have propagated to neighbor"
622        );
623    }
624
625    #[test]
626    fn test_pressure_grid() {
627        let params = AcousticParams::new(343.0, 1.0);
628        let mut grid = SimulationGrid::new(4, 4, params);
629
630        grid.inject_impulse(2, 2, 0.5);
631
632        let pressure_grid = grid.get_pressure_grid();
633        assert_eq!(pressure_grid.len(), 4);
634        assert_eq!(pressure_grid[0].len(), 4);
635        assert_eq!(pressure_grid[2][2], 0.5);
636    }
637
638    #[test]
639    fn test_reset() {
640        let params = AcousticParams::new(343.0, 1.0);
641        let mut grid = SimulationGrid::new(4, 4, params);
642
643        grid.inject_impulse(2, 2, 1.0);
644        grid.step();
645
646        grid.reset();
647
648        for y in 0..4 {
649            for x in 0..4 {
650                assert_eq!(grid.get_pressure(x, y), Some(0.0));
651            }
652        }
653    }
654
655    #[test]
656    fn test_resize() {
657        let params = AcousticParams::new(343.0, 1.0);
658        let mut grid = SimulationGrid::new(4, 4, params);
659
660        grid.resize(8, 6);
661
662        assert_eq!(grid.width, 8);
663        assert_eq!(grid.height, 6);
664        assert_eq!(grid.cell_count(), 48);
665    }
666
667    #[test]
668    fn test_energy_conservation() {
669        let params = AcousticParams::new(343.0, 1.0).with_damping(0.0);
670        let mut grid = SimulationGrid::new(32, 32, params);
671
672        // Set boundaries to full reflection for this test
673        for i in 0..grid.reflection_coeff.len() {
674            grid.reflection_coeff[i] = 1.0;
675        }
676
677        grid.inject_impulse(16, 16, 1.0);
678        let initial_energy = grid.total_energy();
679
680        // Run simulation (short duration to avoid numerical issues)
681        for _ in 0..20 {
682            grid.step();
683        }
684
685        let final_energy = grid.total_energy();
686
687        // With no damping and full reflection, energy should be well conserved
688        // (allowing 20% loss for numerical dissipation)
689        assert!(
690            final_energy > 0.8 * initial_energy,
691            "Energy should be roughly conserved: initial={}, final={}",
692            initial_energy,
693            final_energy
694        );
695    }
696
697    #[test]
698    fn test_pressure_slice() {
699        let params = AcousticParams::new(343.0, 1.0);
700        let mut grid = SimulationGrid::new(4, 4, params);
701
702        grid.inject_impulse(2, 2, 0.5);
703
704        let slice = grid.pressure_slice();
705        assert_eq!(slice.len(), 16);
706        // Cell (2, 2) is at index 2*4 + 2 = 10
707        assert_eq!(slice[10], 0.5);
708    }
709}