Skip to main content

ringkernel_wavesim3d/simulation/
physics.rs

1//! Realistic 3D acoustic physics simulation parameters.
2//!
3//! Implements accurate sound propagation models including:
4//! - Temperature-dependent speed of sound
5//! - Humidity-dependent absorption
6//! - Frequency-dependent atmospheric damping (ISO 9613-1)
7//! - Multiple propagation media (air, water, metal)
8
9/// Physical constants for acoustic simulations.
10pub mod constants {
11    /// Speed of sound at 0°C in dry air (m/s)
12    pub const SPEED_OF_SOUND_0C: f32 = 331.3;
13
14    /// Standard atmospheric pressure (Pa)
15    pub const STANDARD_PRESSURE: f32 = 101325.0;
16
17    /// Reference temperature for calculations (°C)
18    pub const REFERENCE_TEMP_C: f32 = 20.0;
19
20    /// Molar mass of dry air (kg/mol)
21    pub const MOLAR_MASS_AIR: f32 = 0.02897;
22
23    /// Molar mass of water vapor (kg/mol)
24    pub const MOLAR_MASS_WATER: f32 = 0.01802;
25
26    /// Universal gas constant (J/(mol·K))
27    pub const GAS_CONSTANT: f32 = 8.314;
28
29    /// Average ear spacing for binaural audio (m)
30    pub const EAR_SPACING: f32 = 0.17;
31
32    /// Speed of sound in water at 20°C (m/s)
33    pub const SPEED_IN_WATER: f32 = 1481.0;
34
35    /// Speed of sound in steel (m/s)
36    pub const SPEED_IN_STEEL: f32 = 5960.0;
37
38    /// Speed of sound in aluminum (m/s)
39    pub const SPEED_IN_ALUMINUM: f32 = 6420.0;
40}
41
42/// Propagation medium types with distinct physical properties.
43#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
44pub enum Medium {
45    /// Air at given temperature and humidity
46    #[default]
47    Air,
48    /// Fresh water
49    Water,
50    /// Steel (solid metal)
51    Steel,
52    /// Aluminum (lightweight metal)
53    Aluminum,
54    /// Custom medium with user-defined properties
55    Custom,
56}
57
58/// Properties of a propagation medium.
59#[derive(Debug, Clone)]
60pub struct MediumProperties {
61    /// Speed of sound (m/s)
62    pub speed_of_sound: f32,
63    /// Density (kg/m³)
64    pub density: f32,
65    /// Base damping coefficient (linear absorption per meter)
66    pub base_damping: f32,
67    /// Whether frequency-dependent absorption applies
68    pub frequency_dependent: bool,
69    /// Characteristic impedance (Pa·s/m)
70    pub impedance: f32,
71}
72
73impl MediumProperties {
74    /// Create air properties at given temperature and humidity.
75    pub fn air(temperature_c: f32, humidity_percent: f32) -> Self {
76        let speed = speed_of_sound_in_air(temperature_c, humidity_percent);
77        // Air density varies with temperature: ρ = P·M / (R·T)
78        let temp_k = temperature_c + 273.15;
79        let density = constants::STANDARD_PRESSURE * constants::MOLAR_MASS_AIR
80            / (constants::GAS_CONSTANT * temp_k);
81
82        Self {
83            speed_of_sound: speed,
84            density,
85            base_damping: 0.0001, // Very low base damping, frequency-dependent takes over
86            frequency_dependent: true,
87            impedance: density * speed,
88        }
89    }
90
91    /// Create water properties at given temperature.
92    pub fn water(temperature_c: f32) -> Self {
93        // Speed in freshwater varies with temperature (simplified Bilaniuk-Wong formula)
94        // At 20°C, speed should be ~1481 m/s
95        // c ≈ 1402.7 + 5.0 * T - 0.055 * T² + 0.00022 * T³
96        let t = temperature_c;
97        let speed = 1402.7 + 5.0 * t - 0.055 * t * t + 0.00022 * t * t * t;
98        let density = 998.0 - 0.05 * (temperature_c - 20.0); // Approximate
99
100        Self {
101            speed_of_sound: speed,
102            density,
103            base_damping: 0.00002, // Water has very low absorption
104            frequency_dependent: true,
105            impedance: density * speed,
106        }
107    }
108
109    /// Create steel properties.
110    pub fn steel() -> Self {
111        Self {
112            speed_of_sound: constants::SPEED_IN_STEEL,
113            density: 7850.0,
114            base_damping: 0.00001, // Metals have minimal damping
115            frequency_dependent: false,
116            impedance: 7850.0 * constants::SPEED_IN_STEEL,
117        }
118    }
119
120    /// Create aluminum properties.
121    pub fn aluminum() -> Self {
122        Self {
123            speed_of_sound: constants::SPEED_IN_ALUMINUM,
124            density: 2700.0,
125            base_damping: 0.000015,
126            frequency_dependent: false,
127            impedance: 2700.0 * constants::SPEED_IN_ALUMINUM,
128        }
129    }
130
131    /// Create custom medium with specified properties.
132    pub fn custom(speed: f32, density: f32, damping: f32) -> Self {
133        Self {
134            speed_of_sound: speed,
135            density,
136            base_damping: damping,
137            frequency_dependent: false,
138            impedance: density * speed,
139        }
140    }
141}
142
143/// Environmental conditions affecting sound propagation.
144#[derive(Debug, Clone)]
145pub struct Environment {
146    /// Temperature in Celsius
147    pub temperature_c: f32,
148    /// Relative humidity (0-100%)
149    pub humidity_percent: f32,
150    /// Atmospheric pressure in Pascals
151    pub pressure_pa: f32,
152    /// Current medium type
153    pub medium: Medium,
154}
155
156impl Default for Environment {
157    fn default() -> Self {
158        Self {
159            temperature_c: 20.0,
160            humidity_percent: 50.0,
161            pressure_pa: constants::STANDARD_PRESSURE,
162            medium: Medium::Air,
163        }
164    }
165}
166
167impl Environment {
168    /// Create a new environment with air at standard conditions.
169    pub fn new() -> Self {
170        Self::default()
171    }
172
173    /// Set temperature in Celsius.
174    pub fn with_temperature(mut self, temp_c: f32) -> Self {
175        self.temperature_c = temp_c.clamp(-40.0, 60.0);
176        self
177    }
178
179    /// Set relative humidity (0-100%).
180    pub fn with_humidity(mut self, humidity: f32) -> Self {
181        self.humidity_percent = humidity.clamp(0.0, 100.0);
182        self
183    }
184
185    /// Set the propagation medium.
186    pub fn with_medium(mut self, medium: Medium) -> Self {
187        self.medium = medium;
188        self
189    }
190
191    /// Get medium properties for current environment.
192    pub fn medium_properties(&self) -> MediumProperties {
193        match self.medium {
194            Medium::Air => MediumProperties::air(self.temperature_c, self.humidity_percent),
195            Medium::Water => MediumProperties::water(self.temperature_c),
196            Medium::Steel => MediumProperties::steel(),
197            Medium::Aluminum => MediumProperties::aluminum(),
198            Medium::Custom => MediumProperties::custom(343.0, 1.2, 0.001),
199        }
200    }
201
202    /// Get the speed of sound for current conditions.
203    pub fn speed_of_sound(&self) -> f32 {
204        self.medium_properties().speed_of_sound
205    }
206}
207
208/// Calculate speed of sound in air as a function of temperature and humidity.
209///
210/// Uses the formula from Cramer (J. Acoust. Soc. Am., 1993):
211/// c = 331.3 * sqrt(1 + T/273.15) * (1 + humidity_correction)
212///
213/// The humidity effect is small (~0.5% at 100% humidity) but measurable.
214pub fn speed_of_sound_in_air(temperature_c: f32, humidity_percent: f32) -> f32 {
215    let temp_k = temperature_c + 273.15;
216
217    // Base speed from temperature (Laplace's formula)
218    let base_speed = constants::SPEED_OF_SOUND_0C * (temp_k / 273.15).sqrt();
219
220    // Humidity correction (simplified model)
221    // Humid air is less dense than dry air, so sound travels faster
222    // Effect is approximately 0.1-0.6 m/s per 10% humidity at typical temperatures
223    let saturation_pressure = saturation_vapor_pressure(temperature_c);
224    let vapor_pressure = (humidity_percent / 100.0) * saturation_pressure;
225    let molar_fraction = vapor_pressure / constants::STANDARD_PRESSURE;
226
227    // The correction factor accounts for the lower molecular weight of water vapor
228    // Dry air: ~29 g/mol, Water vapor: ~18 g/mol
229    let humidity_correction = 0.14 * molar_fraction;
230
231    base_speed * (1.0 + humidity_correction)
232}
233
234/// Calculate saturation vapor pressure using the Buck equation.
235///
236/// Valid for -40°C to 50°C range.
237pub fn saturation_vapor_pressure(temperature_c: f32) -> f32 {
238    // Buck equation (enhanced accuracy)
239    let t = temperature_c;
240    611.21 * ((18.678 - t / 234.5) * (t / (257.14 + t))).exp()
241}
242
243/// Atmospheric absorption coefficient calculation based on ISO 9613-1.
244///
245/// Returns the absorption coefficient in dB/m for a given frequency.
246/// This model accounts for:
247/// - Molecular relaxation of oxygen and nitrogen
248/// - Classical viscosity and thermal conductivity losses
249/// - Humidity effects on relaxation frequencies
250#[derive(Debug, Clone)]
251pub struct AtmosphericAbsorption {
252    /// Temperature in Celsius
253    temperature_c: f32,
254    /// Relative humidity (0-100%)
255    #[allow(dead_code)]
256    humidity_percent: f32,
257    /// Atmospheric pressure in Pascals
258    pressure_pa: f32,
259    /// Precomputed relaxation frequency for oxygen
260    fr_o: f32,
261    /// Precomputed relaxation frequency for nitrogen
262    fr_n: f32,
263}
264
265impl AtmosphericAbsorption {
266    /// Create a new absorption calculator for given conditions.
267    pub fn new(temperature_c: f32, humidity_percent: f32, pressure_pa: f32) -> Self {
268        let temp_k = temperature_c + 273.15;
269        let temp_ratio = temp_k / 293.15; // Reference temp: 20°C
270
271        // Calculate molar concentration of water vapor
272        let psat = saturation_vapor_pressure(temperature_c);
273        let h = humidity_percent / 100.0 * psat / pressure_pa;
274
275        // Relaxation frequency for oxygen (ISO 9613-1, Eq. 3)
276        let fr_o = (pressure_pa / constants::STANDARD_PRESSURE)
277            * (24.0 + 4.04e4 * h * (0.02 + h) / (0.391 + h));
278
279        // Relaxation frequency for nitrogen (ISO 9613-1, Eq. 4)
280        let fr_n = (pressure_pa / constants::STANDARD_PRESSURE)
281            * temp_ratio.powf(-0.5)
282            * (9.0 + 280.0 * h * (-4.17 * (temp_ratio.powf(-1.0 / 3.0) - 1.0)).exp());
283
284        Self {
285            temperature_c,
286            humidity_percent,
287            pressure_pa,
288            fr_o,
289            fr_n,
290        }
291    }
292
293    /// Calculate absorption coefficient in dB/m for a given frequency.
294    ///
295    /// Based on ISO 9613-1:1993 standard.
296    pub fn coefficient_db_per_meter(&self, frequency_hz: f32) -> f32 {
297        let temp_k = self.temperature_c + 273.15;
298        let temp_ratio = temp_k / 293.15;
299        let pressure_ratio = self.pressure_pa / constants::STANDARD_PRESSURE;
300
301        let f = frequency_hz;
302        let f_sq = f * f;
303
304        // Classical absorption (viscosity + thermal conductivity)
305        let alpha_classic = 1.84e-11 * pressure_ratio.recip() * temp_ratio.sqrt() * f_sq;
306
307        // Vibrational relaxation absorption for oxygen
308        let alpha_o =
309            0.01275 * (-2239.1 / temp_k).exp() * (self.fr_o + f_sq / self.fr_o).recip() * f_sq;
310
311        // Vibrational relaxation absorption for nitrogen
312        let alpha_n =
313            0.1068 * (-3352.0 / temp_k).exp() * (self.fr_n + f_sq / self.fr_n).recip() * f_sq;
314
315        // Total absorption in dB/m (convert from Np/m to dB/m: multiply by 8.686)
316        8.686 * (alpha_classic + temp_ratio.powf(-2.5) * (alpha_o + alpha_n))
317    }
318
319    /// Calculate absorption coefficient as a linear factor per meter.
320    ///
321    /// Returns a multiplier (0-1) representing energy remaining after 1 meter.
322    pub fn coefficient_linear(&self, frequency_hz: f32) -> f32 {
323        let db_per_m = self.coefficient_db_per_meter(frequency_hz);
324        10.0_f32.powf(-db_per_m / 20.0)
325    }
326
327    /// Calculate absorption for a specific distance.
328    ///
329    /// Returns the amplitude multiplier after traveling the given distance.
330    pub fn absorption_at_distance(&self, frequency_hz: f32, distance_m: f32) -> f32 {
331        let db_per_m = self.coefficient_db_per_meter(frequency_hz);
332        10.0_f32.powf(-db_per_m * distance_m / 20.0)
333    }
334}
335
336/// Multi-band damping coefficients for efficient GPU computation.
337///
338/// Pre-computes absorption for multiple frequency bands to avoid
339/// per-sample FFT computations in the main FDTD loop.
340#[derive(Debug, Clone)]
341pub struct MultiBandDamping {
342    /// Center frequencies for each band (Hz)
343    pub center_frequencies: Vec<f32>,
344    /// Damping coefficient per band (linear multiplier per time step)
345    pub damping_coefficients: Vec<f32>,
346    /// Number of frequency bands
347    pub num_bands: usize,
348}
349
350impl MultiBandDamping {
351    /// Create multi-band damping from environment conditions.
352    ///
353    /// Uses octave bands from 31.5 Hz to 16 kHz (standard ISO bands).
354    pub fn from_environment(env: &Environment, time_step: f32, _cell_size: f32) -> Self {
355        let absorption =
356            AtmosphericAbsorption::new(env.temperature_c, env.humidity_percent, env.pressure_pa);
357
358        // ISO octave band center frequencies
359        let center_frequencies: Vec<f32> = vec![
360            31.5, 63.0, 125.0, 250.0, 500.0, 1000.0, 2000.0, 4000.0, 8000.0, 16000.0,
361        ];
362
363        let props = env.medium_properties();
364
365        // Convert dB/m absorption to per-step damping
366        // Distance traveled per step = speed * dt
367        let distance_per_step = props.speed_of_sound * time_step;
368
369        let damping_coefficients: Vec<f32> = center_frequencies
370            .iter()
371            .map(|&freq| {
372                if props.frequency_dependent {
373                    // Frequency-dependent absorption
374                    let db_per_m = absorption.coefficient_db_per_meter(freq);
375                    let db_per_step = db_per_m * distance_per_step;
376                    // Convert dB to linear amplitude factor
377                    10.0_f32.powf(-db_per_step / 20.0)
378                } else {
379                    // Constant damping for metals etc.
380                    1.0 - props.base_damping * distance_per_step
381                }
382            })
383            .collect();
384
385        Self {
386            num_bands: center_frequencies.len(),
387            center_frequencies,
388            damping_coefficients,
389        }
390    }
391
392    /// Create simplified single-band damping.
393    pub fn single_band(damping_factor: f32) -> Self {
394        Self {
395            center_frequencies: vec![1000.0],
396            damping_coefficients: vec![1.0 - damping_factor],
397            num_bands: 1,
398        }
399    }
400
401    /// Get the effective damping for a dominant frequency.
402    pub fn damping_for_frequency(&self, frequency: f32) -> f32 {
403        // Find nearest band
404        let mut min_diff = f32::MAX;
405        let mut best_idx = 0;
406        for (i, &center) in self.center_frequencies.iter().enumerate() {
407            let diff = (frequency - center).abs();
408            if diff < min_diff {
409                min_diff = diff;
410                best_idx = i;
411            }
412        }
413        self.damping_coefficients[best_idx]
414    }
415
416    /// Get average damping across all bands (for simple simulations).
417    pub fn average_damping(&self) -> f32 {
418        self.damping_coefficients.iter().sum::<f32>() / self.num_bands as f32
419    }
420}
421
422/// Parameters for the 3D acoustic wave simulation.
423///
424/// Uses the FDTD (Finite-Difference Time-Domain) method for solving
425/// the 3D wave equation with realistic physics.
426#[derive(Debug, Clone)]
427pub struct AcousticParams3D {
428    /// Environment (temperature, humidity, medium)
429    pub environment: Environment,
430    /// Medium properties derived from environment
431    pub medium: MediumProperties,
432    /// Cell size (spatial step) in meters (uniform in all dimensions)
433    pub cell_size: f32,
434    /// Time step in seconds (computed for numerical stability)
435    pub time_step: f32,
436    /// Courant number squared (c² = (speed * dt / dx)²)
437    pub c_squared: f32,
438    /// Multi-band damping coefficients
439    pub damping: MultiBandDamping,
440    /// Simple average damping factor for basic FDTD (0-1)
441    pub simple_damping: f32,
442}
443
444impl Default for AcousticParams3D {
445    fn default() -> Self {
446        Self::new(Environment::default(), 0.1) // 10cm cell size
447    }
448}
449
450impl AcousticParams3D {
451    /// Create new 3D acoustic parameters from environment settings.
452    ///
453    /// The time step is automatically computed to satisfy the CFL condition
454    /// for numerical stability in 3D: dt <= dx / (c * sqrt(3))
455    ///
456    /// # Arguments
457    /// * `environment` - Environmental conditions (temperature, humidity, medium)
458    /// * `cell_size` - Spatial step size in meters (same for x, y, z)
459    pub fn new(environment: Environment, cell_size: f32) -> Self {
460        let medium = environment.medium_properties();
461        let speed = medium.speed_of_sound;
462
463        // CFL condition for 3D wave equation: c * dt / dx <= 1/sqrt(3)
464        // We use a safety factor of 2.0 instead of sqrt(3) ~= 1.732
465        let time_step = cell_size / (speed * 2.0);
466
467        // Courant number squared for FDTD update
468        let courant = speed * time_step / cell_size;
469        let c_squared = courant * courant;
470
471        // Compute frequency-dependent damping
472        let damping = MultiBandDamping::from_environment(&environment, time_step, cell_size);
473
474        // Simple damping for basic FDTD
475        let simple_damping = 1.0 - damping.average_damping().max(medium.base_damping);
476
477        Self {
478            environment,
479            medium,
480            cell_size,
481            time_step,
482            c_squared,
483            damping,
484            simple_damping: simple_damping.clamp(0.99, 0.9999),
485        }
486    }
487
488    /// Create parameters for a specific medium at standard conditions.
489    pub fn for_medium(medium: Medium, cell_size: f32) -> Self {
490        Self::new(Environment::default().with_medium(medium), cell_size)
491    }
492
493    /// Update environment and recompute derived parameters.
494    pub fn set_environment(&mut self, env: Environment) {
495        *self = Self::new(env, self.cell_size);
496    }
497
498    /// Update cell size and recompute parameters.
499    pub fn set_cell_size(&mut self, size: f32) {
500        *self = Self::new(self.environment.clone(), size);
501    }
502
503    /// Compute the Courant number (c * dt / dx).
504    ///
505    /// For numerical stability in 3D, this should be <= 1/sqrt(3) ~= 0.577
506    pub fn courant_number(&self) -> f32 {
507        self.c_squared.sqrt()
508    }
509
510    /// Check if the parameters satisfy the CFL stability condition.
511    pub fn is_stable(&self) -> bool {
512        self.courant_number() <= 1.0 / 3.0_f32.sqrt()
513    }
514
515    /// Get the wavelength for a given frequency.
516    pub fn wavelength(&self, frequency: f32) -> f32 {
517        self.medium.speed_of_sound / frequency
518    }
519
520    /// Get the number of cells per wavelength for a given frequency.
521    ///
522    /// For accurate simulation, this should be >= 10 (ideally 20+).
523    pub fn cells_per_wavelength(&self, frequency: f32) -> f32 {
524        self.wavelength(frequency) / self.cell_size
525    }
526
527    /// Get recommended maximum frequency for accurate simulation.
528    ///
529    /// Based on the Nyquist-like criterion of ~10 cells per wavelength.
530    pub fn max_accurate_frequency(&self) -> f32 {
531        self.medium.speed_of_sound / (10.0 * self.cell_size)
532    }
533}
534
535/// 3D position in the simulation space.
536#[derive(Debug, Clone, Copy, PartialEq)]
537pub struct Position3D {
538    pub x: f32,
539    pub y: f32,
540    pub z: f32,
541}
542
543impl Position3D {
544    pub fn new(x: f32, y: f32, z: f32) -> Self {
545        Self { x, y, z }
546    }
547
548    pub fn origin() -> Self {
549        Self::new(0.0, 0.0, 0.0)
550    }
551
552    /// Distance to another position.
553    pub fn distance_to(&self, other: &Position3D) -> f32 {
554        let dx = self.x - other.x;
555        let dy = self.y - other.y;
556        let dz = self.z - other.z;
557        (dx * dx + dy * dy + dz * dz).sqrt()
558    }
559
560    /// Convert to grid indices.
561    pub fn to_grid_indices(&self, cell_size: f32) -> (usize, usize, usize) {
562        (
563            (self.x / cell_size).round() as usize,
564            (self.y / cell_size).round() as usize,
565            (self.z / cell_size).round() as usize,
566        )
567    }
568}
569
570/// Orientation in 3D space (for the virtual head).
571#[derive(Debug, Clone, Copy)]
572pub struct Orientation3D {
573    /// Yaw angle in radians (rotation around Y axis)
574    pub yaw: f32,
575    /// Pitch angle in radians (rotation around X axis)
576    pub pitch: f32,
577    /// Roll angle in radians (rotation around Z axis)
578    pub roll: f32,
579}
580
581impl Default for Orientation3D {
582    fn default() -> Self {
583        Self {
584            yaw: 0.0,
585            pitch: 0.0,
586            roll: 0.0,
587        }
588    }
589}
590
591impl Orientation3D {
592    pub fn new(yaw: f32, pitch: f32, roll: f32) -> Self {
593        Self { yaw, pitch, roll }
594    }
595
596    /// Get the forward direction vector.
597    pub fn forward(&self) -> (f32, f32, f32) {
598        let cy = self.yaw.cos();
599        let sy = self.yaw.sin();
600        let cp = self.pitch.cos();
601        let sp = self.pitch.sin();
602
603        (sy * cp, -sp, cy * cp)
604    }
605
606    /// Get the right direction vector (for ear positions).
607    pub fn right(&self) -> (f32, f32, f32) {
608        let cy = self.yaw.cos();
609        let sy = self.yaw.sin();
610        let cr = self.roll.cos();
611        let sr = self.roll.sin();
612
613        (cy * cr, sr, -sy * cr)
614    }
615}
616
617#[cfg(test)]
618mod tests {
619    use super::*;
620
621    #[test]
622    fn test_speed_of_sound_temperature() {
623        // At 0°C, speed should be ~331.3 m/s
624        let speed_0c = speed_of_sound_in_air(0.0, 0.0);
625        assert!((speed_0c - 331.3).abs() < 1.0);
626
627        // At 20°C, speed should be ~343 m/s
628        let speed_20c = speed_of_sound_in_air(20.0, 0.0);
629        assert!((speed_20c - 343.0).abs() < 2.0);
630
631        // Higher temperature = faster sound
632        assert!(speed_20c > speed_0c);
633    }
634
635    #[test]
636    fn test_speed_humidity_effect() {
637        // Humidity should increase speed slightly
638        let speed_dry = speed_of_sound_in_air(20.0, 0.0);
639        let speed_humid = speed_of_sound_in_air(20.0, 100.0);
640
641        // Effect should be small but positive
642        assert!(speed_humid > speed_dry);
643        assert!(speed_humid - speed_dry < 2.0); // Less than 2 m/s difference
644    }
645
646    #[test]
647    fn test_atmospheric_absorption() {
648        let absorption = AtmosphericAbsorption::new(20.0, 50.0, constants::STANDARD_PRESSURE);
649
650        // Low frequencies should have minimal absorption
651        let low_freq = absorption.coefficient_db_per_meter(100.0);
652        assert!(low_freq < 0.01);
653
654        // High frequencies should have more absorption
655        let high_freq = absorption.coefficient_db_per_meter(8000.0);
656        assert!(high_freq > low_freq);
657
658        // Very high frequencies have significant absorption
659        let very_high = absorption.coefficient_db_per_meter(16000.0);
660        assert!(very_high > high_freq);
661    }
662
663    #[test]
664    fn test_medium_properties() {
665        let air = MediumProperties::air(20.0, 50.0);
666        assert!((air.speed_of_sound - 343.0).abs() < 5.0);
667
668        let water = MediumProperties::water(20.0);
669        assert!((water.speed_of_sound - 1481.0).abs() < 10.0);
670
671        let steel = MediumProperties::steel();
672        assert_eq!(steel.speed_of_sound, constants::SPEED_IN_STEEL);
673    }
674
675    #[test]
676    fn test_cfl_stability() {
677        let params = AcousticParams3D::default();
678        assert!(params.is_stable());
679        assert!(params.courant_number() <= 1.0 / 3.0_f32.sqrt());
680    }
681
682    #[test]
683    fn test_multiband_damping() {
684        let env = Environment::default();
685        let damping = MultiBandDamping::from_environment(&env, 0.0001, 0.01);
686
687        // Should have multiple bands
688        assert_eq!(damping.num_bands, 10);
689
690        // Higher frequencies should have more damping (lower coefficient)
691        let low_damping = damping.damping_for_frequency(100.0);
692        let high_damping = damping.damping_for_frequency(8000.0);
693        assert!(low_damping > high_damping);
694    }
695
696    #[test]
697    fn test_position_distance() {
698        let p1 = Position3D::new(0.0, 0.0, 0.0);
699        let p2 = Position3D::new(3.0, 4.0, 0.0);
700        assert!((p1.distance_to(&p2) - 5.0).abs() < 0.001);
701    }
702}