use crate::error::{SpecialError, SpecialResult};
use crate::gamma::{gamma, gammaln};
use crate::validation::{check_finite, check_positive};
use scirs2_core::numeric::{Float, FromPrimitive};
use std::fmt::{Debug, Display};
use std::ops::{AddAssign, MulAssign, SubAssign};
#[allow(dead_code)]
pub fn gammainc_lower<T>(a: T, x: T) -> SpecialResult<T>
where
T: Float + FromPrimitive + Debug + Display + AddAssign + MulAssign,
{
check_positive(a, "a")?;
check_finite(x, "x value")?;
if x <= T::zero() {
return Ok(T::zero());
}
if x < a + T::one() {
let mut sum = T::one() / a;
let mut term = T::one() / a;
let mut n = T::one();
let tol = T::from_f64(1e-15).expect("Operation failed");
while term.abs() > tol * sum.abs() {
term *= x / (a + n);
sum += term;
n += T::one();
if n > T::from_f64(1000.0).expect("Operation failed") {
return Err(SpecialError::ConvergenceError(
"gammainc_lower: series did not converge".to_string(),
));
}
}
Ok(x.powf(a) * (-x).exp() * sum)
} else {
let gamma_a = gamma(a);
let gamma_upper = gammainc_upper(a, x)?;
Ok(gamma_a - gamma_upper)
}
}
#[allow(dead_code)]
pub fn gammainc_upper<T>(a: T, x: T) -> SpecialResult<T>
where
T: Float + FromPrimitive + Debug + Display + AddAssign + MulAssign,
{
check_positive(a, "a")?;
check_finite(x, "x value")?;
if x <= T::zero() {
return Ok(gamma(a));
}
if x >= a + T::one() {
let mut b = x + T::one() - a;
let mut c = T::from_f64(1e30).expect("Operation failed");
let mut d = T::one() / b;
let mut h = d;
let tol = T::from_f64(1e-15).expect("Operation failed");
for i in 1..1000 {
let an = -T::from_usize(i).expect("Operation failed")
* (T::from_usize(i).expect("Operation failed") - a);
b += T::from_f64(2.0).expect("Operation failed");
d = an * d + b;
if d.abs() < T::from_f64(1e-30).expect("Operation failed") {
d = T::from_f64(1e-30).expect("Operation failed");
}
c = b + an / c;
if c.abs() < T::from_f64(1e-30).expect("Operation failed") {
c = T::from_f64(1e-30).expect("Operation failed");
}
d = T::one() / d;
let delta = d * c;
h *= delta;
if (delta - T::one()).abs() < tol {
return Ok(x.powf(a) * (-x).exp() * h);
}
}
Err(SpecialError::ConvergenceError(
"gammainc_upper: continued fraction did not converge".to_string(),
))
} else {
let gamma_a = gamma(a);
let gamma_lower = gammainc_lower(a, x)?;
Ok(gamma_a - gamma_lower)
}
}
#[allow(dead_code)]
pub fn gammainc<T>(a: T, x: T) -> SpecialResult<T>
where
T: Float + FromPrimitive + Debug + Display + AddAssign + MulAssign,
{
check_positive(a, "a")?;
check_finite(x, "x value")?;
if x <= T::zero() {
return Ok(T::zero());
}
if a > T::from_f64(140.0).expect("Operation failed") {
let (p, _q) = gammainc_pair_large_a(a, x)?;
Ok(p)
} else {
let gamma_lower = gammainc_lower(a, x)?;
let gamma_a = gamma(a);
Ok(gamma_lower / gamma_a)
}
}
#[allow(dead_code)]
pub fn gammaincc<T>(a: T, x: T) -> SpecialResult<T>
where
T: Float + FromPrimitive + Debug + Display + AddAssign + MulAssign,
{
check_positive(a, "a")?;
check_finite(x, "x value")?;
if x <= T::zero() {
return Ok(T::one());
}
if a > T::from_f64(140.0).expect("Operation failed") {
let (_p, q) = gammainc_pair_large_a(a, x)?;
return Ok(q);
}
let p = gammainc(a, x)?;
Ok(T::one() - p)
}
#[allow(dead_code)]
pub fn gammaincinv<T>(a: T, p: T) -> SpecialResult<T>
where
T: Float + FromPrimitive + Debug + Display + AddAssign + MulAssign + SubAssign,
{
check_positive(a, "a")?;
crate::validation::check_probability(p, "p")?;
if p == T::zero() {
return Ok(T::zero());
}
if p == T::one() {
return Ok(T::infinity());
}
let x0 = initial_guess_gammaincinv(a, p);
let mut x = x0;
let tol = T::from_f64(1e-12).expect("Operation failed");
for _ in 0..100 {
let f = gammainc(a, x)? - p;
let log_df = (a - T::one()) * x.ln() - x - gammaln(a);
let df = log_df.exp();
let dx = f / df;
x -= dx;
if x <= T::zero() {
x = T::from_f64(1e-10).expect("Operation failed");
}
if dx.abs() < tol * x.abs() {
return Ok(x);
}
}
Err(SpecialError::ConvergenceError(
"gammaincinv: Newton iteration did not converge".to_string(),
))
}
#[allow(dead_code)]
pub fn gammainccinv<T>(a: T, q: T) -> SpecialResult<T>
where
T: Float + FromPrimitive + Debug + Display + AddAssign + MulAssign + SubAssign,
{
check_positive(a, "a")?;
crate::validation::check_probability(q, "q")?;
let p = T::one() - q;
gammaincinv(a, p)
}
#[allow(dead_code)]
fn initial_guess_gammaincinv<T>(a: T, p: T) -> T
where
T: Float + FromPrimitive + Display,
{
let g = T::from_f64(2.0).expect("Operation failed")
/ (T::from_f64(9.0).expect("Operation failed") * a);
let z = crate::distributions::ndtri(p).unwrap_or(T::zero());
let w = T::one() + g * z;
if w > T::zero() {
a * w.powf(T::from_f64(3.0).expect("Operation failed"))
} else {
if p < T::from_f64(0.5).expect("Operation failed") {
a * T::from_f64(0.1).expect("Operation failed")
} else {
a * T::from_f64(2.0).expect("Operation failed")
}
}
}
fn gammainc_pair_large_a<T>(a: T, x: T) -> SpecialResult<(T, T)>
where
T: Float + FromPrimitive + Debug + Display + AddAssign + MulAssign,
{
let log_gamma_a = gammaln(a);
let tol = T::from_f64(1e-15).expect("Operation failed");
let max_iter = 2_000_000usize;
if x < a + T::one() {
let mut term = T::one() / a;
let mut sum = term;
let mut n = 1usize;
loop {
let nt = T::from_usize(n).expect("Operation failed");
term *= x / (a + nt);
sum += term;
if term.abs() <= tol * sum.abs() {
break;
}
n += 1;
if n > max_iter {
return Err(SpecialError::ConvergenceError(
"gammainc: large-a series did not converge".to_string(),
));
}
}
let log_p = a * x.ln() - x - log_gamma_a + sum.ln();
let p = clamp_unit(log_p.exp());
Ok((p, T::one() - p))
} else {
let mut b = x + T::one() - a;
let mut c = T::from_f64(1e300).expect("Operation failed");
let mut d = T::one() / b;
let mut h = d;
let tiny = T::from_f64(1e-300).expect("Operation failed");
let mut i = 1usize;
loop {
let it = T::from_usize(i).expect("Operation failed");
let an = -it * (it - a);
b += T::from_f64(2.0).expect("Operation failed");
d = an * d + b;
if d.abs() < tiny {
d = tiny;
}
c = b + an / c;
if c.abs() < tiny {
c = tiny;
}
d = T::one() / d;
let delta = d * c;
h *= delta;
if (delta - T::one()).abs() <= tol {
break;
}
i += 1;
if i > max_iter {
return Err(SpecialError::ConvergenceError(
"gammaincc: large-a continued fraction did not converge".to_string(),
));
}
}
let log_q = a * x.ln() - x - log_gamma_a + h.ln();
let q = clamp_unit(log_q.exp());
Ok((T::one() - q, q))
}
}
#[inline]
fn clamp_unit<T: Float>(v: T) -> T {
if v < T::zero() {
T::zero()
} else if v > T::one() {
T::one()
} else {
v
}
}
#[allow(dead_code)]
pub fn gammastar<T>(x: T) -> SpecialResult<T>
where
T: Float + FromPrimitive + Debug + Display + AddAssign + MulAssign,
{
check_positive(x, "x")?;
if x >= T::from_f64(10.0).expect("Operation failed") {
let mut sum = T::one();
let x2 = x * x;
let mut xn = x;
let coeffs = [
T::from_f64(1.0 / 12.0).expect("Operation failed"),
T::from_f64(1.0 / 288.0).expect("Operation failed"),
T::from_f64(-139.0 / 51840.0).expect("Operation failed"),
T::from_f64(-571.0 / 2488320.0).expect("Operation failed"),
];
for &c in &coeffs {
sum += c / xn;
xn *= x2;
}
Ok(sum)
} else {
let sqrt_2pi = T::from_f64((2.0 * std::f64::consts::PI).sqrt()).expect("Operation failed");
let gamma_x = gamma(x);
let x_power = x.powf(x - T::from_f64(0.5).expect("Operation failed"));
let exp_neg_x = (-x).exp();
Ok(gamma_x / (sqrt_2pi * x_power * exp_neg_x))
}
}
#[allow(dead_code)]
pub fn gammasgn<T>(x: T) -> T
where
T: Float + FromPrimitive,
{
if x > T::zero() {
T::one()
} else {
let floor_x = x.floor();
if x == floor_x {
T::nan()
} else {
if floor_x.to_isize().unwrap_or(0) % 2 == 0 {
T::one()
} else {
-T::one()
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
#[test]
fn test_gammainc_lower() {
assert_relative_eq!(
gammainc_lower(1.0, 1.0).expect("Operation failed"),
0.6321205588285577, epsilon = 1e-10
);
assert_relative_eq!(
gammainc_lower(2.0, 1.0).expect("Operation failed"),
0.264241117657115, epsilon = 1e-10
);
assert_relative_eq!(
gammainc_lower(3.0, 2.0).expect("Operation failed"),
0.646647167633873, epsilon = 1e-10
);
assert_eq!(gammainc_lower(1.0, 0.0).expect("Operation failed"), 0.0);
}
#[test]
fn test_gammainc() {
assert_relative_eq!(
gammainc(1.0, 1.0).expect("Operation failed"),
0.6321205588285577,
epsilon = 1e-10
);
assert_relative_eq!(
gammainc(2.0, 2.0).expect("Operation failed"),
0.5939941502901619,
epsilon = 1e-10
);
assert_eq!(gammainc(1.0, 0.0).expect("Operation failed"), 0.0);
}
#[test]
fn test_gammaincc() {
let a = 2.0;
let x = 1.5;
let p = gammainc(a, x).expect("Operation failed");
let q = gammaincc(a, x).expect("Operation failed");
assert_relative_eq!(p + q, 1.0, epsilon = 1e-10);
}
#[test]
fn test_gammaincinv() {
let a = 2.5;
let x = 3.0;
let p = gammainc(a, x).expect("Operation failed");
let x_recovered = gammaincinv(a, p).expect("Operation failed");
assert_relative_eq!(x_recovered, x, epsilon = 1e-8);
}
#[test]
fn test_gammasgn() {
assert_eq!(gammasgn(1.0), 1.0);
assert_eq!(gammasgn(2.5), 1.0);
assert_eq!(gammasgn(-0.5), -1.0);
assert_eq!(gammasgn(-1.5), 1.0);
assert_eq!(gammasgn(-2.5), -1.0);
}
fn rel_close(actual: f64, expected: f64, tol: f64) -> bool {
(actual - expected).abs() <= tol * expected.abs().max(f64::MIN_POSITIVE)
}
#[test]
fn test_gammainc_gammaincc_large_a_matches_reference() {
let cases: &[(f64, f64, f64, f64, f64)] = &[
(150.0, 150.0, 0.5108582297493597, 0.4891417702506403, 1e-9),
(141.0, 141.0, 0.5111994345210251, 0.4888005654789749, 1e-9),
(
150.0,
100.0,
1.8842104660386837e-06,
0.999998115789534,
1e-8,
),
(
1000.0,
900.0,
0.0005499022657117819,
0.9994500977342882,
1e-8,
),
(150.0, 1000.0, 1.0, 1.565659386727711e-248, 1e-6),
(150.0, 50.0, 3.530612225414933e-30, 1.0, 1e-6),
(200.0, 50.0, 2.0247590148475566e-57, 1.0, 1e-4),
];
for &(a, x, p_true, q_true, tol) in cases {
let p = gammainc(a, x).expect("gammainc should succeed for a > 140");
let q = gammaincc(a, x).expect("gammaincc should succeed for a > 140");
assert!(
rel_close(p, p_true, tol),
"gammainc({a}, {x}) = {p:e}, expected {p_true:e}"
);
assert!(
rel_close(q, q_true, tol),
"gammaincc({a}, {x}) = {q:e}, expected {q_true:e}"
);
assert!(
(p + q - 1.0).abs() < 1e-9,
"P + Q != 1 for a={a}, x={x}: P={p}, Q={q}"
);
assert!((0.0..=1.0).contains(&p), "P out of [0,1]: {p}");
assert!((0.0..=1.0).contains(&q), "Q out of [0,1]: {q}");
}
}
#[test]
fn test_gammaincinv_large_a_matches_reference() {
let cases: &[(f64, f64, f64)] = &[
(150.0, 0.3, 143.34387895250237),
(200.0, 0.7, 207.16763215577143),
(1000.0, 0.5, 999.6666864269652),
(300.0, 0.01, 261.18256324043705),
(150.0, 0.999, 190.71262426020576),
];
for &(a, p, x_ref) in cases {
let x = gammaincinv(a, p).expect("gammaincinv should succeed for a > 140");
assert!(
rel_close(x, x_ref, 1e-6),
"gammaincinv({a}, {p}) = {x}, expected {x_ref}"
);
let p_recovered = gammainc(a, x).expect("gammainc should succeed for a > 140");
assert!(
rel_close(p_recovered, p, 1e-6),
"gammainc({a}, {x}) round-trip = {p_recovered}, expected {p}"
);
}
}
}