1use std::f64::consts::PI;
12
13use errorfunctions::ComplexErrorFunctions;
14
15use crate::{
16 Complex,
17 numerics::{EULER, finite},
18};
19
20fn 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
28fn 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
59fn 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 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
86pub(super) const RECURRENCE_GROWTH: f64 = 10.0;
89const SERIES_PARABOLA: f64 = 1.0;
92const GAMMA_SERIES_RADIUS: f64 = 600.0;
96
97pub(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 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 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 let (mut value, mut error) = gamma_recurrence(n, z, base, modulus);
144 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
163fn 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 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 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
190fn 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
206fn 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 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
240fn 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 if k > 2.0 * radius && term.norm() <= 1e-17 * sum.norm() {
258 break;
259 }
260 k += 1.0;
261 term *= -z / k;
262 }
263 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#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] fn 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
310fn inverse(z: Complex) -> Complex {
313 crate::numerics::ratio(Complex::new(1.0, 0.0), z)
314}
315
316fn 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#[derive(Clone, Debug)]
329pub(crate) struct ScaledGammaLadder {
330 degree: f64,
331 argument: Complex,
332 modulus: f64,
334 exponential: Complex,
335 reflected: bool,
336 upper: Option<Complex>,
338 pending: Vec<Complex>,
340}
341
342const LADDER_RUN: f64 = 16.0;
346
347impl ScaledGammaLadder {
348 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 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 self.seed(self.degree)
367 } else {
368 match (self.pending.pop(), self.upper) {
369 (Some(value), _) => value,
370 (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 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 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 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 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
447pub(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 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 #[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 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 assert_eq!(incgamma(f64::from(n), z).unwrap().im, 0.0);
509 }
510 for m in 0..=12_i32 {
511 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 #[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 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 #[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 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 #[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 #[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 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 #[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 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 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 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 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 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 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 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}