use crate::math::constants::PI;
const LANCZOS_G: f64 = 7.0;
const LANCZOS_COEFFICIENTS: [f64; 9] = [
0.999_999_999_999_809_9,
676.520_368_121_885_1,
-1_259.139_216_722_402_8,
771.323_428_777_653_1,
-176.615_029_162_140_6,
12.507_343_278_686_905,
-0.138_571_095_265_720_12,
9.984_369_578_019_572e-6,
1.505_632_735_149_311_6e-7,
];
const MAX_ITER: usize = 500;
const EPS: f64 = 1e-15;
const FPMIN: f64 = 1e-300;
#[must_use]
pub fn gamma(z: f64) -> f64 {
if z < 0.5 {
return PI / ((PI * z).sin() * gamma(1.0 - z));
}
let z = z - 1.0;
let mut x = LANCZOS_COEFFICIENTS[0];
for (i, &coeff) in LANCZOS_COEFFICIENTS.iter().enumerate().skip(1) {
x += coeff / (z + i as f64);
}
let t = z + LANCZOS_G + 0.5;
(2.0 * PI).sqrt() * t.powf(z + 0.5) * (-t).exp() * x
}
#[must_use]
pub fn lgamma(z: f64) -> f64 {
assert!(z > 0.0, "lgamma requires z > 0");
if z < 0.5 {
return PI.ln() - (PI * z).sin().ln() - lgamma(1.0 - z);
}
let zm1 = z - 1.0;
let mut x = LANCZOS_COEFFICIENTS[0];
for (i, &coeff) in LANCZOS_COEFFICIENTS.iter().enumerate().skip(1) {
x += coeff / (zm1 + i as f64);
}
let t = zm1 + LANCZOS_G + 0.5;
0.5 * (2.0 * PI).ln() + (zm1 + 0.5) * t.ln() - t + x.ln()
}
fn gamma_p_series(a: f64, x: f64) -> f64 {
let mut ap = a;
let mut sum = 1.0 / a;
let mut del = sum;
for _ in 0..MAX_ITER {
ap += 1.0;
del *= x / ap;
sum += del;
if del.abs() < sum.abs() * EPS {
break;
}
}
sum * (-x + a * x.ln() - lgamma(a)).exp()
}
fn gamma_q_cf(a: f64, x: f64) -> f64 {
let mut b = x + 1.0 - a;
let mut c = 1.0 / FPMIN;
let mut d = 1.0 / b;
let mut h = d;
for i in 1..=MAX_ITER {
let an = -(i as f64) * (i as f64 - a);
b += 2.0;
d = an * d + b;
if d.abs() < FPMIN {
d = FPMIN;
}
c = b + an / c;
if c.abs() < FPMIN {
c = FPMIN;
}
d = 1.0 / d;
let del = d * c;
h *= del;
if (del - 1.0).abs() < EPS {
break;
}
}
(-x + a * x.ln() - lgamma(a)).exp() * h
}
#[must_use]
pub fn gamma_p(a: f64, x: f64) -> f64 {
assert!(a > 0.0, "gamma_p requires a > 0");
assert!(x >= 0.0, "gamma_p requires x >= 0");
if x == 0.0 {
return 0.0;
}
if x < a + 1.0 {
gamma_p_series(a, x)
} else {
1.0 - gamma_q_cf(a, x)
}
}
#[must_use]
pub fn gamma_q(a: f64, x: f64) -> f64 {
assert!(a > 0.0, "gamma_q requires a > 0");
assert!(x >= 0.0, "gamma_q requires x >= 0");
if x == 0.0 {
return 1.0;
}
if x < a + 1.0 {
1.0 - gamma_p_series(a, x)
} else {
gamma_q_cf(a, x)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn approx(a: f64, b: f64, tol: f64) -> bool {
(a - b).abs() < tol
}
#[test]
fn test_gamma_integers() {
assert!(approx(gamma(5.0), 24.0, 1e-10));
assert!(approx(gamma(1.0), 1.0, 1e-12));
assert!(approx(gamma(0.5), PI.sqrt(), 1e-12));
}
#[test]
fn test_lgamma_matches_ln_gamma_small() {
for &z in &[0.1, 0.5, 1.0, 2.5, 10.0, 30.0] {
assert!(
approx(lgamma(z), gamma(z).ln(), 1e-10),
"z={z}: {} vs {}",
lgamma(z),
gamma(z).ln()
);
}
}
#[test]
fn test_lgamma_no_overflow_large() {
let v = lgamma(1e6);
assert!(v.is_finite());
assert!((v / 12_815_504.569_147_771 - 1.0).abs() < 1e-12, "got {v}");
}
#[test]
#[should_panic(expected = "z > 0")]
fn test_lgamma_rejects_nonpositive() {
let _ = lgamma(0.0);
}
#[test]
fn test_gamma_p_known() {
for &x in &[0.1, 1.0, 3.0] {
assert!(approx(gamma_p(1.0, x), 1.0 - (-x).exp(), 1e-13));
}
assert_eq!(gamma_p(2.5, 0.0), 0.0);
assert_eq!(gamma_q(2.5, 0.0), 1.0);
}
#[test]
fn test_gamma_q_large_x_precision() {
let q = gamma_q(2.0, 50.0);
let expected = 51.0 * (-50.0_f64).exp();
assert!((q / expected - 1.0).abs() < 1e-12, "got {q}, expected {expected}");
}
#[test]
fn test_p_plus_q_is_one() {
for &a in &[0.3, 1.0, 2.5, 10.0] {
for &x in &[0.1, 1.0, 5.0, 20.0] {
let s = gamma_p(a, x) + gamma_q(a, x);
assert!(approx(s, 1.0, 1e-12), "a={a}, x={x}: {s}");
}
}
}
}