Skip to main content

sparse_ir_basis/
special_functions.rs

1//! Special functions implementation ported from C++ libsparseir
2//!
3//! This module provides high-precision implementations of special functions
4//! used in the SparseIR library, particularly spherical Bessel functions
5//! and related mathematical functions.
6
7use std::f64;
8use std::f64::consts::PI;
9
10/// sqrt(π/2) - used frequently in spherical Bessel calculations
11const SQPIO2: f64 = 1.253_314_137_315_500_3;
12
13/// sqrt(2π) - prefactor of Stirling's series for the Gamma function
14const SQ2PI: f64 = 2.506_628_274_631_000_7;
15
16/// Above this argument the Gamma function uses Stirling's series
17const GAMMA_STIRLING_MIN: f64 = 11.5;
18
19/// Γ(x) exceeds `f64::MAX` for x > 171.624..., so every argument above this
20/// bound overflows (Stirling's series already rounds to +∞ just below it)
21const GAMMA_OVERFLOW_ARG: f64 = 172.0;
22
23/// |Γ(x)| is below the smallest subnormal for every non-integer x < -184.
24/// Beyond |x| = 200 the reflected Stirling series is not evaluated; its
25/// intermediates stay finite up to about |x| = 256.
26const GAMMA_UNDERFLOW_ARG: f64 = 200.0;
27
28/// Maximum number of iterations for continued fractions
29const MAX_ITER: usize = 5000;
30
31/// Evaluate polynomial using Horner's method
32fn evalpoly(x: f64, coeffs: &[f64]) -> f64 {
33    let mut result = 0.0;
34    for &coeff in coeffs.iter().rev() {
35        result = result * x + coeff;
36    }
37    result
38}
39
40/// Compute sin(π*x) with exact argument reduction
41///
42/// `x = n + r` with `n = x.round()` and `|r| <= 1/2` is exact in floating
43/// point, so `sin(πx) = (-1)^n sin(πr)` is accurate to a few ulp for every
44/// finite `x` and is exactly zero if and only if `x` is an integer. The
45/// unreduced `(PI * x).sin()` is neither: the rounding error of `PI * x` grows
46/// with `|x|`, and `(PI * -1.0).sin()` is -1.2e-16 rather than 0.
47fn sinpi(x: f64) -> f64 {
48    let n = x.round();
49    let s = (PI * (x - n)).sin();
50    if n.rem_euclid(2.0) == 0.0 { s } else { -s }
51}
52
53/// Stirling's series for `x > GAMMA_STIRLING_MIN`
54///
55/// Returns `(v, t, w)` with `Γ(x) = SQ2PI * v * t * w`, where
56/// `v = x^(x/2 - 1/4)` and `t = v / e^x` split `x^(x - 1/2) e^(-x)` so that it
57/// does not overflow before Γ(x) itself, and `w` is the asymptotic series.
58fn gamma_stirling_factors(x: f64) -> (f64, f64, f64) {
59    let coefs = [
60        1.0,
61        8.333_333_333_333_331e-2,
62        3.472_222_222_230_075e-3,
63        -2.681_327_161_876_304_3e-3,
64        -2.294_719_747_873_185_4e-4,
65        7.840_334_842_744_753e-4,
66        6.989_332_260_623_193e-5,
67        -5.950_237_554_056_33e-4,
68        -2.363_848_809_501_759e-5,
69        7.147_391_378_143_611e-4,
70    ];
71    let w = evalpoly(1.0 / x, &coefs);
72    let v = x.powf(0.5 * x - 0.25);
73    (v, v / x.exp(), w)
74}
75
76/// Gamma function Γ(x) for real `x`
77///
78/// Adapted from the C++ libsparseir `gamma_func` (SpM-lab/libsparseir,
79/// `backend/cxx/src/specfuncs.cpp` at commit 4bc58ea), which follows
80/// `gamma(::Float64)` of Bessels.jl v0.2.8 (`src/gamma.jl`), itself adapted
81/// from the Cephes Mathematical Library by Stephen L. Moshier. For `x > 0` it
82/// uses Stirling's series above 11.5 and otherwise a rational approximation on
83/// `[2, 3)` reached with `Γ(x + 1) = xΓ(x)`. It deviates from the C++ source in
84/// three places: the Stirling prefactor is `sqrt(2π)` as in Bessels.jl (the
85/// C++ code uses `sqrt(π/2)`, which halves every result above 11.5); `sin(πx)`
86/// uses exact argument reduction, so that the poles are detected and accuracy
87/// holds next to them; and no input throws or panics.
88///
89/// A negative non-integer `x < -1` uses the reflection formula
90/// `Γ(x) = π / (sin(πx) Γ(1 - x))`, evaluated as `π / (sin(πx) |x| Γ(|x|))`
91/// so that `1 - x` is never rounded. For `-1 < x < 0` the recurrence
92/// `Γ(x) = Γ(1 + x) / x` is used instead, because `|x| sin(πx)` underflows
93/// for tiny `|x|`.
94///
95/// Wherever Γ(x) is a normal `f64`, the relative error is a few ulp.
96///
97/// Special values follow C99 `tgamma`:
98///
99/// - `x = ±0` (pole): `±∞`, the sign of zero selecting the side of the pole.
100/// - `x` a negative integer (a pole without a signed limit) or `x = -∞`: NaN.
101/// - `x = +∞`, and `x > 171.62...` where Γ(x) exceeds `f64::MAX`: `+∞`.
102/// - Non-integer `x < -184`, where |Γ(x)| is below the smallest subnormal:
103///   `±0` carrying the sign of Γ(x), which is the sign of `sin(πx)`.
104/// - `x = NaN`: NaN.
105pub fn gamma_func(x: f64) -> f64 {
106    if x.is_nan() {
107        return x;
108    }
109    if x <= 0.0 && x == x.floor() {
110        // Poles at zero and at the negative integers (x = -∞ also lands here).
111        // Only a zero tells the side of the pole, through its sign.
112        return if x == 0.0 { 1.0 / x } else { f64::NAN };
113    }
114    if x > 0.0 {
115        return gamma_positive(x);
116    }
117    if x > -1.0 {
118        // Γ(x) = Γ(1 + x) / x; the reflection below would underflow for tiny |x|.
119        return gamma_positive(1.0 + x) / x;
120    }
121
122    // Reflection for non-integer x < -1: s = |x| sin(πx) is nonzero, and
123    // Γ(x) = π / (s Γ(|x|)) has the sign of s.
124    let ax = -x;
125    let s = ax * sinpi(x);
126    if ax <= GAMMA_STIRLING_MIN {
127        return PI / (s * gamma_positive(ax));
128    }
129    if ax > GAMMA_UNDERFLOW_ARG {
130        return 0.0_f64.copysign(s);
131    }
132    // Γ(|x|) = SQ2PI * v * t * w overflows f64 for |x| > 171.62 while Γ(x)
133    // next to a pole is still a normal number down to x ≈ -175. Dividing in
134    // stages keeps every intermediate finite.
135    let (v, t, w) = gamma_stirling_factors(ax);
136    PI / (s * SQ2PI * w * v) / t
137}
138
139/// Γ(x) for `x > 0`, including `x = +∞`
140fn gamma_positive(x: f64) -> f64 {
141    if x > GAMMA_STIRLING_MIN {
142        if x > GAMMA_OVERFLOW_ARG {
143            // Also avoids ∞/∞ = NaN in `t` once e^x overflows (x > 709.78).
144            return f64::INFINITY;
145        }
146        let (v, t, w) = gamma_stirling_factors(x);
147        return SQ2PI * v * t * w;
148    }
149
150    let p = [
151        1.0,
152        8.378_004_301_573_126e-1,
153        3.629_515_436_640_239_3e-1,
154        1.113_062_816_019_361_6e-1,
155        2.385_363_243_461_108_3e-2,
156        4.092_666_828_394_036e-3,
157        4.542_931_960_608_009_3e-4,
158        4.212_760_487_471_622e-5,
159    ];
160
161    let q = [
162        1.0,
163        4.150_160_950_588_455_7e-1,
164        -2.243_510_905_670_329_2e-1,
165        -4.633_887_671_244_534e-2,
166        2.773_706_565_840_073e-2,
167        -7.955_933_682_494_738e-4,
168        -1.237_799_246_653_152_3e-3,
169        2.346_584_059_160_635e-4,
170        -1.397_148_517_476_170_5e-5,
171    ];
172
173    // Shift x into [2, 3) with Γ(x + 1) = xΓ(x).
174    let mut x = x;
175    let mut z = 1.0;
176    while x >= 3.0 {
177        x -= 1.0;
178        z *= x;
179    }
180
181    while x < 2.0 {
182        z /= x;
183        x += 1.0;
184    }
185
186    if x == 2.0 {
187        return z;
188    }
189
190    x -= 2.0;
191    let p_val = evalpoly(x, &p);
192    let q_val = evalpoly(x, &q);
193
194    z * p_val / q_val
195}
196
197/// Cylindrical Bessel function of the first kind, J_nu(x)
198///
199/// Uses the series expansion:
200///   J_nu(x) = sum_{m=0}^∞ (-1)^m / (m! * Gamma(nu+m+1)) * (x/2)^(2m+nu)
201pub fn cyl_bessel_j(nu: f64, x: f64) -> f64 {
202    let eps = f64::EPSILON;
203    let mut _sum = 0.0;
204    let mut term = (x / 2.0).powf(nu) / gamma_func(nu + 1.0);
205    _sum = term;
206
207    for m in 1..1000 {
208        term *= -(x * x / 4.0) / (m as f64 * (nu + m as f64));
209        _sum += term;
210        if term.abs() < _sum.abs() * eps {
211            break;
212        }
213    }
214
215    _sum
216}
217
218/// Spherical Bessel function j_n(x) using the relation:
219///   j_n(x) = sqrt(pi/(2x)) * J_{n+1/2}(x)
220fn spherical_bessel_j_generic(nu: f64, x: f64) -> f64 {
221    SQPIO2 * cyl_bessel_j(nu + 0.5, x) / x.sqrt()
222}
223
224/// Approximation for small x
225fn spherical_bessel_j_small_args(nu: f64, x: f64) -> f64 {
226    if x == 0.0 {
227        return if nu == 0.0 { 1.0 } else { 0.0 };
228    }
229
230    let x2 = (x * x) / 4.0;
231    let coef = [
232        1.0,
233        -1.0 / (1.5 + nu), // 3/2 + nu
234        -1.0 / (5.0 + nu),
235        -1.0 / ((21.0 / 2.0) + nu), // 21/2 + nu
236        -1.0 / (18.0 + nu),
237    ];
238
239    let a = SQPIO2 / (gamma_func(1.5 + nu) * 2.0_f64.powf(nu + 0.5));
240    x.powf(nu) * a * evalpoly(x2, &coef)
241}
242
243/// Determines when the small-argument expansion is accurate
244fn spherical_bessel_j_small_args_cutoff(nu: f64, x: f64) -> bool {
245    (x * x) / (4.0 * nu + 110.0) < f64::EPSILON
246}
247
248/// Computes the continued-fraction for the ratio J_{nu}(x) / J_{nu-1}(x)
249fn bessel_j_ratio_jnu_jnum1(n: f64, x: f64) -> f64 {
250    let xinv = 1.0 / x;
251    let xinv2 = 2.0 * xinv;
252    let mut d = x / (2.0 * n);
253    let mut a = d;
254    let mut h = a;
255    let mut b = (2.0 * n + 2.0) * xinv;
256
257    for _i in 0..MAX_ITER {
258        d = 1.0 / (b - d);
259        a *= b * d - 1.0;
260        h += a;
261        b += xinv2;
262
263        if (a / h).abs() <= f64::EPSILON {
264            break;
265        }
266    }
267
268    h
269}
270
271/// Computes forward recurrence for spherical Bessel y.
272/// Returns a pair: (sY_{n-1}, sY_n)
273fn spherical_bessel_y_forward_recurrence(nu: i32, x: f64) -> (f64, f64) {
274    let xinv = 1.0 / x;
275    let s = x.sin();
276    let c = x.cos();
277    let mut s_y0 = -c * xinv;
278    let mut s_y1 = xinv * (s_y0 - s);
279    let mut nu_start = 1.0;
280
281    while nu_start < nu as f64 + 0.5 {
282        let temp = s_y1;
283        s_y1 = (2.0 * nu_start + 1.0) * xinv * s_y1 - s_y0;
284        s_y0 = temp;
285        nu_start += 1.0;
286    }
287
288    (s_y0, s_y1)
289}
290
291/// Uses forward recurrence if stable; otherwise uses spherical Bessel y recurrence
292fn spherical_bessel_j_recurrence(nu: i32, x: f64) -> f64 {
293    if x >= nu as f64 {
294        let xinv = 1.0 / x;
295        let s = x.sin();
296        let c = x.cos();
297        let mut s_j0 = s * xinv;
298        let mut s_j1 = (s_j0 - c) * xinv;
299        let mut nu_start = 1.0;
300
301        while nu_start < nu as f64 + 0.5 {
302            let temp = s_j1;
303            s_j1 = (2.0 * nu_start + 1.0) * xinv * s_j1 - s_j0;
304            s_j0 = temp;
305            nu_start += 1.0;
306        }
307
308        s_j0
309    } else {
310        // For x < nu, use the alternative method
311        // This should return j_nu(x), not j_nu-1(x)
312        let (s_ynm1, s_yn) = spherical_bessel_y_forward_recurrence(nu, x);
313        let h = bessel_j_ratio_jnu_jnum1(nu as f64 + 1.5, x);
314        1.0 / (x * x * (h * s_ynm1 - s_yn))
315    }
316}
317
318/// Selects the proper method for computing j_n(x) for positive arguments
319fn spherical_bessel_j_positive_args(nu: i32, x: f64) -> f64 {
320    if spherical_bessel_j_small_args_cutoff(nu as f64, x) {
321        spherical_bessel_j_small_args(nu as f64, x)
322    } else if (x >= nu as f64 && nu < 250) || (x < nu as f64 && nu < 60) {
323        // Use recurrence for both x >= nu and x < nu (when nu < 60)
324        spherical_bessel_j_recurrence(nu, x)
325    } else {
326        spherical_bessel_j_generic(nu as f64, x)
327    }
328}
329
330/// Main function to calculate spherical Bessel function of the first kind
331///
332/// This is the main entry point that matches the C++ sphericalbesselj function
333pub fn spherical_bessel_j(n: i32, x: f64) -> f64 {
334    // Handle negative arguments
335    if x < 0.0 {
336        panic!("sphericalBesselJ requires non-negative x");
337    }
338
339    // Handle negative orders: j_{-n}(x) = (-1)^n * j_n(x)
340    if n < 0 {
341        let result = spherical_bessel_j_positive_args(-n, x);
342        if n % 2 == 0 { result } else { -result }
343    } else {
344        spherical_bessel_j_positive_args(n, x)
345    }
346}
347
348#[cfg(test)]
349mod tests {
350    use super::*;
351
352    #[test]
353    fn test_gamma_function() {
354        // Test some known values
355        assert!((gamma_func(1.0) - 1.0).abs() < 1e-10);
356        assert!((gamma_func(2.0) - 1.0).abs() < 1e-10);
357        assert!((gamma_func(3.0) - 2.0).abs() < 1e-10);
358        assert!((gamma_func(4.0) - 6.0).abs() < 1e-10);
359
360        // Test half-integer values
361        assert!((gamma_func(0.5) - 1.7724538509055159).abs() < 1e-10); // sqrt(π)
362    }
363
364    // Reference values below are correctly rounded from MPFR, evaluated at the
365    // exact f64 arguments: Julia 1.12.5, SpecialFunctions.jl 2.8.3,
366    // `setprecision(BigFloat, 256)`, Γ values as
367    // `repr(Float64(gamma(BigFloat(x))))`.
368
369    /// Relative tolerance of `gamma_func` against correctly rounded references.
370    ///
371    /// Error model: the Cephes rational approximation and Stirling series
372    /// contribute a few ulp (Bessels.jl tests the same algorithm at 7 eps
373    /// against BigFloat), the reduced `sin(πx)` about one ulp, and the
374    /// reflection a handful of roundings, i.e. at most about 16 ulp (3.6e-15).
375    /// 1e-14 leaves a ~3x margin for platform `pow`/`exp`/`sin` differences
376    /// while staying far below the O(1) relative errors of the defects guarded
377    /// against here (wrong sign, Γ(|x|) instead of Γ(x), a factor of 2).
378    const GAMMA_RTOL: f64 = 1e-14;
379
380    /// Relative tolerance for the Bessel functions built on `gamma_func`. The
381    /// Γ error (at most about 16 ulp, see above) is a common factor of every
382    /// series term; `powf` and the per-term roundings add a few ulp, amplified
383    /// by at most the cancellation ratio sum|terms| / |sum| (about 3, for
384    /// J_{-1/2}(1)). Together at most about 30 ulp (6.7e-15), with the same
385    /// ~3x margin.
386    const BESSEL_RTOL: f64 = 2e-14;
387
388    fn rel_err(got: f64, want: f64) -> f64 {
389        ((got - want) / want).abs()
390    }
391
392    #[test]
393    fn test_gamma_negative_non_integer_reference_values() {
394        // The half-integer rows equal the closed forms -2√π, 4√π/3, -8√π/15
395        // and 16√π/105.
396        let cases = [
397            (-0.5, -3.544907701811032),
398            (-1.5, 2.363271801207355),
399            (-2.5, -0.9453087204829419),
400            (-3.5, 0.2700882058522691),
401            (-0.1, -10.686287021193193),
402            (-1.0e-200, -1.0e200),
403            (-0.999, -1000.4241966812758),
404            (-1.001, 999.5786270024664),
405            (-3.0 + 2f64.powi(-30), -1.789569708760196e8),
406            (-10.1, -2.2134165830856185e-6),
407            (-11.3, 4.656958619058061e-8),
408            (-11.7, 1.7234064143490278e-8),
409            (-12.5, -1.836606483859281e-9),
410            (-20.5, -2.834656574391335e-19),
411            (-30.7, -1.3275373492818893e-33),
412            (-100.25, -1.503087709322751e-158),
413            (-150.9, -1.9468126352925122e-264),
414            (-170.5, -3.3127395215386074e-308),
415            // Γ(175) overflows f64, but this Γ(x) is a normal number.
416            (-175.0 + 2f64.powi(-40), -9.778221578627872e-307),
417        ];
418        for (x, want) in cases {
419            let got = gamma_func(x);
420            let err = rel_err(got, want);
421            assert!(
422                err <= GAMMA_RTOL,
423                "gamma_func({x:e}) = {got:e}, expected {want:e} (relative error {err:e})"
424            );
425        }
426    }
427
428    #[test]
429    fn test_gamma_positive_reference_values() {
430        // Rows above 11.5 use Stirling's series; 13, 20 and 25 are factorials.
431        let cases = [
432            (0.5, 1.772453850905516),
433            (2.5, 1.329340388179137),
434            (11.5, 1.1899423083962249e7),
435            (11.6, 1.5131318919703094e7),
436            (12.5, 1.3684336546556586e8),
437            (13.0, 4.790016e8),
438            (20.0, 1.21645100408832e17),
439            (25.0, 6.204484017332394e23),
440            (50.5, 4.29046291235196e63),
441            (100.0, 9.332621544394415e155),
442            (170.5, 5.56209241456e305),
443            (171.5, 9.4833675668248e307),
444        ];
445        for (x, want) in cases {
446            let got = gamma_func(x);
447            let err = rel_err(got, want);
448            assert!(
449                err <= GAMMA_RTOL,
450                "gamma_func({x:e}) = {got:e}, expected {want:e} (relative error {err:e})"
451            );
452        }
453    }
454
455    #[test]
456    fn test_gamma_recurrence() {
457        // Γ(x + 1) = x Γ(x) for both signs of x, straddling the switch to
458        // Stirling's series at |x| = 11.5 and approaching the poles. Dyadic
459        // fractions keep x and x + 1 exact; |x| < 170 keeps every value a
460        // normal f64. Tolerance: two evaluations within 16 ulp each plus one
461        // product (about 33 ulp, 7.3e-15), with the same ~3x margin as
462        // GAMMA_RTOL.
463        const RECURRENCE_RTOL: f64 = 2e-14;
464        let fracs = [
465            2f64.powi(-20),
466            0.125,
467            0.25,
468            0.5,
469            0.75,
470            0.875,
471            1.0 - 2f64.powi(-20),
472        ];
473        for k in 0..170 {
474            for f in fracs {
475                for x in [-(k as f64 + f), k as f64 + f] {
476                    let lhs = gamma_func(x + 1.0);
477                    let rhs = x * gamma_func(x);
478                    let err = rel_err(rhs, lhs);
479                    assert!(
480                        err <= RECURRENCE_RTOL,
481                        "Γ({x:e} + 1) = {lhs:e} but x Γ(x) = {rhs:e} (relative error {err:e})"
482                    );
483                }
484            }
485        }
486    }
487
488    #[test]
489    fn test_gamma_poles_and_special_values() {
490        // Negative integers are poles without a signed limit: NaN, never a panic.
491        for x in [
492            -1.0,
493            -2.0,
494            -3.0,
495            -171.0,
496            -1e10,
497            -(2f64.powi(52)),
498            -1e300,
499            f64::MIN,
500        ] {
501            let got = gamma_func(x);
502            assert!(got.is_nan(), "gamma_func({x:e}) = {got:e}, expected NaN");
503        }
504        // The pole at zero is approached from the side given by the sign of zero.
505        assert_eq!(gamma_func(0.0), f64::INFINITY);
506        assert_eq!(gamma_func(-0.0), f64::NEG_INFINITY);
507        assert_eq!(gamma_func(f64::INFINITY), f64::INFINITY);
508        assert!(gamma_func(f64::NEG_INFINITY).is_nan());
509        assert!(gamma_func(f64::NAN).is_nan());
510
511        // Overflow: Γ(x) exceeds f64::MAX for x > 171.62...
512        for x in [171.7, 172.0, 709.0, 710.0, 1000.0, 1e300] {
513            let got = gamma_func(x);
514            assert_eq!(got, f64::INFINITY, "gamma_func({x:e}) = {got:e}");
515        }
516
517        // Γ(-171.5) is subnormal and must not be flushed to zero. Its final
518        // rounding onto the subnormal grid costs half a unit of the spacing;
519        // allow two on top of the relative bound.
520        let (got, want) = (gamma_func(-171.5), 1.9316265431712e-310);
521        assert!(
522            (got - want).abs() <= GAMMA_RTOL * want + 2.0 * f64::from_bits(1),
523            "gamma_func(-171.5) = {got:e}, expected {want:e}"
524        );
525
526        // Below x = -184, |Γ(x)| is under the smallest subnormal for every
527        // non-integer x: the result is a zero with the sign of Γ(x).
528        for (x, negative) in [
529            (-184.5, true),
530            (-200.5, true),
531            (-201.5, false),
532            (-1000.25, true),
533            (-(1e15 + 0.5), true),
534        ] {
535            let got = gamma_func(x);
536            assert!(
537                got == 0.0 && got.is_sign_negative() == negative,
538                "gamma_func({x:e}) = {got:e}, expected {}0",
539                if negative { "-" } else { "+" }
540            );
541        }
542    }
543
544    #[test]
545    fn test_cyl_bessel_j_orders_reaching_gamma_reflection_and_stirling() {
546        // cyl_bessel_j(nu, x) divides by gamma_func(nu + 1): nu < -1 reaches
547        // negative non-integer arguments, nu > 10.5 the Stirling branch.
548        // References: the power series
549        // sum((-1)^m (x/2)^(2m+nu) / (gamma(m+1) gamma(m+nu+1)) for m in 0:80)
550        // in 256-bit BigFloat (same Julia setup as above).
551        let cases = [
552            (-0.5, 1.0, 0.4310988680183761),
553            (-1.5, 1.0, -1.1024955751601793),
554            (-2.5, 2.0, 0.8282206324443038),
555            (-12.7, 1.0, 3.9444125287326807e11),
556            (11.5, 1.0, 2.4730845703448897e-12),
557            (12.0, 2.0, 1.9326951487239857e-9),
558        ];
559        for (nu, x, want) in cases {
560            let got = cyl_bessel_j(nu, x);
561            let err = rel_err(got, want);
562            assert!(
563                err <= BESSEL_RTOL,
564                "J_{nu}({x}) = {got:e}, expected {want:e} (relative error {err:e})"
565            );
566        }
567    }
568
569    #[test]
570    fn test_spherical_bessel_j_small_args_high_order() {
571        // Below the small-argument cutoff, j_n(x) uses gamma_func(n + 1.5),
572        // which lies in the Stirling branch for n >= 11. References:
573        // j_n(x) = sqrt(pi/(2x)) J_{n+1/2}(x) from the 256-bit series above.
574        let x = 1e-8;
575        let cases = [
576            (11, 3.162213889372793e-100),
577            (12, 1.2648855557491175e-109),
578            (13, 4.6847613175893235e-119),
579            (14, 1.6154349370997668e-128),
580            (15, 5.2110804422573123e-138),
581        ];
582        for (n, want) in cases {
583            assert!(spherical_bessel_j_small_args_cutoff(n as f64, x));
584            let got = spherical_bessel_j(n, x);
585            let err = rel_err(got, want);
586            assert!(
587                err <= BESSEL_RTOL,
588                "j_{n}({x:e}) = {got:e}, expected {want:e} (relative error {err:e})"
589            );
590        }
591    }
592
593    #[test]
594    fn test_cylindrical_bessel_j() {
595        // Test known values
596        let j0_1 = cyl_bessel_j(0.0, 1.0);
597        let expected_j0_1 = 0.765_197_686_557_966_6;
598        assert!((j0_1 - expected_j0_1).abs() < 1e-10);
599
600        let j1_1 = cyl_bessel_j(1.0, 1.0);
601        let expected_j1_1 = 0.440_050_585_744_933_5;
602        assert!((j1_1 - expected_j1_1).abs() < 1e-10);
603    }
604
605    #[test]
606    fn test_spherical_bessel_j_basic() {
607        // Test j_0(x) = sin(x)/x for x != 0
608        let x = 1.0;
609        let j0 = spherical_bessel_j(0, x);
610        let expected_j0 = x.sin() / x;
611        println!("j_0({}) = {}, expected = {}", x, j0, expected_j0);
612        assert!((j0 - expected_j0).abs() < 1e-10);
613
614        // Test j_1(x) = sin(x)/x² - cos(x)/x
615        let j1 = spherical_bessel_j(1, x);
616        let expected_j1 = x.sin() / (x * x) - x.cos() / x;
617        println!("j_1({}) = {}, expected = {}", x, j1, expected_j1);
618        assert!((j1 - expected_j1).abs() < 1e-10);
619
620        // Test j_0(0) = 1
621        let j0_zero = spherical_bessel_j(0, 0.0);
622        println!("j_0(0) = {}, expected = 1.0", j0_zero);
623        assert!((j0_zero - 1.0).abs() < 1e-10);
624
625        // Test j_n(0) = 0 for n > 0
626        let j1_zero = spherical_bessel_j(1, 0.0);
627        println!("j_1(0) = {}, expected = 0.0", j1_zero);
628        assert!(j1_zero.abs() < 1e-10);
629    }
630
631    #[test]
632    fn test_spherical_bessel_j_various_values() {
633        // Test various values to ensure accuracy
634        // These are reference values from mathematical tables
635        let test_cases = [
636            (0, 0.1, 0.9983341664682815),
637            (0, 0.5, 0.958_851_077_208_406),
638            (0, 1.0, 0.8414709848078965),
639            (0, 2.0, 0.4546487134128409),
640            (0, 5.0, -0.1917848549326277),
641            // Corrected expected values for j_1 using analytical formulas
642            (1, 0.1, 0.0333000128900053),
643            (1, 0.5, 0.1625370306360665),
644            (1, 1.0, 0.3011686789397568),
645            // j_1(2) = sin(2)/4 - cos(2)/2 = 0.43539777497999166
646            (1, 2.0, 0.43539777497999166),
647            // j_1(5) = sin(5)/25 - cos(5)/5 = -0.0950894080791708
648            (1, 5.0, -0.0950894080791708),
649        ];
650
651        for (n, x, expected) in test_cases {
652            let result = spherical_bessel_j(n, x);
653            println!(
654                "j_{}({}) = {}, expected = {}, diff = {}",
655                n,
656                x,
657                result,
658                expected,
659                (result - expected).abs()
660            );
661
662            // For now, just check that the result is finite and reasonable
663            assert!(
664                result.is_finite(),
665                "j_{}({}) should be finite, got {}",
666                n,
667                x,
668                result
669            );
670
671            // Check accuracy with more lenient tolerance for now
672            if (result - expected).abs() > 1e-6 {
673                println!(
674                    "WARNING: j_{}({}) accuracy issue: got {}, expected {}, diff = {}",
675                    n,
676                    x,
677                    result,
678                    expected,
679                    (result - expected).abs()
680                );
681            }
682        }
683    }
684
685    #[test]
686    fn test_debug_spherical_bessel_j() {
687        // Debug specific problematic cases
688        println!("=== Debug j_1(0.1) ===");
689        let x = 0.1;
690        let n = 1;
691
692        // Check which method is being used
693        let cutoff = spherical_bessel_j_small_args_cutoff(n as f64, x);
694        println!("small_args_cutoff: {}", cutoff);
695
696        if cutoff {
697            let small_result = spherical_bessel_j_small_args(n as f64, x);
698            println!("small_args result: {}", small_result);
699        }
700
701        let recurrence_result = spherical_bessel_j_recurrence(n, x);
702        println!("recurrence result: {}", recurrence_result);
703
704        let generic_result = spherical_bessel_j_generic(n as f64, x);
705        println!("generic result: {}", generic_result);
706
707        let final_result = spherical_bessel_j(n, x);
708        println!("final result: {}", final_result);
709
710        // Expected: j_1(0.1) = sin(0.1)/0.1^2 - cos(0.1)/0.1 = 0.033300...
711        let expected = x.sin() / (x * x) - x.cos() / x;
712        println!("expected (sin(x)/x^2 - cos(x)/x): {}", expected);
713    }
714
715    #[test]
716    fn test_spherical_bessel_j_large_values() {
717        // Test large values to ensure stability
718        let large_x = 100.0;
719        let j0_large = spherical_bessel_j(0, large_x);
720        println!("j_0({}) = {}", large_x, j0_large);
721        assert!(j0_large.is_finite());
722
723        let j1_large = spherical_bessel_j(1, large_x);
724        println!("j_1({}) = {}", large_x, j1_large);
725        assert!(j1_large.is_finite());
726    }
727
728    #[test]
729    fn test_spherical_bessel_j_cpp_style_high_order() {
730        // Test high-order spherical Bessel functions like C++ implementation
731        // Reference values from Julia (same as C++ test)
732        // julia> using Bessels
733        // julia> for i in 0:15; println(sphericalbesselj(i, 1.)); end
734        let refs = [
735            0.8414709848078965,
736            0.30116867893975674,
737            0.06203505201137386,
738            0.009006581117112517,
739            0.0010110158084137527,
740            9.256115861125818e-5,
741            7.156936310087086e-6,
742            4.790134198739489e-7,
743            2.82649880221473e-8,
744            1.4913765025551456e-9,
745            7.116552640047314e-11,
746            3.09955185479008e-12,
747            1.2416625969871055e-13,
748            4.604637677683788e-15,
749            1.5895759875169764e-16,
750            5.1326861154437626e-18,
751        ];
752
753        let x = 1.0;
754        for (l, &expected) in refs.iter().enumerate() {
755            let expected: f64 = expected;
756            let result = spherical_bessel_j(l as i32, x);
757
758            // Use same tolerance as C++ Approx: relative error 1e-6, absolute error 1e-12
759            let relative_tolerance = 1e-6;
760            let absolute_tolerance = 1e-12;
761
762            // Check relative error for non-zero expected values
763            if expected.abs() > absolute_tolerance {
764                let relative_error = (result - expected).abs() / expected.abs();
765                assert!(
766                    relative_error <= relative_tolerance,
767                    "j_{}({}) relative error too large: {} > {}",
768                    l,
769                    x,
770                    relative_error,
771                    relative_tolerance
772                );
773            } else {
774                // For very small expected values, check absolute error
775                assert!(
776                    (result - expected).abs() <= absolute_tolerance,
777                    "j_{}({}) absolute error too large: {} > {}",
778                    l,
779                    x,
780                    (result - expected).abs(),
781                    absolute_tolerance
782                );
783            }
784
785            // Check that result is finite
786            assert!(
787                result.is_finite(),
788                "j_{}({}) should be finite, got {}",
789                l,
790                x,
791                result
792            );
793        }
794    }
795
796    #[test]
797    fn test_spherical_bessel_j_zero_argument() {
798        // Test behavior at x = 0
799        // j_0(0) = 1, j_n(0) = 0 for n > 0
800        let j0_zero = spherical_bessel_j(0, 0.0);
801        assert!(
802            (j0_zero - 1.0).abs() < 1e-15,
803            "j_0(0) should be 1, got {}",
804            j0_zero
805        );
806
807        for n in 1..=10 {
808            let jn_zero = spherical_bessel_j(n, 0.0);
809            assert!(
810                jn_zero.abs() < 1e-15,
811                "j_{}(0) should be 0, got {}",
812                n,
813                jn_zero
814            );
815        }
816    }
817
818    #[test]
819    fn test_spherical_bessel_j_negative_orders() {
820        // Test behavior for negative orders: j_{-n}(x) = (-1)^n * j_n(x)
821        for n in -5..0 {
822            let result_neg = spherical_bessel_j(n, 1.0);
823            let result_pos = spherical_bessel_j(-n, 1.0);
824            let expected = if n % 2 == 0 { result_pos } else { -result_pos };
825            assert!(
826                (result_neg - expected).abs() < 1e-15,
827                "j_{}(1.0) should be {} for negative n, got {}",
828                n,
829                expected,
830                result_neg
831            );
832        }
833    }
834
835    #[test]
836    fn test_spherical_bessel_j_small_arguments() {
837        // Test very small arguments to ensure numerical stability
838        let small_x_values = [1e-10, 1e-8, 1e-6, 1e-4, 1e-2];
839
840        for &x in &small_x_values {
841            for n in 0..=5 {
842                let result = spherical_bessel_j(n, x);
843                assert!(result.is_finite(), "j_{}({}) should be finite", n, x);
844
845                // For very small x, j_n(x) ≈ x^n / (2n+1)!!
846                if x < 1e-6 && n < 3 {
847                    let expected_approx = x.powi(n) / double_factorial(2 * n + 1);
848                    let relative_error = (result - expected_approx).abs() / expected_approx.abs();
849                    assert!(
850                        relative_error < 1e-6,
851                        "j_{}({}) small argument approximation failed: relative error = {}",
852                        n,
853                        x,
854                        relative_error
855                    );
856                }
857            }
858        }
859    }
860
861    #[test]
862    fn test_spherical_bessel_j_large_arguments() {
863        // Test large arguments to ensure asymptotic behavior
864        let large_x_values = [10.0, 50.0, 100.0, 500.0];
865
866        for &x in &large_x_values {
867            for n in 0..=3 {
868                let result = spherical_bessel_j(n, x);
869                assert!(result.is_finite(), "j_{}({}) should be finite", n, x);
870
871                // For large x, j_n(x) ≈ sin(x - n*π/2) / x
872                let expected_approx = (x - (n as f64) * PI / 2.0).sin() / x;
873                let relative_error =
874                    (result - expected_approx).abs() / expected_approx.abs().max(1e-10);
875
876                if x > 50.0 && relative_error > 1e-2 {
877                    println!(
878                        "WARNING: j_{}({}) large argument approximation: got {}, expected ≈ {}, relative error = {}",
879                        n, x, result, expected_approx, relative_error
880                    );
881                }
882            }
883        }
884    }
885
886    /// Helper function for double factorial: n!!
887    fn double_factorial(n: i32) -> f64 {
888        if n <= 0 {
889            1.0
890        } else if n % 2 == 0 {
891            // Even: n!! = 2^(n/2) * (n/2)!
892            let half_n = n / 2;
893            2.0_f64.powi(half_n) * factorial(half_n)
894        } else {
895            // Odd: n!! = n! / (2^((n-1)/2) * ((n-1)/2)!)
896            let half_n_minus_1 = (n - 1) / 2;
897            factorial(n) / (2.0_f64.powi(half_n_minus_1) * factorial(half_n_minus_1))
898        }
899    }
900
901    /// Helper function for factorial: n!
902    fn factorial(n: i32) -> f64 {
903        if n <= 1 {
904            1.0
905        } else {
906            let mut result = 1.0;
907            for i in 2..=n {
908                result *= i as f64;
909            }
910            result
911        }
912    }
913}