use crate::material::NUM_BANDS;
use crate::propagation::speed_of_sound;
use hisab::Vec3;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct VibratingSurface {
pub position: Vec3,
pub normal: Vec3,
pub area: f32,
pub velocity: f32,
pub frequency: f32,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct VibrationMode {
pub frequency: f32,
pub damping: f32,
pub mode_shape: Vec<f32>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct RadiationResult {
pub sound_power_watts: f32,
pub sound_power_level_db: f32,
pub power_per_band: [f32; NUM_BANDS],
}
#[must_use]
#[inline]
pub fn radiation_efficiency(
frequency: f32,
critical_frequency: f32,
plate_area: f32,
plate_perimeter: f32,
temperature_celsius: f32,
) -> f32 {
if frequency <= 0.0 || critical_frequency <= 0.0 || plate_area <= 0.0 {
return 0.0;
}
let c = speed_of_sound(temperature_celsius);
let wavelength = c / frequency;
let f_ratio = frequency / critical_frequency;
if f_ratio < 0.5 {
let sigma = plate_perimeter * wavelength / (4.0 * std::f32::consts::PI * plate_area);
sigma.clamp(0.0, 1.0)
} else if f_ratio < 1.0 {
let sigma = (1.0 / (1.0 - f_ratio)).sqrt();
sigma
.clamp(0.0, 10.0)
.min(1.0 / (1.0 - f_ratio + 0.01).sqrt())
.clamp(0.0, 1.0)
} else {
let sigma = 1.0 / (1.0 - 1.0 / (f_ratio * f_ratio)).abs().sqrt().max(0.01);
sigma.clamp(0.0, 1.0)
}
}
#[must_use]
#[tracing::instrument(skip(surfaces), fields(num_surfaces = surfaces.len()))]
pub fn radiated_sound_power(
surfaces: &[VibratingSurface],
critical_frequency: f32,
temperature_celsius: f32,
) -> RadiationResult {
if surfaces.is_empty() {
return RadiationResult {
sound_power_watts: 0.0,
sound_power_level_db: f32::NEG_INFINITY,
power_per_band: [0.0; NUM_BANDS],
};
}
let c = speed_of_sound(temperature_celsius);
let rho = 1.21_f32;
let total_area: f32 = surfaces.iter().map(|s| s.area).sum();
let perimeter = 4.0 * total_area.sqrt();
let mut total_power = 0.0_f32;
let mut power_per_band = [0.0_f32; NUM_BANDS];
for surface in surfaces {
let sigma = radiation_efficiency(
surface.frequency,
critical_frequency,
total_area,
perimeter,
temperature_celsius,
);
let power = rho * c * sigma * surface.area * surface.velocity * surface.velocity;
total_power += power;
let band = nearest_band(surface.frequency);
power_per_band[band] += power;
}
let swl = if total_power > 0.0 {
10.0 * (total_power / 1e-12).log10() } else {
f32::NEG_INFINITY
};
RadiationResult {
sound_power_watts: total_power,
sound_power_level_db: swl,
power_per_band,
}
}
#[must_use]
pub fn modal_radiation(
positions: &[Vec3],
areas: &[f32],
modes: &[VibrationMode],
excitation_amplitude: f32,
critical_frequency: f32,
temperature_celsius: f32,
) -> RadiationResult {
if positions.is_empty() || areas.is_empty() || modes.is_empty() {
return RadiationResult {
sound_power_watts: 0.0,
sound_power_level_db: f32::NEG_INFINITY,
power_per_band: [0.0; NUM_BANDS],
};
}
let n = positions.len().min(areas.len());
let mut surfaces = Vec::with_capacity(n * modes.len());
for mode in modes {
if mode.mode_shape.len() < n {
continue;
}
let omega = std::f32::consts::TAU * mode.frequency;
let modal_amp = if omega > 0.0 && mode.damping > 0.0 {
excitation_amplitude / (2.0 * mode.damping * omega)
} else {
0.0
};
for i in 0..n {
let velocity = modal_amp * mode.mode_shape[i];
if velocity.abs() > f32::EPSILON {
surfaces.push(VibratingSurface {
position: positions[i],
normal: Vec3::Y, area: areas[i],
velocity: velocity.abs(),
frequency: mode.frequency,
});
}
}
}
radiated_sound_power(&surfaces, critical_frequency, temperature_celsius)
}
#[must_use]
#[inline]
fn nearest_band(frequency: f32) -> usize {
let bands = &crate::material::FREQUENCY_BANDS;
let mut best = 0;
let mut best_dist = (frequency - bands[0]).abs();
for (i, &f) in bands.iter().enumerate().skip(1) {
let dist = (frequency - f).abs();
if dist < best_dist {
best = i;
best_dist = dist;
}
}
best
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn radiation_efficiency_below_critical() {
let sigma = radiation_efficiency(100.0, 2500.0, 10.0, 12.0, 20.0);
assert!(
sigma > 0.0 && sigma < 0.5,
"below critical should have low sigma for large plate, got {sigma}"
);
}
#[test]
fn radiation_efficiency_above_critical() {
let sigma = radiation_efficiency(5000.0, 2500.0, 1.0, 4.0, 20.0);
assert!(
sigma > 0.5,
"above critical should have high sigma, got {sigma}"
);
}
#[test]
fn radiation_efficiency_in_range() {
for f in [63.0, 125.0, 500.0, 1000.0, 4000.0, 8000.0] {
let sigma = radiation_efficiency(f, 2000.0, 2.0, 6.0, 20.0);
assert!(
(0.0..=1.0).contains(&sigma),
"sigma should be [0,1] at {f} Hz, got {sigma}"
);
}
}
#[test]
fn radiated_power_from_vibrating_plate() {
let surfaces = vec![VibratingSurface {
position: Vec3::ZERO,
normal: Vec3::Y,
area: 1.0,
velocity: 0.001, frequency: 1000.0,
}];
let result = radiated_sound_power(&surfaces, 2500.0, 20.0);
assert!(result.sound_power_watts > 0.0);
assert!(result.sound_power_level_db.is_finite());
}
#[test]
fn radiated_power_empty() {
let result = radiated_sound_power(&[], 2500.0, 20.0);
assert_eq!(result.sound_power_watts, 0.0);
}
#[test]
fn radiated_power_increases_with_velocity() {
let make = |v: f32| {
radiated_sound_power(
&[VibratingSurface {
position: Vec3::ZERO,
normal: Vec3::Y,
area: 1.0,
velocity: v,
frequency: 1000.0,
}],
2500.0,
20.0,
)
};
let low = make(0.001);
let high = make(0.01);
assert!(high.sound_power_watts > low.sound_power_watts);
}
#[test]
fn modal_radiation_produces_output() {
let positions = vec![Vec3::ZERO, Vec3::X, Vec3::new(0.0, 0.0, 1.0)];
let areas = vec![0.5, 0.5, 0.5];
let modes = vec![VibrationMode {
frequency: 500.0,
damping: 0.02,
mode_shape: vec![1.0, 0.5, -0.5],
}];
let result = modal_radiation(&positions, &areas, &modes, 1.0, 2500.0, 20.0);
assert!(result.sound_power_watts > 0.0);
}
#[test]
fn modal_radiation_empty() {
let result = modal_radiation(&[], &[], &[], 1.0, 2500.0, 20.0);
assert_eq!(result.sound_power_watts, 0.0);
}
#[test]
fn nearest_band_test() {
assert_eq!(nearest_band(63.0), 0);
assert_eq!(nearest_band(1000.0), 4);
assert_eq!(nearest_band(8000.0), 7);
assert_eq!(nearest_band(3000.0), 5); }
#[test]
fn radiation_result_serializes() {
let result = RadiationResult {
sound_power_watts: 0.001,
sound_power_level_db: 90.0,
power_per_band: [0.0; NUM_BANDS],
};
let json = serde_json::to_string(&result).unwrap();
let back: RadiationResult = serde_json::from_str(&json).unwrap();
assert_eq!(result, back);
}
}