Skip to main content

math_audio_wave/special/
helmholtz.rs

1//! Helmholtz Green's function and derivatives
2//!
3//! The 3D Helmholtz Green's function is:
4//! ```text
5//! G(x, y) = exp(ik|x-y|) / (4π|x-y|)
6//! ```
7//!
8//! This module provides computation of G and its normal derivatives,
9//! which form the kernels for BEM integration.
10
11use crate::Point;
12use num_complex::Complex64;
13use std::f64::consts::PI;
14
15/// 3D Helmholtz Green's function G = exp(ikr)/(4πr)
16///
17/// # Arguments
18/// * `r` - Distance |x - y|
19/// * `k` - Wave number
20///
21/// # Returns
22/// Complex value exp(ikr)/(4πr)
23///
24/// # Example
25/// ```
26/// use math_audio_wave::special::helmholtz::greens_function_3d;
27///
28/// let g = greens_function_3d(1.0, 2.0);
29/// // |G| = 1/(4.0 * std::f64::consts::PI) for any k
30/// assert!((g.norm() - 1.0/(4.0 * std::f64::consts::PI)).abs() < 1e-10);
31/// ```
32#[inline]
33pub fn greens_function_3d(r: f64, k: f64) -> Complex64 {
34    if r < 1e-15 {
35        // Singular at r = 0
36        return Complex64::new(f64::INFINITY, 0.0);
37    }
38
39    let kr = k * r;
40    let exp_ikr = Complex64::new(kr.cos(), kr.sin());
41    exp_ikr / (4.0 * PI * r)
42}
43
44/// 2D Helmholtz Green's function G = (i/4) H_0^(1)(kr)
45///
46/// Uses the Hankel function of the first kind.
47///
48/// # Arguments
49/// * `r` - Distance |x - y|
50/// * `k` - Wave number
51#[inline]
52pub fn greens_function_2d(r: f64, k: f64) -> Complex64 {
53    use spec_math::Bessel;
54
55    if r < 1e-15 {
56        return Complex64::new(f64::INFINITY, 0.0);
57    }
58
59    let kr = k * r;
60    let j0 = kr.bessel_jv(0.0);
61    let y0 = kr.bessel_yv(0.0);
62    let h0 = Complex64::new(j0, y0);
63
64    Complex64::new(0.0, 0.25) * h0
65}
66
67/// Gradient of 3D Green's function ∇_y G
68///
69/// ```text
70/// ∇_y G = (ik - 1/r) * G * (y - x)/r
71/// ```
72///
73/// # Arguments
74/// * `source` - Source point x
75/// * `field` - Field point y
76/// * `k` - Wave number
77///
78/// # Returns
79/// Complex 3-vector [∂G/∂y_x, ∂G/∂y_y, ∂G/∂y_z]
80pub fn greens_function_gradient_3d(source: &Point, field: &Point, k: f64) -> [Complex64; 3] {
81    let rx = field.x - source.x;
82    let ry = field.y - source.y;
83    let rz = field.z - source.z;
84    let r = (rx * rx + ry * ry + rz * rz).sqrt();
85
86    if r < 1e-15 {
87        return [
88            Complex64::new(f64::INFINITY, 0.0),
89            Complex64::new(f64::INFINITY, 0.0),
90            Complex64::new(f64::INFINITY, 0.0),
91        ];
92    }
93
94    let g = greens_function_3d(r, k);
95    let factor = Complex64::new(-1.0 / r, k) * g;
96
97    [factor * rx / r, factor * ry / r, factor * rz / r]
98}
99
100/// Normal derivative of 3D Green's function ∂G/∂n_y
101///
102/// ```text
103/// ∂G/∂n_y = ∇_y G · n_y = (ik - 1/r) * G * (y-x)·n_y / r
104/// ```
105///
106/// This is the kernel for the single-layer potential.
107///
108/// # Arguments
109/// * `source` - Source point x
110/// * `field` - Field point y
111/// * `normal` - Unit normal at field point (n_x, n_y, n_z)
112/// * `k` - Wave number
113#[inline]
114pub fn greens_function_normal_derivative_3d(
115    source: &Point,
116    field: &Point,
117    normal: &[f64; 3],
118    k: f64,
119) -> Complex64 {
120    let rx = field.x - source.x;
121    let ry = field.y - source.y;
122    let rz = field.z - source.z;
123    let r = (rx * rx + ry * ry + rz * rz).sqrt();
124
125    if r < 1e-15 {
126        return Complex64::new(f64::INFINITY, 0.0);
127    }
128
129    let g = greens_function_3d(r, k);
130    let r_dot_n = rx * normal[0] + ry * normal[1] + rz * normal[2];
131
132    // Factor: (ik - 1/r)
133    let factor = Complex64::new(-1.0 / r, k);
134
135    factor * g * r_dot_n / r
136}
137
138/// Adjoint double layer kernel ∂G/∂n_x
139///
140/// ```text
141/// ∂G/∂n_x = -∇_y G · n_x = -(ik - 1/r) * G * (y-x)·n_x / r
142/// ```
143///
144/// This is the kernel for the adjoint double-layer potential.
145#[inline]
146pub fn greens_function_adjoint_derivative_3d(
147    source: &Point,
148    field: &Point,
149    normal_source: &[f64; 3],
150    k: f64,
151) -> Complex64 {
152    let rx = field.x - source.x;
153    let ry = field.y - source.y;
154    let rz = field.z - source.z;
155    let r = (rx * rx + ry * ry + rz * rz).sqrt();
156
157    if r < 1e-15 {
158        return Complex64::new(f64::INFINITY, 0.0);
159    }
160
161    let g = greens_function_3d(r, k);
162    let r_dot_n = rx * normal_source[0] + ry * normal_source[1] + rz * normal_source[2];
163
164    // Adjoint has opposite sign
165    let factor = Complex64::new(1.0 / r, -k);
166
167    factor * g * r_dot_n / r
168}
169
170/// Hypersingular kernel ∂²G/(∂n_x ∂n_y)
171///
172/// ```text
173/// ∂²G/(∂n_x ∂n_y) = [((ik)² - 3ik/r + 3/r²) (r·n_x)(r·n_y)/r²
174///                    - (ik - 1/r)(n_x·n_y)/r] * G
175/// ```
176pub fn greens_function_hypersingular_3d(
177    source: &Point,
178    field: &Point,
179    normal_source: &[f64; 3],
180    normal_field: &[f64; 3],
181    k: f64,
182) -> Complex64 {
183    let rx = field.x - source.x;
184    let ry = field.y - source.y;
185    let rz = field.z - source.z;
186    let r2 = rx * rx + ry * ry + rz * rz;
187    let r = r2.sqrt();
188
189    if r < 1e-15 {
190        return Complex64::new(f64::INFINITY, 0.0);
191    }
192
193    let g = greens_function_3d(r, k);
194    let ik = Complex64::new(0.0, k);
195
196    let r_dot_nx = rx * normal_source[0] + ry * normal_source[1] + rz * normal_source[2];
197    let r_dot_ny = rx * normal_field[0] + ry * normal_field[1] + rz * normal_field[2];
198    let nx_dot_ny = normal_source[0] * normal_field[0]
199        + normal_source[1] * normal_field[1]
200        + normal_source[2] * normal_field[2];
201
202    // Term 1: ((ik)² - 3ik/r + 3/r²) (r·n_x)(r·n_y)/r² * G
203    let coef1 = ik * ik - 3.0 * ik / r + Complex64::new(3.0 / r2, 0.0);
204    let term1 = coef1 * r_dot_nx * r_dot_ny / r2;
205
206    // Term 2: (ik - 1/r)(n_x·n_y)/r * G
207    let coef2 = ik - Complex64::new(1.0 / r, 0.0);
208    let term2 = coef2 * nx_dot_ny / r;
209
210    (term1 - term2) * g
211}
212
213/// Compute all four BEM kernels at once for efficiency
214///
215/// Returns (G, ∂G/∂n_y, ∂G/∂n_x, ∂²G/∂n_x∂n_y)
216pub fn all_kernels_3d(
217    source: &Point,
218    field: &Point,
219    normal_source: &[f64; 3],
220    normal_field: &[f64; 3],
221    k: f64,
222) -> (Complex64, Complex64, Complex64, Complex64) {
223    let rx = field.x - source.x;
224    let ry = field.y - source.y;
225    let rz = field.z - source.z;
226    let r2 = rx * rx + ry * ry + rz * rz;
227    let r = r2.sqrt();
228
229    if r < 1e-15 {
230        let inf = Complex64::new(f64::INFINITY, 0.0);
231        return (inf, inf, inf, inf);
232    }
233
234    let kr = k * r;
235    let exp_ikr = Complex64::new(kr.cos(), kr.sin());
236    let g = exp_ikr / (4.0 * PI * r);
237
238    let r_dot_nx = rx * normal_source[0] + ry * normal_source[1] + rz * normal_source[2];
239    let r_dot_ny = rx * normal_field[0] + ry * normal_field[1] + rz * normal_field[2];
240    let nx_dot_ny = normal_source[0] * normal_field[0]
241        + normal_source[1] * normal_field[1]
242        + normal_source[2] * normal_field[2];
243
244    let ik = Complex64::new(0.0, k);
245
246    // ∂G/∂n_y
247    let factor_dg = ik - Complex64::new(1.0 / r, 0.0);
248    let dg_dny = factor_dg * g * r_dot_ny / r;
249
250    // ∂G/∂n_x (adjoint - opposite sign)
251    let dg_dnx = -factor_dg * g * r_dot_nx / r;
252
253    // Hypersingular
254    let coef1 = ik * ik - 3.0 * ik / r + Complex64::new(3.0 / r2, 0.0);
255    let term1 = coef1 * r_dot_nx * r_dot_ny / r2;
256    let term2 = factor_dg * nx_dot_ny / r;
257    let d2g = (term1 - term2) * g;
258
259    (g, dg_dny, dg_dnx, d2g)
260}
261
262/// Distance between two points
263#[inline]
264pub fn distance(p1: &Point, p2: &Point) -> f64 {
265    p1.distance_to(p2)
266}
267
268/// Laplace Green's function (k=0 limit): G = 1/(4πr)
269#[inline]
270pub fn laplace_greens_function_3d(r: f64) -> f64 {
271    if r < 1e-15 {
272        f64::INFINITY
273    } else {
274        1.0 / (4.0 * PI * r)
275    }
276}
277
278/// 2D Laplace Green's function: G = -ln(r)/(2π)
279#[inline]
280pub fn laplace_greens_function_2d(r: f64) -> f64 {
281    if r < 1e-15 {
282        f64::INFINITY
283    } else {
284        -r.ln() / (2.0 * PI)
285    }
286}
287
288#[cfg(test)]
289mod tests {
290    use super::*;
291
292    const EPSILON: f64 = 1e-10;
293
294    #[test]
295    fn test_greens_function_magnitude() {
296        // |G| = 1/(4πr) for any k
297        let r = 2.0;
298        let k = 1.5;
299        let g = greens_function_3d(r, k);
300
301        let expected_magnitude = 1.0 / (4.0 * PI * r);
302        assert!((g.norm() - expected_magnitude).abs() < EPSILON);
303    }
304
305    #[test]
306    fn test_greens_function_k_zero() {
307        // For k = 0, G = 1/(4πr) (Laplace Green's function)
308        let r = 1.5;
309        let g = greens_function_3d(r, 0.0);
310
311        let expected = 1.0 / (4.0 * PI * r);
312        assert!((g.re - expected).abs() < EPSILON);
313        assert!(g.im.abs() < EPSILON);
314    }
315
316    #[test]
317    fn test_normal_derivative_radial() {
318        // When y-x is parallel to n_y (radial direction)
319        let source = Point::new_3d(0.0, 0.0, 0.0);
320        let field = Point::new_3d(1.0, 0.0, 0.0);
321        let normal = [1.0, 0.0, 0.0]; // Outward normal
322
323        let k = 2.0;
324        let dg_dn = greens_function_normal_derivative_3d(&source, &field, &normal, k);
325
326        // Should be finite
327        assert!(dg_dn.norm().is_finite());
328    }
329
330    #[test]
331    fn test_normal_derivative_tangential() {
332        // When y-x is perpendicular to n_y (tangential)
333        let source = Point::new_3d(0.0, 0.0, 0.0);
334        let field = Point::new_3d(1.0, 0.0, 0.0);
335        let normal = [0.0, 1.0, 0.0]; // Tangent to radial
336
337        let k = 2.0;
338        let dg_dn = greens_function_normal_derivative_3d(&source, &field, &normal, k);
339
340        // Should be zero since (y-x)·n = 0
341        assert!(dg_dn.norm() < EPSILON);
342    }
343
344    #[test]
345    fn test_all_kernels_consistency() {
346        let source = Point::new_3d(0.0, 0.0, 0.0);
347        let field = Point::new_3d(1.0, 0.5, 0.3);
348        let n_source = [0.0, 0.0, 1.0];
349        let n_field = [1.0, 0.0, 0.0];
350        let k = 2.5;
351
352        let (g, dg_dny, dg_dnx, _d2g) = all_kernels_3d(&source, &field, &n_source, &n_field, k);
353
354        // Compare with individual functions
355        let g_single = greens_function_3d(distance(&source, &field), k);
356        let dg_dny_single = greens_function_normal_derivative_3d(&source, &field, &n_field, k);
357        let dg_dnx_single = greens_function_adjoint_derivative_3d(&source, &field, &n_source, k);
358
359        assert!((g - g_single).norm() < EPSILON);
360        assert!((dg_dny - dg_dny_single).norm() < EPSILON);
361        assert!((dg_dnx - dg_dnx_single).norm() < EPSILON);
362    }
363
364    #[test]
365    fn test_laplace_greens() {
366        let r = 2.0;
367
368        // 3D: G = 1/(4πr)
369        let g3d = laplace_greens_function_3d(r);
370        assert!((g3d - 1.0 / (4.0 * PI * r)).abs() < EPSILON);
371
372        // 2D: G = -ln(r)/(2π)
373        let g2d = laplace_greens_function_2d(r);
374        assert!((g2d - (-r.ln() / (2.0 * PI))).abs() < EPSILON);
375    }
376
377    #[test]
378    fn test_gradient() {
379        let source = Point::new_3d(0.0, 0.0, 0.0);
380        let field = Point::new_3d(1.0, 0.0, 0.0);
381        let k = 1.0;
382
383        let grad = greens_function_gradient_3d(&source, &field, k);
384
385        // Gradient should point along (field - source) direction
386        // Only x-component should be non-zero
387        assert!(grad[0].norm() > EPSILON);
388        assert!(grad[1].norm() < EPSILON);
389        assert!(grad[2].norm() < EPSILON);
390    }
391}