Skip to main content

math_audio_wave/analytical/
solutions_2d.rs

1//! 2D Analytical Solutions
2//!
3//! Exact solutions for 2D acoustic scattering problems, primarily
4//! cylinder scattering using Bessel and Hankel function series.
5
6use super::{AnalyticalSolution, Point, SPEED_OF_SOUND};
7use num_complex::Complex64;
8use spec_math::Bessel;
9use std::f64::consts::PI;
10
11/// Cylinder scattering: rigid circular cylinder in a plane wave
12///
13/// Analytical solution using Bessel/Hankel function series:
14///
15/// ```text
16/// p(r,θ) = exp(ikr cos θ) + Σ aₙ Hₙ⁽¹⁾(kr) cos(nθ)
17/// ```
18///
19/// where Hₙ⁽¹⁾ are Hankel functions of the first kind.
20///
21/// For a **rigid cylinder** (Neumann boundary condition: ∂p/∂n = 0):
22/// ```text
23/// aₙ = -εₙ iⁿ Jₙ'(ka) / Hₙ⁽¹⁾'(ka)
24/// ```
25/// where εₙ = 1 for n=0, εₙ = 2 for n>0 (Neumann factor).
26///
27/// # Arguments
28///
29/// * `wave_number` - k = 2πf/c
30/// * `radius` - Cylinder radius a
31/// * `num_terms` - Number of terms in series (typically 2*ka + 10)
32/// * `r_points` - Radial evaluation points (must be >= radius)
33/// * `theta_points` - Angular evaluation points
34///
35/// # References
36///
37/// - Bowman, Senior, Uslenghi, "Electromagnetic and Acoustic Scattering
38///   by Simple Shapes", 1987, Section 5.3
39///
40/// # Example
41///
42/// ```rust
43/// use math_audio_wave::analytical::cylinder_scattering_2d;
44/// use std::f64::consts::PI;
45///
46/// // ka = 1 (low frequency)
47/// let solution = cylinder_scattering_2d(
48///     1.0, 1.0, 20,
49///     vec![1.5, 2.0, 3.0],
50///     vec![0.0, PI/4.0, PI/2.0, PI]
51/// );
52/// ```
53pub fn cylinder_scattering_2d(
54    wave_number: f64,
55    radius: f64,
56    num_terms: usize,
57    r_points: Vec<f64>,
58    theta_points: Vec<f64>,
59) -> AnalyticalSolution {
60    let ka = wave_number * radius;
61
62    // Compute scattering coefficients aₙ for rigid cylinder
63    let coefficients = compute_rigid_cylinder_coefficients(ka, num_terms);
64
65    // Generate grid
66    let mut positions = Vec::new();
67    let mut pressure = Vec::new();
68
69    for &r in &r_points {
70        let kr = wave_number * r;
71
72        // Precompute Hankel functions at this radius
73        let hankels: Vec<Complex64> = (0..num_terms)
74            .map(|n| {
75                let n_i64 = n as i64;
76                Complex64::new(bessel_j(n_i64, kr), bessel_y(n_i64, kr))
77            })
78            .collect();
79
80        for &theta in &theta_points {
81            positions.push(Point::from_polar(r, theta));
82
83            // Incident wave: exp(ikr cos θ)
84            let incident = Complex64::new((kr * theta.cos()).cos(), (kr * theta.cos()).sin());
85
86            // Scattered wave: uses cosine expansion (exploiting symmetry)
87            let mut scattered = Complex64::new(0.0, 0.0);
88
89            for n in 0..num_terms {
90                let epsilon_n = if n == 0 { 1.0 } else { 2.0 };
91                let cos_term = (n as f64 * theta).cos();
92                let contribution = coefficients[n] * hankels[n];
93                scattered += contribution * (epsilon_n * cos_term);
94            }
95
96            pressure.push(incident + scattered);
97        }
98    }
99
100    let frequency = wave_number * SPEED_OF_SOUND / (2.0 * PI);
101
102    AnalyticalSolution {
103        name: format!("2D Cylinder Scattering (ka={:.2})", ka),
104        dimensions: 2,
105        positions,
106        pressure,
107        wave_number,
108        frequency,
109        metadata: serde_json::json!({
110            "radius": radius,
111            "ka": ka,
112            "num_terms": num_terms,
113            "boundary_condition": "rigid",
114            "r_points": r_points,
115            "theta_range": [theta_points.first(), theta_points.last()],
116            "regime": classify_regime_2d(ka),
117        }),
118    }
119}
120
121/// Classify scattering regime based on ka
122fn classify_regime_2d(ka: f64) -> &'static str {
123    if ka < 0.3 {
124        "Rayleigh (ka << 1)"
125    } else if ka < 3.0 {
126        "Resonance (ka ~ 1)"
127    } else {
128        "Geometric (ka >> 1)"
129    }
130}
131
132/// Compute scattering coefficients for rigid cylinder
133///
134/// For Neumann BC (∂p/∂n = 0 on surface):
135/// ```text
136/// aₙ = -iⁿ Jₙ'(ka) / Hₙ⁽¹⁾'(ka)
137/// ```
138fn compute_rigid_cylinder_coefficients(ka: f64, num_terms: usize) -> Vec<Complex64> {
139    let mut coefficients = Vec::with_capacity(num_terms);
140
141    for n in 0..num_terms {
142        let n_i64 = n as i64;
143        let n_f64 = n as f64;
144
145        // Derivatives using recurrence: J_n'(x) = J_{n-1}(x) - n/x * J_n(x)
146        let jn = bessel_j(n_i64, ka);
147        let jn_minus_1 = if n > 0 {
148            bessel_j(n_i64 - 1, ka)
149        } else {
150            -bessel_j(1, ka) // J_{-1} = -J_1
151        };
152        let jn_prime = jn_minus_1 - n_f64 / ka * jn;
153
154        // Same for Hankel: H_n'(x) = J_n'(x) + i*Y_n'(x)
155        let yn = bessel_y(n_i64, ka);
156        let yn_minus_1 = if n > 0 {
157            bessel_y(n_i64 - 1, ka)
158        } else {
159            -bessel_y(1, ka)
160        };
161        let yn_prime = yn_minus_1 - n_f64 / ka * yn;
162
163        let hankel_prime = Complex64::new(jn_prime, yn_prime);
164
165        // i^n = exp(i*n*π/2)
166        let i_power_n = Complex64::new((n_f64 * PI / 2.0).cos(), (n_f64 * PI / 2.0).sin());
167
168        let a_n = -jn_prime / hankel_prime * i_power_n;
169
170        coefficients.push(a_n);
171    }
172
173    coefficients
174}
175
176/// Cylindrical Bessel function of the first kind, order `n`
177fn bessel_j(n: i64, x: f64) -> f64 {
178    x.bessel_jv(n as f64)
179}
180
181/// Cylindrical Bessel function of the second kind (Neumann), order `n`
182fn bessel_y(n: i64, x: f64) -> f64 {
183    x.bessel_yv(n as f64)
184}
185
186/// Pressure directivity pattern (far-field)
187///
188/// In the far-field (kr >> 1), the scattered pressure becomes:
189/// ```text
190/// p_scattered ~ exp(ikr) / √(2πkr) * f(θ)
191/// ```
192///
193/// where f(θ) is the scattering amplitude (directivity).
194pub fn cylinder_directivity_2d(
195    wave_number: f64,
196    radius: f64,
197    num_terms: usize,
198    theta_points: Vec<f64>,
199) -> Vec<Complex64> {
200    let ka = wave_number * radius;
201    let coefficients = compute_rigid_cylinder_coefficients(ka, num_terms);
202
203    theta_points
204        .iter()
205        .map(|&theta| {
206            let mut directivity = Complex64::new(0.0, 0.0);
207
208            for (n, coeff) in coefficients.iter().enumerate().take(num_terms) {
209                let epsilon_n = if n == 0 { 1.0 } else { 2.0 };
210                let cos_term = (n as f64 * theta).cos();
211                directivity += coeff * (epsilon_n * cos_term);
212            }
213
214            directivity
215        })
216        .collect()
217}
218
219/// Total scattering cross-section (2D)
220///
221/// The scattering cross-section measures the total scattered power
222/// normalized by the incident intensity.
223///
224/// ```text
225/// σ = (4/k) * [|a₀|² + 2 * Σₙ₌₁ |aₙ|²]
226/// ```
227pub fn cylinder_scattering_cross_section_2d(
228    wave_number: f64,
229    radius: f64,
230    num_terms: usize,
231) -> f64 {
232    let ka = wave_number * radius;
233    let coefficients = compute_rigid_cylinder_coefficients(ka, num_terms);
234
235    let mut sum_sq = 0.0;
236    for (n, a_n) in coefficients.iter().enumerate() {
237        let epsilon_n = if n == 0 { 1.0 } else { 2.0 };
238        sum_sq += epsilon_n * a_n.norm_sqr();
239    }
240
241    4.0 / wave_number * sum_sq
242}
243
244/// 2D plane wave: p(x,y) = exp(ik(x cos θ + y sin θ))
245///
246/// A plane wave propagating in direction θ (from positive x-axis).
247///
248/// # Arguments
249///
250/// * `wave_number` - k = 2πf/c
251/// * `direction` - θ, propagation direction (radians)
252/// * `x_points` - x-coordinates
253/// * `y_points` - y-coordinates
254pub fn plane_wave_2d(
255    wave_number: f64,
256    direction: f64,
257    x_points: Vec<f64>,
258    y_points: Vec<f64>,
259) -> AnalyticalSolution {
260    let cos_theta = direction.cos();
261    let sin_theta = direction.sin();
262
263    let mut positions = Vec::new();
264    let mut pressure = Vec::new();
265
266    for &x in &x_points {
267        for &y in &y_points {
268            positions.push(Point::new_2d(x, y));
269
270            let phase = wave_number * (x * cos_theta + y * sin_theta);
271            pressure.push(Complex64::new(phase.cos(), phase.sin()));
272        }
273    }
274
275    let frequency = wave_number * SPEED_OF_SOUND / (2.0 * PI);
276
277    AnalyticalSolution {
278        name: format!("2D Plane Wave (k={}, θ={:.2})", wave_number, direction),
279        dimensions: 2,
280        positions,
281        pressure,
282        wave_number,
283        frequency,
284        metadata: serde_json::json!({
285            "direction": direction,
286            "direction_vector": [cos_theta, sin_theta],
287            "wavelength": 2.0 * PI / wave_number,
288        }),
289    }
290}
291
292#[cfg(test)]
293mod tests {
294    use super::*;
295    use approx::assert_abs_diff_eq;
296
297    #[test]
298    fn test_cylinder_low_frequency() {
299        // Low frequency: ka = 0.1 (Rayleigh scattering)
300        let k = 0.1;
301        let a = 1.0;
302
303        let solution = cylinder_scattering_2d(k, a, 10, vec![2.0], vec![0.0, PI / 2.0, PI]);
304
305        // At low frequency, scattering should be weak
306        // Total field ≈ incident field
307        for p in &solution.pressure {
308            assert!(p.norm() > 0.5); // Not completely scattered
309            assert!(p.norm() < 2.0); // Not strongly scattered
310        }
311    }
312
313    #[test]
314    fn test_cylinder_boundary_condition() {
315        // On surface of rigid cylinder, solution should be well-defined
316        let k = 2.0;
317        let a = 1.0;
318
319        let solution = cylinder_scattering_2d(
320            k,
321            a,
322            30,
323            vec![a], // On surface
324            (0..36).map(|i| i as f64 * 2.0 * PI / 36.0).collect(),
325        );
326
327        // All pressures should be finite
328        for p in &solution.pressure {
329            assert!(p.norm().is_finite());
330        }
331    }
332
333    #[test]
334    fn test_symmetry() {
335        // Plane wave at θ=0 → solution should be symmetric about x-axis
336        let k = 1.0;
337        let a = 1.0;
338
339        let solution = cylinder_scattering_2d(k, a, 20, vec![2.0], vec![PI / 4.0, -PI / 4.0]);
340
341        // Magnitudes should be equal due to symmetry
342        assert_abs_diff_eq!(
343            solution.pressure[0].norm(),
344            solution.pressure[1].norm(),
345            epsilon = 1e-6
346        );
347    }
348
349    #[test]
350    fn test_bessel_functions() {
351        // Verify Bessel function implementation
352        let x = 1.0;
353
354        // J_0(1) ≈ 0.7651976865579666
355        assert_abs_diff_eq!(bessel_j(0, x), 0.7651976865579666, epsilon = 1e-10);
356
357        // J_1(1) ≈ 0.4400505857449335
358        assert_abs_diff_eq!(bessel_j(1, x), 0.4400505857449335, epsilon = 1e-10);
359    }
360
361    #[test]
362    fn test_scattering_cross_section() {
363        // Scattering cross-section should be positive
364        let k = 1.0;
365        let a = 1.0;
366
367        let sigma = cylinder_scattering_cross_section_2d(k, a, 30);
368        assert!(sigma > 0.0);
369        assert!(sigma.is_finite());
370    }
371
372    #[test]
373    fn test_plane_wave_2d() {
374        let k = 1.0;
375        let direction = 0.0; // Along x-axis
376
377        let solution = plane_wave_2d(k, direction, vec![0.0, 1.0, 2.0], vec![0.0]);
378
379        // At (0,0): exp(0) = 1
380        assert_abs_diff_eq!(solution.pressure[0].re, 1.0, epsilon = 1e-10);
381
382        // |exp(ikx)| = 1 for all points
383        for p in &solution.pressure {
384            assert_abs_diff_eq!(p.norm(), 1.0, epsilon = 1e-10);
385        }
386    }
387}