Skip to main content

ringkernel_wavesim/simulation/
cell.rs

1//! Cell state for a single grid element in the acoustic simulation.
2
3use super::AcousticParams;
4
5/// Direction to a neighboring cell.
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub enum Direction {
8    North,
9    South,
10    East,
11    West,
12}
13
14impl Direction {
15    /// All four cardinal directions.
16    pub const ALL: [Direction; 4] = [
17        Direction::North,
18        Direction::South,
19        Direction::East,
20        Direction::West,
21    ];
22
23    /// Get the opposite direction.
24    pub fn opposite(self) -> Direction {
25        match self {
26            Direction::North => Direction::South,
27            Direction::South => Direction::North,
28            Direction::East => Direction::West,
29            Direction::West => Direction::East,
30        }
31    }
32}
33
34/// State for a single cell in the acoustic grid.
35///
36/// Each cell maintains pressure values and communicates with 4 neighbors
37/// (North, South, East, West) to propagate acoustic waves.
38#[derive(Debug, Clone)]
39pub struct CellState {
40    /// X coordinate in the grid.
41    pub x: u32,
42    /// Y coordinate in the grid.
43    pub y: u32,
44
45    /// Current pressure value.
46    pub pressure: f32,
47    /// Previous pressure value (for time-stepping).
48    pub pressure_prev: f32,
49
50    /// Neighbor pressures (received via neighbor exchange).
51    pub p_north: f32,
52    pub p_south: f32,
53    pub p_east: f32,
54    pub p_west: f32,
55
56    /// True if this cell is on the grid boundary.
57    pub is_boundary: bool,
58    /// Reflection coefficient (0 = full absorb, 1 = full reflect).
59    pub reflection_coeff: f32,
60}
61
62impl CellState {
63    /// Create a new cell at the given grid position.
64    pub fn new(x: u32, y: u32) -> Self {
65        Self {
66            x,
67            y,
68            pressure: 0.0,
69            pressure_prev: 0.0,
70            p_north: 0.0,
71            p_south: 0.0,
72            p_east: 0.0,
73            p_west: 0.0,
74            is_boundary: false,
75            reflection_coeff: 1.0,
76        }
77    }
78
79    /// Get the kernel ID for this cell.
80    pub fn kernel_id(&self) -> String {
81        format!("cell_{}_{}", self.x, self.y)
82    }
83
84    /// Get the kernel ID of a neighbor in the given direction.
85    ///
86    /// Returns None if the neighbor would be outside the grid bounds.
87    pub fn neighbor_id(&self, dir: Direction, width: u32, height: u32) -> Option<String> {
88        let (nx, ny) = match dir {
89            Direction::North if self.y > 0 => (self.x, self.y - 1),
90            Direction::South if self.y < height - 1 => (self.x, self.y + 1),
91            Direction::West if self.x > 0 => (self.x - 1, self.y),
92            Direction::East if self.x < width - 1 => (self.x + 1, self.y),
93            _ => return None,
94        };
95        Some(format!("cell_{}_{}", nx, ny))
96    }
97
98    /// Compute the next pressure value using FDTD scheme.
99    ///
100    /// Uses the 2D wave equation: ∂²p/∂t² = c²∇²p
101    ///
102    /// Discretized as:
103    /// p[n+1] = 2*p[n] - p[n-1] + c²*(p_N + p_S + p_E + p_W - 4*p)
104    pub fn step(&mut self, params: &AcousticParams) {
105        let c = params.courant_number();
106        let c2 = c * c;
107
108        // 2D Laplacian approximation
109        let laplacian =
110            self.p_north + self.p_south + self.p_east + self.p_west - 4.0 * self.pressure;
111
112        // FDTD time-stepping
113        let p_new = 2.0 * self.pressure - self.pressure_prev + c2 * laplacian;
114
115        // Apply damping to simulate energy loss
116        let p_damped = p_new * (1.0 - params.damping);
117
118        // Apply boundary conditions
119        let p_final = if self.is_boundary {
120            p_damped * self.reflection_coeff
121        } else {
122            p_damped
123        };
124
125        // Update state
126        self.pressure_prev = self.pressure;
127        self.pressure = p_final;
128    }
129
130    /// Inject an impulse (Dirac delta) at this cell.
131    pub fn inject_impulse(&mut self, amplitude: f32) {
132        self.pressure += amplitude;
133    }
134
135    /// Reset the cell to initial state.
136    pub fn reset(&mut self) {
137        self.pressure = 0.0;
138        self.pressure_prev = 0.0;
139        self.p_north = 0.0;
140        self.p_south = 0.0;
141        self.p_east = 0.0;
142        self.p_west = 0.0;
143    }
144}
145
146#[cfg(test)]
147mod tests {
148    use super::*;
149
150    #[test]
151    fn test_cell_state_new() {
152        let cell = CellState::new(5, 3);
153        assert_eq!(cell.x, 5);
154        assert_eq!(cell.y, 3);
155        assert_eq!(cell.pressure, 0.0);
156        assert!(!cell.is_boundary);
157    }
158
159    #[test]
160    fn test_kernel_id() {
161        let cell = CellState::new(5, 3);
162        assert_eq!(cell.kernel_id(), "cell_5_3");
163    }
164
165    #[test]
166    fn test_neighbor_id() {
167        let cell = CellState::new(5, 5);
168        assert_eq!(
169            cell.neighbor_id(Direction::North, 10, 10),
170            Some("cell_5_4".to_string())
171        );
172        assert_eq!(
173            cell.neighbor_id(Direction::South, 10, 10),
174            Some("cell_5_6".to_string())
175        );
176        assert_eq!(
177            cell.neighbor_id(Direction::East, 10, 10),
178            Some("cell_6_5".to_string())
179        );
180        assert_eq!(
181            cell.neighbor_id(Direction::West, 10, 10),
182            Some("cell_4_5".to_string())
183        );
184    }
185
186    #[test]
187    fn test_neighbor_id_at_boundary() {
188        let cell = CellState::new(0, 0);
189        assert_eq!(cell.neighbor_id(Direction::North, 10, 10), None);
190        assert_eq!(cell.neighbor_id(Direction::West, 10, 10), None);
191        assert!(cell.neighbor_id(Direction::South, 10, 10).is_some());
192        assert!(cell.neighbor_id(Direction::East, 10, 10).is_some());
193    }
194
195    #[test]
196    fn test_impulse_injection() {
197        let mut cell = CellState::new(0, 0);
198        cell.inject_impulse(0.5);
199        assert_eq!(cell.pressure, 0.5);
200        cell.inject_impulse(0.3);
201        assert_eq!(cell.pressure, 0.8);
202    }
203
204    #[test]
205    fn test_reset() {
206        let mut cell = CellState::new(0, 0);
207        cell.pressure = 1.0;
208        cell.pressure_prev = 0.5;
209        cell.p_north = 0.2;
210        cell.reset();
211        assert_eq!(cell.pressure, 0.0);
212        assert_eq!(cell.pressure_prev, 0.0);
213        assert_eq!(cell.p_north, 0.0);
214    }
215}