Skip to main content

dace_rs/
special.rs

1//! Special functions of DA values: Bessel functions, gamma, and polygamma.
2//!
3//! Ports the special-function layer of `core/dacemath.c` (lines 1523-1995):
4//! the Bessel coefficient tables are built from scalar Bessel values of
5//! orders `n - nocut .. n + nocut` at the constant part (integer orders,
6//! using `J_{-k} = (-1)^k J_k`, `Y_{-k} = (-1)^k Y_k`, `I_{-k} = I_k`,
7//! `K_{-k} = K_k`), then composed with the Kahan-summation derivative
8//! recurrences of DLMF 10.6. Scalar Bessel values come from
9//! [`puruspe`] (`besseljy`/`besselik`), the gamma function from
10//! the `puruspe::gamma` function, and the polygamma/Hurwitz zeta from a
11//! transcription of DACE's `contrib/psi.c` (netlib PSIFN, f2c translation)
12//! and `contrib/zeta.c` (Cephes 2.8) in the `netlib_psi_zeta` module.
13
14use crate::context::truncation_order;
15use crate::da::Da;
16use crate::elementary::{evaluate_series, exp};
17use crate::error::{codes, dace_panic};
18
19pub(crate) mod netlib_psi_zeta;
20
21// ---------------------------------------------------------------------------
22// Scalar providers
23// ---------------------------------------------------------------------------
24
25/// Values of `J_k(x)` (kind `J`) or `Y_k(x)` (kind `Y`) for integer orders
26/// `n0..=n1`, with the negative-order reflections (`BesselWrapper`,
27/// dacemath.c:1523). Returns `None` on evaluation failure.
28fn bessel_jy_orders(x: f64, n0: i32, n1: i32, bessel_y: bool) -> Option<Vec<f64>> {
29    let max_order = n0.unsigned_abs().max(n1.unsigned_abs());
30    let mut b = Vec::with_capacity(max_order as usize + 1);
31    for k in 0..=max_order {
32        // Integer-order dedicated routines; note puruspe::besseljy returns a
33        // wrong Y at integer nu (verified against libm), so it is not used.
34        b.push(if bessel_y {
35            puruspe::Yn(k, x)
36        } else {
37            puruspe::Jn(k, x)
38        });
39    }
40    let mut out = Vec::with_capacity((n1 - n0 + 1) as usize);
41    let mut s = if n0 % 2 == 0 { 1.0 } else { -1.0 };
42    for i in n0..=n1 {
43        if i >= 0 {
44            out.push(b[i as usize]);
45        } else {
46            out.push(s * b[i.unsigned_abs() as usize]);
47            s *= -1.0;
48        }
49    }
50    Some(out)
51}
52
53/// Values of `I_k(x)` (unscaled), `e^{-x}·I_k(x)` (scaled I), `K_k(x)`
54/// (unscaled), or `e^{x}·K_k(x)` (scaled K) for integer orders `n0..=n1`
55/// (`ModifiedBesselWrapper`, dacemath.c:1578). Returns `None` on failure.
56///
57/// Note: the scaled variants are computed from the unscaled values, so the
58/// scaled K function loses accuracy once the unscaled `K` underflows
59/// (arguments beyond roughly 690); the C library computes it scaled
60/// internally. Divergence documented at [`bessel_k`].
61fn bessel_ik_orders(x: f64, n0: i32, n1: i32, bessel_k: bool, scaled: bool) -> Option<Vec<f64>> {
62    let max_order = n0.unsigned_abs().max(n1.unsigned_abs());
63    let mut b = Vec::with_capacity(max_order as usize + 1);
64    for k in 0..=max_order {
65        if bessel_k {
66            b.push(if scaled {
67                puruspe::Kn(k, x) * x.exp()
68            } else {
69                puruspe::Kn(k, x)
70            });
71        } else {
72            b.push(if scaled {
73                puruspe::In(k, x) * (-x).exp()
74            } else {
75                puruspe::In(k, x)
76            });
77        }
78    }
79    // I_{-k} = I_k and K_{-k} = K_k for all k.
80    Some((n0..=n1).map(|i| b[i.unsigned_abs() as usize]).collect())
81}
82
83// ---------------------------------------------------------------------------
84// Bessel composition (dacemath.c:1776-1875)
85// ---------------------------------------------------------------------------
86
87/// Compose a Bessel function from its order-`n±nocut` values `bz`
88/// (`daceEvaluateBesselFunction`): `type` is -1 for ordinary (J/Y) and +1
89/// for modified (I) functions, `ktype` is -1 for K and +1 otherwise.
90fn evaluate_bessel_function(a: &Da, bz: &[f64], kind: f64, kkind: f64) -> Da {
91    let nocut = truncation_order();
92    let mut xf = vec![0.0; nocut as usize + 1];
93    let mut binomial = vec![0.0; nocut as usize + 1];
94
95    xf[0] = bz[nocut as usize];
96    binomial[0] = 1.0;
97    let mut factor = 1.0;
98    for i in 1..=nocut as usize {
99        factor *= kkind * 0.5 / i as f64;
100        // binomial coefficients i choose j from i-1 choose j
101        binomial[i] = 1.0;
102        for j in (1..i).rev() {
103            binomial[j] += binomial[j - 1];
104        }
105        // n-th derivative of the Bessel function (DLMF 10.6), Kahan-summed.
106        let mut sign = 1.0;
107        let mut c = 0.0;
108        xf[i] = 0.0;
109        for j in 0..=i {
110            let y = binomial[j] * sign * bz[nocut as usize - i + 2 * j] - c;
111            let t = xf[i] + y;
112            c = (t - xf[i]) - y;
113            xf[i] = t;
114            sign *= kind;
115        }
116        xf[i] *= factor;
117    }
118
119    evaluate_series(a, &xf)
120}
121
122/// Compose a scaled modified Bessel function from its order-`n±nocut`
123/// values `bz` (`daceEvaluateScaledModifiedBesselFunction`); `kkind` is +1
124/// for scaled I and -1 for scaled K.
125fn evaluate_scaled_modified_bessel_function(a: &Da, bz: &[f64], kkind: f64) -> Da {
126    let nocut = truncation_order();
127    let mut xf = vec![0.0; nocut as usize + 1];
128    let mut binomial = vec![0.0; 2 * nocut as usize + 1];
129
130    xf[0] = bz[nocut as usize];
131    binomial[0] = 1.0;
132    let mut factor = 1.0;
133    for i in 1..=nocut as usize {
134        factor *= kkind * 0.5 / i as f64;
135        // binomial coefficients 2i-1 choose j, then 2i choose j
136        binomial[2 * i - 1] = 1.0;
137        for j in (1..2 * i - 1).rev() {
138            binomial[j] += binomial[j - 1];
139        }
140        binomial[2 * i] = 1.0;
141        for j in (1..2 * i).rev() {
142            binomial[j] += binomial[j - 1];
143        }
144        let mut sign = 1.0;
145        let mut c = 0.0;
146        xf[i] = 0.0;
147        for j in 0..=2 * i {
148            let y = binomial[j] * sign * bz[nocut as usize - i + j] - c;
149            let t = xf[i] + y;
150            c = (t - xf[i]) - y;
151            xf[i] = t;
152            sign *= -1.0;
153        }
154        xf[i] *= factor;
155    }
156
157    evaluate_series(a, &xf)
158}
159
160// ---------------------------------------------------------------------------
161// Public Bessel functions of DA values (dacemath.c:1619-1766)
162// ---------------------------------------------------------------------------
163
164/// The Bessel function of the first kind `J_n(a)` (`daceBesselJFunction`).
165pub fn bessel_j(a: &Da, n: i32) -> Da {
166    bessel_common(a, n, false, "bessel_j")
167}
168
169/// The Bessel function of the second kind `Y_n(a)`
170/// (`daceBesselYFunction`).
171pub fn bessel_y(a: &Da, n: i32) -> Da {
172    bessel_common(a, n, false, "bessel_y")
173}
174
175/// The modified Bessel function of the first kind `I_n(a)`
176/// (`daceBesselIFunction`); with `scaled`, computes `e^{-a}·I_n(a)`.
177pub fn bessel_i(a: &Da, n: i32, scaled: bool) -> Da {
178    bessel_common(a, n, scaled, "bessel_i")
179}
180
181/// The modified Bessel function of the second kind `K_n(a)`
182/// (`daceBesselKFunction`); with `scaled`, computes `e^{a}·K_n(a)`.
183///
184/// Divergence from C: the scaled variant is derived from the unscaled one,
185/// so it loses accuracy (and eventually returns 0/∞ products) once the
186/// unscaled `K` underflows, around arguments of 690; the C library computes
187/// the scaled function directly and remains accurate there.
188pub fn bessel_k(a: &Da, n: i32, scaled: bool) -> Da {
189    bessel_common(a, n, scaled, "bessel_k")
190}
191
192fn bessel_common(a: &Da, n: i32, scaled: bool, kind: &str) -> Da {
193    let a0 = a.cons();
194    if a0 <= 0.0 {
195        dace_panic(codes::OUT_OF_DOMAIN, "Out of domain");
196    }
197    let nocut = truncation_order() as i32;
198    let n0 = n - nocut;
199    let n1 = n + nocut;
200    let bz = match kind {
201        "bessel_i" => bessel_ik_orders(a0, n0, n1, false, scaled),
202        "bessel_k" => bessel_ik_orders(a0, n0, n1, true, scaled),
203        "bessel_j" => bessel_jy_orders(a0, n0, n1, false),
204        _ => bessel_jy_orders(a0, n0, n1, true),
205    };
206    match bz {
207        Some(bz) => match kind {
208            "bessel_j" | "bessel_y" => evaluate_bessel_function(a, &bz, -1.0, 1.0),
209            "bessel_k" if scaled => evaluate_scaled_modified_bessel_function(a, &bz, -1.0),
210            "bessel_k" => evaluate_bessel_function(a, &bz, 1.0, -1.0),
211            _ if scaled => evaluate_scaled_modified_bessel_function(a, &bz, 1.0),
212            _ => evaluate_bessel_function(a, &bz, 1.0, 1.0),
213        },
214        None => dace_panic(codes::OUT_OF_DOMAIN, "Out of domain"),
215    }
216}
217
218// ---------------------------------------------------------------------------
219// Gamma and polygamma (dacemath.c:1884-1995)
220// ---------------------------------------------------------------------------
221
222/// Partial logarithmic gamma: the series without the constant term
223/// (`daceLogGammaFunction0`).
224fn log_gamma0(a: &Da, a0: f64) -> Da {
225    let nocut = truncation_order();
226    let mut xf = vec![0.0; nocut as usize + 1];
227    xf[0] = 0.0;
228    xf[1] = netlib_psi_zeta::psi(a0);
229    let mut s = 1.0;
230    for (i, item) in xf.iter_mut().enumerate().skip(2) {
231        *item = (s / i as f64) * netlib_psi_zeta::zeta(i as f64, a0);
232        s *= -1.0;
233    }
234    evaluate_series(a, &xf)
235}
236
237/// The logarithmic gamma function `ln Γ(a)` (`daceLogGammaFunction`).
238///
239/// # Panics
240///
241/// Panics with [`crate::DaceError`] code 650 ("Out of domain") when the constant
242/// part is zero or a negative integer.
243pub fn log_gamma(a: &Da) -> Da {
244    let a0 = a.cons();
245    if a0 <= 0.0 && a0.trunc() == a0 {
246        dace_panic(codes::OUT_OF_DOMAIN, "Out of domain");
247    }
248    let mut r = log_gamma0(a, a0);
249    let c = puruspe::gamma(a0).ln();
250    r.set_coefficient0(0, c);
251    r
252}
253
254/// The gamma function `Γ(a)` (`daceGammaFunction`).
255///
256/// # Panics
257///
258/// Panics with [`crate::DaceError`] code 650 ("Out of domain") when the constant
259/// part is zero or a negative integer.
260pub fn gamma(a: &Da) -> Da {
261    let a0 = a.cons();
262    if a0 <= 0.0 && a0.trunc() == a0 {
263        dace_panic(codes::OUT_OF_DOMAIN, "Out of domain");
264    }
265    let lg = log_gamma0(a, a0);
266    let mut r = exp(&lg);
267    // multiply by gamma(a0) via a scalar multiply of every term
268    let g = puruspe::gamma(a0);
269    r = crate::kernels::weighted_sum(&r, g, &r, 0.0);
270    r
271}
272
273/// The polygamma function of order `n`: the `(n+1)`-th derivative of
274/// `ln Γ` (`dacePsiFunction`).
275///
276/// # Panics
277///
278/// Panics with [`crate::DaceError`] code 650 ("Out of domain") when the constant
279/// part is zero or a negative integer.
280pub fn psi(a: &Da, n: u32) -> Da {
281    let a0 = a.cons();
282    if a0 <= 0.0 && a0.trunc() == a0 {
283        dace_panic(codes::OUT_OF_DOMAIN, "Out of domain");
284    }
285
286    let nocut = truncation_order();
287    let mut xf = vec![0.0; nocut as usize + 1];
288
289    if n == 0 {
290        xf[0] = netlib_psi_zeta::psi(a0);
291        let mut s = 1.0;
292        for (i, item) in xf.iter_mut().enumerate().skip(1) {
293            *item = s * netlib_psi_zeta::zeta(f64::from(i as u32 + 1), a0);
294            s *= -1.0;
295        }
296    } else {
297        // fac = (-1)^(n+1) * n!  (C: sign then 2..=n in order)
298        let mut fac = if n % 2 != 0 { 1.0 } else { -1.0 };
299        for i in 2..=n {
300            fac *= i as f64;
301        }
302        for (i, item) in xf.iter_mut().enumerate() {
303            *item = fac * netlib_psi_zeta::zeta(f64::from(n + i as u32 + 1), a0);
304            fac = -(fac / (i as f64 + 1.0)) * f64::from(n + i as u32 + 1);
305        }
306    }
307
308    evaluate_series(a, &xf)
309}
310
311impl Da {
312    /// Method form of [`bessel_j`](crate::special::bessel_j).
313    pub fn bessel_j(&self, n: i32) -> Da {
314        bessel_j(self, n)
315    }
316
317    /// Method form of [`bessel_y`](crate::special::bessel_y).
318    pub fn bessel_y(&self, n: i32) -> Da {
319        bessel_y(self, n)
320    }
321
322    /// Method form of [`bessel_i`](crate::special::bessel_i).
323    pub fn bessel_i(&self, n: i32, scaled: bool) -> Da {
324        bessel_i(self, n, scaled)
325    }
326
327    /// Method form of [`bessel_k`](crate::special::bessel_k).
328    pub fn bessel_k(&self, n: i32, scaled: bool) -> Da {
329        bessel_k(self, n, scaled)
330    }
331
332    /// Method form of [`log_gamma`](crate::special::log_gamma).
333    pub fn log_gamma(&self) -> Da {
334        log_gamma(self)
335    }
336
337    /// Method form of [`gamma`](crate::special::gamma).
338    pub fn gamma(&self) -> Da {
339        gamma(self)
340    }
341
342    /// Method form of [`psi`](crate::special::psi).
343    pub fn psi(&self, n: u32) -> Da {
344        psi(self, n)
345    }
346}
347
348#[cfg(test)]
349mod tests {
350    use super::*;
351    use crate::test_support::CONTEXT_LOCK;
352
353    #[test]
354    fn netlib_psi_zeta_values() {
355        // psi(1) = -gamma
356        assert!((netlib_psi_zeta::psi(1.0) + 0.5772156649015329).abs() < 1e-13);
357        // psi'(1) = zeta(2,1) = pi^2/6
358        assert!(
359            (netlib_psi_zeta::zeta(2.0, 1.0) - std::f64::consts::PI.powi(2) / 6.0).abs() < 1e-12
360        );
361        // psi(1/2) = -gamma - 2 ln 2
362        assert!((netlib_psi_zeta::psi(0.5) + 0.5772156649015329 + 2.0 * 2.0f64.ln()).abs() < 1e-12);
363        // zeta(4,1) = pi^4/90
364        assert!(
365            (netlib_psi_zeta::zeta(4.0, 1.0) - std::f64::consts::PI.powi(4) / 90.0).abs() < 1e-10
366        );
367    }
368
369    #[test]
370    fn gamma_and_bessel_constants() {
371        let _g = CONTEXT_LOCK.lock();
372        crate::context::init(6, 2).unwrap();
373
374        // Gamma(1/2) = sqrt(pi): constant coefficient matches puruspe
375        let g = gamma(&(0.5 + Da::variable(1)));
376        assert!((g.cons() - std::f64::consts::PI.sqrt()).abs() < 1e-13);
377
378        // log_gamma constant matches ln of puruspe gamma
379        let lg = log_gamma(&(0.5 + Da::variable(1)));
380        assert!((lg.cons() - puruspe::gamma(0.5).ln()).abs() < 1e-14);
381
382        // bessel_j(0) of constant 1 matches scalar J0(1) in the constant term
383        let j = bessel_j(&(1.0 + Da::variable(1)), 0);
384        assert!((j.cons() - puruspe::besseljy(0.0, 1.0).0).abs() < 1e-13);
385
386        // J0'(x) = -J1(x): linear coefficient of bessel_j(x, 0) at a0=1
387        let lin = j.get_coefficient(&[1, 0]);
388        assert!((lin + puruspe::besseljy(1.0, 1.0).0).abs() < 1e-12);
389
390        // psi(0) of a DA == netlib psi at the constant; linear term = zeta(2,a0)
391        let p0 = psi(&(1.0 + Da::variable(1)), 0);
392        assert!((p0.cons() - netlib_psi_zeta::psi(1.0)).abs() < 1e-14);
393        assert!((p0.get_coefficient(&[1, 0]) - netlib_psi_zeta::zeta(2.0, 1.0)).abs() < 1e-12);
394
395        // psi(1) of a DA: xf[i] = fac*zeta(n+i+1)
396        let p1 = psi(&(1.0 + Da::variable(1)), 1);
397        assert!((p1.cons() - netlib_psi_zeta::zeta(2.0, 1.0)).abs() < 1e-12);
398        assert!(
399            (p1.get_coefficient(&[1, 0]) + 2.0 * netlib_psi_zeta::zeta(3.0, 1.0)).abs() < 1e-11
400        );
401
402        // bessel_k scaled / unscaled relation at the constant part
403        let kx = 2.0 + Da::variable(1);
404        let ku = bessel_k(&kx, 1, false);
405        let ks = bessel_k(&kx, 1, true);
406        assert!((ks.cons() - ku.cons() * 2.0f64.exp()).abs() < 1e-6 * ks.cons().abs());
407
408        // bessel_i scaled / unscaled
409        let iu = bessel_i(&kx, 1, false);
410        let is_ = bessel_i(&kx, 1, true);
411        assert!((is_.cons() - iu.cons() * (-2.0f64).exp()).abs() < 1e-13);
412    }
413
414    #[test]
415    fn special_domain_panics() {
416        let _g = CONTEXT_LOCK.lock();
417        crate::context::init(6, 2).unwrap();
418
419        fn expect_code(f: impl FnOnce() -> Da + std::panic::UnwindSafe, code: u32) {
420            let err = std::panic::catch_unwind(f).expect_err("must panic");
421            let e = err
422                .downcast_ref::<crate::error::DaceError>()
423                .expect("DaceError payload");
424            assert_eq!(e.code, code, "{}", e);
425        }
426
427        expect_code(|| gamma(&Da::constant(0.0)), codes::OUT_OF_DOMAIN);
428        expect_code(|| gamma(&Da::constant(-3.0)), codes::OUT_OF_DOMAIN);
429        expect_code(|| log_gamma(&Da::constant(-2.0)), codes::OUT_OF_DOMAIN);
430        expect_code(|| psi(&Da::constant(-1.0), 2), codes::OUT_OF_DOMAIN);
431        expect_code(|| bessel_j(&Da::constant(0.0), 1), codes::OUT_OF_DOMAIN);
432        expect_code(|| bessel_y(&Da::constant(-1.0), 1), codes::OUT_OF_DOMAIN);
433        expect_code(
434            || bessel_i(&Da::constant(-1.0), 1, false),
435            codes::OUT_OF_DOMAIN,
436        );
437        expect_code(
438            || bessel_k(&Da::constant(0.0), 1, true),
439            codes::OUT_OF_DOMAIN,
440        );
441    }
442}