Skip to main content

comnum_math/
special.rs

1//! Special functions: the error function and its inverse, the Gaussian tail, the
2//! Fresnel integrals, Bessel functions, the gamma function and the regularized
3//! incomplete gamma function, the binomial law, the regularized incomplete beta function
4//! and its inverse, the exponential integral (all re-exported at the root).
5//!
6//! The power series share one private summation (the terms until the last falls below
7//! $10^{-17}$ of the sum), the continued fractions one modified Lentz iteration (the
8//! partial denominators floored at $10^{-300}$, until a factor is within $10^{-16}$ of
9//! 1), and the periodic integrands one midpoint rule; the order of the operations of
10//! each function is the one it had before they were shared (the same results, bit for
11//! bit).
12//!
13//! # References
14//!
15//! 1. W. H. Press, S. A. Teukolsky, W. T. Vetterling and B. P. Flannery, *Numerical
16//!    Recipes*, 3rd ed., Cambridge University Press, 2007, §5.2 (the modified Lentz
17//!    method), §6.2, §6.3 and §6.4.
18//! 2. M. Abramowitz and I. A. Stegun, *Handbook of Mathematical Functions*, Dover, 1965.
19
20use crate::search::bisect;
21use crate::{C64, cis};
22use std::ops::{Add, AddAssign, Div, Mul, MulAssign};
23
24/// The smallest magnitude of a partial denominator of the modified Lentz method: a
25/// smaller one is replaced by it (Press et al., §5.2).
26const LENTZ_TINY: f64 = 1e-300;
27
28/// The convergence of the modified Lentz method: the last factor $\Delta_{k}$ is within
29/// this of 1.
30const LENTZ_TOLERANCE: f64 = 1e-16;
31
32/// The convergence of a power series: the last term is below this fraction of the sum.
33const SERIES_TOLERANCE: f64 = 1e-17;
34
35/// The real or complex numbers the series and continued fractions are evaluated in.
36trait Scalar: Copy + Add<Output = Self> + Mul<Output = Self> + MulAssign + AddAssign + Div<f64, Output = Self> {
37    /// [`LENTZ_TINY`] in this field.
38    fn tiny() -> Self;
39    /// The modulus.
40    fn magnitude(self) -> f64;
41    /// `a · x`.
42    fn scale(a: f64, x: Self) -> Self;
43    /// `a / x`.
44    fn over(a: f64, x: Self) -> Self;
45    /// `1 / x`.
46    fn recip(self) -> Self;
47    /// `|x - 1|`.
48    fn distance_to_one(self) -> f64;
49}
50
51impl Scalar for f64 {
52    fn tiny() -> f64 {
53        LENTZ_TINY
54    }
55    fn magnitude(self) -> f64 {
56        self.abs()
57    }
58    fn scale(a: f64, x: f64) -> f64 {
59        a * x
60    }
61    fn over(a: f64, x: f64) -> f64 {
62        a / x
63    }
64    fn recip(self) -> f64 {
65        1.0 / self
66    }
67    fn distance_to_one(self) -> f64 {
68        (self - 1.0).abs()
69    }
70}
71
72impl Scalar for C64 {
73    fn tiny() -> C64 {
74        C64::new(LENTZ_TINY, 0.0)
75    }
76    fn magnitude(self) -> f64 {
77        self.norm()
78    }
79    fn scale(a: f64, x: C64) -> C64 {
80        a * x
81    }
82    fn over(a: f64, x: C64) -> C64 {
83        a / x
84    }
85    fn recip(self) -> C64 {
86        self.inv()
87    }
88    fn distance_to_one(self) -> f64 {
89        (self - 1.0).norm()
90    }
91}
92
93/// The power series $\sum_{k} t_{k} / d_{k}$ from the partial sum `sum`, with $t_{k} =
94/// t_{k-1} r_{k}$ from `term` $= t_{0}$, the ratios `ratio(k)` and the divisors
95/// `divisor(k)` for $k = 1, 2, \dots$ below `max_terms`, until the last term is below
96/// [`SERIES_TOLERANCE`] of the sum.
97fn series<T: Scalar>(
98    mut term: T,
99    mut sum: T,
100    max_terms: usize,
101    mut ratio: impl FnMut(usize) -> T,
102    divisor: impl Fn(usize) -> f64,
103) -> T {
104    for k in 1..max_terms {
105        term *= ratio(k);
106        let t = term / divisor(k);
107        sum += t;
108        if t.magnitude() < SERIES_TOLERANCE * sum.magnitude() {
109            break;
110        }
111    }
112    sum
113}
114
115/// The modified Lentz method (Press et al., §5.2) for the continued fraction
116/// $f = b_{0} + \cfrac{a_{1}}{b_{1} + \cfrac{a_{2}}{b_{2} + \dotsb}}$ from the state
117/// $(f_{0}, C_{0}, D_{0})$ and the terms $(a_{k}, b_{k})$ = `term(k)`, $k = 1, 2, \dots$
118/// below `max_terms`: $D_{k} = 1/(b_{k} + a_{k} D_{k-1})$, $C_{k} = b_{k} + a_{k}/C_{k-1}$
119/// (both floored at [`LENTZ_TINY`]), $f_{k} = f_{k-1} C_{k} D_{k}$, until $|C_{k} D_{k} -
120/// 1|$ is below [`LENTZ_TOLERANCE`].
121fn lentz<T: Scalar>(f0: T, c0: T, d0: T, max_terms: usize, mut term: impl FnMut(usize) -> (f64, T)) -> T {
122    let (mut f, mut c, mut d) = (f0, c0, d0);
123    for k in 1..max_terms {
124        let (a, b) = term(k);
125        d = b + T::scale(a, d);
126        if d.magnitude() < LENTZ_TINY {
127            d = T::tiny();
128        }
129        c = b + T::over(a, c);
130        if c.magnitude() < LENTZ_TINY {
131            c = T::tiny();
132        }
133        d = d.recip();
134        let delta = c * d;
135        f *= delta;
136        if delta.distance_to_one() < LENTZ_TOLERANCE {
137            break;
138        }
139    }
140    f
141}
142
143/// The midpoint rule on $n$ points of $\frac{1}{\pi} \int_{0}^{\pi} f(\theta)\, d\theta$,
144/// $\frac{1}{n} \sum_{k} f(h (k + \frac{1}{2}))$ with $h = \pi/n$ (exponentially
145/// convergent on a smooth periodic integrand).
146fn midpoint_mean(f: impl Fn(f64) -> f64, n: usize) -> f64 {
147    let h = std::f64::consts::PI / n as f64;
148    (0..n).map(|k| f(h * (k as f64 + 0.5))).sum::<f64>() / n as f64
149}
150
151/// Complementary error function $\operatorname{erfc}(x) = \frac{2}{\sqrt{\pi}}
152/// \int_{x}^{\infty} e^{-t^{2}}\, dt$.
153///
154/// The rational Chebyshev approximations of Cody (1969), as in his `CALERF` (SPECFUN,
155/// 1990): $1 - x R_{1}(x^{2})$ for $|x| \le 0.46875$,
156/// $e^{-x^{2}} R_{2}(x)$ up to 4 and $\frac{e^{-x^{2}}}{x} \left(\frac{1}{\sqrt{\pi}} +
157/// \frac{1}{x^{2}} R_{3}(1/x^{2})\right)$ beyond, $e^{-x^{2}}$ computed in two factors so
158/// that its rounding does not grow with $x^{2}$; $2 - \operatorname{erfc}(-x)$ for
159/// $x < -0.46875$. Its relative error is below $10^{-15}$ wherever the result is a
160/// normal number (against mpmath, the tail included: $\operatorname{erfc}(26) \approx
161/// 5.7 \cdot 10^{-296}$); it underflows to 0 beyond 27.3.
162///
163/// # Examples
164/// ```
165/// use comnum_math::erfc;
166/// assert!((erfc(1.0) / 0.157_299_207_050_285_13 - 1.0).abs() < 1e-15);
167/// assert!((erfc(10.0) / 2.088_487_583_762_544_8e-45 - 1.0).abs() < 1e-15);
168/// ```
169///
170/// # References
171///
172/// 1. W. J. Cody, "Rational Chebyshev approximations for the error function," *Math.
173///    Comp.*, 23(107), 631–637, 1969.
174/// 2. W. J. Cody, "Algorithm 715: SPECFUN — a portable FORTRAN package of special
175///    function routines and test drivers," *ACM Trans. Math. Softw.*, 19(1), 22–32, 1993.
176#[allow(
177    clippy::unreadable_literal,
178    clippy::excessive_precision,
179    reason = "the coefficients of Cody's CALERF, digit for digit"
180)]
181pub fn erfc(x: f64) -> f64 {
182    const A: [f64; 5] = [
183        3.16112374387056560e00,
184        1.13864154151050156e02,
185        3.77485237685302021e02,
186        3.20937758913846947e03,
187        1.85777706184603153e-1,
188    ];
189    const B: [f64; 4] =
190        [2.36012909523441209e01, 2.44024637934444173e02, 1.28261652607737228e03, 2.84423683343917062e03];
191    const C: [f64; 9] = [
192        5.64188496988670089e-1,
193        8.88314979438837594e00,
194        6.61191906371416295e01,
195        2.98635138197400131e02,
196        8.81952221241769090e02,
197        1.71204761263407058e03,
198        2.05107837782607147e03,
199        1.23033935479799725e03,
200        2.15311535474403846e-8,
201    ];
202    const D: [f64; 8] = [
203        1.57449261107098347e01,
204        1.17693950891312499e02,
205        5.37181101862009858e02,
206        1.62138957456669019e03,
207        3.29079923573345963e03,
208        4.36261909014324716e03,
209        3.43936767414372164e03,
210        1.23033935480374942e03,
211    ];
212    const P: [f64; 6] = [
213        3.05326634961232344e-1,
214        3.60344899949804439e-1,
215        1.25781726111229246e-1,
216        1.60837851487422766e-2,
217        6.58749161529837803e-4,
218        1.63153871373020978e-2,
219    ];
220    const Q: [f64; 5] = [
221        2.56852019228982242e00,
222        1.87295284992346725e00,
223        5.27905102951428412e-1,
224        6.05183413124413191e-2,
225        2.33520497626869185e-3,
226    ];
227    const FRAC_1_SQRT_PI: f64 = 5.6418958354775628695e-1;
228    let y = x.abs();
229    if y <= 0.46875 {
230        let ysq = if y > 1.11e-16 { y * y } else { 0.0 };
231        let (mut num, mut den) = (A[4] * ysq, ysq);
232        for i in 0..3 {
233            num = (num + A[i]) * ysq;
234            den = (den + B[i]) * ysq;
235        }
236        return 1.0 - x * (num + A[3]) / (den + B[3]);
237    }
238    // erfc(27.3) is below the smallest subnormal number
239    let r = if y > 27.3 {
240        0.0
241    } else {
242        let r = if y <= 4.0 {
243            let (mut num, mut den) = (C[8] * y, y);
244            for i in 0..7 {
245                num = (num + C[i]) * y;
246                den = (den + D[i]) * y;
247            }
248            (num + C[7]) / (den + D[7])
249        } else {
250            let ysq = 1.0 / (y * y);
251            let (mut num, mut den) = (P[5] * ysq, ysq);
252            for i in 0..4 {
253                num = (num + P[i]) * ysq;
254                den = (den + Q[i]) * ysq;
255            }
256            (FRAC_1_SQRT_PI - ysq * (num + P[4]) / (den + Q[4])) / y
257        };
258        // e^{-y²} = e^{-u²} e^{-(y - u)(y + u)} with u = y rounded down to 1/16: exact u²
259        let u = (y * 16.0).trunc() / 16.0;
260        (-u * u).exp() * (-(y - u) * (y + u)).exp() * r
261    };
262    if x < 0.0 { 2.0 - r } else { r }
263}
264
265/// Inverse of [`erfc`] on `(0, 2)`, by bisection (`±∞` at the ends).
266pub fn erfcinv(y: f64) -> f64 {
267    if y <= 0.0 {
268        return f64::INFINITY;
269    }
270    if y >= 2.0 {
271        return f64::NEG_INFINITY;
272    }
273    bisect(|x| erfc(x) > y, -27.0, 27.0, 200)
274}
275
276/// Gaussian tail probability `Q(x) = P(N(0,1) > x) = ½ erfc(x/√2)`.
277#[inline]
278pub fn qfunc(x: f64) -> f64 {
279    0.5 * erfc(x / std::f64::consts::SQRT_2)
280}
281
282/// Inverse of [`qfunc`] on $(0, 1)$: $Q^{-1}(p) = \sqrt{2}\, \operatorname{erfc}^{-1}(2p)$
283/// (`±∞` at the ends).
284///
285/// # Examples
286/// ```
287/// use comnum_math::{qfunc, qfuncinv};
288/// assert!((qfuncinv(0.025) - 1.959_964).abs() < 1e-6);
289/// assert!((qfunc(qfuncinv(1e-6)) / 1e-6 - 1.0).abs() < 1e-9);
290/// ```
291#[inline]
292pub fn qfuncinv(p: f64) -> f64 {
293    std::f64::consts::SQRT_2 * erfcinv(2.0 * p)
294}
295
296/// The standard normal density $\varphi(x) = e^{-x^{2}/2}/\sqrt{2\pi}$ (its
297/// distribution function is $1 - Q(x)$, [`qfunc`]).
298///
299/// # Examples
300/// ```
301/// use comnum_math::normal_pdf;
302/// assert!((normal_pdf(0.0) - 0.398_942_280_401_432_7).abs() < 1e-16);
303/// ```
304pub fn normal_pdf(x: f64) -> f64 {
305    (-0.5 * x * x).exp() / std::f64::consts::TAU.sqrt()
306}
307
308/// The Fresnel integrals $C(x) = \int_{0}^{x} \cos(\pi t^{2}/2)\, dt$ and
309/// $S(x) = \int_{0}^{x} \sin(\pi t^{2}/2)\, dt$, as `(C, S)`, to about $10^{-15}$.
310///
311/// For $|x| < 1.5$ the power series $C + jS = \sum_{k} (j\pi/2)^{k} x^{2k+1} / (k!\,(2k+1))$
312/// (Abramowitz & Stegun 7.3.11, 7.3.13); beyond, the relation to the error function
313/// $C + jS = \frac{1+j}{2} \operatorname{erf}(z)$, $z = \frac{\sqrt{\pi}}{2}(1-j)x$
314/// (A&S 7.3.22), with $\operatorname{erfc}(z) = \frac{e^{-z^{2}}}{\sqrt{\pi}}
315/// \cfrac{1}{z + \cfrac{1/2}{z + \cfrac{1}{z + \cfrac{3/2}{z + \dotsb}}}}$
316/// (A&S 7.1.14, valid for $\operatorname{Re} z > 0$), evaluated by the modified Lentz
317/// method. $C$ and $S$ are odd.
318pub fn fresnel(x: f64) -> (f64, f64) {
319    let ax = x.abs();
320    let v = if ax < 1.5 {
321        let a = C64::new(0.0, std::f64::consts::FRAC_PI_2 * ax * ax);
322        series(C64::new(ax, 0.0), C64::new(ax, 0.0), 60, |k| a / k as f64, |k| (2 * k + 1) as f64)
323    } else {
324        let z = C64::new(1.0, -1.0) * (std::f64::consts::PI.sqrt() / 2.0 * ax);
325        // modified Lentz for z + (1/2)/(z + 1/(z + (3/2)/(z + ...)))
326        let f = lentz(z, z, C64::new(0.0, 0.0), 100_000, |k| (k as f64 / 2.0, z));
327        // e^{-z²} = e^{jπx²/2}
328        let erfc = cis(std::f64::consts::FRAC_PI_2 * ax * ax) / (std::f64::consts::PI.sqrt() * f);
329        C64::new(0.5, 0.5) * (1.0 - erfc)
330    };
331    (v.re.copysign(x), v.im.copysign(x))
332}
333
334/// The Bessel function of the first kind of order zero, $J_{0}(x) = \frac{1}{\pi}
335/// \int_{0}^{\pi} \cos(x \sin\theta)\, d\theta$, by the midpoint rule on that
336/// periodic integrand (exponentially convergent: $64 + 2|x|$ points give the machine
337/// precision), as for the correlation $J_{0}(2\pi f_{D} \tau)$ of Jakes' fading.
338pub fn bessel_j0(x: f64) -> f64 {
339    let n = 64 + 2 * x.abs().ceil() as usize;
340    midpoint_mean(|theta| (x * theta.sin()).cos(), n)
341}
342
343/// The natural logarithm of the modified Bessel function of the second kind $K_{\nu}(x)$
344/// of real order $\nu$ and $x > 0$, from $K_{\nu}(x) = \int_{0}^{\infty} e^{-x \cosh t}
345/// \cosh(\nu t)\, dt$ by the trapezoidal rule (exponentially convergent on this even,
346/// entire integrand), summed in the log domain so that neither the large values at
347/// small $x$ and large $\nu$ nor the small ones at large $x$ overflow; to about
348/// $10^{-13}$ relative. The limits: $-\infty$ for $x = +\infty$ ($K_{\nu}(x) \to 0$),
349/// $+\infty$ for an infinite order, and the leading term $\ln\left(\tfrac{1}{2}
350/// \Gamma(\nu)\right) + \nu \ln(2/x)$ when $\nu / x$ overflows (a subnormal $x$; for
351/// $\nu \ge 0.05$, where the next term is below $e^{-70}$ relative); `NaN` for
352/// $x \le 0$, a `NaN` argument, or an overflowing $\nu / x$ with a smaller order.
353///
354/// # Examples
355/// ```
356/// use comnum_math::{bessel_k, ln_bessel_k};
357/// // K₀(1) and K₁(1) (Abramowitz & Stegun, Table 9.8); K_{1/2}(x) = √(π/2x) e^{-x}
358/// assert!((bessel_k(0.0, 1.0) - 0.421_024_438_240_708_3).abs() < 1e-13);
359/// assert!((bessel_k(1.0, 1.0) - 0.601_907_230_197_234_6).abs() < 1e-13);
360/// let x = 3.7;
361/// let half = (std::f64::consts::PI / (2.0 * x)).sqrt() * (-x).exp();
362/// assert!((bessel_k(0.5, x) / half - 1.0).abs() < 1e-13);
363/// // small x: K_ν(x) ≈ ½ Γ(ν) (2/x)^ν, here about 10^{129}
364/// let asymptotic = (0.5f64).ln() + comnum_math::ln_gamma(30.0) + 30.0 * 2000f64.ln();
365/// assert!((ln_bessel_k(30.0, 1e-3) - asymptotic).abs() < 1e-3);
366/// ```
367pub fn ln_bessel_k(nu: f64, x: f64) -> f64 {
368    if x.is_nan() || nu.is_nan() || x <= 0.0 {
369        return f64::NAN;
370    }
371    let nu = nu.abs();
372    if x == f64::INFINITY {
373        return f64::NEG_INFINITY;
374    }
375    if nu == f64::INFINITY {
376        return f64::INFINITY;
377    }
378    if nu >= 0.05 && !(nu / x).is_finite() {
379        // K_ν(x) = ½ Γ(ν) (2/x)^ν (1 + O((x/2)^{2ν} + x²)), the correction below e^{-70}
380        return -std::f64::consts::LN_2 + ln_gamma(nu) + nu * (std::f64::consts::LN_2 - x.ln());
381    }
382    // ln cosh(νt) - x cosh t, without overflow
383    let f = |t: f64| {
384        let a = nu * t;
385        a + (-2.0 * a).exp().ln_1p() - std::f64::consts::LN_2 - x * t.cosh()
386    };
387    // the maximum of the integrand: sinh t = (ν/x) tanh(νt) ≈ ν/x
388    let t_max = if nu > 0.0 { (nu / x).asinh() } else { 0.0 };
389    let peak = f(t_max);
390    // step: a fraction of the width of the peak, (x cosh t_max)^{-1/2}
391    let width = 1.0 / (x * t_max.cosh()).sqrt().max(1.0);
392    let h = 0.1 * width.min(1.0);
393    let mut acc = 0.5 * (f(0.0) - peak).exp();
394    let mut k = 1usize;
395    loop {
396        let t = k as f64 * h;
397        let v = f(t) - peak;
398        if v.is_nan() {
399            return f64::NAN;
400        }
401        acc += v.exp();
402        if t > t_max && v < -50.0 {
403            break;
404        }
405        k += 1;
406    }
407    peak + (acc * h).ln()
408}
409
410/// The modified Bessel function of the second kind $K_{\nu}(x)$ of real order:
411/// `exp(`[`ln_bessel_k`]`)`.
412pub fn bessel_k(nu: f64, x: f64) -> f64 {
413    ln_bessel_k(nu, x).exp()
414}
415
416/// The scaled modified Bessel function of the first kind of order zero,
417/// $e^{-|x|} I_{0}(x)$, with $I_{0}(x) = \frac{1}{\pi} \int_{0}^{\pi} e^{x\cos\theta}
418/// \, d\theta$: the midpoint rule on the periodic integrand
419/// $e^{|x|(\cos\theta - 1)}$ for $|x| \le 60$ (exponentially convergent), and the
420/// asymptotic series $\frac{1}{\sqrt{2\pi x}} \sum_{k} \frac{((2k-1)!!)^{2}}{k!
421/// \, (8x)^{k}}$ beyond; to about $10^{-14}$ relative. The density of the Rice
422/// distribution, $y e^{-(y - v)^{2}/2} \, e^{-yv} I_{0}(yv)$, needs the scaled form.
423///
424/// # Examples
425/// ```
426/// use comnum_math::bessel_i0e;
427/// // I₀(1) = 1.266065877752008, I₀(10) = 2815.716628466254
428/// assert!((bessel_i0e(1.0) * 1f64.exp() - 1.266_065_877_752_008).abs() < 1e-13);
429/// assert!((bessel_i0e(10.0) * 10f64.exp() / 2815.716_628_466_254 - 1.0).abs() < 1e-13);
430/// assert!((bessel_i0e(1e4) * (2.0 * std::f64::consts::PI * 1e4).sqrt() - 1.0).abs() < 2e-5);
431/// ```
432pub fn bessel_i0e(x: f64) -> f64 {
433    let ax = x.abs();
434    if ax <= 60.0 {
435        let n = 32 + 8 * ax.sqrt().ceil() as usize + ax.ceil() as usize;
436        midpoint_mean(|theta| (ax * (theta.cos() - 1.0)).exp(), n)
437    } else {
438        // Σ_k ((2k-1)!!)² / (k! (8x)^k): the terms fall fast for x > 60
439        let ratio = |k: usize| {
440            let odd = (2 * k - 1) as f64;
441            odd * odd / (k as f64 * 8.0 * ax)
442        };
443        series(1.0, 1.0, 30, ratio, |_| 1.0) / (std::f64::consts::TAU * ax).sqrt()
444    }
445}
446
447/// The error function $\operatorname{erf}(x) = \frac{2}{\sqrt{\pi}} \int_{0}^{x}
448/// e^{-t^{2}}\, dt$: its Maclaurin series $\frac{2}{\sqrt{\pi}} \sum_{k} \frac{(-1)^{k}
449/// x^{2k+1}}{k!\,(2k+1)}$ for $|x| < 2$ (relative error about $10^{-15}$, small
450/// arguments included), $1 - \operatorname{erfc}(x)$ beyond (absolute error about
451/// $10^{-16}$). Odd.
452///
453/// # Examples
454/// ```
455/// use comnum_math::erf;
456/// assert!((erf(1e-4) / 1.128_379_163_334_248_7e-4 - 1.0).abs() < 1e-14);
457/// assert!((erf(0.9) - 0.796_908_212_422_831_9).abs() < 1e-15);
458/// ```
459pub fn erf(x: f64) -> f64 {
460    if x.abs() >= 2.0 {
461        return (1.0 - erfc(x.abs())).copysign(x);
462    }
463    let x2 = x * x;
464    series(x, x, 60, |k| -x2 / k as f64, |k| (2 * k + 1) as f64) * std::f64::consts::FRAC_2_SQRT_PI
465}
466
467/// The logarithm of the gamma function, $\ln \Gamma(x)$ for $x > 0$, by the Lanczos
468/// approximation ($g = 7$, nine coefficients; relative error about $10^{-15}$) and the
469/// reflection formula $\Gamma(x)\Gamma(1-x) = \pi / \sin(\pi x)$ below $1/2$.
470///
471/// # Examples
472/// ```
473/// use comnum_math::ln_gamma;
474/// // Γ(10) = 9! = 362 880, Γ(1/2) = √π
475/// assert!((ln_gamma(10.0) - 362_880f64.ln()).abs() < 1e-13);
476/// assert!((ln_gamma(0.5) - std::f64::consts::PI.sqrt().ln()).abs() < 1e-14);
477/// ```
478#[allow(clippy::unreadable_literal, reason = "the Lanczos coefficients, digit for digit")]
479pub fn ln_gamma(x: f64) -> f64 {
480    const G: f64 = 7.0;
481    const C: [f64; 9] = [
482        0.999_999_999_999_809_9,
483        676.5203681218851,
484        -1259.1392167224028,
485        771.323_428_777_653_1,
486        -176.615_029_162_140_6,
487        12.507343278686905,
488        -0.13857109526572012,
489        9.984_369_578_019_572e-6,
490        1.5056327351493116e-7,
491    ];
492    if x < 0.5 {
493        let pi = std::f64::consts::PI;
494        return (pi / (pi * x).sin()).ln() - ln_gamma(1.0 - x);
495    }
496    let x = x - 1.0;
497    let t = x + G + 0.5;
498    let a = C[0] + C.iter().enumerate().skip(1).map(|(i, c)| c / (x + i as f64)).sum::<f64>();
499    0.5 * std::f64::consts::TAU.ln() + (x + 0.5) * t.ln() - t + a.ln()
500}
501
502/// The regularized lower incomplete gamma function
503/// $P(a, x) = \frac{1}{\Gamma(a)} \int_{0}^{x} u^{a-1} e^{-u}\, du$, $a > 0$, $x \ge 0$:
504/// the distribution function of a gamma variable of shape $a$ and unit scale (a
505/// $\chi^{2}$ variable with $2a$ degrees of freedom, halved). The series
506/// $e^{-x} x^{a} \sum_{k} x^{k} / \Gamma(a + k + 1)$ for $x < a + 1$, and $1 - Q(a, x)$
507/// with the continued fraction of $Q$ (modified Lentz) beyond (Press et al., *Numerical
508/// Recipes*, §6.2); relative error about $10^{-14}$ on the side computed directly.
509///
510/// # Examples
511/// ```
512/// use comnum_math::gamma_p;
513/// // P(1, x) = 1 - e^{-x}
514/// assert!((gamma_p(1.0, 0.5) - (1.0 - (-0.5f64).exp())).abs() < 1e-15);
515/// assert!((gamma_p(20.0, 5.0) / 3.452_135_820_914_455e-7 - 1.0).abs() < 1e-12);
516/// ```
517pub fn gamma_p(a: f64, x: f64) -> f64 {
518    if x <= 0.0 {
519        return 0.0;
520    }
521    let ln_front = a * x.ln() - x - ln_gamma(a);
522    if x < a + 1.0 {
523        let mut ap = a;
524        let ratio = |_| {
525            ap += 1.0;
526            x / ap
527        };
528        series(1.0 / a, 1.0 / a, 10_001, ratio, |_| 1.0) * ln_front.exp()
529    } else {
530        // Q(a, x) = e^{-x} x^a / Γ(a) · 1/(x+1-a- 1·(1-a)/(x+3-a- 2(2-a)/(x+5-a- …)))
531        let mut b = x + 1.0 - a;
532        let d = 1.0 / b;
533        let term = |i: usize| {
534            b += 2.0;
535            (-(i as f64) * (i as f64 - a), b)
536        };
537        let h = lentz(d, 1.0 / LENTZ_TINY, d, 10_000, term);
538        1.0 - ln_front.exp() * h
539    }
540}
541
542/// The logarithms of the binomial probabilities
543/// $\ln \binom{n}{k} + k \ln p + (n - k) \ln (1 - p)$, $k = 0, \dots, n$: the law of
544/// the number of successes among $n$ trials of probability $p \in [0, 1]$ (the
545/// coefficients by the running product $\binom{n}{k} = \binom{n}{k-1} (n + 1 - k)/k$;
546/// at $p = 0$ or 1 the impossible counts are $-\infty$).
547///
548/// # Examples
549/// ```
550/// use comnum_math::ln_binomial_pmf;
551/// let l = ln_binomial_pmf(20, 0.3);
552/// assert!((l[7].exp() - 0.164_261_985_217_236_67).abs() < 1e-15);
553/// assert!((l.iter().map(|v| v.exp()).sum::<f64>() - 1.0).abs() < 1e-14);
554/// assert_eq!(ln_binomial_pmf(3, 0.0), [0.0, f64::NEG_INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY]);
555/// ```
556pub fn ln_binomial_pmf(n: usize, p: f64) -> Vec<f64> {
557    if p <= 0.0 || p >= 1.0 {
558        let certain = if p <= 0.0 { 0 } else { n };
559        return (0..=n).map(|k| if k == certain { 0.0 } else { f64::NEG_INFINITY }).collect();
560    }
561    let (lp, lq) = (p.ln(), (1.0 - p).ln());
562    let mut terms = Vec::with_capacity(n + 1);
563    let mut log_c = 0.0;
564    for k in 0..=n {
565        if k > 0 {
566            log_c += ((n + 1 - k) as f64).ln() - (k as f64).ln();
567        }
568        terms.push(log_c + k as f64 * lp + (n - k) as f64 * lq);
569    }
570    terms
571}
572
573/// The regularized incomplete beta function
574/// $I_{x}(a, b) = \frac{1}{B(a, b)} \int_{0}^{x} u^{a-1} (1 - u)^{b-1}\, du$, $a, b > 0$,
575/// $x \in [0, 1]$: the distribution function of a $\mathrm{Beta}(a, b)$ variable, and
576/// the binomial tail $P(X \ge k) = I_{p}(k, n - k + 1)$ of $X \sim \mathrm{Bin}(n, p)$.
577/// The continued fraction of Press et al. (*Numerical Recipes*, §6.4) by the modified
578/// Lentz method, on $I_{1-x}(b, a) = 1 - I_{x}(a, b)$ when
579/// $x > (a + 1)/(a + b + 2)$, where it converges faster; relative error about
580/// $10^{-14}$.
581///
582/// # Examples
583/// ```
584/// use comnum_math::beta_inc;
585/// // I_x(a, 1) = x^a, and Beta(a, a) is symmetric about 1/2
586/// assert!((beta_inc(3.0, 1.0, 0.7) - 0.343).abs() < 1e-15);
587/// assert!((beta_inc(4.5, 4.5, 0.5) - 0.5).abs() < 1e-15);
588/// assert!((beta_inc(3.5, 4.5, 0.4) - 0.429_090_217_274_695_76).abs() < 1e-14);
589/// ```
590pub fn beta_inc(a: f64, b: f64, x: f64) -> f64 {
591    if x <= 0.0 {
592        return 0.0;
593    }
594    if x >= 1.0 {
595        return 1.0;
596    }
597    if x > (a + 1.0) / (a + b + 2.0) {
598        return 1.0 - beta_inc(b, a, 1.0 - x);
599    }
600    let ln_front = a * x.ln() + b * (1.0 - x).ln() + ln_gamma(a + b) - ln_gamma(a) - ln_gamma(b);
601    // 1/(1 + d_1/(1 + d_2/(1 + …))), d_{2m+1} = -(a+m)(a+b+m)x/((a+2m)(a+2m+1)),
602    // d_{2m} = m(b-m)x/((a+2m-1)(a+2m))
603    let term = |k: usize| {
604        let m = (k / 2) as f64;
605        let d = if k % 2 == 1 {
606            -(a + m) * (a + b + m) * x / ((a + 2.0 * m) * (a + 2.0 * m + 1.0))
607        } else {
608            m * (b - m) * x / ((a + 2.0 * m - 1.0) * (a + 2.0 * m))
609        };
610        (d, 1.0)
611    };
612    ln_front.exp() / (a * lentz(1.0, 1.0, 0.0, 10_000, term))
613}
614
615/// The inverse of [`beta_inc`] in $x$: the $q$ quantile of $\mathrm{Beta}(a, b)$, by
616/// bisection on $[0, 1]$ (0 and 1 at the ends of $q$).
617///
618/// # Examples
619/// ```
620/// use comnum_math::{beta_inc, beta_inv};
621/// let x = beta_inv(0.975, 3.5, 4.5);
622/// assert!((x - 0.765_498_750_747_906_2).abs() < 1e-14);
623/// assert!((beta_inc(3.5, 4.5, x) - 0.975).abs() < 1e-14);
624/// ```
625pub fn beta_inv(q: f64, a: f64, b: f64) -> f64 {
626    if q <= 0.0 {
627        return 0.0;
628    }
629    if q >= 1.0 {
630        return 1.0;
631    }
632    bisect(|x| beta_inc(a, b, x) < q, 0.0, 1.0, 200)
633}
634
635/// The exponential integral $E_{1}(x) = \int_{x}^{\infty} \frac{e^{-t}}{t}\, dt$,
636/// $x > 0$: the series $-\gamma - \ln x - \sum_{k \ge 1} \frac{(-x)^{k}}{k\, k!}$ for
637/// $x \le 1$, the continued fraction $\cfrac{e^{-x}}{x + 1 - \cfrac{1}{x + 3 -
638/// \cfrac{4}{x + 5 - \dotsb}}}$ beyond (Press et al., *Numerical Recipes*, §6.3);
639/// relative error about $10^{-15}$. $e^{1/\rho} E_{1}(1/\rho)$ is the ergodic capacity
640/// (nats) of the Rayleigh channel at the mean SNR $\rho$.
641///
642/// # Examples
643/// ```
644/// use comnum_math::expint_e1;
645/// assert!((expint_e1(1.0) - 0.219_383_934_395_520_5).abs() < 1e-15);
646/// assert!((expint_e1(10.0) / 4.156_968_929_685_325e-6 - 1.0).abs() < 1e-14);
647/// ```
648pub fn expint_e1(x: f64) -> f64 {
649    if x <= 0.0 {
650        return f64::INFINITY;
651    }
652    if x <= 1.0 {
653        const EULER: f64 = 0.577_215_664_901_532_9;
654        -EULER - x.ln() - series(1.0, 0.0, 60, |k| -x / k as f64, |k| k as f64)
655    } else {
656        // e^{-x} · 1/(x + 1 - 1/(x + 3 - 4/(x + 5 - …)))
657        let mut b = x + 1.0;
658        let d = 1.0 / b;
659        let term = |i: usize| {
660            b += 2.0;
661            (-((i * i) as f64), b)
662        };
663        lentz(d, 1.0 / LENTZ_TINY, d, 10_000, term) * (-x).exp()
664    }
665}