1use std::f64::consts::PI;
6
7use super::types::WeightingFilter;
8
9#[allow(dead_code)]
13pub fn reflection_coefficient(z1: f64, z2: f64) -> f64 {
14 (z2 - z1) / (z2 + z1)
15}
16#[allow(dead_code)]
20pub fn transmission_coefficient(z1: f64, z2: f64) -> f64 {
21 2.0 * z2 / (z2 + z1)
22}
23#[allow(dead_code)]
28pub fn transmission_loss_db(z1: f64, z2: f64) -> f64 {
29 let tau = 4.0 * z1 * z2 / ((z1 + z2) * (z1 + z2));
30 -10.0 * tau.log10()
31}
32#[allow(dead_code)]
39pub fn multilayer_transmission(impedances: &[f64], thicknesses: &[f64], freq: f64) -> f64 {
40 let n = impedances.len();
41 if n < 2 {
42 return 1.0;
43 }
44 let n_layers = n - 2;
45 let mut m11 = 1.0_f64;
46 let mut m12 = 0.0_f64;
47 let mut m21 = 0.0_f64;
48 let mut m22 = 1.0_f64;
49 for i in 0..n_layers {
50 let z = impedances[i + 1];
51 let omega = 2.0 * PI * freq;
52 let c = z / 1.2;
53 let k = omega / c;
54 let d = thicknesses[i];
55 let kd = k * d;
56 let cos_kd = kd.cos();
57 let sin_kd = kd.sin();
58 let a11 = cos_kd * m11 + (z * sin_kd) * m21;
59 let a12 = cos_kd * m12 + (z * sin_kd) * m22;
60 let a21 = (sin_kd / z) * m11 + cos_kd * m21;
61 let a22 = (sin_kd / z) * m12 + cos_kd * m22;
62 m11 = a11;
63 m12 = a12;
64 m21 = a21;
65 m22 = a22;
66 }
67 let z1 = impedances[0];
68 let z2 = impedances[n - 1];
69 let denom = m11 + m12 / z2 + z1 * m21 + z1 / z2 * m22;
70 if denom.abs() < f64::EPSILON {
71 return 0.0;
72 }
73 let t_amp = 2.0 / denom;
74 t_amp * t_amp
75}
76#[allow(dead_code)]
80pub fn viscous_attenuation(freq: f64, viscosity: f64, density: f64, c: f64) -> f64 {
81 let omega = 2.0 * PI * freq;
82 2.0 * viscosity * omega * omega / (3.0 * density * c * c * c)
83}
84#[allow(dead_code)]
89pub fn atmospheric_attenuation_db_per_m(freq: f64, temp_c: f64, humidity: f64) -> f64 {
90 let t_kelvin = temp_c + 273.15;
91 let t_ref = 293.15_f64;
92 let t_ratio = t_kelvin / t_ref;
93 let h = humidity * t_ratio.powf(-5.0 / 2.0) * (-2239.1 / t_kelvin).exp() * 1.0e4;
94 let f_ro = 24.0 + 4.04e4 * h * (0.02 + h) / (0.391 + h);
95 let f_rn =
96 t_ratio.powf(-0.5) * (9.0 + 280.0 * h * (-4.17 * (t_ratio.powf(-1.0 / 3.0) - 1.0)).exp());
97 let f2 = freq * freq;
98 let alpha_classic = 1.84e-11 * t_ratio.powf(0.5) * f2;
99 let alpha_o2 = 0.01275 * (-2239.1 / t_kelvin).exp() * (f_ro + f2 / f_ro).recip() * freq;
100 let alpha_n2 = 0.1068 * (-3352.0 / t_kelvin).exp() * (f_rn + f2 / f_rn).recip() * freq;
101 (alpha_classic + alpha_o2 + alpha_n2) * 8.686
102}
103#[allow(dead_code)]
105pub fn attenuation_db(alpha_per_m: f64, distance: f64) -> f64 {
106 alpha_per_m * distance
107}
108#[allow(dead_code)]
112pub fn sabine_reverberation_time(volume: f64, surface_area: f64, absorption_coeff: f64) -> f64 {
113 0.161 * volume / (surface_area * absorption_coeff)
114}
115#[allow(dead_code)]
119pub fn eyring_reverberation_time(volume: f64, surface_area: f64, absorption_coeff: f64) -> f64 {
120 0.161 * volume / (-surface_area * (1.0 - absorption_coeff).ln())
121}
122#[allow(dead_code)]
124pub fn critical_distance(volume: f64, t60: f64) -> f64 {
125 0.057 * (volume / t60).sqrt()
126}
127#[allow(dead_code)]
131pub fn noise_reduction(
132 nrc_sender: f64,
133 nrc_receiver: f64,
134 area: f64,
135 volume_receiver: f64,
136 t60: f64,
137) -> f64 {
138 let absorption_receiver = 0.161 * volume_receiver / t60;
139 nrc_sender - nrc_receiver + 10.0 * (area / absorption_receiver).log10()
140}
141#[allow(dead_code)]
143pub fn wave_number(freq: f64, c: f64) -> f64 {
144 2.0 * PI * freq / c
145}
146#[allow(dead_code)]
148pub fn plane_wave_pressure(amplitude: f64, k: f64, x: f64, omega: f64, t: f64) -> f64 {
149 amplitude * (k * x - omega * t).cos()
150}
151#[allow(dead_code)]
153pub fn spl_db(pressure_pa: f64) -> f64 {
154 let p_ref = 20.0e-6;
155 20.0 * (pressure_pa.abs() / p_ref).log10()
156}
157#[allow(dead_code)]
162pub fn loudness_sone(spl_db: f64, _freq_hz: f64) -> f64 {
163 2.0_f64.powf((spl_db - 40.0) / 10.0)
164}
165#[allow(dead_code)]
173pub fn mass_law_tl(surface_mass_kg_m2: f64, freq_hz: f64) -> f64 {
174 20.0 * (surface_mass_kg_m2 * freq_hz).log10() - 47.5
175}
176#[allow(dead_code)]
186pub fn coincidence_frequency(c_air: f64, c_l: f64, h: f64) -> f64 {
187 c_air * c_air / (1.8 * c_l * h)
188}
189#[allow(dead_code)]
193pub fn a_weighting_db(f: f64) -> f64 {
194 if f <= 0.0 {
195 return f64::NEG_INFINITY;
196 }
197 let f1 = 20.598_997_0;
198 let f2 = 107.652_72;
199 let f3 = 737.862_2;
200 let f4 = 12_194.217;
201 let f2s = f * f;
202 let num = (f4 * f4) * f2s * f2s;
203 let d1 = f2s + f1 * f1;
204 let d2 = (f2s + f2 * f2).sqrt() * (f2s + f3 * f3).sqrt();
205 let d3 = f2s + f4 * f4;
206 let ra = num / (d1 * d2 * d3);
207 20.0 * ra.log10() + 2.0
208}
209#[allow(dead_code)]
211pub fn c_weighting_db(f: f64) -> f64 {
212 if f <= 0.0 {
213 return f64::NEG_INFINITY;
214 }
215 let f1 = 20.598_997_0;
216 let f4 = 12_194.217;
217 let f2s = f * f;
218 let num = (f4 * f4) * f2s;
219 let d1 = f2s + f1 * f1;
220 let d3 = f2s + f4 * f4;
221 let rc = num / (d1 * d3);
222 20.0 * rc.log10() + 0.06
223}
224#[allow(dead_code)]
226pub const OCTAVE_BAND_CENTRES: [f64; 10] = [
227 31.5, 63.0, 125.0, 250.0, 500.0, 1000.0, 2000.0, 4000.0, 8000.0, 16_000.0,
228];
229#[allow(dead_code)]
234pub fn overall_spl_from_octave_bands(band_spl: &[f64]) -> f64 {
235 let sum: f64 = band_spl.iter().map(|&l| 10.0_f64.powf(l / 10.0)).sum();
236 10.0 * sum.log10()
237}
238#[allow(dead_code)]
245pub fn apply_weighting(band_spl: &[f64], band_freqs: &[f64], filter: WeightingFilter) -> Vec<f64> {
246 band_spl
247 .iter()
248 .zip(band_freqs.iter())
249 .map(|(&l, &f)| {
250 let w = match filter {
251 WeightingFilter::A => a_weighting_db(f),
252 WeightingFilter::B => a_weighting_db(f) * 0.5,
253 WeightingFilter::C => c_weighting_db(f),
254 };
255 l + w
256 })
257 .collect()
258}
259#[allow(dead_code)]
266pub fn impedance_matching_layer(z1: f64, z2: f64) -> f64 {
267 (z1 * z2).sqrt()
268}
269#[allow(dead_code)]
281pub fn matching_layer_reflection(z1: f64, z_m: f64, z2: f64, f_over_f0: f64) -> f64 {
282 let phi = 0.5 * PI * f_over_f0;
283 let cos_phi = phi.cos();
284 let sin_phi = phi.sin();
285 let a = cos_phi;
286 let b = z_m * sin_phi;
287 let c = sin_phi / z_m;
288 let d = cos_phi;
289 let denom_re = a + b / z2 + z1 * c + z1 * d / z2;
290 let t = 2.0 / denom_re.max(1e-20);
291 let tau = z1 / z2 * t * t;
292 let r_sq = (1.0 - tau).clamp(0.0, 1.0);
293 r_sq.sqrt()
294}
295#[allow(dead_code)]
299pub fn matching_layer_bandwidth(z1: f64, z_m: f64, z2: f64, threshold: f64) -> (f64, f64) {
300 let n = 1000;
301 let mut f_low = 2.0_f64;
302 let mut f_high = 0.0_f64;
303 for i in 1..=n {
304 let f_ratio = 0.1 + 1.9 * i as f64 / n as f64;
305 let r = matching_layer_reflection(z1, z_m, z2, f_ratio);
306 if r < threshold {
307 if f_ratio < f_low {
308 f_low = f_ratio;
309 }
310 if f_ratio > f_high {
311 f_high = f_ratio;
312 }
313 }
314 }
315 (f_low, f_high)
316}
317#[allow(dead_code)]
329pub fn angle_dependent_transmission(
330 z1: f64,
331 z2: f64,
332 c1: f64,
333 c2: f64,
334 theta_i: f64,
335) -> Option<f64> {
336 let sin_t = (c2 / c1) * theta_i.sin();
337 if sin_t > 1.0 {
338 return None;
339 }
340 let cos_i = theta_i.cos();
341 let cos_t = (1.0 - sin_t * sin_t).sqrt();
342 let num = 4.0 * z1 * z2 * cos_i * cos_t;
343 let denom = (z1 * cos_t + z2 * cos_i).powi(2);
344 Some(if denom < 1e-30 { 0.0 } else { num / denom })
345}
346#[allow(dead_code)]
350pub fn critical_angle(c1: f64, c2: f64) -> Option<f64> {
351 if c2 <= c1 {
352 return None;
353 }
354 let sin_c = c1 / c2;
355 if sin_c > 1.0 {
356 None
357 } else {
358 Some(sin_c.asin())
359 }
360}
361#[allow(dead_code)]
370pub fn room_resonance_frequency(
371 lx: f64,
372 ly: f64,
373 lz: f64,
374 nx: u32,
375 ny: u32,
376 nz: u32,
377 c: f64,
378) -> f64 {
379 let fx = (nx as f64) / lx;
380 let fy = (ny as f64) / ly;
381 let fz = (nz as f64) / lz;
382 0.5 * c * (fx * fx + fy * fy + fz * fz).sqrt()
383}
384#[allow(dead_code)]
395pub fn room_modes(lx: f64, ly: f64, lz: f64, c: f64, max_order: u32, n_modes: usize) -> Vec<f64> {
396 let mut freqs: Vec<f64> = Vec::new();
397 for nx in 0..=max_order {
398 for ny in 0..=max_order {
399 for nz in 0..=max_order {
400 if nx == 0 && ny == 0 && nz == 0 {
401 continue;
402 }
403 let f = room_resonance_frequency(lx, ly, lz, nx, ny, nz, c);
404 freqs.push(f);
405 }
406 }
407 }
408 freqs.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
409 freqs.dedup_by(|a, b| (*a - *b).abs() < 0.01);
410 freqs.truncate(n_modes);
411 freqs
412}
413#[allow(dead_code)]
417pub fn room_lowest_mode(lx: f64, ly: f64, lz: f64, c: f64) -> f64 {
418 let l_max = lx.max(ly).max(lz);
419 c / (2.0 * l_max)
420}
421#[allow(dead_code)]
430pub fn schroeder_frequency(t60: f64, volume: f64) -> f64 {
431 2000.0 * (t60 / volume).sqrt()
432}
433#[allow(dead_code)]
448pub fn double_leaf_tl(m1_kg_m2: f64, m2_kg_m2: f64, gap_m: f64, freq_hz: f64) -> f64 {
449 let tl1 = mass_law_tl(m1_kg_m2, freq_hz);
450 let tl2 = mass_law_tl(m2_kg_m2, freq_hz);
451 let f_cav = 343.0 / (2.0 * gap_m);
452 let q_factor = 5.0;
453 let resonance_dip = 10.0 / (1.0 + ((freq_hz - f_cav) / (f_cav / q_factor)).powi(2));
454 (tl1 + tl2 + 6.0 - resonance_dip).max(tl1.max(tl2))
455}
456#[allow(dead_code)]
465pub fn weighted_sound_reduction_index(tl_values: &[f64]) -> f64 {
466 let reference: [f64; 16] = [
467 33.0, 36.0, 39.0, 42.0, 45.0, 48.0, 51.0, 52.0, 53.0, 54.0, 55.0, 56.0, 56.0, 56.0, 56.0,
468 56.0,
469 ];
470 let n = tl_values.len().min(reference.len());
471 if n == 0 {
472 return 0.0;
473 }
474 let avg_tl: f64 = tl_values[..n].iter().sum::<f64>() / n as f64;
475 let avg_ref: f64 = reference[..n].iter().sum::<f64>() / n as f64;
476 avg_tl - avg_ref + 52.0
477}
478#[allow(dead_code)]
482pub fn sound_intensity(pressure_pa_peak: f64, impedance: f64) -> f64 {
483 let p_rms = pressure_pa_peak / 2.0_f64.sqrt();
484 p_rms * p_rms / impedance
485}
486#[allow(dead_code)]
490pub fn sound_intensity_level_db(intensity_w_m2: f64) -> f64 {
491 let i_ref = 1.0e-12;
492 10.0 * (intensity_w_m2 / i_ref).log10()
493}
494#[allow(dead_code)]
496pub fn acoustic_power(intensity: f64, area: f64) -> f64 {
497 intensity * area
498}
499#[allow(dead_code)]
503pub fn sound_power_level_db(power_w: f64) -> f64 {
504 let w_ref = 1.0e-12;
505 10.0 * (power_w / w_ref).log10()
506}
507#[allow(dead_code)]
522pub fn doppler_frequency(f0: f64, c: f64, v_source: f64, v_observer: f64) -> f64 {
523 if (c - v_source).abs() < 1e-10 {
524 return f64::INFINITY;
525 }
526 f0 * (c + v_observer) / (c - v_source)
527}
528#[allow(dead_code)]
530pub fn mach_number(v: f64, c: f64) -> f64 {
531 v / c
532}
533#[allow(dead_code)]
543pub fn barrier_insertion_loss_maekawa(delta: f64, freq: f64, c: f64) -> f64 {
544 let lambda = c / freq;
545 let n_fresnel = 2.0 * delta / lambda;
546 (10.0 * (3.0 + 20.0 * n_fresnel).log10()).max(0.0)
547}
548#[allow(dead_code)]
550pub fn fresnel_number(delta: f64, freq: f64, c: f64) -> f64 {
551 2.0 * delta * freq / c
552}
553#[allow(dead_code)]
563pub fn helmholtz_resonator_frequency(area_m2: f64, volume_m3: f64, l_eff_m: f64, c: f64) -> f64 {
564 (c / (2.0 * PI)) * (area_m2 / (volume_m3 * l_eff_m)).sqrt()
565}
566#[allow(dead_code)]
575pub fn quarter_wave_resonator_frequency(length: f64, c: f64, n: u32) -> f64 {
576 let n_f = (2 * n - 1) as f64;
577 n_f * c / (4.0 * length)
578}
579#[allow(dead_code)]
583pub fn half_wave_resonator_frequency(length: f64, c: f64, n: u32) -> f64 {
584 (n as f64) * c / (2.0 * length)
585}
586#[allow(dead_code)]
594pub fn mean_free_path(volume: f64, surface_area: f64) -> f64 {
595 4.0 * volume / surface_area
596}
597#[allow(dead_code)]
603pub fn room_constant(surface_area: f64, absorption_coeff: f64) -> f64 {
604 surface_area * absorption_coeff / (1.0 - absorption_coeff.min(0.9999))
605}
606#[allow(dead_code)]
614pub fn diffuse_field_spl(power_w: f64, room_const: f64) -> f64 {
615 let pwl = sound_power_level_db(power_w);
616 pwl + 10.0 * (4.0 / room_const).log10()
617}
618#[allow(dead_code)]
628pub fn total_spl_in_room(
629 power_w: f64,
630 room_const: f64,
631 distance_m: f64,
632 directivity_q: f64,
633) -> f64 {
634 let pwl = sound_power_level_db(power_w);
635 let direct_term = directivity_q / (4.0 * PI * distance_m * distance_m);
636 let reverb_term = 4.0 / room_const;
637 pwl + 10.0 * (direct_term + reverb_term).log10()
638}
639#[allow(dead_code)]
648pub fn sound_speed_ideal_gas(gamma: f64, r_specific: f64, temp_k: f64) -> f64 {
649 (gamma * r_specific * temp_k).sqrt()
650}
651#[allow(dead_code)]
657pub fn sound_speed_liquid(bulk_modulus_pa: f64, density_kg_m3: f64) -> f64 {
658 (bulk_modulus_pa / density_kg_m3).sqrt()
659}
660#[allow(dead_code)]
664pub fn sound_speed_air_temperature(temp_celsius: f64) -> f64 {
665 331.3 * (1.0 + temp_celsius / 273.15).sqrt()
666}
667#[allow(dead_code)]
677pub fn group_velocity<F>(k: f64, dk: f64, omega_fn: F) -> f64
678where
679 F: Fn(f64) -> f64,
680{
681 (omega_fn(k + dk) - omega_fn(k - dk)) / (2.0 * dk)
682}
683#[allow(dead_code)]
689pub fn pressure_from_intensity(intensity_w_m2: f64, impedance: f64) -> f64 {
690 (2.0 * intensity_w_m2 * impedance).sqrt()
691}
692#[allow(dead_code)]
696pub fn rms_pressure(p_peak: f64) -> f64 {
697 p_peak / 2.0_f64.sqrt()
698}
699#[allow(dead_code)]
703pub fn particle_velocity(pressure_pa: f64, impedance: f64) -> f64 {
704 if impedance.abs() < f64::EPSILON {
705 return 0.0;
706 }
707 pressure_pa / impedance
708}
709#[allow(dead_code)]
715pub fn standing_wave_pressure_closed_closed(amplitude: f64, k: f64, x: f64) -> f64 {
716 2.0 * amplitude * (k * x).cos()
717}
718#[allow(dead_code)]
723pub fn standing_wave_pressure_open_closed(amplitude: f64, k: f64, x: f64) -> f64 {
724 2.0 * amplitude * (k * x).sin()
725}
726#[allow(dead_code)]
735pub fn closed_closed_resonance(length: f64, c: f64, n: u32) -> f64 {
736 (n as f64) * c / (2.0 * length)
737}
738#[allow(dead_code)]
742pub fn resonator_q_factor(f0: f64, bandwidth_hz: f64) -> f64 {
743 if bandwidth_hz < f64::EPSILON {
744 return f64::INFINITY;
745 }
746 f0 / bandwidth_hz
747}
748#[allow(dead_code)]
752pub fn resonance_decay_time(q: f64, f0: f64) -> f64 {
753 if f0 < f64::EPSILON {
754 return f64::INFINITY;
755 }
756 q / (PI * f0)
757}
758#[allow(dead_code)]
762pub fn acoustic_energy_density(p_rms: f64, density: f64, c: f64) -> f64 {
763 let denom = density * c * c;
764 if denom < f64::EPSILON {
765 return 0.0;
766 }
767 p_rms * p_rms / denom
768}
769#[allow(dead_code)]
776pub fn ula_array_factor(n: u32, d: f64, freq: f64, c: f64, theta: f64, theta_steer: f64) -> f64 {
777 let k = 2.0 * PI * freq / c;
778 let psi = k * d * (theta.cos() - theta_steer.cos());
779 let n_f = n as f64;
780 let num = (n_f * psi / 2.0).sin();
781 let den = (psi / 2.0).sin();
782 if den.abs() < 1e-12 {
783 return 1.0;
784 }
785 (num / (n_f * den)).powi(2)
786}
787#[allow(dead_code)]
795pub fn tube_input_impedance_open_end_imag(z0: f64, freq: f64, c: f64, length: f64) -> f64 {
796 let k = 2.0 * PI * freq / c;
797 let kl = k * length;
798 let sin_kl = kl.sin();
799 if sin_kl.abs() < 1e-12 {
800 return f64::INFINITY;
801 }
802 -z0 * kl.cos() / sin_kl
803}
804#[allow(dead_code)]
810pub fn tube_input_impedance_closed_end_imag(z0: f64, freq: f64, c: f64, length: f64) -> f64 {
811 let k = 2.0 * PI * freq / c;
812 let kl = k * length;
813 let sin_kl = kl.sin();
814 if sin_kl.abs() < 1e-12 {
815 return f64::INFINITY;
816 }
817 z0 * kl.cos() / sin_kl
818}
819#[allow(dead_code)]
831pub fn power_law_attenuation(alpha_0: f64, freq_mhz: f64, b: f64) -> f64 {
832 alpha_0 * freq_mhz.powf(b)
833}
834#[allow(dead_code)]
838pub fn pulse_echo_attenuation_db(alpha_0: f64, freq_mhz: f64, b: f64, depth_cm: f64) -> f64 {
839 2.0 * power_law_attenuation(alpha_0, freq_mhz, b) * depth_cm
840}
841#[allow(dead_code)]
853pub fn piston_radiation_resistance_normalised(freq: f64, radius: f64, c: f64) -> f64 {
854 let ka = 2.0 * PI * freq * radius / c;
855 if ka < 0.1 {
856 (ka * ka) / 2.0
857 } else if ka > 10.0 {
858 1.0
859 } else {
860 1.0 - (2.0 * ka).sin() / (2.0 * ka)
861 }
862}
863#[allow(dead_code)]
867pub fn piston_directivity_index_db(freq: f64, radius: f64, c: f64) -> f64 {
868 let ka = 2.0 * PI * freq * radius / c;
869 if ka < f64::EPSILON {
870 return 0.0;
871 }
872 10.0 * (2.0 * ka * ka).log10().max(0.0)
873}
874#[cfg(test)]
875mod tests {
876 use super::*;
877 use crate::acoustics::AcousticMaterial;
878 use crate::acoustics::LocallyResonantUnit;
879 use crate::acoustics::PhononicCrystal1D;
880 use crate::acoustics::PorousAbsorber;
881 use crate::acoustics::UltrasonicNDE;
882 #[test]
883 fn test_water_density() {
884 let m = AcousticMaterial::water();
885 assert!((m.density - 998.2).abs() < 1.0);
886 }
887 #[test]
888 fn test_water_is_fluid() {
889 let m = AcousticMaterial::water();
890 assert_eq!(m.shear_modulus, 0.0);
891 assert_eq!(m.shear_velocity(), 0.0);
892 }
893 #[test]
894 fn test_water_longitudinal_velocity() {
895 let m = AcousticMaterial::water();
896 let c = m.longitudinal_velocity();
897 assert!(c > 1400.0 && c < 1600.0, "c_L = {c}");
898 }
899 #[test]
900 fn test_water_impedance() {
901 let m = AcousticMaterial::water();
902 let z = m.acoustic_impedance();
903 assert!(z > 1.4e6 && z < 1.6e6, "Z = {z}");
904 }
905 #[test]
906 fn test_air_longitudinal_velocity() {
907 let m = AcousticMaterial::air();
908 let c = m.longitudinal_velocity();
909 assert!(c > 330.0 && c < 360.0, "c_L = {c}");
910 }
911 #[test]
912 fn test_steel_shear_velocity() {
913 let m = AcousticMaterial::steel();
914 let cs = m.shear_velocity();
915 assert!(cs > 3000.0 && cs < 3500.0, "c_S = {cs}");
916 }
917 #[test]
918 fn test_steel_longitudinal_velocity() {
919 let m = AcousticMaterial::steel();
920 let cl = m.longitudinal_velocity();
921 assert!(cl > 5500.0 && cl < 6500.0, "c_L = {cl}");
922 }
923 #[test]
924 fn test_concrete_density() {
925 let m = AcousticMaterial::concrete();
926 assert!((m.density - 2300.0).abs() < 1.0);
927 }
928 #[test]
929 fn test_impedance_equals_rho_times_cl() {
930 let m = AcousticMaterial::steel();
931 let z = m.acoustic_impedance();
932 assert!((z - m.density * m.longitudinal_velocity()).abs() < 1.0);
933 }
934 #[test]
935 fn test_reflection_same_medium_is_zero() {
936 let r = reflection_coefficient(1500.0, 1500.0);
937 assert!(r.abs() < 1e-12);
938 }
939 #[test]
940 fn test_reflection_total_at_rigid_wall() {
941 let r = reflection_coefficient(1.0, 1e15);
942 assert!((r - 1.0).abs() < 1e-10);
943 }
944 #[test]
945 fn test_reflection_soft_boundary() {
946 let r = reflection_coefficient(1000.0, 1e-9);
947 assert!((r + 1.0).abs() < 1e-6);
948 }
949 #[test]
950 fn test_transmission_same_medium_is_one() {
951 let t = transmission_coefficient(500.0, 500.0);
952 assert!((t - 1.0).abs() < 1e-12);
953 }
954 #[test]
955 fn test_r_and_t_energy_conservation() {
956 let z1 = 400.0_f64;
957 let z2 = 1.5e6_f64;
958 let r = reflection_coefficient(z1, z2);
959 let tau = 4.0 * z1 * z2 / ((z1 + z2) * (z1 + z2));
960 let lhs = r * r + tau;
961 assert!((lhs - 1.0).abs() < 1e-10, "lhs = {lhs}");
962 }
963 #[test]
964 fn test_transmission_loss_same_medium_is_zero() {
965 let tl = transmission_loss_db(1000.0, 1000.0);
966 assert!(tl.abs() < 1e-10);
967 }
968 #[test]
969 fn test_transmission_loss_large_impedance_mismatch() {
970 let z_air = AcousticMaterial::air().acoustic_impedance();
971 let z_water = AcousticMaterial::water().acoustic_impedance();
972 let tl = transmission_loss_db(z_air, z_water);
973 assert!(tl > 20.0, "TL = {tl} dB");
974 }
975 #[test]
976 fn test_multilayer_trivial_no_layers() {
977 let t2 = multilayer_transmission(&[400.0, 400.0], &[], 1000.0);
978 assert!((t2 - 1.0).abs() < 1e-6, "|T|² = {t2}");
979 }
980 #[test]
981 fn test_multilayer_single_layer_returns_nonzero() {
982 let t2 = multilayer_transmission(&[400.0, 400.0, 400.0], &[0.1], 1000.0);
983 assert!(t2 > 0.0, "|T|² = {t2}");
984 }
985 #[test]
986 fn test_viscous_attenuation_increases_with_freq() {
987 let alpha1 = viscous_attenuation(1000.0, 1e-3, 998.0, 1500.0);
988 let alpha2 = viscous_attenuation(4000.0, 1e-3, 998.0, 1500.0);
989 assert!(alpha2 > alpha1 * 10.0);
990 }
991 #[test]
992 fn test_viscous_attenuation_positive() {
993 let alpha = viscous_attenuation(1000.0, 1e-3, 998.0, 1500.0);
994 assert!(alpha > 0.0);
995 }
996 #[test]
997 fn test_atmospheric_attenuation_positive() {
998 let alpha = atmospheric_attenuation_db_per_m(1000.0, 20.0, 0.50);
999 assert!(alpha > 0.0, "alpha = {alpha}");
1000 }
1001 #[test]
1002 fn test_atmospheric_attenuation_increases_with_freq() {
1003 let a1 = atmospheric_attenuation_db_per_m(1000.0, 20.0, 0.50);
1004 let a2 = atmospheric_attenuation_db_per_m(8000.0, 20.0, 0.50);
1005 assert!(a2 > a1, "a1={a1}, a2={a2}");
1006 }
1007 #[test]
1008 fn test_attenuation_db_linear_in_distance() {
1009 let d1 = attenuation_db(0.5, 10.0);
1010 let d2 = attenuation_db(0.5, 20.0);
1011 assert!((d2 - 2.0 * d1).abs() < 1e-12);
1012 }
1013 #[test]
1014 fn test_sabine_t60_known_value() {
1015 let t60 = sabine_reverberation_time(200.0, 120.0, 0.20);
1016 assert!((t60 - 1.342).abs() < 0.01, "T60 = {t60}");
1017 }
1018 #[test]
1019 fn test_eyring_less_than_sabine_for_high_absorption() {
1020 let t_s = sabine_reverberation_time(200.0, 120.0, 0.50);
1021 let t_e = eyring_reverberation_time(200.0, 120.0, 0.50);
1022 assert!(t_e < t_s, "T_e={t_e}, T_s={t_s}");
1023 }
1024 #[test]
1025 fn test_sabine_eyring_agree_at_low_absorption() {
1026 let t_s = sabine_reverberation_time(500.0, 300.0, 0.05);
1027 let t_e = eyring_reverberation_time(500.0, 300.0, 0.05);
1028 assert!((t_s - t_e).abs() / t_s < 0.05, "T_s={t_s}, T_e={t_e}");
1029 }
1030 #[test]
1031 fn test_critical_distance_positive() {
1032 let rc = critical_distance(200.0, 1.5);
1033 assert!(rc > 0.0);
1034 }
1035 #[test]
1036 fn test_critical_distance_known_value() {
1037 let rc = critical_distance(500.0, 2.0);
1038 assert!((rc - 0.9013).abs() < 0.01, "r_c = {rc}");
1039 }
1040 #[test]
1041 fn test_noise_reduction_finite() {
1042 let nr = noise_reduction(50.0, 30.0, 10.0, 100.0, 1.0);
1043 assert!(nr.is_finite());
1044 }
1045 #[test]
1046 fn test_wave_number_at_1khz_in_air() {
1047 let k = wave_number(1000.0, 343.0);
1048 assert!((k - 18.33).abs() < 0.1, "k = {k}");
1049 }
1050 #[test]
1051 fn test_plane_wave_at_origin_zero_time() {
1052 let p = plane_wave_pressure(2.0, 1.0, 0.0, 1.0, 0.0);
1053 assert!((p - 2.0).abs() < 1e-12);
1054 }
1055 #[test]
1056 fn test_spl_at_reference_pressure_is_zero() {
1057 let spl = spl_db(20.0e-6);
1058 assert!(spl.abs() < 1e-10, "SPL = {spl}");
1059 }
1060 #[test]
1061 fn test_spl_doubles_pressure_is_6db() {
1062 let spl1 = spl_db(20.0e-6);
1063 let spl2 = spl_db(40.0e-6);
1064 assert!(
1065 (spl2 - spl1 - 6.020).abs() < 0.01,
1066 "delta = {}",
1067 spl2 - spl1
1068 );
1069 }
1070 #[test]
1071 fn test_loudness_at_40db_is_one_sone() {
1072 let s = loudness_sone(40.0, 1000.0);
1073 assert!((s - 1.0).abs() < 1e-12);
1074 }
1075 #[test]
1076 fn test_loudness_doubles_every_10db() {
1077 let s1 = loudness_sone(40.0, 1000.0);
1078 let s2 = loudness_sone(50.0, 1000.0);
1079 assert!((s2 / s1 - 2.0).abs() < 1e-10);
1080 }
1081 #[test]
1082 fn test_mass_law_tl_positive_at_1khz() {
1083 let tl = mass_law_tl(480.0, 1000.0);
1084 assert!(tl > 30.0, "TL = {tl} dB");
1085 }
1086 #[test]
1087 fn test_mass_law_tl_increases_with_frequency() {
1088 let tl1 = mass_law_tl(50.0, 500.0);
1089 let tl2 = mass_law_tl(50.0, 1000.0);
1090 assert!((tl2 - tl1 - 6.0).abs() < 0.1, "tl1={tl1}, tl2={tl2}");
1091 }
1092 #[test]
1093 fn test_mass_law_tl_increases_with_mass() {
1094 let tl1 = mass_law_tl(50.0, 1000.0);
1095 let tl2 = mass_law_tl(100.0, 1000.0);
1096 assert!((tl2 - tl1 - 6.0).abs() < 0.1, "tl1={tl1}, tl2={tl2}");
1097 }
1098 #[test]
1099 fn test_ultrasonic_nde_time_of_flight() {
1100 let nde = UltrasonicNDE::new(6000.0, 5.0e6, 1.0, 0.0);
1101 let tof = nde.time_of_flight(0.030);
1102 assert!((tof - 10.0e-6).abs() < 1e-9, "TOF = {tof}");
1103 }
1104 #[test]
1105 fn test_ultrasonic_nde_echo_amplitude_decreases_with_depth() {
1106 let nde = UltrasonicNDE::new(6000.0, 5.0e6, 1.0, 5.0);
1107 let a1 = nde.echo_amplitude(0.010, 1.0);
1108 let a2 = nde.echo_amplitude(0.050, 1.0);
1109 assert!(a1 > a2, "echo should decrease with depth: a1={a1}, a2={a2}");
1110 }
1111 #[test]
1112 fn test_ultrasonic_nde_wavelength() {
1113 let nde = UltrasonicNDE::new(5920.0, 5.0e6, 1.0, 0.0);
1114 let lam = nde.wavelength();
1115 assert!((lam - 5920.0 / 5.0e6).abs() < 1e-12, "λ = {lam}");
1116 }
1117 #[test]
1118 fn test_ultrasonic_nde_near_field_positive() {
1119 let nde = UltrasonicNDE::new(6000.0, 5.0e6, 1.0, 0.0);
1120 let nf = nde.near_field_distance(0.006);
1121 assert!(nf > 0.0, "near-field distance should be positive, got {nf}");
1122 }
1123 #[test]
1124 fn test_a_weighting_at_1khz_near_zero() {
1125 let w = a_weighting_db(1000.0);
1126 assert!(
1127 w.abs() < 1.0,
1128 "A-weighting at 1 kHz should be ~0 dB, got {w}"
1129 );
1130 }
1131 #[test]
1132 fn test_a_weighting_low_freq_negative() {
1133 let w = a_weighting_db(100.0);
1134 assert!(
1135 w < -10.0,
1136 "A-weighting at 100 Hz should be strongly negative, got {w}"
1137 );
1138 }
1139 #[test]
1140 fn test_c_weighting_near_1khz_near_zero() {
1141 let w = c_weighting_db(1000.0);
1142 assert!(
1143 w.abs() < 2.0,
1144 "C-weighting at 1 kHz should be ~0 dB, got {w}"
1145 );
1146 }
1147 #[test]
1148 fn test_overall_spl_from_two_equal_bands() {
1149 let overall = overall_spl_from_octave_bands(&[60.0, 60.0]);
1150 assert!((overall - 63.01).abs() < 0.02, "got {overall}");
1151 }
1152 #[test]
1153 fn test_apply_weighting_returns_same_length() {
1154 let spl = [55.0, 60.0, 65.0, 70.0, 72.0];
1155 let freqs = [250.0, 500.0, 1000.0, 2000.0, 4000.0];
1156 let weighted = apply_weighting(&spl, &freqs, WeightingFilter::A);
1157 assert_eq!(weighted.len(), spl.len());
1158 }
1159 #[test]
1160 fn test_apply_a_weighting_increases_at_high_freq_vs_low_freq() {
1161 let spl = [60.0, 60.0];
1162 let freqs_low = [100.0, 100.0];
1163 let freqs_high = [1000.0, 1000.0];
1164 let w_low = apply_weighting(&spl, &freqs_low, WeightingFilter::A);
1165 let w_high = apply_weighting(&spl, &freqs_high, WeightingFilter::A);
1166 assert!(w_high[0] > w_low[0], "A-weighting at 1 kHz > 100 Hz");
1167 }
1168 #[test]
1169 fn test_coincidence_frequency_positive() {
1170 let fc = coincidence_frequency(343.0, 5000.0, 0.012);
1171 assert!(fc > 0.0, "got {fc}");
1172 }
1173 #[test]
1174 fn test_matching_layer_geometric_mean() {
1175 let z1 = 400.0_f64;
1176 let z2 = 1.5e6_f64;
1177 let z_m = impedance_matching_layer(z1, z2);
1178 let expected = (z1 * z2).sqrt();
1179 assert!(
1180 (z_m - expected).abs() < 1.0,
1181 "Z_m = {z_m}, expected {expected}"
1182 );
1183 }
1184 #[test]
1185 fn test_matching_layer_zero_reflection_at_design_freq() {
1186 let z1 = 400.0_f64;
1187 let z2 = 1.5e6_f64;
1188 let z_m = impedance_matching_layer(z1, z2);
1189 let r = matching_layer_reflection(z1, z_m, z2, 1.0);
1190 assert!(
1191 r < 0.1,
1192 "Matching layer at design frequency should give near-zero reflection, got {r}"
1193 );
1194 }
1195 #[test]
1196 fn test_matching_layer_bandwidth_positive() {
1197 let z1 = 400.0;
1198 let z2 = 1.5e6;
1199 let z_m = impedance_matching_layer(z1, z2);
1200 let (f_low, f_high) = matching_layer_bandwidth(z1, z_m, z2, 0.5);
1201 assert!(
1202 f_high > f_low || (f_low > 1.9 && f_high < 0.2),
1203 "Bandwidth should be positive"
1204 );
1205 }
1206 #[test]
1207 fn test_matching_layer_impedance_symmetric() {
1208 let z1 = 1000.0;
1209 let z2 = 4000.0;
1210 let zm12 = impedance_matching_layer(z1, z2);
1211 let zm21 = impedance_matching_layer(z2, z1);
1212 assert!((zm12 - zm21).abs() < 1e-10);
1213 }
1214 #[test]
1215 fn test_angle_transmission_normal_incidence() {
1216 let z1 = 400.0;
1217 let z2 = 1.5e6;
1218 let c1 = 343.0;
1219 let c2 = 1480.0;
1220 let t = angle_dependent_transmission(z1, z2, c1, c2, 0.0).unwrap();
1221 let expected = 4.0 * z1 * z2 / (z1 + z2).powi(2);
1222 assert!(
1223 (t - expected).abs() < 1e-6,
1224 "Normal incidence T_I mismatch: got {t}"
1225 );
1226 }
1227 #[test]
1228 fn test_angle_transmission_total_internal_reflection() {
1229 let z_air = 400.0;
1230 let z_water = 1.5e6;
1231 let c_air = 343.0;
1232 let c_water = 1480.0;
1233 let t = angle_dependent_transmission(z_air, z_water, c_air, c_water, 80.0_f64.to_radians());
1234 assert!(
1235 t.is_none(),
1236 "Should be total internal reflection at 80° (air→water)"
1237 );
1238 }
1239 #[test]
1240 fn test_critical_angle_water_to_air() {
1241 let c1 = 1480.0;
1242 let c2 = 343.0;
1243 assert!(
1244 critical_angle(c1, c2).is_none(),
1245 "No critical angle when c2 < c1"
1246 );
1247 }
1248 #[test]
1249 fn test_critical_angle_air_to_water() {
1250 let c1 = 343.0;
1251 let c2 = 1480.0;
1252 let theta_c = critical_angle(c1, c2);
1253 assert!(theta_c.is_some());
1254 let angle = theta_c.unwrap();
1255 assert!(
1256 angle > 0.0 && angle < PI / 2.0,
1257 "Critical angle should be in (0, π/2)"
1258 );
1259 }
1260 #[test]
1261 fn test_lram_resonance_frequency_positive() {
1262 let unit = LocallyResonantUnit::new(0.1, 1e6, 1e-6, 7800.0);
1263 let f0 = unit.resonance_frequency();
1264 assert!(
1265 f0 > 0.0 && f0.is_finite(),
1266 "Resonance frequency should be positive finite: {f0}"
1267 );
1268 }
1269 #[test]
1270 fn test_lram_negative_mass_above_resonance() {
1271 let unit = LocallyResonantUnit::new(0.1, 1e6, 1e-6, 1.0e-6);
1272 let f0 = unit.resonance_frequency();
1273 let f_above = f0 * 1.01;
1274 assert!(
1275 unit.is_negative_mass(f_above),
1276 "Effective mass should be negative just above resonance"
1277 );
1278 }
1279 #[test]
1280 fn test_lram_positive_mass_below_resonance() {
1281 let unit = LocallyResonantUnit::new(0.1, 1e6, 1e-6, 7800.0);
1282 let f0 = unit.resonance_frequency();
1283 let f_below = f0 * 0.5;
1284 let rho_eff = unit.effective_density(f_below);
1285 assert!(
1286 rho_eff > 0.0,
1287 "Effective density should be positive below resonance: {rho_eff}"
1288 );
1289 }
1290 #[test]
1291 fn test_phononic_crystal_bragg_frequency_positive() {
1292 let pc = PhononicCrystal1D::new(400.0, 1.5e6, 343.0, 1480.0, 0.01, 0.01);
1293 let f_b = pc.bragg_frequency();
1294 assert!(
1295 f_b > 0.0 && f_b.is_finite(),
1296 "Bragg frequency should be positive: {f_b}"
1297 );
1298 }
1299 #[test]
1300 fn test_phononic_crystal_lattice_constant() {
1301 let pc = PhononicCrystal1D::new(400.0, 1.5e6, 343.0, 1480.0, 0.015, 0.010);
1302 assert!((pc.lattice_constant() - 0.025).abs() < 1e-12);
1303 }
1304 #[test]
1305 fn test_phononic_crystal_gap_width_positive_for_mismatch() {
1306 let pc = PhononicCrystal1D::new(400.0, 1.5e6, 343.0, 1480.0, 0.01, 0.01);
1307 let gap = pc.relative_gap_width();
1308 assert!(
1309 gap > 0.0,
1310 "Gap width should be positive for impedance mismatch: {gap}"
1311 );
1312 }
1313 #[test]
1314 fn test_phononic_crystal_zero_gap_for_equal_impedance() {
1315 let pc = PhononicCrystal1D::new(400.0, 400.0, 343.0, 343.0, 0.01, 0.01);
1316 let gap = pc.relative_gap_width();
1317 assert!(
1318 gap.abs() < 1e-12,
1319 "No gap for identical materials, got {gap}"
1320 );
1321 }
1322 #[test]
1323 fn test_phononic_crystal_is_in_gap_center() {
1324 let pc = PhononicCrystal1D::new(400.0, 1.5e6, 343.0, 1480.0, 0.01, 0.01);
1325 let f0 = pc.bragg_frequency();
1326 assert!(pc.is_in_gap(f0), "Bragg frequency should be in gap");
1327 }
1328 #[test]
1329 fn test_porous_absorber_coefficient_in_range() {
1330 let abs = PorousAbsorber::new(10_000.0, 0.05);
1331 let alpha = abs.absorption_coefficient(1000.0);
1332 assert!(
1333 (0.0..=1.0 + 1e-10).contains(&alpha),
1334 "Absorption coefficient should be in [0,1], got {alpha}"
1335 );
1336 }
1337 #[test]
1338 fn test_porous_absorber_impedance_real_positive() {
1339 let abs = PorousAbsorber::new(10_000.0, 0.05);
1340 let zr = abs.impedance_real(1000.0);
1341 assert!(
1342 zr > 1.0,
1343 "Real impedance should be > 1 (normalised), got {zr}"
1344 );
1345 }
1346 #[test]
1347 fn test_porous_absorber_absorption_increases_with_thickness() {
1348 let abs1 = PorousAbsorber::new(10_000.0, 0.03);
1349 let abs2 = PorousAbsorber::new(10_000.0, 0.10);
1350 let a1 = abs1.absorption_coefficient(500.0);
1351 let a2 = abs2.absorption_coefficient(500.0);
1352 assert!(
1353 a2 >= a1 - 0.1,
1354 "Thicker absorber should have >= absorption: a1={a1}, a2={a2}"
1355 );
1356 }
1357 #[test]
1358 fn test_porous_absorber_propagation_imag_positive() {
1359 let abs = PorousAbsorber::new(5_000.0, 0.05);
1360 let ki = abs.propagation_imag(2000.0);
1361 assert!(ki > 0.0, "Propagation attenuation should be positive: {ki}");
1362 }
1363 #[test]
1364 fn test_room_resonance_axial_x() {
1365 let lx = 5.0;
1366 let f = room_resonance_frequency(lx, 4.0, 3.0, 1, 0, 0, 343.0);
1367 assert!((f - 343.0 / (2.0 * lx)).abs() < 1e-6, "f = {f}");
1368 }
1369 #[test]
1370 fn test_room_resonance_axial_y() {
1371 let ly = 4.0;
1372 let f = room_resonance_frequency(5.0, ly, 3.0, 0, 1, 0, 343.0);
1373 assert!((f - 343.0 / (2.0 * ly)).abs() < 1e-6, "f = {f}");
1374 }
1375 #[test]
1376 fn test_room_resonance_modes_ordered() {
1377 let modes = room_modes(5.0, 4.0, 3.0, 343.0, 3, 6);
1378 assert_eq!(modes.len(), 6);
1379 for i in 0..modes.len() - 1 {
1380 assert!(
1381 modes[i] <= modes[i + 1],
1382 "modes not ordered: {} > {}",
1383 modes[i],
1384 modes[i + 1]
1385 );
1386 }
1387 }
1388 #[test]
1389 fn test_room_modes_all_positive() {
1390 let modes = room_modes(6.0, 5.0, 4.0, 343.0, 2, 10);
1391 for &f in &modes {
1392 assert!(f > 0.0, "all modes should be positive, found {f}");
1393 }
1394 }
1395 #[test]
1396 fn test_room_lowest_mode_correct() {
1397 let f = room_lowest_mode(6.0, 4.0, 3.0, 343.0);
1398 assert!((f - 343.0 / 12.0).abs() < 1e-6, "f = {f}");
1399 }
1400 #[test]
1401 fn test_schroeder_frequency_typical_room() {
1402 let fs = schroeder_frequency(1.0, 200.0);
1403 assert!((fs - 2000.0 / 200.0_f64.sqrt()).abs() < 0.1, "f_S = {fs}");
1404 }
1405 #[test]
1406 fn test_double_leaf_tl_better_than_single() {
1407 let tl_single = mass_law_tl(10.0, 1000.0);
1408 let tl_double = double_leaf_tl(10.0, 10.0, 0.1, 1000.0);
1409 assert!(
1410 tl_double > tl_single,
1411 "double leaf should outperform single: {tl_double} vs {tl_single}"
1412 );
1413 }
1414 #[test]
1415 fn test_double_leaf_tl_positive() {
1416 let tl = double_leaf_tl(12.0, 12.0, 0.08, 500.0);
1417 assert!(tl > 0.0, "TL should be positive, got {tl}");
1418 }
1419 #[test]
1420 fn test_sound_intensity_positive() {
1421 let i = sound_intensity(1.0, 400.0);
1422 assert!(i > 0.0, "intensity should be positive, got {i}");
1423 }
1424 #[test]
1425 fn test_sound_intensity_scales_with_pressure_squared() {
1426 let i1 = sound_intensity(1.0, 400.0);
1427 let i2 = sound_intensity(2.0, 400.0);
1428 assert!(
1429 (i2 / i1 - 4.0).abs() < 1e-10,
1430 "I ∝ p², got ratio {}",
1431 i2 / i1
1432 );
1433 }
1434 #[test]
1435 fn test_sound_intensity_level_reference_is_0db() {
1436 let sil = sound_intensity_level_db(1.0e-12);
1437 assert!(sil.abs() < 1e-10, "SIL at ref = 0 dB, got {sil}");
1438 }
1439 #[test]
1440 fn test_sound_power_level_reference_is_0db() {
1441 let pwl = sound_power_level_db(1.0e-12);
1442 assert!(pwl.abs() < 1e-10, "PWL at ref = 0 dB, got {pwl}");
1443 }
1444 #[test]
1445 fn test_doppler_stationary_source_and_observer() {
1446 let f = doppler_frequency(1000.0, 343.0, 0.0, 0.0);
1447 assert!((f - 1000.0).abs() < 1e-6, "no motion → no shift, got {f}");
1448 }
1449 #[test]
1450 fn test_doppler_approaching_source_raises_pitch() {
1451 let f = doppler_frequency(1000.0, 343.0, 34.3, 0.0);
1452 assert!(f > 1000.0, "approaching source → higher pitch, got {f}");
1453 }
1454 #[test]
1455 fn test_doppler_receding_source_lowers_pitch() {
1456 let f = doppler_frequency(1000.0, 343.0, -34.3, 0.0);
1457 assert!(f < 1000.0, "receding source → lower pitch, got {f}");
1458 }
1459 #[test]
1460 fn test_mach_number_subsonic() {
1461 let m = mach_number(100.0, 343.0);
1462 assert!(m < 1.0, "100 m/s is subsonic, got M = {m}");
1463 }
1464 #[test]
1465 fn test_mach_number_supersonic() {
1466 let m = mach_number(500.0, 343.0);
1467 assert!(m > 1.0, "500 m/s is supersonic, got M = {m}");
1468 }
1469 #[test]
1470 fn test_maekawa_il_positive_for_positive_delta() {
1471 let il = barrier_insertion_loss_maekawa(0.5, 1000.0, 343.0);
1472 assert!(
1473 il > 0.0,
1474 "barrier with positive path diff should give positive IL, got {il}"
1475 );
1476 }
1477 #[test]
1478 fn test_maekawa_il_increases_with_delta() {
1479 let il1 = barrier_insertion_loss_maekawa(0.2, 1000.0, 343.0);
1480 let il2 = barrier_insertion_loss_maekawa(1.0, 1000.0, 343.0);
1481 assert!(
1482 il2 > il1,
1483 "larger path diff → more insertion loss: {il1} vs {il2}"
1484 );
1485 }
1486 #[test]
1487 fn test_fresnel_number_formula() {
1488 let n = fresnel_number(0.5, 1000.0, 343.0);
1489 assert!((n - 2.0 * 0.5 * 1000.0 / 343.0).abs() < 1e-10, "got {n}");
1490 }
1491 #[test]
1492 fn test_helmholtz_resonator_typical_value() {
1493 let f = helmholtz_resonator_frequency(5.0e-4, 5.0e-4, 0.06, 343.0);
1494 assert!(
1495 f > 50.0 && f < 1000.0,
1496 "typical Helmholtz freq should be 50-1000 Hz, got {f}"
1497 );
1498 }
1499 #[test]
1500 fn test_quarter_wave_resonator_fundamental() {
1501 let f = quarter_wave_resonator_frequency(0.25, 343.0, 1);
1502 assert!((f - 343.0).abs() < 1e-6, "f_1 = {f}");
1503 }
1504 #[test]
1505 fn test_quarter_wave_resonator_third_harmonic() {
1506 let f1 = quarter_wave_resonator_frequency(0.25, 343.0, 1);
1507 let f2 = quarter_wave_resonator_frequency(0.25, 343.0, 2);
1508 assert!(
1509 (f2 / f1 - 3.0).abs() < 1e-6,
1510 "3rd harmonic ratio = {}",
1511 f2 / f1
1512 );
1513 }
1514 #[test]
1515 fn test_half_wave_resonator_fundamental() {
1516 let f = half_wave_resonator_frequency(0.25, 343.0, 1);
1517 assert!((f - 686.0).abs() < 1e-6, "f_1 = {f}");
1518 }
1519 #[test]
1520 fn test_half_wave_second_harmonic() {
1521 let f1 = half_wave_resonator_frequency(0.5, 343.0, 1);
1522 let f2 = half_wave_resonator_frequency(0.5, 343.0, 2);
1523 assert!((f2 / f1 - 2.0).abs() < 1e-10, "ratio = {}", f2 / f1);
1524 }
1525 #[test]
1526 fn test_mean_free_path_cube_room() {
1527 let l = mean_free_path(64.0, 96.0);
1528 assert!((l - 64.0 * 4.0 / 96.0).abs() < 1e-10, "L_mfp = {l}");
1529 }
1530 #[test]
1531 fn test_room_constant_increases_with_absorption() {
1532 let r1 = room_constant(100.0, 0.1);
1533 let r2 = room_constant(100.0, 0.5);
1534 assert!(
1535 r2 > r1,
1536 "room constant increases with absorption: {r1} vs {r2}"
1537 );
1538 }
1539 #[test]
1540 fn test_diffuse_field_spl_finite() {
1541 let r = room_constant(200.0, 0.2);
1542 let spl = diffuse_field_spl(0.01, r);
1543 assert!(spl.is_finite(), "diffuse SPL should be finite, got {spl}");
1544 }
1545 #[test]
1546 fn test_total_spl_in_room_decreases_with_distance() {
1547 let r = room_constant(500.0, 0.15);
1548 let spl1 = total_spl_in_room(0.1, r, 1.0, 1.0);
1549 let spl2 = total_spl_in_room(0.1, r, 5.0, 1.0);
1550 assert!(
1551 spl1 > spl2,
1552 "SPL should decrease with distance: {spl1} vs {spl2}"
1553 );
1554 }
1555 #[test]
1556 fn test_insertion_loss_positive() {
1557 let mat = AcousticMaterial::concrete();
1558 let il = mat.compute_insertion_loss(500.0, 0.2, 1.0);
1559 assert!(il > 0.0, "insertion loss should be positive, got {il}");
1560 }
1561 #[test]
1562 fn test_insertion_loss_increases_with_frequency() {
1563 let mat = AcousticMaterial::concrete();
1564 let il_low = mat.compute_insertion_loss(250.0, 0.2, 1.0);
1565 let il_high = mat.compute_insertion_loss(2000.0, 0.2, 1.0);
1566 assert!(
1567 il_high > il_low,
1568 "IL should increase with frequency: {il_low} vs {il_high}"
1569 );
1570 }
1571 #[test]
1572 fn test_insertion_loss_increases_with_thickness() {
1573 let mat = AcousticMaterial::concrete();
1574 let il_thin = mat.compute_insertion_loss(500.0, 0.1, 0.5);
1575 let il_thick = mat.compute_insertion_loss(500.0, 0.5, 0.5);
1576 assert!(
1577 il_thick > il_thin,
1578 "thicker barrier → higher IL: {il_thin} vs {il_thick}"
1579 );
1580 }
1581 #[test]
1582 fn test_insertion_loss_zero_fresnel_still_has_mass_term() {
1583 let mat = AcousticMaterial::concrete();
1584 let il = mat.compute_insertion_loss(1000.0, 0.2, 0.0);
1585 assert!(
1586 il > 0.0,
1587 "mass-law contribution should give positive IL: {il}"
1588 );
1589 }
1590 #[test]
1591 fn test_nrc_between_zero_and_one() {
1592 let mat = AcousticMaterial::concrete();
1593 let nrc = mat.compute_noise_reduction_coefficient(0.05);
1594 assert!((0.0..=1.0).contains(&nrc), "NRC must be in [0, 1]: {nrc}");
1595 }
1596 #[test]
1597 fn test_nrc_rounded_to_nearest_0_05() {
1598 let mat = AcousticMaterial::concrete();
1599 let nrc = mat.compute_noise_reduction_coefficient(0.02);
1600 let remainder = (nrc / 0.05 - (nrc / 0.05).round()).abs();
1601 assert!(remainder < 1e-10, "NRC should be rounded to 0.05: {nrc}");
1602 }
1603 #[test]
1604 fn test_nrc_higher_loss_factor_gives_higher_nrc() {
1605 let mat_lo = AcousticMaterial::new(2300.0, 13.33e9, 10.0e9, 0.01);
1606 let mat_hi = AcousticMaterial::new(2300.0, 13.33e9, 10.0e9, 0.20);
1607 let nrc_lo = mat_lo.compute_noise_reduction_coefficient(0.05);
1608 let nrc_hi = mat_hi.compute_noise_reduction_coefficient(0.05);
1609 assert!(
1610 nrc_hi >= nrc_lo,
1611 "higher loss factor → higher NRC: {nrc_lo} vs {nrc_hi}"
1612 );
1613 }
1614 #[test]
1615 fn test_mass_law_tl_positive_heavy_panel() {
1616 let mat = AcousticMaterial::concrete();
1617 let tl = mat.compute_transmission_loss_mass_law(500.0, 0.2);
1618 assert!(tl > 0.0, "heavy panel TL should be positive, got {tl}");
1619 }
1620 #[test]
1621 fn test_mass_law_tl_doubles_6db_per_octave() {
1622 let mat = AcousticMaterial::concrete();
1623 let tl1 = mat.compute_transmission_loss_mass_law(500.0, 0.1);
1624 let tl2 = mat.compute_transmission_loss_mass_law(1000.0, 0.1);
1625 let delta = tl2 - tl1;
1626 assert!(
1627 (delta - 6.0).abs() < 0.1,
1628 "mass law should give ~6 dB per octave: got {delta:.3} dB"
1629 );
1630 }
1631 #[test]
1632 fn test_mass_law_tl_doubles_6db_per_mass_doubling() {
1633 let mat = AcousticMaterial::concrete();
1634 let tl1 = mat.compute_transmission_loss_mass_law(1000.0, 0.1);
1635 let tl2 = mat.compute_transmission_loss_mass_law(1000.0, 0.2);
1636 let delta = tl2 - tl1;
1637 assert!(
1638 (delta - 6.0).abs() < 0.1,
1639 "doubling mass should add ~6 dB: got {delta:.3} dB"
1640 );
1641 }
1642}