Skip to main content

math_audio_bem/core/
incident.rs

1//! Incident field computation
2//!
3//! Computes incident acoustic fields for BEM excitation sources:
4//! - Plane waves
5//! - Point sources (monopoles)
6//!
7//! These are used to form the right-hand side of the BEM system.
8
9use ndarray::{Array1, Array2};
10use num_complex::Complex64;
11use std::f64::consts::PI;
12
13use crate::core::types::PhysicsParams;
14
15/// Incident field source type
16#[derive(Debug, Clone)]
17pub enum IncidentField {
18    /// Plane wave: p = A * exp(i k·x)
19    PlaneWave {
20        /// Direction of propagation (unit vector)
21        direction: [f64; 3],
22        /// Complex amplitude
23        amplitude: Complex64,
24    },
25
26    /// Point source (monopole): p = A * exp(ikr) / (4πr)
27    PointSource {
28        /// Source position
29        position: [f64; 3],
30        /// Source strength (volume velocity amplitude)
31        strength: Complex64,
32    },
33
34    /// Multiple plane waves
35    MultiplePlaneWaves(Vec<([f64; 3], Complex64)>),
36
37    /// Multiple point sources
38    MultiplePointSources(Vec<([f64; 3], Complex64)>),
39}
40
41impl IncidentField {
42    /// Create a plane wave with unit amplitude traveling in +z direction
43    ///
44    /// This matches the standard convention for Mie scattering theory where
45    /// the incident wave travels toward +z (positive z direction).
46    pub fn plane_wave_z() -> Self {
47        IncidentField::PlaneWave {
48            direction: [0.0, 0.0, 1.0],
49            amplitude: Complex64::new(1.0, 0.0),
50        }
51    }
52
53    /// Create a plane wave with unit amplitude traveling in -z direction
54    pub fn plane_wave_neg_z() -> Self {
55        IncidentField::PlaneWave {
56            direction: [0.0, 0.0, -1.0],
57            amplitude: Complex64::new(1.0, 0.0),
58        }
59    }
60
61    /// Create a plane wave with specified direction and amplitude
62    pub fn plane_wave(direction: [f64; 3], amplitude: f64) -> Self {
63        // Normalize direction
64        let len = (direction[0].powi(2) + direction[1].powi(2) + direction[2].powi(2)).sqrt();
65        let dir = if len > 1e-10 {
66            [direction[0] / len, direction[1] / len, direction[2] / len]
67        } else {
68            [0.0, 0.0, -1.0]
69        };
70
71        IncidentField::PlaneWave {
72            direction: dir,
73            amplitude: Complex64::new(amplitude, 0.0),
74        }
75    }
76
77    /// Create a point source at given position
78    pub fn point_source(position: [f64; 3], strength: f64) -> Self {
79        IncidentField::PointSource {
80            position,
81            strength: Complex64::new(strength, 0.0),
82        }
83    }
84
85    /// Evaluate incident pressure at given points
86    ///
87    /// # Arguments
88    /// * `points` - Evaluation points (N × 3 array)
89    /// * `physics` - Physical parameters (contains wave number k)
90    ///
91    /// # Returns
92    /// Complex pressure values at each point
93    pub fn evaluate_pressure(
94        &self,
95        points: &Array2<f64>,
96        physics: &PhysicsParams,
97    ) -> Array1<Complex64> {
98        let n = points.nrows();
99        let k = physics.wave_number;
100        let mut pressure = Array1::zeros(n);
101
102        match self {
103            IncidentField::PlaneWave {
104                direction,
105                amplitude,
106            } => {
107                for i in 0..n {
108                    // k·x = k * (d·x)
109                    let kdotx = k
110                        * (direction[0] * points[[i, 0]]
111                            + direction[1] * points[[i, 1]]
112                            + direction[2] * points[[i, 2]]);
113
114                    // p = A * exp(i k·x)
115                    pressure[i] = *amplitude * Complex64::new(kdotx.cos(), kdotx.sin());
116                }
117            }
118
119            IncidentField::PointSource { position, strength } => {
120                for i in 0..n {
121                    let dx = points[[i, 0]] - position[0];
122                    let dy = points[[i, 1]] - position[1];
123                    let dz = points[[i, 2]] - position[2];
124                    let r = (dx * dx + dy * dy + dz * dz).sqrt();
125
126                    if r > 1e-10 {
127                        let kr = k * r;
128                        // p = S * exp(ikr) / (4πr)
129                        let green = Complex64::new(kr.cos(), kr.sin()) / (4.0 * PI * r);
130                        pressure[i] = *strength * green;
131                    }
132                }
133            }
134
135            IncidentField::MultiplePlaneWaves(waves) => {
136                for (direction, amplitude) in waves {
137                    for i in 0..n {
138                        let kdotx = k
139                            * (direction[0] * points[[i, 0]]
140                                + direction[1] * points[[i, 1]]
141                                + direction[2] * points[[i, 2]]);
142                        pressure[i] += *amplitude * Complex64::new(kdotx.cos(), kdotx.sin());
143                    }
144                }
145            }
146
147            IncidentField::MultiplePointSources(sources) => {
148                for (position, strength) in sources {
149                    for i in 0..n {
150                        let dx = points[[i, 0]] - position[0];
151                        let dy = points[[i, 1]] - position[1];
152                        let dz = points[[i, 2]] - position[2];
153                        let r = (dx * dx + dy * dy + dz * dz).sqrt();
154
155                        if r > 1e-10 {
156                            let kr = k * r;
157                            let green = Complex64::new(kr.cos(), kr.sin()) / (4.0 * PI * r);
158                            pressure[i] += *strength * green;
159                        }
160                    }
161                }
162            }
163        }
164
165        pressure
166    }
167
168    /// Evaluate incident velocity (∂p/∂n) at given points with normals
169    ///
170    /// # Arguments
171    /// * `points` - Evaluation points (N × 3 array)
172    /// * `normals` - Unit normal vectors at each point (N × 3 array)
173    /// * `physics` - Physical parameters
174    ///
175    /// # Returns
176    /// Complex normal velocity values (actually ∂p/∂n, not v_n)
177    pub fn evaluate_normal_derivative(
178        &self,
179        points: &Array2<f64>,
180        normals: &Array2<f64>,
181        physics: &PhysicsParams,
182    ) -> Array1<Complex64> {
183        let n = points.nrows();
184        let k = physics.wave_number;
185        let mut dpdn = Array1::zeros(n);
186
187        match self {
188            IncidentField::PlaneWave {
189                direction,
190                amplitude,
191            } => {
192                for i in 0..n {
193                    let kdotx = k
194                        * (direction[0] * points[[i, 0]]
195                            + direction[1] * points[[i, 1]]
196                            + direction[2] * points[[i, 2]]);
197
198                    // ∂p/∂n = ik (d·n) p
199                    let kdotn = k
200                        * (direction[0] * normals[[i, 0]]
201                            + direction[1] * normals[[i, 1]]
202                            + direction[2] * normals[[i, 2]]);
203
204                    let p = *amplitude * Complex64::new(kdotx.cos(), kdotx.sin());
205                    dpdn[i] = Complex64::new(0.0, kdotn) * p;
206                }
207            }
208
209            IncidentField::PointSource { position, strength } => {
210                for i in 0..n {
211                    let dx = points[[i, 0]] - position[0];
212                    let dy = points[[i, 1]] - position[1];
213                    let dz = points[[i, 2]] - position[2];
214                    let r = (dx * dx + dy * dy + dz * dz).sqrt();
215
216                    if r > 1e-10 {
217                        let kr = k * r;
218                        let _r2 = r * r;
219
220                        // ∂G/∂r = (ik - 1/r) * G
221                        let exp_ikr = Complex64::new(kr.cos(), kr.sin());
222                        let g = exp_ikr / (4.0 * PI * r);
223                        let dgdr = (Complex64::new(0.0, k) - Complex64::new(1.0 / r, 0.0)) * g;
224
225                        // ∂r/∂n = (x-x0)·n / r
226                        let drdn =
227                            (dx * normals[[i, 0]] + dy * normals[[i, 1]] + dz * normals[[i, 2]])
228                                / r;
229
230                        dpdn[i] = *strength * dgdr * drdn;
231                    }
232                }
233            }
234
235            IncidentField::MultiplePlaneWaves(waves) => {
236                for (direction, amplitude) in waves {
237                    for i in 0..n {
238                        let kdotx = k
239                            * (direction[0] * points[[i, 0]]
240                                + direction[1] * points[[i, 1]]
241                                + direction[2] * points[[i, 2]]);
242
243                        let kdotn = k
244                            * (direction[0] * normals[[i, 0]]
245                                + direction[1] * normals[[i, 1]]
246                                + direction[2] * normals[[i, 2]]);
247
248                        let p = *amplitude * Complex64::new(kdotx.cos(), kdotx.sin());
249                        dpdn[i] += Complex64::new(0.0, kdotn) * p;
250                    }
251                }
252            }
253
254            IncidentField::MultiplePointSources(sources) => {
255                for (position, strength) in sources {
256                    for i in 0..n {
257                        let dx = points[[i, 0]] - position[0];
258                        let dy = points[[i, 1]] - position[1];
259                        let dz = points[[i, 2]] - position[2];
260                        let r = (dx * dx + dy * dy + dz * dz).sqrt();
261
262                        if r > 1e-10 {
263                            let kr = k * r;
264                            let exp_ikr = Complex64::new(kr.cos(), kr.sin());
265                            let g = exp_ikr / (4.0 * PI * r);
266                            let dgdr = (Complex64::new(0.0, k) - Complex64::new(1.0 / r, 0.0)) * g;
267                            let drdn = (dx * normals[[i, 0]]
268                                + dy * normals[[i, 1]]
269                                + dz * normals[[i, 2]])
270                                / r;
271
272                            dpdn[i] += *strength * dgdr * drdn;
273                        }
274                    }
275                }
276            }
277        }
278
279        dpdn
280    }
281
282    /// Compute the right-hand side vector for BEM
283    ///
284    /// For exterior Neumann problem (rigid scatterer) using DIRECT formulation
285    /// where the unknown is the total surface pressure p:
286    ///
287    /// The RHS comes from the incident field contribution to the integral equation.
288    /// From NumCalc NC_IncidentWaveRHS:
289    ///   RHS = -(γ*p_inc + β*τ*∂p_inc/∂n)
290    ///
291    /// For exterior problems: τ = +1, γ = 1
292    /// For interior problems: τ = -1
293    pub fn compute_rhs(
294        &self,
295        element_centers: &Array2<f64>,
296        element_normals: &Array2<f64>,
297        physics: &PhysicsParams,
298        use_burton_miller: bool,
299    ) -> Array1<Complex64> {
300        if use_burton_miller {
301            let beta = physics.burton_miller_beta();
302            self.compute_rhs_with_beta(element_centers, element_normals, physics, beta)
303        } else {
304            // CBIE only: RHS = -γ*p_inc
305            let gamma = Complex64::new(physics.gamma(), 0.0);
306            let p_inc = self.evaluate_pressure(element_centers, physics);
307            -gamma * p_inc
308        }
309    }
310
311    /// Compute RHS with custom Burton-Miller coupling parameter
312    ///
313    /// From NumCalc NC_IncidentWaveRHS:
314    ///   RHS = -(γ*p_inc + β*τ*∂p_inc/∂n)
315    ///
316    /// This formula matches the C++ implementation for proper BEM assembly.
317    pub fn compute_rhs_with_beta(
318        &self,
319        element_centers: &Array2<f64>,
320        element_normals: &Array2<f64>,
321        physics: &PhysicsParams,
322        beta: Complex64,
323    ) -> Array1<Complex64> {
324        // Get incident pressure at collocation points
325        let p_inc = self.evaluate_pressure(element_centers, physics);
326
327        // Get incident velocity (normal derivative) at collocation points
328        let dpdn = self.evaluate_normal_derivative(element_centers, element_normals, physics);
329
330        // Parameters from physics
331        let gamma = Complex64::new(physics.gamma(), 0.0);
332        let tau = Complex64::new(physics.tau, 0.0);
333
334        // Burton-Miller RHS: -(γ*p_inc + β*τ*∂p_inc/∂n)
335        // This matches NC_IncidentWaveRHS: zrc -= zg*Gama3 + zv*(zBta3*Tao_)
336        let mut rhs = Array1::zeros(element_centers.nrows());
337        for i in 0..rhs.len() {
338            rhs[i] = -(gamma * p_inc[i] + beta * tau * dpdn[i]);
339        }
340
341        rhs
342    }
343}
344
345#[cfg(test)]
346mod tests {
347    use super::*;
348
349    fn make_physics(k: f64) -> PhysicsParams {
350        let c = 343.0;
351        let freq = k * c / (2.0 * PI);
352        PhysicsParams::new(freq, c, 1.21, false)
353    }
354
355    #[test]
356    fn test_plane_wave_on_axis() {
357        let incident = IncidentField::plane_wave([0.0, 0.0, 1.0], 1.0);
358        let physics = make_physics(1.0);
359
360        // Points on z-axis
361        let points =
362            Array2::from_shape_vec((3, 3), vec![0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, -1.0])
363                .unwrap();
364
365        let p = incident.evaluate_pressure(&points, &physics);
366
367        // At z=0: exp(0) = 1
368        assert!((p[0].re - 1.0).abs() < 1e-10);
369        assert!(p[0].im.abs() < 1e-10);
370
371        // At z=1: exp(ik) = exp(i) = cos(1) + i*sin(1)
372        // Since direction is +z, kdotx = k*z = 1 at z=1
373        assert!((p[1].re - (1.0_f64).cos()).abs() < 1e-10);
374        assert!((p[1].im - (1.0_f64).sin()).abs() < 1e-10);
375    }
376
377    #[test]
378    fn test_point_source_decay() {
379        let incident = IncidentField::point_source([0.0, 0.0, 0.0], 1.0);
380        let physics = make_physics(1.0);
381
382        // Points at different distances
383        let points =
384            Array2::from_shape_vec((3, 3), vec![1.0, 0.0, 0.0, 2.0, 0.0, 0.0, 4.0, 0.0, 0.0])
385                .unwrap();
386
387        let p = incident.evaluate_pressure(&points, &physics);
388
389        // Pressure should decay as 1/r
390        let ratio_1_2 = p[0].norm() / p[1].norm();
391        let ratio_2_4 = p[1].norm() / p[2].norm();
392
393        assert!((ratio_1_2 - 2.0).abs() < 0.1);
394        assert!((ratio_2_4 - 2.0).abs() < 0.1);
395    }
396
397    #[test]
398    fn test_plane_wave_normal_derivative() {
399        let incident = IncidentField::plane_wave([0.0, 0.0, 1.0], 1.0);
400        let physics = make_physics(1.0);
401
402        // Point with normal pointing in +z direction (along wave direction)
403        let points = Array2::from_shape_vec((1, 3), vec![0.0, 0.0, 0.0]).unwrap();
404        let normals = Array2::from_shape_vec((1, 3), vec![0.0, 0.0, 1.0]).unwrap();
405
406        let dpdn = incident.evaluate_normal_derivative(&points, &normals, &physics);
407
408        // ∂p/∂n = ik (d·n) p = ik * (+1) * 1 = +ik
409        assert!(dpdn[0].re.abs() < 1e-10);
410        assert!((dpdn[0].im - 1.0).abs() < 1e-10);
411    }
412
413    #[test]
414    fn test_rhs_computation() {
415        let incident = IncidentField::plane_wave([0.0, 0.0, 1.0], 1.0);
416        let physics = make_physics(1.0);
417
418        let centers = Array2::from_shape_vec((1, 3), vec![0.0, 0.0, 1.0]).unwrap();
419        let normals = Array2::from_shape_vec((1, 3), vec![0.0, 0.0, 1.0]).unwrap();
420
421        let rhs = incident.compute_rhs(&centers, &normals, &physics, false);
422
423        // RHS = -∂p_inc/∂n, should be non-zero
424        assert!(rhs[0].norm() > 0.0);
425    }
426}