use crate::errors::QlResult;
use crate::fail;
use crate::math::gammafunction::log_gamma;
use crate::types::Real;
const ACCURACY: Real = 1.0e-13;
fn max_iterations(a: Real) -> u32 {
100u32.saturating_add(a.ceil() as u32)
}
pub fn incomplete_gamma(a: Real, x: Real) -> QlResult<Real> {
if !a.is_finite() || a <= 0.0 {
fail!("incomplete_gamma requires a finite a > 0, got a={a}");
}
if !x.is_finite() || x < 0.0 {
fail!("incomplete_gamma requires a finite x >= 0, got x={x}");
}
if x < a + 1.0 {
series_repr(a, x)
} else {
Ok(1.0 - continued_fraction_repr(a, x)?)
}
}
fn series_repr(a: Real, x: Real) -> QlResult<Real> {
if x == 0.0 {
return Ok(0.0);
}
let gln = log_gamma(a)?;
let mut ap = a;
let mut del = 1.0 / a;
let mut sum = del;
for _ in 1..=max_iterations(a) {
ap += 1.0;
del *= x / ap;
sum += del;
if del.abs() < sum.abs() * ACCURACY {
return Ok(sum * (-x + a * x.ln() - gln).exp());
}
}
fail!("incomplete_gamma series did not converge (a={a}, x={x})");
}
fn continued_fraction_repr(a: Real, x: Real) -> QlResult<Real> {
let eps = Real::EPSILON;
let gln = log_gamma(a)?;
let mut b = x + 1.0 - a;
let mut c = 1.0 / eps;
let mut d = 1.0 / b;
let mut h = d;
for i in 1..=max_iterations(a) {
let an = -(i as Real) * (i as Real - a);
b += 2.0;
d = an * d + b;
if d.abs() < eps {
d = eps;
}
c = b + an / c;
if c.abs() < eps {
c = eps;
}
d = 1.0 / d;
let del = d * c;
h *= del;
if (del - 1.0).abs() < ACCURACY {
return Ok((-x + a * x.ln() - gln).exp() * h);
}
}
fail!("incomplete_gamma continued fraction did not converge (a={a}, x={x})");
}
#[cfg(test)]
mod tests {
use super::*;
use crate::math::errorfunction::erf;
const TOL: Real = 1e-12;
fn assert_close(got: Real, expected: Real) {
let tol = TOL * (1.0 + expected.abs());
assert!(
(got - expected).abs() <= tol,
"got {got}, expected {expected}, diff {}",
(got - expected).abs()
);
}
#[test]
fn boundary_at_zero_is_zero() {
for &a in &[0.5, 1.0, 2.0, 7.5] {
assert_eq!(incomplete_gamma(a, 0.0).unwrap(), 0.0);
}
}
#[test]
fn matches_exponential_cdf_a_eq_1() {
for &x in &[0.1, 0.5, 1.0, 1.9, 2.0, 5.0, 20.0_f64] {
assert_close(incomplete_gamma(1.0, x).unwrap(), 1.0 - (-x).exp());
}
}
#[test]
fn matches_integer_a_closed_forms() {
for &x in &[0.3, 1.0, 2.5, 3.0, 8.0_f64] {
let e = (-x).exp();
assert_close(incomplete_gamma(2.0, x).unwrap(), 1.0 - e * (1.0 + x));
assert_close(
incomplete_gamma(3.0, x).unwrap(),
1.0 - e * (1.0 + x + 0.5 * x * x),
);
}
}
#[test]
fn matches_erf_a_eq_half() {
for &x in &[0.05, 0.5, 1.0, 1.5, 4.0, 10.0_f64] {
assert_close(incomplete_gamma(0.5, x).unwrap(), erf(x.sqrt()));
}
}
#[test]
fn approaches_one_in_the_tail() {
assert_close(incomplete_gamma(3.0, 60.0).unwrap(), 1.0);
}
#[test]
fn is_monotonic_increasing_in_x() {
let a = 4.0;
let mut prev = incomplete_gamma(a, 0.0).unwrap();
let mut x = 0.0;
while x < 30.0 {
x += 0.1;
let cur = incomplete_gamma(a, x).unwrap();
assert!(cur >= prev, "not increasing at x={x}: {prev} -> {cur}");
prev = cur;
}
}
#[test]
fn converges_for_large_a() {
assert_close(incomplete_gamma(500.0, 500.0).unwrap(), 0.5059471460854907);
assert_close(incomplete_gamma(200.0, 200.0).unwrap(), 0.5094034179355048);
assert_close(incomplete_gamma(500.0, 600.0).unwrap(), 0.9999877440576714);
}
#[test]
fn invalid_args_rejected() {
assert!(incomplete_gamma(0.0, 1.0).is_err()); assert!(incomplete_gamma(-1.0, 1.0).is_err()); assert!(incomplete_gamma(1.0, -1.0).is_err()); assert!(incomplete_gamma(Real::NAN, 1.0).is_err()); assert!(incomplete_gamma(1.0, Real::NAN).is_err()); assert!(incomplete_gamma(Real::INFINITY, 1.0).is_err()); assert!(incomplete_gamma(1.0, Real::INFINITY).is_err()); }
}