const EPS: f64 = 1e-14;
const TINY: f64 = 1e-300;
const MAX_ITER: usize = 300;
fn gamma_max_iter(a: f64) -> usize {
300usize.max((30.0 * a.sqrt()).ceil() as usize)
}
pub(crate) fn ln_gamma(x: f64) -> f64 {
const C: [f64; 9] = [
0.999_999_999_999_809_93,
676.520_368_121_885_1,
-1_259.139_216_722_402_8,
771.323_428_777_653_13,
-176.615_029_162_140_59,
12.507_343_278_686_905,
-0.138_571_095_265_720_12,
9.984_369_578_019_571_6e-6,
1.505_632_735_149_311_6e-7,
];
if x < 0.5 {
std::f64::consts::PI.ln() - (std::f64::consts::PI * x).sin().abs().ln() - ln_gamma(1.0 - x)
} else {
let z = x - 1.0;
let mut s = C[0];
for (i, &ci) in C[1..].iter().enumerate() {
s += ci / (z + (i as f64) + 1.0);
}
let t = z + 7.0 + 0.5;
0.5 * (2.0 * std::f64::consts::PI).ln() + (z + 0.5) * t.ln() - t + s.ln()
}
}
fn gamma_p_series(a: f64, x: f64) -> f64 {
if x <= 0.0 {
return 0.0;
}
let max_iter = gamma_max_iter(a);
let mut ap = a;
let mut del = 1.0 / a;
let mut sum = del;
let mut converged = false;
for _ in 0..max_iter {
ap += 1.0;
del *= x / ap;
sum += del;
if del.abs() < sum.abs() * EPS {
converged = true;
break;
}
}
if !converged {
return f64::NAN;
}
(sum.ln() + (-x + a * x.ln() - ln_gamma(a))).exp()
}
fn gamma_q_cf(a: f64, x: f64) -> f64 {
let max_iter = gamma_max_iter(a);
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 converged = false;
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() < 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() < EPS {
converged = true;
break;
}
}
if !converged {
return f64::NAN;
}
(h.ln() + (-x + a * x.ln() - ln_gamma(a))).exp()
}
pub(crate) 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)
}
}
pub(crate) fn gamma_q(a: f64, x: f64) -> f64 {
if x < 0.0 || a <= 0.0 {
return f64::NAN;
}
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)
}
}
fn betacf(a: f64, b: f64, x: f64) -> f64 {
let qab = a + b;
let qap = a + 1.0;
let qam = a - 1.0;
let mut c = 1.0;
let mut d = 1.0 - qab * x / qap;
if d.abs() < TINY {
d = TINY;
}
d = 1.0 / d;
let mut h = d;
for m in 1..MAX_ITER {
let m = m as f64;
let m2 = 2.0 * m;
let aa = m * (b - m) * x / ((qam + m2) * (a + m2));
d = 1.0 + aa * d;
if d.abs() < TINY {
d = TINY;
}
c = 1.0 + aa / c;
if c.abs() < TINY {
c = TINY;
}
d = 1.0 / d;
h *= d * c;
let aa = -(a + m) * (qab + m) * x / ((a + m2) * (qap + m2));
d = 1.0 + aa * d;
if d.abs() < TINY {
d = TINY;
}
c = 1.0 + aa / c;
if c.abs() < TINY {
c = TINY;
}
d = 1.0 / d;
let del = d * c;
h *= del;
if (del - 1.0).abs() < EPS {
break;
}
}
h
}
pub(crate) fn betai(a: f64, b: f64, x: f64) -> f64 {
if x <= 0.0 {
return 0.0;
}
if x >= 1.0 {
return 1.0;
}
let front =
(ln_gamma(a + b) - ln_gamma(a) - ln_gamma(b) + a * x.ln() + b * (1.0 - x).ln()).exp();
if x < (a + 1.0) / (a + b + 2.0) {
front * betacf(a, b, x) / a
} else {
1.0 - front * betacf(b, a, 1.0 - x) / b
}
}
pub(crate) fn normal_sf(z: f64) -> f64 {
let q = gamma_q(0.5, 0.5 * z * z);
(if z >= 0.0 { 0.5 * q } else { 1.0 - 0.5 * q }).clamp(0.0, 1.0)
}
pub(crate) fn normal_cdf(z: f64) -> f64 {
normal_sf(-z)
}
pub(crate) fn chi2_cdf(x: f64, df: f64) -> f64 {
if x <= 0.0 {
return 0.0;
}
gamma_p(df / 2.0, x / 2.0)
}
pub(crate) fn chi2_sf(x: f64, df: f64) -> f64 {
if x <= 0.0 {
return 1.0;
}
gamma_q(df / 2.0, x / 2.0)
}
pub(crate) fn chi2_ppf(p: f64, df: f64) -> f64 {
if p <= 0.0 {
return 0.0;
}
if p >= 1.0 {
return f64::INFINITY;
}
let hi0 = df.max(1.0);
if p > 0.5 {
let target = 1.0 - p;
if target == 0.0 {
return f64::INFINITY;
}
bisect(&|x| chi2_sf(x, df) - target, 0.0, hi0)
} else {
bisect(&|x| chi2_cdf(x, df) - p, 0.0, hi0)
}
}
pub(crate) fn student_t_cdf(t: f64, df: f64) -> f64 {
if df <= 0.0 {
return f64::NAN;
}
let x = df / (df + t * t);
let half_tail = 0.5 * betai(df / 2.0, 0.5, x);
if t >= 0.0 {
1.0 - half_tail
} else {
half_tail
}
}
pub(crate) fn student_t_two_sided_p(t: f64, df: f64) -> f64 {
if df <= 0.0 {
return f64::NAN;
}
let x = df / (df + t * t);
betai(df / 2.0, 0.5, x).clamp(0.0, 1.0)
}
fn student_t_sf_nonneg(t: f64, df: f64) -> f64 {
let x = df / (df + t * t);
0.5 * betai(df / 2.0, 0.5, x)
}
pub(crate) fn student_t_sf(t: f64, df: f64) -> f64 {
let two_sided = student_t_two_sided_p(t, df);
(if t >= 0.0 {
0.5 * two_sided
} else {
1.0 - 0.5 * two_sided
})
.clamp(0.0, 1.0)
}
pub(crate) fn student_t_ppf(p: f64, df: f64) -> f64 {
if p <= 0.0 {
return f64::NEG_INFINITY;
}
if p >= 1.0 {
return f64::INFINITY;
}
if p == 0.5 {
return 0.0;
}
if p < 0.5 {
-bisect(&|t| student_t_sf_nonneg(t, df) - p, 0.0, 1.0)
} else {
let target = 1.0 - p;
if target == 0.0 {
return f64::INFINITY;
}
bisect(&|t| student_t_sf_nonneg(t, df) - target, 0.0, 1.0)
}
}
pub(crate) fn f_cdf(x: f64, df1: f64, df2: f64) -> f64 {
if x <= 0.0 {
return 0.0;
}
let v = df1 * x / (df1 * x + df2);
betai(df1 / 2.0, df2 / 2.0, v)
}
pub(crate) fn f_sf(x: f64, df1: f64, df2: f64) -> f64 {
if x <= 0.0 {
return 1.0;
}
let v = df2 / (df1 * x + df2);
betai(df2 / 2.0, df1 / 2.0, v)
}
pub(crate) fn f_ppf(p: f64, df1: f64, df2: f64) -> f64 {
if p <= 0.0 {
return 0.0;
}
if p >= 1.0 {
return f64::INFINITY;
}
if p > 0.5 {
let target = 1.0 - p;
if target == 0.0 {
return f64::INFINITY;
}
bisect(&|x| f_sf(x, df1, df2) - target, 0.0, 1.0)
} else {
bisect(&|x| f_cdf(x, df1, df2) - p, 0.0, 1.0)
}
}
fn bisect(f: &dyn Fn(f64) -> f64, lo0: f64, hi0: f64) -> f64 {
let mut lo = lo0;
let mut hi = hi0;
let mut flo = f(lo);
let mut fhi = f(hi);
if !flo.is_finite() || !fhi.is_finite() {
return f64::NAN;
}
if flo == 0.0 {
return lo;
}
if fhi == 0.0 {
return hi;
}
let mut expansions = 0;
while (fhi < 0.0) == (flo < 0.0) {
let width = (hi - lo).abs().max(1.0);
lo = hi;
flo = fhi;
hi += 2.0 * width;
fhi = f(hi);
expansions += 1;
if expansions > 2000 || !hi.is_finite() || !fhi.is_finite() {
return f64::NAN;
}
}
for _ in 0..2000 {
let mid = 0.5 * (lo + hi);
if mid == lo || mid == hi {
return mid;
}
let fmid = f(mid);
if !fmid.is_finite() {
return f64::NAN;
}
if fmid == 0.0 {
return mid;
}
if (fmid < 0.0) == (flo < 0.0) {
lo = mid;
flo = fmid;
} else {
hi = mid;
}
}
0.5 * (lo + hi)
}
#[cfg(test)]
mod tests {
use super::*;
fn close(a: f64, b: f64, tol: f64) -> bool {
(a - b).abs() < tol
}
#[test]
fn chi2_survival_matches_known_critical_values() {
assert!(close(chi2_sf(3.841, 1.0), 0.05, 1e-3));
assert!(close(chi2_sf(9.488, 4.0), 0.05, 1e-3));
assert!(close(chi2_sf(18.307, 10.0), 0.05, 1e-3));
assert!(chi2_sf(50.0, 4.0) < 1e-6);
assert!(close(chi2_cdf(9.488, 4.0) + chi2_sf(9.488, 4.0), 1.0, 1e-9));
}
#[test]
fn student_t_two_sided_matches_known_values() {
assert!(close(student_t_two_sided_p(2.776, 4.0), 0.05, 1e-3));
assert!(close(student_t_two_sided_p(2.228, 10.0), 0.05, 1e-3));
assert!(close(student_t_two_sided_p(2.042, 30.0), 0.05, 1e-3));
let c = student_t_cdf(2.776, 4.0);
assert!((0.0..=1.0).contains(&c));
assert!(close(c, 0.975, 1e-3));
}
#[test]
fn f_cdf_matches_known_critical_values() {
assert!(close(f_sf(3.885, 2.0, 12.0), 0.05, 1e-3));
assert!(close(f_sf(3.098, 3.0, 20.0), 0.05, 1e-3));
assert!(close(f_cdf(1.0, 10.0, 10.0), 0.5, 1e-3));
}
#[test]
fn quantiles_invert_their_cdfs() {
assert!(close(chi2_cdf(chi2_ppf(0.95, 7.0), 7.0), 0.95, 1e-6));
assert!(close(f_cdf(f_ppf(0.9, 5.0, 9.0), 5.0, 9.0), 0.9, 1e-6));
assert!(close(
student_t_cdf(student_t_ppf(0.975, 8.0), 8.0),
0.975,
1e-6
));
}
#[test]
fn normal_tails_are_correct() {
assert!(close(normal_sf(1.959_963_98), 0.025, 1e-6));
assert!(close(normal_cdf(0.0), 0.5, 1e-10));
assert!(close(normal_cdf(1.0) + normal_sf(1.0), 1.0, 1e-12));
}
fn close_rel(actual: f64, expected: f64, rel_tol: f64) -> bool {
if expected == 0.0 {
return actual.abs() < rel_tol;
}
((actual - expected) / expected).abs() < rel_tol
}
#[test]
fn scipy_fixture_matches_at_1e_minus_10() {
assert!(close_rel(chi2_sf(7.5, 5.0), 0.18602983360286693, 1e-10));
assert!(close_rel(chi2_cdf(7.5, 5.0), 0.813970166397133, 1e-10));
assert!(close_rel(chi2_ppf(0.95, 5.0), 11.070497693516351, 1e-10));
assert!(close_rel(chi2_ppf(0.05, 5.0), 1.1454762260617692, 1e-10));
assert!(close_rel(
student_t_cdf(2.5, 9.0),
0.9830690861585072,
1e-10
));
assert!(close_rel(
student_t_sf_nonneg(2.5, 9.0),
0.016930913841492864,
1e-10
));
assert!(close_rel(
student_t_ppf(0.9, 9.0),
1.3830287383966324,
1e-10
));
assert!(close_rel(
student_t_ppf(0.1, 9.0),
-1.3830287383966322,
1e-10
));
assert!(close_rel(f_cdf(2.5, 5.0, 12.0), 0.9101758463950645, 1e-10));
assert!(close_rel(f_sf(2.5, 5.0, 12.0), 0.08982415360493556, 1e-10));
assert!(close_rel(f_ppf(0.9, 5.0, 12.0), 2.3940222568422334, 1e-10));
assert!(close_rel(betai(2.0, 3.0, 0.4), 0.5247999999999999, 1e-10));
assert!(close_rel(normal_cdf(1.5), 0.9331927987311419, 1e-10));
assert!(close_rel(normal_sf(1.5), 0.06680720126885806, 1e-10));
assert!(close_rel(normal_cdf(-3.0), 0.001349898031630093, 1e-10));
assert!(close_rel(normal_sf(5.0), 2.8665157187919344e-07, 1e-10));
}
#[test]
fn scipy_fixture_edge_cases() {
assert!(close_rel(
f_sf(1.0e4, 10.0, 10.0),
1.2589504948268003e-18,
1e-6
));
assert!(close_rel(gamma_p(1.0e6, 1.0e6), 0.5001329807608725, 1e-6));
assert!(close_rel(
gamma_q(1.0e6, 1.0e6),
1.0 - 0.5001329807608725,
1e-6
));
assert!(close_rel(
student_t_ppf(1e-10, 1.0),
-3183098861.8379064,
1e-6
));
assert!(
student_t_ppf(1e-10, 1.0) < 0.0,
"must be negative, not +1e7"
);
assert!(close_rel(
chi2_ppf(1e-12, 1.0),
1.5707963267948931e-24,
1e-6
));
assert!(close_rel(
f_ppf(0.99999999, 1.0, 1.0),
4052847304964346.0,
1e-6
));
}
#[test]
fn student_t_sf_matches_two_sided_p_and_avoids_double_cancellation() {
assert!(close_rel(
student_t_sf(2.5, 9.0),
0.016930913841492864, 1e-10
));
assert_eq!(student_t_sf(2.5, 9.0), student_t_sf_nonneg(2.5, 9.0));
assert!(close_rel(
student_t_sf(-2.5, 9.0),
0.9830690861585072,
1e-10
));
assert!(close(
student_t_cdf(2.5, 9.0) + student_t_sf(2.5, 9.0),
1.0,
1e-9
));
assert!(close(
student_t_cdf(-2.5, 9.0) + student_t_sf(-2.5, 9.0),
1.0,
1e-9
));
let t = 1.0e6;
let df = 5.0;
let naive_double_subtraction = 1.0 - student_t_cdf(t, df);
assert_eq!(
naive_double_subtraction, 0.0,
"sanity check: the naive `1.0 - cdf` path really does collapse to exactly 0.0 here"
);
let direct = student_t_sf(t, df);
assert!(
direct > 0.0,
"student_t_sf({t}, {df}) must not collapse to exactly 0.0 like the naive path does"
);
assert!(close_rel(direct, 9.490167245460678e-30, 1e-6));
assert!(close_rel(direct, 0.5 * student_t_two_sided_p(t, df), 1e-10));
}
}