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