math_wave/analytical/
solutions_3d.rs

1//! 3D Analytical Solutions
2//!
3//! Exact solutions for 3D acoustic scattering problems, primarily
4//! sphere scattering using Mie theory (spherical Bessel/Hankel functions).
5
6use super::{AnalyticalSolution, Point, SPEED_OF_SOUND};
7use num_complex::Complex64;
8use std::f64::consts::PI;
9
10/// Sphere scattering: rigid sphere in a plane wave (Mie theory)
11///
12/// Analytical solution using spherical harmonics expansion:
13///
14/// ```text
15/// p(r,θ) = Σₙ (2n+1) iⁿ [jₙ(kr) - aₙ hₙ⁽¹⁾(kr)] Pₙ(cos θ)
16/// ```
17///
18/// For a **rigid sphere** (Neumann BC: ∂p/∂r = 0 on surface):
19/// ```text
20/// aₙ = jₙ'(ka) / hₙ⁽¹⁾'(ka)
21/// ```
22///
23/// where:
24/// - `jₙ` = spherical Bessel functions of the first kind
25/// - `hₙ⁽¹⁾` = spherical Hankel functions of the first kind
26/// - `Pₙ` = Legendre polynomials
27///
28/// # Arguments
29///
30/// * `wave_number` - k = 2πf/c
31/// * `radius` - Sphere radius a
32/// * `num_terms` - Number of terms (typically ka + 10)
33/// * `r_points` - Radial evaluation points (must be >= radius)
34/// * `theta_points` - Polar angle points (from z-axis, 0 to π)
35///
36/// # References
37///
38/// - Morse & Ingard, "Theoretical Acoustics", 1968, Section 8.3
39/// - Bowman et al., "Electromagnetic and Acoustic Scattering", 1987
40///
41/// # Example
42///
43/// ```rust
44/// use math_wave::analytical::sphere_scattering_3d;
45/// use std::f64::consts::PI;
46///
47/// // ka = 1 (Mie regime)
48/// let solution = sphere_scattering_3d(
49///     1.0,  // wave_number
50///     1.0,  // radius
51///     20,   // num_terms
52///     vec![1.5, 2.0, 3.0],  // r
53///     vec![0.0, PI/4.0, PI/2.0, PI],  // theta
54/// );
55/// ```
56pub fn sphere_scattering_3d(
57    wave_number: f64,
58    radius: f64,
59    num_terms: usize,
60    r_points: Vec<f64>,
61    theta_points: Vec<f64>,
62) -> AnalyticalSolution {
63    let ka = wave_number * radius;
64
65    // Compute scattering coefficients for rigid sphere
66    let coefficients = compute_rigid_sphere_coefficients(ka, num_terms);
67
68    // Generate grid (axisymmetric, so φ = 0)
69    let mut positions = Vec::new();
70    let mut pressure = Vec::new();
71
72    for &r in &r_points {
73        for &theta in &theta_points {
74            positions.push(Point::from_spherical(r, theta, 0.0));
75
76            let kr = wave_number * r;
77            let cos_theta = theta.cos();
78
79            // Total field: Σₙ (2n+1) iⁿ [jₙ(kr) - aₙ hₙ⁽¹⁾(kr)] Pₙ(cos θ)
80            let mut total = Complex64::new(0.0, 0.0);
81
82            for (n, coeff) in coefficients.iter().enumerate().take(num_terms) {
83                let n_f64 = n as f64;
84
85                // (2n+1)
86                let prefactor = 2.0 * n_f64 + 1.0;
87
88                // iⁿ = exp(i*n*π/2)
89                let i_power_n = Complex64::new((n_f64 * PI / 2.0).cos(), (n_f64 * PI / 2.0).sin());
90
91                // Spherical Bessel jₙ(kr)
92                let jn = spherical_bessel_j(n, kr);
93
94                // Spherical Hankel hₙ⁽¹⁾(kr) = jₙ(kr) + i*yₙ(kr)
95                let yn = spherical_bessel_y(n, kr);
96                let hn = Complex64::new(jn, yn);
97
98                // Legendre polynomial Pₙ(cos θ)
99                let pn = legendre_p(n, cos_theta);
100
101                // Add term
102                total += prefactor * i_power_n * (jn - coeff * hn) * pn;
103            }
104
105            pressure.push(total);
106        }
107    }
108
109    let frequency = wave_number * SPEED_OF_SOUND / (2.0 * PI);
110
111    AnalyticalSolution {
112        name: format!("3D Sphere Scattering (ka={:.2})", ka),
113        dimensions: 3,
114        positions,
115        pressure,
116        wave_number,
117        frequency,
118        metadata: serde_json::json!({
119            "radius": radius,
120            "ka": ka,
121            "num_terms": num_terms,
122            "boundary_condition": "rigid",
123            "r_points": r_points,
124            "theta_range": [theta_points.first(), theta_points.last()],
125            "regime": classify_regime(ka),
126        }),
127    }
128}
129
130/// Classify scattering regime based on ka
131pub fn classify_regime(ka: f64) -> &'static str {
132    if ka < 0.3 {
133        "Rayleigh (ka << 1)"
134    } else if ka < 3.0 {
135        "Mie (ka ~ 1)"
136    } else {
137        "Geometric (ka >> 1)"
138    }
139}
140
141/// Compute scattering coefficients for rigid sphere
142///
143/// For Neumann BC (∂p/∂r = 0 on surface):
144/// ```text
145/// aₙ = jₙ'(ka) / hₙ⁽¹⁾'(ka)
146/// ```
147fn compute_rigid_sphere_coefficients(ka: f64, num_terms: usize) -> Vec<Complex64> {
148    let mut coefficients = Vec::with_capacity(num_terms);
149
150    for n in 0..num_terms {
151        let n_f64 = n as f64;
152
153        // Spherical Bessel and derivatives
154        let jn = spherical_bessel_j(n, ka);
155        let yn = spherical_bessel_y(n, ka);
156
157        // Derivatives: jₙ'(x) = jₙ₋₁(x) - (n+1)/x * jₙ(x)
158        let jn_minus_1 = if n > 0 {
159            spherical_bessel_j(n - 1, ka)
160        } else {
161            // j₋₁(x) = cos(x)/x
162            ka.cos() / ka
163        };
164        let jn_prime = jn_minus_1 - (n_f64 + 1.0) / ka * jn;
165
166        // Same for yₙ
167        let yn_minus_1 = if n > 0 {
168            spherical_bessel_y(n - 1, ka)
169        } else {
170            // y₋₁(x) = sin(x)/x
171            -ka.sin() / ka
172        };
173        let yn_prime = yn_minus_1 - (n_f64 + 1.0) / ka * yn;
174
175        // Hankel derivative
176        let hn_prime = Complex64::new(jn_prime, yn_prime);
177
178        let a_n = Complex64::new(jn_prime, 0.0) / hn_prime;
179
180        coefficients.push(a_n);
181    }
182
183    coefficients
184}
185
186/// Spherical Bessel function jₙ(x)
187///
188/// Relation to cylindrical Bessel: jₙ(x) = √(π/2x) * J_{n+1/2}(x)
189///
190/// Uses Miller's downward recurrence for numerical stability.
191pub fn spherical_bessel_j(n: usize, x: f64) -> f64 {
192    if x.abs() < 1e-10 {
193        return if n == 0 { 1.0 } else { 0.0 };
194    }
195
196    // For small x or small n, direct formulas
197    match n {
198        0 => x.sin() / x,
199        1 => x.sin() / (x * x) - x.cos() / x,
200        _ => {
201            // Miller's downward recurrence for stability when n > x
202            let start_n = n + (x.abs() as usize) + 20;
203
204            let mut j_next = 0.0;
205            let mut j_curr = 1e-30;
206
207            let mut values = vec![0.0; start_n + 1];
208            values[start_n] = j_curr;
209
210            for k in (0..start_n).rev() {
211                let j_prev = (2 * k + 3) as f64 / x * j_curr - j_next;
212                values[k] = j_prev;
213                j_next = j_curr;
214                j_curr = j_prev;
215            }
216
217            // Normalize using j₀(x) = sin(x)/x
218            let true_j0 = x.sin() / x;
219            let scale = true_j0 / values[0];
220
221            values[n] * scale
222        }
223    }
224}
225
226/// Spherical Bessel function yₙ(x) (Neumann function)
227///
228/// Relation: yₙ(x) = √(π/2x) * Y_{n+1/2}(x)
229pub fn spherical_bessel_y(n: usize, x: f64) -> f64 {
230    if x.abs() < 1e-10 {
231        return f64::NEG_INFINITY;
232    }
233
234    match n {
235        0 => -x.cos() / x,
236        1 => -x.cos() / (x * x) - x.sin() / x,
237        _ => {
238            // Upward recurrence (stable for y_n)
239            let mut y_nm2 = -x.cos() / x;
240            let mut y_nm1 = -x.cos() / (x * x) - x.sin() / x;
241
242            for k in 2..=n {
243                let y_n = (2 * k - 1) as f64 / x * y_nm1 - y_nm2;
244                y_nm2 = y_nm1;
245                y_nm1 = y_n;
246            }
247
248            y_nm1
249        }
250    }
251}
252
253/// Legendre polynomial Pₙ(x)
254///
255/// Uses recurrence: (n+1)Pₙ₊₁(x) = (2n+1)x Pₙ(x) - n Pₙ₋₁(x)
256pub fn legendre_p(n: usize, x: f64) -> f64 {
257    match n {
258        0 => 1.0,
259        1 => x,
260        _ => {
261            let mut p_nm2 = 1.0;
262            let mut p_nm1 = x;
263
264            for k in 2..=n {
265                let p_n = ((2 * k - 1) as f64 * x * p_nm1 - (k - 1) as f64 * p_nm2) / k as f64;
266                p_nm2 = p_nm1;
267                p_nm1 = p_n;
268            }
269
270            p_nm1
271        }
272    }
273}
274
275/// Radar Cross Section (RCS) for sphere
276///
277/// RCS = σ = 4π/k² * Σₙ (2n+1) |aₙ|²
278pub fn sphere_rcs_3d(wave_number: f64, radius: f64, num_terms: usize) -> f64 {
279    let ka = wave_number * radius;
280    let coefficients = compute_rigid_sphere_coefficients(ka, num_terms);
281
282    let mut rcs = 0.0;
283    for (n, a_n) in coefficients.iter().enumerate() {
284        rcs += (2 * n + 1) as f64 * a_n.norm_sqr();
285    }
286
287    4.0 * PI * rcs / (wave_number * wave_number)
288}
289
290/// Scattering efficiency Q_scat = σ / (πa²)
291///
292/// Ratio of scattering cross-section to geometric cross-section.
293pub fn sphere_scattering_efficiency_3d(wave_number: f64, radius: f64, num_terms: usize) -> f64 {
294    let rcs = sphere_rcs_3d(wave_number, radius, num_terms);
295    rcs / (PI * radius * radius)
296}
297
298/// 3D plane wave: p(x,y,z) = exp(ik·r)
299///
300/// A plane wave propagating in direction (θ, φ).
301///
302/// # Arguments
303///
304/// * `wave_number` - k = 2πf/c
305/// * `theta` - Polar angle (from z-axis)
306/// * `phi` - Azimuthal angle
307/// * `points` - Evaluation points
308pub fn plane_wave_3d(
309    wave_number: f64,
310    theta: f64,
311    phi: f64,
312    points: Vec<Point>,
313) -> AnalyticalSolution {
314    // Wave vector direction
315    let kx = wave_number * theta.sin() * phi.cos();
316    let ky = wave_number * theta.sin() * phi.sin();
317    let kz = wave_number * theta.cos();
318
319    let pressure: Vec<Complex64> = points
320        .iter()
321        .map(|p| {
322            let phase = kx * p.x + ky * p.y + kz * p.z;
323            Complex64::new(phase.cos(), phase.sin())
324        })
325        .collect();
326
327    let frequency = wave_number * SPEED_OF_SOUND / (2.0 * PI);
328
329    AnalyticalSolution {
330        name: format!(
331            "3D Plane Wave (k={}, θ={:.2}, φ={:.2})",
332            wave_number, theta, phi
333        ),
334        dimensions: 3,
335        positions: points,
336        pressure,
337        wave_number,
338        frequency,
339        metadata: serde_json::json!({
340            "direction_theta": theta,
341            "direction_phi": phi,
342            "wave_vector": [kx, ky, kz],
343            "wavelength": 2.0 * PI / wave_number,
344        }),
345    }
346}
347
348/// Point source (monopole): G(r) = exp(ikr)/(4πr)
349///
350/// Green's function for the Helmholtz equation in 3D.
351///
352/// # Arguments
353///
354/// * `wave_number` - k
355/// * `source` - Source location
356/// * `points` - Field points
357pub fn point_source_3d(wave_number: f64, source: Point, points: Vec<Point>) -> AnalyticalSolution {
358    let pressure: Vec<Complex64> = points
359        .iter()
360        .map(|p| {
361            let r = p.distance_to(&source);
362            if r < 1e-15 {
363                Complex64::new(f64::INFINITY, 0.0)
364            } else {
365                let kr = wave_number * r;
366                Complex64::new(kr.cos(), kr.sin()) / (4.0 * PI * r)
367            }
368        })
369        .collect();
370
371    let frequency = wave_number * SPEED_OF_SOUND / (2.0 * PI);
372
373    AnalyticalSolution {
374        name: format!("3D Point Source (k={})", wave_number),
375        dimensions: 3,
376        positions: points,
377        pressure,
378        wave_number,
379        frequency,
380        metadata: serde_json::json!({
381            "source": [source.x, source.y, source.z],
382        }),
383    }
384}
385
386#[cfg(test)]
387mod tests {
388    use super::*;
389    use approx::assert_abs_diff_eq;
390
391    #[test]
392    fn test_spherical_bessel_j0() {
393        // j₀(x) = sin(x)/x
394        let x = 1.0;
395        assert_abs_diff_eq!(spherical_bessel_j(0, x), x.sin() / x, epsilon = 1e-10);
396
397        let x = PI;
398        assert_abs_diff_eq!(spherical_bessel_j(0, x), 0.0, epsilon = 1e-10);
399    }
400
401    #[test]
402    fn test_spherical_bessel_j1() {
403        // j₁(x) = sin(x)/x² - cos(x)/x
404        let x: f64 = 2.0;
405        let expected = x.sin() / (x * x) - x.cos() / x;
406        assert_abs_diff_eq!(spherical_bessel_j(1, x), expected, epsilon = 1e-10);
407    }
408
409    #[test]
410    fn test_spherical_bessel_y0() {
411        // y₀(x) = -cos(x)/x
412        let x = 1.0;
413        assert_abs_diff_eq!(spherical_bessel_y(0, x), -x.cos() / x, epsilon = 1e-10);
414    }
415
416    #[test]
417    fn test_legendre_polynomials() {
418        // P₀(x) = 1
419        assert_abs_diff_eq!(legendre_p(0, 0.5), 1.0, epsilon = 1e-10);
420
421        // P₁(x) = x
422        assert_abs_diff_eq!(legendre_p(1, 0.5), 0.5, epsilon = 1e-10);
423
424        // P₂(x) = (3x² - 1)/2
425        let x = 0.5;
426        let expected = (3.0 * x * x - 1.0) / 2.0;
427        assert_abs_diff_eq!(legendre_p(2, x), expected, epsilon = 1e-10);
428    }
429
430    #[test]
431    fn test_sphere_rayleigh_scattering() {
432        // Rayleigh regime: ka << 1
433        let k = 0.1;
434        let a = 1.0;
435        let ka = k * a;
436
437        let rcs = sphere_rcs_3d(k, a, 10);
438
439        // RCS should be positive and finite
440        assert!(rcs > 0.0);
441        assert!(rcs.is_finite());
442
443        // In Rayleigh regime, RCS ~ (ka)⁴
444        let rayleigh_scaling = ka.powi(4);
445        assert!(rcs < rayleigh_scaling * 1000.0); // Rough check
446    }
447
448    #[test]
449    fn test_sphere_geometric_limit() {
450        // Geometric regime: ka >> 1
451        // RCS → 2πa² (twice geometric cross section)
452        let k = 20.0;
453        let a = 1.0;
454
455        let rcs = sphere_rcs_3d(k, a, 50);
456        let geometric = 2.0 * PI * a * a;
457
458        // Should approach geometric limit
459        assert!((rcs / geometric - 1.0).abs() < 0.2); // Within 20%
460    }
461
462    #[test]
463    fn test_sphere_scattering_3d() {
464        // Basic sanity test
465        let k = 1.0;
466        let a = 1.0;
467
468        let solution = sphere_scattering_3d(k, a, 20, vec![2.0], vec![0.0, PI / 2.0, PI]);
469
470        assert_eq!(solution.pressure.len(), 3);
471
472        // All pressures should be finite
473        for p in &solution.pressure {
474            assert!(p.re.is_finite());
475            assert!(p.im.is_finite());
476        }
477    }
478
479    #[test]
480    fn test_scattering_efficiency() {
481        let k = 1.0;
482        let a = 1.0;
483
484        let q_scat = sphere_scattering_efficiency_3d(k, a, 30);
485
486        // Efficiency should be positive and reasonable
487        assert!(q_scat > 0.0);
488        assert!(q_scat < 10.0);
489    }
490
491    #[test]
492    fn test_regime_classification() {
493        assert_eq!(classify_regime(0.1), "Rayleigh (ka << 1)");
494        assert_eq!(classify_regime(1.0), "Mie (ka ~ 1)");
495        assert_eq!(classify_regime(10.0), "Geometric (ka >> 1)");
496    }
497
498    #[test]
499    fn test_point_source_3d() {
500        let k = 1.0;
501        let source = Point::new_3d(0.0, 0.0, 0.0);
502        let points = vec![Point::new_3d(1.0, 0.0, 0.0), Point::new_3d(2.0, 0.0, 0.0)];
503
504        let solution = point_source_3d(k, source, points);
505
506        // Magnitude should decay as 1/r
507        let ratio = solution.pressure[1].norm() / solution.pressure[0].norm();
508        assert_abs_diff_eq!(ratio, 0.5, epsilon = 1e-10);
509    }
510
511    #[test]
512    fn test_plane_wave_3d() {
513        let k = 1.0;
514        let theta = 0.0; // Along z-axis
515        let phi = 0.0;
516
517        let points = vec![Point::new_3d(0.0, 0.0, 0.0), Point::new_3d(0.0, 0.0, 1.0)];
518
519        let solution = plane_wave_3d(k, theta, phi, points);
520
521        // |exp(ikz)| = 1
522        for p in &solution.pressure {
523            assert_abs_diff_eq!(p.norm(), 1.0, epsilon = 1e-10);
524        }
525    }
526}