Skip to main content

ringkernel_wavesim/simulation/
physics.rs

1//! Acoustic simulation parameters and physics calculations.
2
3/// Parameters for the acoustic wave simulation.
4///
5/// Uses the FDTD (Finite-Difference Time-Domain) method for solving
6/// the 2D wave equation.
7#[derive(Debug, Clone)]
8pub struct AcousticParams {
9    /// Speed of sound in m/s (adjustable via UI slider).
10    pub speed_of_sound: f32,
11
12    /// Cell size (spatial step) in meters.
13    pub cell_size: f32,
14
15    /// Time step in seconds (computed for numerical stability).
16    pub time_step: f32,
17
18    /// Damping factor (energy loss per step, 0-1).
19    pub damping: f32,
20}
21
22impl Default for AcousticParams {
23    fn default() -> Self {
24        Self::new(343.0, 1.0)
25    }
26}
27
28impl AcousticParams {
29    /// Create new acoustic parameters.
30    ///
31    /// The time step is automatically computed to satisfy the CFL condition
32    /// for numerical stability: dt <= dx / (c * sqrt(2))
33    ///
34    /// # Arguments
35    /// * `speed_of_sound` - Speed of sound in m/s (343 m/s in air at 20°C)
36    /// * `cell_size` - Spatial step size in meters
37    pub fn new(speed_of_sound: f32, cell_size: f32) -> Self {
38        // CFL condition for 2D wave equation: c * dt / dx <= 1/sqrt(2)
39        // We use a safety factor of 1.5 instead of sqrt(2) ~= 1.414
40        let dt = cell_size / (speed_of_sound * 1.5);
41
42        Self {
43            speed_of_sound,
44            cell_size,
45            time_step: dt,
46            damping: 0.001,
47        }
48    }
49
50    /// Create parameters with custom damping.
51    pub fn with_damping(mut self, damping: f32) -> Self {
52        self.damping = damping.clamp(0.0, 1.0);
53        self
54    }
55
56    /// Compute the Courant number (c * dt / dx).
57    ///
58    /// For numerical stability in 2D, this should be <= 1/sqrt(2) ~= 0.707
59    pub fn courant_number(&self) -> f32 {
60        self.speed_of_sound * self.time_step / self.cell_size
61    }
62
63    /// Check if the parameters satisfy the CFL stability condition.
64    pub fn is_stable(&self) -> bool {
65        self.courant_number() <= 1.0 / std::f32::consts::SQRT_2
66    }
67
68    /// Update speed of sound and recompute time step for stability.
69    pub fn set_speed_of_sound(&mut self, speed: f32) {
70        self.speed_of_sound = speed.max(1.0); // Minimum speed
71        self.time_step = self.cell_size / (self.speed_of_sound * 1.5);
72    }
73
74    /// Update cell size and recompute time step for stability.
75    pub fn set_cell_size(&mut self, size: f32) {
76        self.cell_size = size.max(0.001); // Minimum 1mm
77        self.time_step = self.cell_size / (self.speed_of_sound * 1.5);
78    }
79
80    /// Get the wavelength for a given frequency.
81    pub fn wavelength(&self, frequency: f32) -> f32 {
82        self.speed_of_sound / frequency
83    }
84
85    /// Get the number of cells per wavelength for a given frequency.
86    pub fn cells_per_wavelength(&self, frequency: f32) -> f32 {
87        self.wavelength(frequency) / self.cell_size
88    }
89}
90
91#[cfg(test)]
92mod tests {
93    use super::*;
94
95    #[test]
96    fn test_default_params() {
97        let params = AcousticParams::default();
98        assert_eq!(params.speed_of_sound, 343.0);
99        assert_eq!(params.cell_size, 1.0);
100    }
101
102    #[test]
103    fn test_stability() {
104        let params = AcousticParams::new(343.0, 1.0);
105        assert!(params.is_stable(), "Default parameters should be stable");
106        assert!(
107            params.courant_number() <= 1.0 / std::f32::consts::SQRT_2,
108            "Courant number should satisfy CFL condition"
109        );
110    }
111
112    #[test]
113    fn test_speed_change() {
114        let mut params = AcousticParams::new(343.0, 1.0);
115        params.set_speed_of_sound(1000.0);
116        assert_eq!(params.speed_of_sound, 1000.0);
117        assert!(
118            params.is_stable(),
119            "Should remain stable after speed change"
120        );
121    }
122
123    #[test]
124    fn test_wavelength() {
125        let params = AcousticParams::new(343.0, 1.0);
126        // At 343 Hz, wavelength should be ~1 meter
127        let wavelength = params.wavelength(343.0);
128        assert!((wavelength - 1.0).abs() < 0.01);
129    }
130
131    #[test]
132    fn test_courant_number() {
133        let params = AcousticParams::new(343.0, 1.0);
134        let c = params.courant_number();
135        // With safety factor of 1.5, courant number should be 1/1.5 ~= 0.667
136        assert!((c - 1.0 / 1.5).abs() < 0.01);
137    }
138}