treams_core/special/integrals/
gamma.rs

1//! The upper incomplete gamma function `Gamma(n, z)` of integer and half-integer degree.
2//!
3//! [`upper_gamma`] takes whichever of the continued fraction, power series, Kummer sum and
4//! degree recurrence (DLMF 8) keeps relative accuracy at the argument.
5//! [`ScaledGammaLadder`] steps through the scaled values `Gamma(d, a) / a^d` at
6//! descending degrees, which the Kambe series sums.
7//!
8//! Upstream: `incgamma` of `treams/special/_integrals.pyx`, which runs the degree
9//! recurrence only. The choice of method and the scaled ladder are treams-rs extensions.
10
11use std::f64::consts::PI;
12
13use errorfunctions::ComplexErrorFunctions;
14
15use crate::{
16    Complex,
17    numerics::{EULER, finite},
18};
19
20/// `Gamma(n, z)` from DLMF 8.9.2 by modified Lentz iteration, which keeps relative
21/// accuracy for negative degree and large `|z|` away from the negative real axis, and
22/// whether it converged.
23fn gamma_fraction(n: f64, z: Complex) -> (Complex, bool) {
24    let (fraction, converged) = continued_fraction(n, z);
25    ((n * z.ln() - z).exp() * fraction, converged)
26}
27
28/// The continued fraction `h` of DLMF 8.9.2, `Gamma(n, z) = z^n e^-z h` on the
29/// principal branch, with whether it converged.
30fn continued_fraction(n: f64, z: Complex) -> (Complex, bool) {
31    let mut b = z + 1.0 - n;
32    let mut c = Complex::new(1e300, 0.0);
33    let mut d = 1.0 / b;
34    let mut h = d;
35    let mut converged = false;
36    for j in 1..=1000 {
37        let j = f64::from(j);
38        let a = -j * (j - n);
39        b += 2.0;
40        d = b + a * d;
41        c = b + a / c;
42        if d.norm() < 1e-300 {
43            d = Complex::new(1e-300, 0.0);
44        }
45        if c.norm() < 1e-300 {
46            c = Complex::new(1e-300, 0.0);
47        }
48        d = 1.0 / d;
49        let delta = c * d;
50        h *= delta;
51        if (delta - 1.0).norm() < 3e-15 {
52            converged = true;
53            break;
54        }
55    }
56    (h, converged)
57}
58
59/// The exponential integral `E1(z) = Gamma(0, z)`, the start of the integer-degree
60/// recurrence. The power series cancels by about `e^(|z| + Re z)`, while the continued
61/// fraction converges slowly next to the negative real axis; beyond `|z| = 40` the
62/// fraction converges there too.
63fn exp1(z: Complex) -> Complex {
64    if z.norm() + z.re > 4.0 || z.norm() >= 40.0 {
65        let (mut result, _) = gamma_fraction(0.0, z);
66        if z.re < 0.0 && z.im == 0.0 {
67            result.im = -PI.copysign(z.im);
68        }
69        return result;
70    }
71    // E1(z) = -gamma - log(z) - sum (-z)^j/(j j!).
72    let mut term = -z;
73    let mut sum = term;
74    for j in 2..=500 {
75        let j = f64::from(j);
76        term *= -z / j;
77        let change = term / j;
78        sum += change;
79        if change.norm() <= 2e-16 * sum.norm() {
80            break;
81        }
82    }
83    -EULER - z.ln() - sum
84}
85
86/// Largest growth of the relative rounding error accepted from one method without
87/// trying another (one digit).
88pub(super) const RECURRENCE_GROWTH: f64 = 10.0;
89/// Bound on `|z| + Re z` of the parabola around the negative real axis inside which
90/// the power series terms share a phase: their cancellation stays below `e`.
91const SERIES_PARABOLA: f64 = 1.0;
92/// Largest `|z|` at which [`gamma_series`] is used. It needs about `2|z|` terms of size
93/// up to `e^|z|`; with the power `z^n` split in two factors every intermediate stays
94/// representable for `|n| <=` [`MAX_DEGREE`](crate::MAX_DEGREE).
95const GAMMA_SERIES_RADIUS: f64 = 600.0;
96
97/// The upper incomplete gamma function `Gamma(n, z)` of integer or half-integer `n`.
98///
99/// It takes the continued fraction where `Re z > 4` and `|z| > |n| + 2`; the power
100/// series or the continued fraction where a downward recurrence would lose accuracy;
101/// otherwise the degree recurrence from `E1` or `erfc`, or the Kummer sum or the power
102/// series where their error bounds are smaller.
103pub(crate) fn upper_gamma(n: f64, z: Complex) -> Complex {
104    if z == Complex::default() {
105        return Complex::new(
106            if n <= 0.0 {
107                f64::INFINITY
108            } else {
109                libm::tgamma(n)
110            },
111            0.0,
112        );
113    }
114    let modulus = z.norm();
115    if z.re > 4.0 && modulus > n.abs() + 2.0 {
116        return gamma_fraction(n, z).0;
117    }
118    if n == 2.0 {
119        // DLMF 8.4.8: factor before multiplying by exp(-z). Adding exp(-z)
120        // and z*exp(-z) instead loses relative accuracy at the zero z = -1.
121        return (1.0 + z) * (-z).exp();
122    }
123    let base = if n.fract() == 0.0 { 0.0 } else { 0.5 };
124    let series = modulus <= GAMMA_SERIES_RADIUS;
125    // A downward step subtracts the leading asymptotic term z^d e^-z, losing a
126    // factor |z| / |d| when |z| exceeds the degree. There the series (near the
127    // cut) or the continued fraction (elsewhere) keeps full relative accuracy.
128    if downward_unstable(n, base, modulus) {
129        if series && modulus + z.re <= SERIES_PARABOLA {
130            return gamma_series(n, z).0;
131        }
132        if let (value, true) = gamma_fraction(n, z) {
133            return value;
134        }
135    }
136    // An upward step cancels where Gamma(n, z) falls far below the terms z^d e^-z it
137    // sums, by up to about e^(|z| - Re z): at large positive degree away from the
138    // positive real axis. Two sums of the lower function gamma(n, z) = Gamma(n) -
139    // Gamma(n, z) avoid that loss, and the smallest error bound wins. For |z| < n the
140    // Kummer form has falling terms at any phase of z. The power series has terms of
141    // one phase near the negative real axis; elsewhere they cancel by about
142    // e^(|z| + Re z), and it is only tried when that is the smaller loss.
143    let (mut value, mut error) = gamma_recurrence(n, z, base, modulus);
144    // |value| is at least its larger part: most values skip the `hypot`.
145    let larger = value.re.abs().max(value.im.abs());
146    if error > RECURRENCE_GROWTH * larger && error > RECURRENCE_GROWTH * value.norm() {
147        if n > 0.0 && modulus < n {
148            let kummer = gamma_kummer(n, z);
149            if kummer.1 < error {
150                (value, error) = kummer;
151            }
152        }
153        if series && (modulus + z.re).exp() * value.norm() < error {
154            let (value_series, error_series) = gamma_series(n, z);
155            if error_series < error {
156                return value_series;
157            }
158        }
159    }
160    value
161}
162
163/// The Kummer sum of DLMF 8.7.1 with 8.2.7,
164/// `Gamma(a, z) = Gamma(a) - z^a e^-z sum_k z^k / (a (a+1) ... (a+k))`, whose terms fall
165/// at any phase of `z` for `0 < |z| < a`, with the magnitude of the terms as the
166/// absolute error bound in units of epsilon.
167fn gamma_kummer(n: f64, z: Complex) -> (Complex, f64) {
168    let mut term = Complex::new(n.recip(), 0.0);
169    let (mut sum, mut magnitude) = (term, term.l1_norm());
170    let mut k = 0.0;
171    // The terms fall at least like n^k / (n+1)...(n+k), below 1e-17 of the first
172    // within 9 sqrt(n) + 40 terms.
173    while k < 9.0 * n.sqrt() + 40.0 && term.norm() > 1e-17 * sum.norm() {
174        k += 1.0;
175        term *= z / (n + k);
176        sum += term;
177        magnitude += term.l1_norm();
178    }
179    // |z|^a < a^a and |e^-z| < e^a stay representable for the degrees used (a < 131),
180    // and their product only overflows with gamma(a, z).
181    let exponential = (-z).exp();
182    let power = half_power(z, n);
183    let complete = libm::tgamma(n);
184    (
185        complete - power * (exponential * sum),
186        complete + power.norm() * (exponential.norm() * magnitude),
187    )
188}
189
190/// Whether the rounding error of the downward recurrence from `base` to `n` grows
191/// beyond [`RECURRENCE_GROWTH`]: the step to degree `d` amplifies it by
192/// `max(1, |z|/|d|)`.
193fn downward_unstable(n: f64, base: f64, radius: f64) -> bool {
194    let mut growth = 1.0;
195    let mut degree = base - 1.0;
196    while degree >= n {
197        growth *= (radius / degree.abs()).max(1.0);
198        if growth > RECURRENCE_GROWTH {
199            return true;
200        }
201        degree -= 1.0;
202    }
203    false
204}
205
206/// The degree recurrence DLMF 8.8.2 from `E1` (integer degree) or `sqrt(pi) erfc(sqrt z)`
207/// (half-integer degree), with powers of `z` updated by one multiplication per step
208/// (integer degrees stay real on the real axis), and a first-order bound on the absolute
209/// error in units of epsilon from the start value and each endpoint term. `modulus` is
210/// `|z|`.
211fn gamma_recurrence(n: f64, z: Complex, base: f64, modulus: f64) -> (Complex, f64) {
212    let (mut value, mut power, ulps) = if base == 0.0 {
213        (exp1(z), Complex::new(1.0, 0.0), 1.0)
214    } else {
215        // erfc forms e^-z from the rounded square of sqrt z, losing about |z| ulps.
216        let root = crate::numerics::complex_sqrt(z);
217        (PI.sqrt() * root.erfc(), root, 1.0 + modulus)
218    };
219    let mut error = ulps * value.l1_norm();
220    let exponential = (-z).exp();
221    let mut degree = base;
222    while degree < n {
223        let endpoint = power * exponential;
224        value = degree * value + endpoint;
225        error = degree * error + endpoint.l1_norm();
226        power *= z;
227        degree += 1.0;
228    }
229    let inverse = inverse(z);
230    while degree > n {
231        degree -= 1.0;
232        power *= inverse;
233        let endpoint = power * exponential;
234        value = (value - endpoint) / degree;
235        error = (error + endpoint.l1_norm()) / degree.abs();
236    }
237    (value, error)
238}
239
240/// The power series of DLMF 8.7.3, `Gamma(a,z) = Gamma(a) - z^a sum_k (-z)^k / (k! (a+k))`,
241/// whose terms share a phase near the negative real axis; for `a = -m`, DLMF 8.4.15
242/// replaces the `Gamma(a)` pole pair by `(-1)^m (psi(m+1) - ln z) / m!`. The principal
243/// logarithm and the signed zero of `Im z` select the side of the cut; the magnitude of
244/// the terms bounds the error.
245fn gamma_series(n: f64, z: Complex) -> (Complex, f64) {
246    let radius = z.norm();
247    let mut term = Complex::new(1.0, 0.0);
248    let (mut sum, mut magnitude) = (Complex::default(), 0.0);
249    let mut k = 0.0;
250    while k <= 2.0 * radius + 200.0 {
251        if n + k != 0.0 {
252            let scaled = term / (n + k);
253            sum += scaled;
254            magnitude += scaled.l1_norm();
255        }
256        // Beyond k = 2|z| the tail is below the current term.
257        if k > 2.0 * radius && term.norm() <= 1e-17 * sum.norm() {
258            break;
259        }
260        k += 1.0;
261        term *= -z / k;
262    }
263    // |z|^n may leave the normal range of f64 where z^n sum does not, as at n = -128
264    // and |z| = 500 (1e-345 and 1e217); z^n then enters in two factors.
265    let power = half_power(z, n);
266    let (sum, magnitude) = if (1e-300..1e300).contains(&power.l1_norm()) {
267        (power * sum, power.l1_norm() * magnitude)
268    } else {
269        let half = (0.5 * n).trunc();
270        let [outer, inner] = [half, n - half].map(|degree| half_power(z, degree));
271        (
272            outer * (inner * sum),
273            outer.l1_norm() * (inner.l1_norm() * magnitude),
274        )
275    };
276    if n.fract() != 0.0 || n > 0.0 {
277        let complete = libm::tgamma(n);
278        return (complete - sum, complete.abs() + magnitude);
279    }
280    let (mut digamma, mut scale, mut j) = (-EULER, 1.0, 1.0);
281    while j <= -n {
282        digamma += 1.0 / j;
283        scale /= -j;
284        j += 1.0;
285    }
286    let logarithm = z.ln();
287    (
288        scale * (digamma - logarithm) - sum,
289        scale.abs() * (digamma.abs() + logarithm.norm()) + magnitude,
290    )
291}
292
293/// `z^degree` on the principal branch for an integer or half-integer degree. Integer
294/// degrees stay exactly real on the real axis, unlike `exp(degree ln z)`.
295#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] // Bounded integral degrees.
296fn half_power(z: Complex, degree: f64) -> Complex {
297    let floor = degree.floor();
298    let power = if floor < 0.0 {
299        inverse(z).powu((-floor) as u32)
300    } else {
301        z.powu(floor as u32)
302    };
303    if degree == floor {
304        power
305    } else {
306        power * crate::numerics::complex_sqrt(z)
307    }
308}
309
310/// `1/z` without squaring `|z|`: `num_complex`'s `inv()` divides by `|z|^2`, which loses
311/// precision below `|z| = 1e-154` and underflows to NaN results below `1e-162`.
312fn inverse(z: Complex) -> Complex {
313    crate::numerics::ratio(Complex::new(1.0, 0.0), z)
314}
315
316/// Whether `1e-290 <= |x| < 1e290`, from the larger component of a finite `x` unless
317/// `|x|` lies near either end (`|x|` is at most `sqrt 2` times that component).
318fn normal(x: Complex) -> bool {
319    let larger = x.re.abs().max(x.im.abs());
320    (finite(x) && (2e-290..7e289).contains(&larger)) || (1e-290..1e290).contains(&x.norm())
321}
322
323/// Scaled incomplete gamma functions `R(d) = Gamma(d, a) / a^d` of one argument at
324/// descending degrees `d = n, n-1, ...`: downward by `R(d) = (a R(d+1) - e^-a) / d`,
325/// which is stable once `|d| >= |a|`, and above that upward from one [`upper_gamma`]
326/// evaluation up to [`LADDER_RUN`] degrees lower. At `a = 0` every value is the limit
327/// `-1/d`. `reflected` selects the branch `-sqrt(a)` for half-integer degrees.
328#[derive(Clone, Debug)]
329pub(crate) struct ScaledGammaLadder {
330    degree: f64,
331    argument: Complex,
332    /// `|a|`, which every step compares with the degree.
333    modulus: f64,
334    exponential: Complex,
335    reflected: bool,
336    /// `R` one degree higher, once one value exists.
337    upper: Option<Complex>,
338    /// Values of the next degrees from the upward recurrence; the last one is next.
339    pending: Vec<Complex>,
340}
341
342/// Degrees per upward run of [`ScaledGammaLadder`]. Short runs keep the seed far
343/// below `|a|` in degree magnitude, where the continued fraction converges quickly,
344/// and avoid evaluating degrees that a short series never reaches.
345const LADDER_RUN: f64 = 16.0;
346
347impl ScaledGammaLadder {
348    /// A ladder whose first value is `R(n)`.
349    pub(crate) fn new(n: f64, argument: Complex, reflected: bool) -> Self {
350        Self {
351            degree: n,
352            argument,
353            modulus: argument.norm(),
354            exponential: (-argument).exp(),
355            reflected,
356            upper: None,
357            pending: Vec::new(),
358        }
359    }
360
361    /// The next value: `R(n)` first, then one degree lower each call.
362    pub(crate) fn next_lower(&mut self) -> Complex {
363        let stable = self.degree.abs() >= self.modulus;
364        let value = if self.argument == Complex::default() {
365            // The limit needs no recurrence, which R(0) = infinity would spoil.
366            self.seed(self.degree)
367        } else {
368            match (self.pending.pop(), self.upper) {
369                (Some(value), _) => value,
370                // |degree| >= |z| controls propagation, but does not prevent
371                // cancellation at a zero. Restart at the factored degree-two
372                // value instead of subtracting nearly equal degree-three terms.
373                (None, _) if stable && self.degree == 2.0 => self.seed(self.degree),
374                (None, Some(upper)) if stable => {
375                    (self.argument * upper - self.exponential) / self.degree
376                }
377                (None, None) if stable => self.run_down(),
378                _ => self.run_up(),
379            }
380        };
381        self.upper = Some(value);
382        self.degree -= 1.0;
383        value
384    }
385
386    /// `R` on the selected branch from one gamma evaluation, or its limit at `a = 0`.
387    /// Where `Gamma(d, a)` or `a^d` leave the normal range of `f64` (near `|d| = |a|` from
388    /// `|a|` of about 120, at far evanescent orders of the lattice sums) `R = e^-a h`
389    /// comes from the continued fraction `h` itself; the division scales the denominator
390    /// first.
391    fn seed(&self, degree: f64) -> Complex {
392        if self.argument == Complex::default() {
393            let limit = if degree < 0.0 {
394                -1.0 / degree
395            } else {
396                f64::INFINITY
397            };
398            return Complex::new(limit, 0.0);
399        }
400        let power = half_power(self.argument, degree);
401        let mut value = upper_gamma(degree, self.argument);
402        if self.reflected {
403            value -= 2.0 * libm::tgamma(degree);
404        }
405        // Near Gamma(2, -1), a finite factored value can be zero or subnormal.
406        // Divide it directly when the power is normal, without the continued
407        // fraction's tiny-denominator clamps.
408        let representable = (normal(value) || (degree == 2.0 && finite(value))) && normal(power);
409        if !(self.reflected || representable)
410            && let (fraction, true) = continued_fraction(degree, self.argument)
411        {
412            return (-self.argument).exp() * fraction;
413        }
414        crate::numerics::ratio(value, power)
415    }
416
417    /// `R` at the current, stable degree from the degree nearest zero (at most -1/2)
418    /// below which every downward step is stable, so that tiny `|a|` never meets `a^d` at
419    /// a large one.
420    fn run_down(&self) -> Complex {
421        let start = (1.0 - self.modulus).min(-0.5);
422        let mut degree = self.degree + (start - self.degree).floor().max(0.0);
423        let mut value = self.seed(degree);
424        while degree > self.degree {
425            degree -= 1.0;
426            value = (self.argument * value - self.exponential) / degree;
427        }
428        value
429    }
430
431    /// `R` at the current degree by the upward recurrence `R(d+1) = (d R(d) + e^-a) / a`,
432    /// whose error shrinks by `|d| / |a|` per step, from one evaluation at the lowest
433    /// degree with `|d| < |a|` (or the current one), queuing the values in between.
434    fn run_up(&mut self) -> Complex {
435        let steps = ((self.degree + self.modulus).ceil() - 1.0).clamp(0.0, LADDER_RUN);
436        let mut degree = self.degree - steps;
437        let mut value = self.seed(degree);
438        while degree < self.degree {
439            self.pending.push(value);
440            value = (degree * value + self.exponential) / self.argument;
441            degree += 1.0;
442        }
443        value
444    }
445}
446
447/// The argument derivative `dGamma(n, z)/dz = -z^(n-1) e^-z`, with its limits `-1`
448/// (`n = 1`) and `0` (`n > 1`) at `z = 0`.
449pub(super) fn gamma_derivative(n: f64, z: Complex) -> Complex {
450    if z == Complex::default() && n >= 1.0 {
451        Complex::new(if n == 1.0 { -1.0 } else { 0.0 }, 0.0)
452    } else {
453        -((n - 1.0) * z.ln() - z).exp()
454    }
455}
456
457#[cfg(test)]
458mod tests {
459    //! `upper_gamma` and its scaled ladder against reference tables, DLMF identities and
460    //! quadrature; the lattice sums built on them are checked in `properties/lattice/`.
461
462    use std::f64::consts::PI;
463
464    use super::{ScaledGammaLadder, half_power, upper_gamma};
465    use crate::{
466        Complex,
467        numerics::finite,
468        special::integrals::{incgamma, incgamma_array, reference},
469        test_support::{ALGEBRA_CASES, complex, gauss_legendre, log_polar, prop_assert_close},
470    };
471    use proptest::{prelude::*, test_runner::TestCaseError};
472
473    /// The accuracy of `incgamma` against 40-digit mpmath values in
474    /// `references/incgamma.txt`: every regime (continued fraction, power series,
475    /// degree recurrence), both sides of the branch cut and `|z|` up to 63.
476    #[test]
477    fn incgamma_matches_reference_table() {
478        let cases = reference(include_str!(concat!(
479            env!("CARGO_MANIFEST_DIR"),
480            "/references/incgamma.txt"
481        )));
482        assert_eq!(cases.len(), 1494);
483        for (key, expected) in cases {
484            let (n, z) = (key[0], Complex::new(key[1], key[2]));
485            let actual = incgamma(n, z).unwrap();
486            // Positive integer degrees have zeros; measure them on the scale of
487            // their leading term z^(n-1) e^-z.
488            let scale = expected.norm()
489                + if n >= 1.0 {
490                    (half_power(z, n - 1.0) * (-z).exp()).norm()
491                } else {
492                    0.0
493                };
494            assert!(
495                (actual - expected).norm() <= 1e-13 * scale,
496                "Gamma({n}, {z}) = {actual}, expected {expected}"
497            );
498        }
499    }
500
501    #[test]
502    fn integer_degrees_are_exact_on_the_negative_real_axis() {
503        for x in [0.5, 3.0, 9.0, 36.0] {
504            for side in [0.0, -0.0] {
505                let z = Complex::new(-x, side);
506                for n in 1..=12 {
507                    // Gamma(n, z) = (n-1)! e^-z sum_k<n z^k/k! is entire and real here.
508                    assert_eq!(incgamma(f64::from(n), z).unwrap().im, 0.0);
509                }
510                for m in 0..=12_i32 {
511                    // The cut carries Im Gamma(-m, -x +- i0) = -+ pi (-1)^m / m!.
512                    let factorial: f64 = (1..=m).map(f64::from).product();
513                    let expected =
514                        -PI.copysign(side) * f64::from((-1_i32).pow(m.unsigned_abs())) / factorial;
515                    let actual = incgamma(f64::from(-m), z).unwrap().im;
516                    assert!(
517                        (actual - expected).abs() <= 1e-14 * expected.abs(),
518                        "{m} {z}"
519                    );
520                }
521            }
522        }
523    }
524
525    /// Negative degrees at `|z|` below 1e-154, where squaring `|z|` to invert it
526    /// underflows: each part within 1e-13 of its 500-digit mpmath reference (the real
527    /// parts are 1e-100 to 1e-158 of the modulus).
528    #[test]
529    fn incgamma_at_tiny_arguments() {
530        for (key, expected) in reference(TINY_ARGUMENTS) {
531            let (n, z) = (key[0], Complex::new(key[1], key[2]));
532            let actual = incgamma(n, z).unwrap();
533            assert!(
534                (actual.re - expected.re).abs() <= 1e-13 * expected.re.abs()
535                    && (actual.im - expected.im).abs() <= 1e-13 * expected.im.abs(),
536                "Gamma({n}, {z}) = {actual}, expected {expected}"
537            );
538        }
539    }
540
541    /// `n re(z) im(z): re im` of [`incgamma_at_tiny_arguments`].
542    const TINY_ARGUMENTS: &str = "
543        -0.5 1e-200 0.0: 2e100 0.0
544        -0.5 -5e-201 0.0: -3.544907701811032 -2.82842712474619e100
545        -1.5 0.0 1e-160: -4.7140452079103165e239 -4.7140452079103165e239
546        -1.0 0.0 1e-160: -368.8363992141458 -1e160
547        -1.0 3e-170 4e-170: 1.2e169 -1.6e169";
548
549    /// Large degrees against 60-digit mpmath references to 1e-13: near the negative real
550    /// axis, where `z^n` leaves the range of `f64` at negative degree and the upward
551    /// recurrence cancels at positive degree, and off it with `|z| < n`, where the Kummer
552    /// sum takes over.
553    #[test]
554    fn incgamma_at_large_degrees() {
555        for (key, expected) in reference(LARGE_DEGREES) {
556            let (n, z) = (key[0], Complex::new(key[1], key[2]));
557            let actual = incgamma(n, z).unwrap();
558            assert!(
559                (actual - expected).norm() <= 1e-13 * expected.norm(),
560                "Gamma({n}, {z}) = {actual}, expected {expected}"
561            );
562        }
563    }
564
565    /// `n re(z) im(z): re im` of [`incgamma_at_large_degrees`].
566    const LARGE_DEGREES: &str = "
567        -128.0 -500.0 0.0: -1.2885957841150845e-131 -3.7795814367747375e-197
568        -127.5 -500.0 5.0: 1.542698211604819e-130 2.407001953488497e-130
569        -120.0 -550.0 -20.0: 2.2305931400450102e-93 -2.606522282697957e-94
570        -118.0 -500.0 0.0: -1.2252201215779826e-104 -3.901330675138805e-170
571        -128.0 -300.0 0.0: -9.678079515916463e-190 -8.146851106928248e-216
572        128.0 -40.0 0.0: -1.624661197110015e220 0.0
573        100.0 -30.0 -0.0: -4.2347612953807334e158 0.0
574        125.0 -33.8 5.2: 1.507213843157233e207 1.308006804208391e204
575        128.0 -5.128 45.35: 2.805917215158467e213 -2.1505509958846317e212
576        100.5 0.1129 -25.538: 9.320963104082716e156 4.155006301443276e138
577        120.0 1.0 15.0: 5.574585761207606e196 -4.1455631801194624e138
578        64.5 -20.0 20.0: -2.2838716862512102e100 -6.047806419221159e99";
579
580    /// Recorded cancellation near Gamma(2, -1) and a reflected half-integer branch.
581    #[test]
582    fn gamma_ladder_regressions() -> Result<(), TestCaseError> {
583        check_gamma_ladder(3.0, Complex::new(-0.999_044_462_484_192_2, 0.0), false)?;
584        check_gamma_ladder(
585            0.5,
586            Complex::new(-9.001_950_249_594_67, 7.370_858_461_664_222_5),
587            true,
588        )
589    }
590
591    /// Independent mpmath values, including the exact zero and perturbations
592    /// too small for either recurrence to preserve relative accuracy unaided.
593    #[test]
594    fn gamma_and_ladder_keep_accuracy_near_degree_two_zero() {
595        for (key, expected) in reference(include_str!(concat!(
596            env!("CARGO_MANIFEST_DIR"),
597            "/references/incgamma_zero.txt"
598        ))) {
599            let (degree, z) = (key[0], Complex::new(key[1], key[2]));
600            // The smallest subnormal bounds rounding without hiding small seeds.
601            let tolerance = 1e-13 * expected.norm() + f64::from_bits(1);
602            let actual = incgamma(degree, z).unwrap();
603            assert!(
604                (actual - expected).norm() <= tolerance,
605                "Gamma({degree}, {z}) = {actual}, expected {expected}"
606            );
607            let seed =
608                ScaledGammaLadder::new(degree, z, false).seed(degree) * half_power(z, degree);
609            assert!(
610                (seed - expected).norm() <= tolerance,
611                "seed({degree}, {z}) = {seed}, expected {expected}"
612            );
613            for top in [3, 16] {
614                let mut ladder = ScaledGammaLadder::new(f64::from(top), z, false);
615                for n in (1..=top).rev() {
616                    let actual = ladder.next_lower() * half_power(z, f64::from(n));
617                    if f64::from(n) == degree {
618                        assert!(
619                            (actual - expected).norm() <= tolerance,
620                            "ladder({top}, {degree}, {z}) = {actual}, expected {expected}"
621                        );
622                        break;
623                    }
624                }
625            }
626        }
627    }
628
629    /// Gamma(2, -1) / (-1)^2 is exactly zero, on either signed-zero side.
630    #[test]
631    fn scaled_gamma_ladder_preserves_exact_degree_two_zero() {
632        for imaginary in [0.0, -0.0] {
633            let z = Complex::new(-1.0, imaginary);
634            for top in [2, 3, 16] {
635                let mut ladder = ScaledGammaLadder::new(f64::from(top), z, false);
636                for _ in 2..top {
637                    ladder.next_lower();
638                }
639                assert_eq!(ladder.next_lower(), Complex::default(), "top={top}, z={z}");
640            }
641        }
642    }
643
644    proptest! {
645        #![proptest_config(ProptestConfig::with_cases(ALGEBRA_CASES))]
646
647        #[test]
648        fn gamma_recurrence_and_reflection(
649            twice_n in -32_i32..=32,
650            re in -60.0_f64..60.0,
651            im in prop_oneof![Just(0.0), Just(-0.0), -20.0_f64..20.0],
652            g in complex(1.0),
653        ) {
654            check_gamma(0.5 * f64::from(twice_n), Complex::new(re, im), g)?;
655        }
656
657        #[test]
658        fn gamma_recurrence_near_the_cut(
659            twice_n in -256_i32..=254,
660            x in (-2.0_f64..600_f64.log10()).prop_map(|e| 10_f64.powf(e)),
661            parabola in prop_oneof![Just(0.0), 0.0_f64..1.0],
662            side in prop_oneof![Just(1.0), Just(-1.0)],
663        ) {
664            // |z| + Re z = parabola, or the cut itself from either side.
665            let im = side * (parabola * (parabola + 2.0 * x)).sqrt();
666            check_gamma_relations(0.5 * f64::from(twice_n), Complex::new(-x, im))?;
667        }
668
669        #[test]
670        fn gamma_difference_is_an_integral(
671            twice_n in 1_i32..=256,
672            ratio in 0.05_f64..1.5,
673            phase in -PI..PI,
674            step in prop_oneof![Just(-0.25), Just(0.25)],
675        ) {
676            let n = 0.5 * f64::from(twice_n);
677            let z = Complex::from_polar((ratio * n).max(1.0), phase);
678            check_gamma_integral(n, z, Complex::new(step, 0.0))?;
679        }
680
681        #[test]
682        fn gamma_ladder_matches_fresh_values(
683            twice_n in -8_i32..=8,
684            z in prop_oneof![
685                (-40.0_f64..40.0, prop_oneof![Just(0.0), -10.0_f64..10.0])
686                    .prop_map(|(re, im)| Complex::new(re, im)),
687                // Small |z|, where an upward run from |d| >= |z| would amplify rounding.
688                log_polar(-12.0..0.0, -PI..PI),
689            ],
690            reflected: bool,
691        ) {
692            check_gamma_ladder(0.5 * f64::from(twice_n), z, reflected)?;
693        }
694    }
695
696    /// `Gamma(n+1, z) = n Gamma(n, z) + z^n e^-z` relative to its largest term, which
697    /// ties together the continued-fraction, series and recurrence regimes and their
698    /// switches; Schwarz reflection `Gamma(n, conj z) = conj Gamma(n, z)` including the
699    /// signed-zero side of the cut; and the argument pullback `g conj(-z^(n-1) e^-z)`.
700    fn check_gamma(n: f64, z: Complex, g: Complex) -> Result<(), TestCaseError> {
701        check_gamma_relations(n, z)?;
702        let (_, residual) = incgamma_array(vec![n], vec![z]).unwrap();
703        let derivative = -half_power(z, n - 1.0) * (-z).exp();
704        let gradient = residual.pullback(&[g]).unwrap();
705        prop_assert_close!(
706            gradient[0],
707            g * derivative.conj(),
708            1e-13 * (g * derivative).norm()
709        );
710        Ok(())
711    }
712
713    /// The degree recurrence and Schwarz reflection of [`check_gamma`], where the
714    /// values and the endpoint term are representable (`z^n` alone may not be).
715    fn check_gamma_relations(n: f64, z: Complex) -> Result<(), TestCaseError> {
716        prop_assume!(z != Complex::default());
717        let (value, upper) = (incgamma(n, z).unwrap(), incgamma(n + 1.0, z).unwrap());
718        let half = (0.5 * n).trunc();
719        let endpoint = half_power(z, half) * (half_power(z, n - half) * (-z).exp());
720        prop_assume!(finite(value) && finite(upper) && finite(endpoint));
721        prop_assume!(endpoint.norm() > 1e-290);
722        let scale = upper.norm().max((n * value).norm()).max(endpoint.norm());
723        prop_assert_close!(upper - n * value, endpoint, 1e-13 * scale);
724        let reflected = incgamma(n, z.conj()).unwrap();
725        prop_assert_close!(reflected, value.conj(), 1e-15 * value.norm());
726        Ok(())
727    }
728
729    /// `Gamma(n, z) - Gamma(n, z + h)` is the integral of `t^(n-1) e^-t` along the
730    /// segment, by Gauss-Legendre quadrature: an identity no method of `upper_gamma`
731    /// builds on, unlike the degree recurrence, which the upward recurrence satisfies
732    /// even where it has lost every digit. The segment keeps to one side of the branch
733    /// cut, and the error is measured against the values and the largest integrand, as
734    /// in the reference table test.
735    fn check_gamma_integral(n: f64, z: Complex, h: Complex) -> Result<(), TestCaseError> {
736        let (start, end) = (incgamma(n, z).unwrap(), incgamma(n, z + h).unwrap());
737        let mut integral = Complex::default();
738        let mut largest = 0.0_f64;
739        for (node, weight) in gauss_legendre(32) {
740            let t = z + 1.0_f64.midpoint(node) * h;
741            let integrand = half_power(t, n - 1.0) * (-t).exp();
742            integral += 0.5 * weight * h * integrand;
743            largest = largest.max(integrand.norm());
744        }
745        prop_assume!(finite(start) && finite(end) && finite(integral));
746        let scale = start.norm() + end.norm() + largest;
747        prop_assert_close!(start - end, integral, 3e-13 * scale);
748        Ok(())
749    }
750
751    /// Each ladder value is the scaled incomplete gamma function `Gamma(d, z) / z^d`
752    /// one degree lower, on the branch `-sqrt(z)` for reflected half-integer degrees,
753    /// where `Gamma(d, z)` becomes `2 Gamma(d) - Gamma(d, z)` (measured on the scale of
754    /// its terms, which cancel).
755    fn check_gamma_ladder(n: f64, z: Complex, reflected: bool) -> Result<(), TestCaseError> {
756        prop_assume!(z != Complex::default());
757        let reflected = reflected && n.fract() != 0.0;
758        let mut ladder = ScaledGammaLadder::new(n, z, reflected);
759        for step in 0..60 {
760            let degree = n - f64::from(step);
761            let value = upper_gamma(degree, z);
762            let (expected, power, scale) = if reflected {
763                let complete = 2.0 * libm::tgamma(degree);
764                (
765                    complete - value,
766                    -half_power(z, degree),
767                    complete.abs() + value.norm(),
768                )
769            } else {
770                (value, half_power(z, degree), value.norm())
771            };
772            if !finite(expected * power) {
773                // |z|^d leaves the range of f64 at large negative d for small |z|.
774                break;
775            }
776            prop_assert_close!(
777                ladder.next_lower() * power,
778                expected,
779                1e-13 * scale,
780                "degree {degree}"
781            );
782        }
783        Ok(())
784    }
785}