Skip to main content

neco_radiation/
lib.rs

1//! Acoustic radiation power estimators for vibrating surfaces.
2
3#[derive(Debug, Clone)]
4pub struct RadiationCalculator {
5    rho_air: f64,
6    c_air: f64,
7}
8
9impl RadiationCalculator {
10    pub fn new() -> Self {
11        Self {
12            rho_air: 1.225,
13            c_air: 343.0,
14        }
15    }
16
17    pub fn with_params(rho_air: f64, c_air: f64) -> Self {
18        Self { rho_air, c_air }
19    }
20
21    /// Compute radiated power from active sample points and their velocities.
22    ///
23    /// `points` and `values` must have identical length and corresponding order.
24    /// `cell_area` is the area associated with each active sample.
25    pub fn radiated_power(
26        &self,
27        points: &[[f64; 2]],
28        values: &[f64],
29        cell_area: f64,
30        freq_dominant: f64,
31    ) -> f64 {
32        assert_eq!(points.len(), values.len());
33
34        let omega = 2.0 * std::f64::consts::PI * freq_dominant;
35        let k = omega / self.c_air;
36        let prefactor = self.rho_air * omega * omega / (4.0 * std::f64::consts::PI * self.c_air);
37
38        let mut power = 0.0;
39        for a in 0..values.len() {
40            let va = values[a];
41            power += va * va * cell_area * cell_area;
42            for b in a + 1..values.len() {
43                let dx = points[a][0] - points[b][0];
44                let dy = points[a][1] - points[b][1];
45                let dist = (dx * dx + dy * dy).sqrt();
46                let kr = k * dist;
47                let sinc = if kr < 1e-10 { 1.0 } else { kr.sin() / kr };
48                power += 2.0 * va * values[b] * sinc * cell_area * cell_area;
49            }
50        }
51
52        prefactor * power
53    }
54
55    pub fn modal_efficiency(&self, radius: f64, m: i32, freq: f64) -> f64 {
56        let k = 2.0 * std::f64::consts::PI * freq / self.c_air;
57        let ka = k * radius;
58        let sigma_base = match m.unsigned_abs() {
59            0 => 1.0,
60            1 => 0.5,
61            _ => 0.25,
62        };
63        sigma_base * (ka * ka).min(1.0)
64    }
65}
66
67impl Default for RadiationCalculator {
68    fn default() -> Self {
69        Self::new()
70    }
71}
72
73#[cfg_attr(feature = "serde", derive(serde::Deserialize))]
74#[derive(Debug, Clone)]
75pub struct RadiationParams {
76    pub rho_air: f64,
77    pub c_air: f64,
78    pub max_modes: usize,
79}
80
81#[derive(Debug, Clone)]
82struct ModeData {
83    phi: Vec<f64>,
84    r_factor: f64,
85}
86
87/// Modal radiation estimator for simply-supported rectangular plates.
88#[derive(Debug, Clone)]
89pub struct ModalRadiationCalculator {
90    modes: Vec<ModeData>,
91    da: f64,
92    norm: f64,
93    n_active: usize,
94}
95
96impl ModalRadiationCalculator {
97    pub fn new(
98        params: &RadiationParams,
99        nx: usize,
100        ny: usize,
101        dx: f64,
102        active_cells: &[(usize, usize)],
103        d_val: f64,
104        rho_h: f64,
105    ) -> Self {
106        use std::f64::consts::PI;
107
108        let da = dx * dx;
109        let lx_eff = (nx as f64 - 3.0) * dx;
110        let ly_eff = (ny as f64 - 3.0) * dx;
111        let norm = 4.0 / (lx_eff * ly_eff);
112
113        let active_points: Vec<[f64; 2]> = active_cells
114            .iter()
115            .map(|&(i, j)| centered_point(i, j, nx, ny, dx))
116            .collect();
117
118        let n_cells = active_cells.len();
119        let mut dist_table = vec![0.0_f64; n_cells * (n_cells + 1) / 2];
120        let mut dist_idx = 0;
121        for a in 0..n_cells {
122            for b in a..n_cells {
123                if a == b {
124                    dist_table[dist_idx] = 0.0;
125                } else {
126                    let dx = active_points[a][0] - active_points[b][0];
127                    let dy = active_points[a][1] - active_points[b][1];
128                    dist_table[dist_idx] = (dx * dx + dy * dy).sqrt();
129                }
130                dist_idx += 1;
131            }
132        }
133
134        let m_max = (nx - 3) / 2;
135        let n_max = (ny - 3) / 2;
136        let coeff = (d_val / rho_h).sqrt();
137
138        let mut mode_list: Vec<(usize, usize, f64)> = Vec::new();
139        for m in 1..=m_max {
140            for n in 1..=n_max {
141                let kx = m as f64 * PI / lx_eff;
142                let ky = n as f64 * PI / ly_eff;
143                let omega_mn = coeff * (kx * kx + ky * ky);
144                mode_list.push((m, n, omega_mn / (2.0 * PI)));
145            }
146        }
147        mode_list.sort_by(|a, b| a.2.total_cmp(&b.2));
148        mode_list.truncate(params.max_modes);
149
150        let mut modes = Vec::with_capacity(mode_list.len());
151        for &(m, n, f_mn) in &mode_list {
152            let phi: Vec<f64> = active_cells
153                .iter()
154                .map(|&(i, j)| {
155                    (m as f64 * PI * (i as f64 - 1.0) / (nx as f64 - 3.0)).sin()
156                        * (n as f64 * PI * (j as f64 - 1.0) / (ny as f64 - 3.0)).sin()
157                })
158                .collect();
159
160            let omega = 2.0 * PI * f_mn;
161            let k = omega / params.c_air;
162            let prefactor = params.rho_air * omega * omega / (4.0 * PI * params.c_air);
163
164            let mut sum = 0.0;
165            let mut dist_idx = 0;
166            for a in 0..n_cells {
167                let phi_a = phi[a];
168                for (b, &phi_b) in phi.iter().enumerate().take(n_cells).skip(a) {
169                    let r = dist_table[dist_idx];
170                    dist_idx += 1;
171                    let kr = k * r;
172                    let sinc = if kr < 1e-10 { 1.0 } else { kr.sin() / kr };
173                    let contrib = phi_a * phi_b * sinc;
174                    if a == b {
175                        sum += contrib;
176                    } else {
177                        sum += 2.0 * contrib;
178                    }
179                }
180            }
181
182            modes.push(ModeData {
183                phi,
184                r_factor: prefactor * sum * da * da,
185            });
186        }
187
188        Self {
189            modes,
190            da,
191            norm,
192            n_active: n_cells,
193        }
194    }
195
196    pub fn radiated_power(&self, active_values: &[f64]) -> f64 {
197        assert_eq!(active_values.len(), self.n_active);
198        self.modes
199            .iter()
200            .map(|mode| {
201                let mut a_mn = 0.0;
202                for (value, phi) in active_values.iter().zip(mode.phi.iter()) {
203                    a_mn += value * phi;
204                }
205                a_mn *= self.da * self.norm;
206                mode.r_factor * a_mn * a_mn
207            })
208            .sum()
209    }
210
211    pub fn num_modes(&self) -> usize {
212        self.modes.len()
213    }
214}
215
216fn centered_point(i: usize, j: usize, nx: usize, ny: usize, dx: f64) -> [f64; 2] {
217    let cx = (nx / 2) as f64 * dx;
218    let cy = (ny / 2) as f64 * dx;
219    [i as f64 * dx - cx, j as f64 * dx - cy]
220}