use std::f64::consts::PI;
const G: f64 = 7.0;
const LANCZOS: [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 TINY: f64 = 1e-300;
const REL_EPS: f64 = 1e-16;
const MAX_ITERS: usize = 1000;
#[must_use]
pub fn ln_gamma(x: f64) -> f64 {
if x < 0.5 {
return (PI / (PI * x).sin()).ln() - ln_gamma(1.0 - x);
}
let x = x - 1.0;
let mut a = first_lanczos();
let t = x + G + 0.5;
let mut k = 0.0_f64;
for &c in LANCZOS.iter().skip(1) {
k += 1.0;
a += c / (x + k);
}
(x + 0.5).mul_add(t.ln(), 0.5 * (2.0 * PI).ln()) - t + a.ln()
}
fn first_lanczos() -> f64 {
LANCZOS.first().copied().unwrap_or(0.0)
}
#[must_use]
pub fn ln_beta(a: f64, b: f64) -> f64 {
ln_gamma(a) + ln_gamma(b) - ln_gamma(a + b)
}
#[must_use]
pub fn ln_choose(n: usize, k: usize) -> f64 {
if k > n {
return f64::NEG_INFINITY;
}
let nf = usize_to_f64(n);
let kf = usize_to_f64(k);
ln_gamma(nf + 1.0) - ln_gamma(kf + 1.0) - ln_gamma(nf - kf + 1.0)
}
fn usize_to_f64(n: usize) -> f64 {
let wide = u64::try_from(n).unwrap_or(u64::MAX);
let hi = u32::try_from(wide >> 32).unwrap_or(0);
let lo = u32::try_from(wide & 0xFFFF_FFFF).unwrap_or(0);
f64::from(hi).mul_add(4_294_967_296.0, f64::from(lo))
}
#[must_use]
pub fn gamma_p(a: f64, x: f64) -> f64 {
if x < 0.0 || a <= 0.0 {
return f64::NAN;
}
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 {
if x < 0.0 || a <= 0.0 {
return f64::NAN;
}
if x < a + 1.0 {
1.0 - gamma_p_series(a, x)
} else {
gamma_q_cf(a, x)
}
}
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_ITERS {
ap += 1.0;
del *= x / ap;
sum += del;
if del.abs() < sum.abs() * REL_EPS {
break;
}
}
sum * (a.mul_add(x.ln(), -x) - ln_gamma(a)).exp()
}
#[allow(clippy::many_single_char_names)]
fn gamma_q_cf(a: f64, x: f64) -> f64 {
let mut b = x + 1.0 - a;
let mut c = 1.0 / TINY;
let mut d = 1.0 / b;
let mut h = d;
let mut i = 0.0_f64;
for _ in 0..MAX_ITERS {
i += 1.0;
let an = -i * (i - a);
b += 2.0;
d = an.mul_add(d, b);
if d.abs() < TINY {
d = TINY;
}
c = b + an / c;
if c.abs() < TINY {
c = TINY;
}
d = 1.0 / d;
let del = d * c;
h *= del;
if (del - 1.0).abs() < REL_EPS {
break;
}
}
(a.mul_add(x.ln(), -x) - ln_gamma(a)).exp() * h
}
#[must_use]
pub fn ln_gamma_q(a: f64, x: f64) -> f64 {
if x < 0.0 || a <= 0.0 {
return f64::NAN;
}
if x == 0.0 {
return 0.0;
}
if x < a + 1.0 {
(-gamma_p_series(a, x)).ln_1p()
} else {
ln_gamma_q_cf(a, x)
}
}
#[must_use]
pub fn ln_gamma_p(a: f64, x: f64) -> f64 {
if x < 0.0 || a <= 0.0 {
return f64::NAN;
}
if x == 0.0 {
return f64::NEG_INFINITY;
}
if x < a + 1.0 {
ln_gamma_p_series(a, x)
} else {
(-gamma_q_cf(a, x)).ln_1p()
}
}
fn ln_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_ITERS {
ap += 1.0;
del *= x / ap;
sum += del;
if del.abs() < sum.abs() * REL_EPS {
break;
}
}
sum.ln() + a.mul_add(x.ln(), -x) - ln_gamma(a)
}
#[allow(clippy::many_single_char_names)]
fn ln_gamma_q_cf(a: f64, x: f64) -> f64 {
let mut b = x + 1.0 - a;
let mut c = 1.0 / TINY;
let mut d = 1.0 / b;
let mut h = d;
let mut i = 0.0_f64;
for _ in 0..MAX_ITERS {
i += 1.0;
let an = -i * (i - a);
b += 2.0;
d = an.mul_add(d, b);
if d.abs() < TINY {
d = TINY;
}
c = b + an / c;
if c.abs() < TINY {
c = TINY;
}
d = 1.0 / d;
let del = d * c;
h *= del;
if (del - 1.0).abs() < REL_EPS {
break;
}
}
a.mul_add(x.ln(), -x) - ln_gamma(a) + h.ln()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ln_gamma_q_matches_log_of_linear() {
for &(a, x) in &[(1.5, 3.0), (2.5, 1.0)] {
let want = gamma_q(a, x).ln();
let got = ln_gamma_q(a, x);
assert!(
((got - want) / want.abs().max(1.0)).abs() < 1e-12,
"ln_gamma_q({a},{x}) = {got}, want {want}"
);
}
}
#[test]
fn ln_gamma_p_matches_log_of_linear() {
for &(a, x) in &[(1.5, 3.0), (2.5, 1.0)] {
let want = gamma_p(a, x).ln();
let got = ln_gamma_p(a, x);
assert!(
((got - want) / want.abs().max(1.0)).abs() < 1e-12,
"ln_gamma_p({a},{x}) = {got}, want {want}"
);
}
}
#[test]
fn ln_gamma_q_finite_in_deep_tail() {
let got = ln_gamma_q(1.5, 400.0);
let want = -396.882_237_824_145_1; assert!(got.is_finite(), "ln_gamma_q(1.5, 400) was {got}");
assert!(
((got - want) / want).abs() < 1e-9,
"ln_gamma_q(1.5, 400) rel error too large: got {got}, want {want}"
);
}
}