Skip to main content

math_audio_wave/special/
spherical.rs

1//! Spherical Bessel and Hankel functions
2//!
3//! Direct port of NC_SphericalHankel from NC_3dFunctions.cpp.
4//! These functions are critical for FMM translation operators.
5//!
6//! ## Definitions
7//!
8//! Spherical Bessel function of first kind:
9//! ```text
10//! j_n(x) = √(π/2x) * J_{n+1/2}(x)
11//! ```
12//!
13//! Spherical Bessel function of second kind (Neumann):
14//! ```text
15//! y_n(x) = √(π/2x) * Y_{n+1/2}(x)
16//! ```
17//!
18//! Spherical Hankel function of first kind:
19//! ```text
20//! h_n^(1)(x) = j_n(x) + i * y_n(x)
21//! ```
22
23use num_complex::Complex64;
24
25/// Compute spherical Bessel functions j_n(x) for n = 0, 1, ..., order-1
26///
27/// Uses Miller's downward recurrence for numerical stability, which is
28/// essential when n > x. The recurrence relation is:
29/// ```text
30/// j_{n-1}(x) = (2n+1)/x * j_n(x) - j_{n+1}(x)
31/// ```
32///
33/// Normalization uses j_0(x) = sin(x)/x.
34///
35/// # Arguments
36/// * `order` - Number of terms (returns j_0 through j_{order-1})
37/// * `x` - Argument (must be > 0)
38///
39/// # Example
40/// ```
41/// use math_audio_wave::special::spherical::spherical_bessel_j;
42/// let j = spherical_bessel_j(5, 1.0);
43/// // j[0] = sin(1)/1 ≈ 0.8414709848
44/// ```
45pub fn spherical_bessel_j(order: usize, x: f64) -> Vec<f64> {
46    assert!(order >= 1, "Order must be at least 1");
47
48    let mut result = vec![0.0; order];
49
50    // Handle very small x
51    if x.abs() < 1e-15 {
52        result[0] = 1.0;
53        return result;
54    }
55
56    if x.abs() < 1e-10 {
57        // Series expansion for small x
58        result[0] = 1.0 - x * x / 6.0;
59        if order > 1 {
60            result[1] = x / 3.0;
61        }
62        for item in result.iter_mut().take(order).skip(2) {
63            *item = 0.0;
64        }
65        return result;
66    }
67
68    // Miller's downward recurrence algorithm
69    // Start from N >> order and x
70    let start_n = order + (x.abs() as usize) + 20;
71
72    let mut values = vec![0.0; start_n + 2];
73    values[start_n + 1] = 0.0;
74    values[start_n] = 1e-30; // Arbitrary small starting value
75
76    // Downward recurrence: j_{n-1} = (2n+1)/x * j_n - j_{n+1}
77    for k in (0..start_n).rev() {
78        values[k] = (2 * k + 3) as f64 / x * values[k + 1] - values[k + 2];
79    }
80
81    // Normalize using j_0(x) = sin(x)/x
82    let true_j0 = x.sin() / x;
83    let scale = true_j0 / values[0];
84
85    for n in 0..order {
86        result[n] = values[n] * scale;
87    }
88
89    result
90}
91
92/// Compute spherical Bessel functions y_n(x) (Neumann functions) for n = 0, 1, ..., order-1
93///
94/// Uses upward recurrence, which is stable for y_n:
95/// ```text
96/// y_{n+1}(x) = (2n+1)/x * y_n(x) - y_{n-1}(x)
97/// ```
98///
99/// Starting values:
100/// ```text
101/// y_0(x) = -cos(x)/x
102/// y_1(x) = -cos(x)/x² - sin(x)/x
103/// ```
104///
105/// # Arguments
106/// * `order` - Number of terms (returns y_0 through y_{order-1})
107/// * `x` - Argument (must be > 0)
108pub fn spherical_bessel_y(order: usize, x: f64) -> Vec<f64> {
109    assert!(order >= 1, "Order must be at least 1");
110
111    let mut result = vec![0.0; order];
112
113    if x.abs() < 1e-15 {
114        // y_n → -∞ as x → 0
115        for item in result.iter_mut().take(order) {
116            *item = f64::NEG_INFINITY;
117        }
118        return result;
119    }
120
121    let cos_x = x.cos();
122    let sin_x = x.sin();
123
124    // y_0(x) = -cos(x)/x
125    result[0] = -cos_x / x;
126
127    if order == 1 {
128        return result;
129    }
130
131    // y_1(x) = -cos(x)/x² - sin(x)/x
132    result[1] = -cos_x / (x * x) - sin_x / x;
133
134    // Upward recurrence: y_{n+1} = (2n+1)/x * y_n - y_{n-1}
135    for n in 2..order {
136        result[n] = (2 * n - 1) as f64 / x * result[n - 1] - result[n - 2];
137    }
138
139    result
140}
141
142/// Compute spherical Hankel functions of the first kind h_n^(1)(x) for n = 0, 1, ..., order-1
143///
144/// Uses the algorithm from NC_SphericalHankel in NC_3dFunctions.cpp.
145///
146/// The spherical Hankel function is:
147/// ```text
148/// h_n^(1)(x) = j_n(x) + i * y_n(x)
149/// ```
150///
151/// For the real part, uses Giebermann's continued fraction method which is
152/// more numerically stable than direct computation for large orders.
153///
154/// # Arguments
155/// * `order` - Number of terms (must be >= 2)
156/// * `x` - Argument (must be > 0)
157/// * `harmonic_factor` - +1 or -1 for time convention
158///
159/// # Returns
160/// Vector of Complex64 values h_n^(1)(x) for n = 0, ..., order-1
161pub fn spherical_hankel_first_kind(order: usize, x: f64, harmonic_factor: f64) -> Vec<Complex64> {
162    assert!(order >= 2, "Order must be at least 2");
163    assert!(x > 0.0, "Argument must be positive");
164
165    let mut result = vec![Complex64::new(0.0, 0.0); order];
166
167    // Compute imaginary part (y_n) using direct formula
168    let cos_x = x.cos();
169    let sin_x = x.sin();
170
171    let mut y_n = vec![0.0; order];
172    y_n[0] = -cos_x / x;
173    y_n[1] = -(cos_x / x + sin_x) / x;
174
175    for n in 2..order {
176        y_n[n] = (2 * n - 1) as f64 / x * y_n[n - 1] - y_n[n - 2];
177    }
178
179    // Compute real part (j_n) using Giebermann's continued fraction method
180    // This is the algorithm from NC_SphericalHankel
181    let nu = (order - 1) as f64;
182    let eps_gn = 1e-9;
183
184    // Continued fraction for g_N = j'_N / j_N
185    let mut di = (2.0 * (nu + 1.0) + 1.0) / x;
186    let mut cj = di;
187    let mut dj = 0.0;
188    let mut err_gn = 1.0;
189    let mut j = 1;
190
191    while err_gn > eps_gn {
192        let aj = -1.0;
193        let bj = (2.0 * (nu + j as f64 + 1.0) + 1.0) / x;
194
195        dj = bj + aj * dj;
196        if dj == 0.0 {
197            dj = 1e-30;
198        }
199        dj = 1.0 / dj;
200
201        cj = bj + aj / cj;
202        if cj == 0.0 {
203            cj = 1e-30;
204        }
205
206        di = di * cj * dj;
207        err_gn = (cj * dj - 1.0).abs();
208        j += 1;
209
210        if j > 1000 {
211            // Safety limit to prevent infinite loop
212            break;
213        }
214    }
215
216    let gnu = nu / x - 1.0 / di;
217
218    // Compute g_n and j_n by downward recurrence
219    let mut gg_n = vec![0.0; order];
220    let mut dg_n = vec![0.0; order];
221
222    gg_n[order - 1] = 1.0;
223    dg_n[order - 1] = gnu;
224
225    for i in (0..order - 1).rev() {
226        let di = i as f64;
227        gg_n[i] = (di + 2.0) / x * gg_n[i + 1] + dg_n[i + 1];
228        dg_n[i] = di / x * gg_n[i] - gg_n[i + 1];
229    }
230
231    // Normalize using known value
232    let dp = if gg_n[0].abs() > 1e-5 {
233        sin_x / x / gg_n[0]
234    } else {
235        (cos_x - sin_x / x) / x / dg_n[0]
236    };
237
238    // Assemble complex result
239    for n in 0..order {
240        result[n] = Complex64::new(dp * gg_n[n], harmonic_factor * y_n[n]);
241    }
242
243    result
244}
245
246/// Compute derivative of spherical Bessel j_n'(x)
247///
248/// Uses the recurrence relation:
249/// ```text
250/// j_n'(x) = j_{n-1}(x) - (n+1)/x * j_n(x)
251/// ```
252pub fn spherical_bessel_j_derivative(order: usize, x: f64) -> Vec<f64> {
253    let j = spherical_bessel_j(order + 1, x);
254    let mut result = vec![0.0; order];
255
256    for n in 0..order {
257        if n == 0 {
258            // j_0' = -j_1
259            result[0] = -j[1];
260        } else {
261            result[n] = j[n - 1] - (n + 1) as f64 / x * j[n];
262        }
263    }
264
265    result
266}
267
268/// Compute derivative of spherical Bessel y_n'(x)
269///
270/// Uses the recurrence relation:
271/// ```text
272/// y_n'(x) = y_{n-1}(x) - (n+1)/x * y_n(x)
273/// ```
274pub fn spherical_bessel_y_derivative(order: usize, x: f64) -> Vec<f64> {
275    let y = spherical_bessel_y(order + 1, x);
276    let mut result = vec![0.0; order];
277
278    for n in 0..order {
279        if n == 0 {
280            // y_0' = -y_1
281            result[0] = -y[1];
282        } else {
283            result[n] = y[n - 1] - (n + 1) as f64 / x * y[n];
284        }
285    }
286
287    result
288}
289
290#[cfg(test)]
291mod tests {
292    use super::*;
293    use std::f64::consts::PI;
294
295    const EPSILON: f64 = 1e-10;
296
297    #[test]
298    fn test_spherical_bessel_j0() {
299        // j_0(x) = sin(x)/x
300        let j = spherical_bessel_j(1, 1.0);
301        let expected = 1.0_f64.sin() / 1.0;
302        assert!((j[0] - expected).abs() < EPSILON);
303
304        let j = spherical_bessel_j(1, PI);
305        let expected = PI.sin() / PI;
306        assert!((j[0] - expected).abs() < EPSILON);
307    }
308
309    #[test]
310    fn test_spherical_bessel_j1() {
311        // j_1(x) = sin(x)/x² - cos(x)/x
312        let x = 2.0;
313        let j = spherical_bessel_j(2, x);
314        let expected = x.sin() / (x * x) - x.cos() / x;
315        assert!((j[1] - expected).abs() < EPSILON);
316    }
317
318    #[test]
319    fn test_spherical_bessel_y0() {
320        // y_0(x) = -cos(x)/x
321        let x = 1.0;
322        let y = spherical_bessel_y(1, x);
323        let expected = -x.cos() / x;
324        assert!((y[0] - expected).abs() < EPSILON);
325    }
326
327    #[test]
328    fn test_spherical_bessel_y1() {
329        // y_1(x) = -cos(x)/x² - sin(x)/x
330        let x = 2.0;
331        let y = spherical_bessel_y(2, x);
332        let expected = -x.cos() / (x * x) - x.sin() / x;
333        assert!((y[1] - expected).abs() < EPSILON);
334    }
335
336    #[test]
337    fn test_spherical_hankel_consistency() {
338        // h_n^(1) = j_n + i*y_n
339        let x = 3.0;
340        let order = 5;
341        let j = spherical_bessel_j(order, x);
342        let y = spherical_bessel_y(order, x);
343        let h = spherical_hankel_first_kind(order, x, 1.0);
344
345        for n in 0..order {
346            assert!(
347                (h[n].re - j[n]).abs() < 1e-8,
348                "Real part mismatch at n={}: {} vs {}",
349                n,
350                h[n].re,
351                j[n]
352            );
353            assert!(
354                (h[n].im - y[n]).abs() < 1e-8,
355                "Imag part mismatch at n={}: {} vs {}",
356                n,
357                h[n].im,
358                y[n]
359            );
360        }
361    }
362
363    #[test]
364    fn test_hankel_asymptotic() {
365        // For large x, h_n^(1)(x) → (-i)^{n+1} * exp(ix)/x
366        let x = 50.0;
367        let h = spherical_hankel_first_kind(3, x, 1.0);
368
369        // h_0(x) → -i * exp(ix)/x = (sin(x) - i*cos(x))/x for large x
370        let expected_re = x.sin() / x;
371        let expected_im = -x.cos() / x;
372
373        assert!(
374            (h[0].re - expected_re).abs() < 0.01,
375            "Asymptotic real mismatch"
376        );
377        assert!(
378            (h[0].im - expected_im).abs() < 0.01,
379            "Asymptotic imag mismatch"
380        );
381    }
382
383    #[test]
384    fn test_bessel_derivative_j0() {
385        // j_0'(x) = -j_1(x)
386        let x = 2.0;
387        let jp = spherical_bessel_j_derivative(1, x);
388        let j = spherical_bessel_j(2, x);
389        assert!((jp[0] + j[1]).abs() < EPSILON);
390    }
391
392    #[test]
393    fn test_recurrence_stability() {
394        // Test that computation is stable for order > x
395        let x = 5.0;
396        let order = 20;
397        let j = spherical_bessel_j(order, x);
398
399        // All values should be finite
400        for (n, &jn) in j.iter().enumerate().take(order) {
401            assert!(jn.is_finite(), "j_{} is not finite", n);
402        }
403
404        // Values should decrease for n >> x
405        assert!(j[15].abs() < j[5].abs());
406    }
407}