use statrs::function::beta::beta_reg;
use statrs::function::erf::erfc_inv;
use statrs::function::gamma::ln_gamma;
use crate::errors::CoreErrors;
const INC_GAMMA_EPS: f64 = f64::EPSILON;
const INC_GAMMA_MAX_ITER: usize = 10_000;
const LENTZ_TINY: f64 = 1e-300;
const T_INNER_OUTER_SWITCH: f64 = 0.7;
fn check_positive(name: &str, value: f64) -> Result<(), CoreErrors> {
if !(value.is_finite() && value > 0.0) {
return Err(CoreErrors::InvalidArgument(format!(
"'{name}' must be finite and strictly positive; got {value}."
)));
}
Ok(())
}
fn inc_gamma_prefactor(a: f64, x: f64) -> f64 {
(a * x.ln() - x - ln_gamma(a)).exp()
}
fn reg_gamma_lower(a: f64, x: f64) -> f64 {
if x <= 0.0 {
return 0.0;
}
if x < a + 1.0 {
gamma_series(a, x)
} else {
1.0 - gamma_cont_frac(a, x)
}
}
fn reg_gamma_upper(a: f64, x: f64) -> f64 {
if x <= 0.0 {
return 1.0;
}
if x < a + 1.0 {
1.0 - gamma_series(a, x)
} else {
gamma_cont_frac(a, x)
}
}
fn gamma_series(a: f64, x: f64) -> f64 {
let mut ap = a;
let mut term = 1.0 / a;
let mut sum = term;
for _ in 0..INC_GAMMA_MAX_ITER {
ap += 1.0;
term *= x / ap;
sum += term;
if term.abs() < sum.abs() * INC_GAMMA_EPS {
break;
}
}
sum * inc_gamma_prefactor(a, x)
}
fn gamma_cont_frac(a: f64, x: f64) -> f64 {
let mut b = x + 1.0 - a;
let mut c = 1.0 / LENTZ_TINY;
let mut d = 1.0 / b;
let mut h = d;
for i in 1..=INC_GAMMA_MAX_ITER {
let i = i as f64;
let an = -i * (i - a);
b += 2.0;
d = an * d + b;
if d.abs() < LENTZ_TINY {
d = LENTZ_TINY;
}
c = b + an / c;
if c.abs() < LENTZ_TINY {
c = LENTZ_TINY;
}
d = 1.0 / d;
let delta = d * c;
h *= delta;
if (delta - 1.0).abs() <= INC_GAMMA_EPS {
break;
}
}
h * inc_gamma_prefactor(a, x)
}
pub fn norm_sf(x: f64) -> f64 {
let h = 0.5 * x * x;
if x > 0.0 {
0.5 * reg_gamma_upper(0.5, h)
} else {
0.5 * (1.0 + reg_gamma_lower(0.5, h))
}
}
pub fn norm_cdf(x: f64) -> f64 {
norm_sf(-x)
}
pub fn norm_ppf(p: f64) -> Result<f64, CoreErrors> {
if !(p.is_finite() && (0.0..=1.0).contains(&p)) {
return Err(CoreErrors::InvalidArgument(format!(
"'p' must be a probability in [0, 1]; got {p}."
)));
}
Ok(-std::f64::consts::SQRT_2 * erfc_inv(2.0 * p))
}
pub fn gamma_cdf(x: f64, shape: f64, scale: f64) -> Result<f64, CoreErrors> {
check_positive("shape", shape)?;
check_positive("scale", scale)?;
if x <= 0.0 {
return Ok(0.0);
}
if x.is_infinite() {
return Ok(1.0);
}
Ok(reg_gamma_lower(shape, x / scale))
}
pub fn gamma_sf(x: f64, shape: f64, scale: f64) -> Result<f64, CoreErrors> {
check_positive("shape", shape)?;
check_positive("scale", scale)?;
if x <= 0.0 {
return Ok(1.0);
}
if x.is_infinite() {
return Ok(0.0);
}
Ok(reg_gamma_upper(shape, x / scale))
}
pub fn chisq_sf(x: f64, df: f64) -> Result<f64, CoreErrors> {
check_positive("df", df)?;
if x <= 0.0 {
return Ok(1.0);
}
if x.is_infinite() {
return Ok(0.0);
}
Ok(reg_gamma_upper(0.5 * df, 0.5 * x))
}
fn t_half_masses(x: f64, df: f64) -> (f64, f64) {
let x2 = x * x;
if x2 < T_INNER_OUTER_SWITCH {
let inner = 0.5 * beta_reg(0.5, 0.5 * df, x2 / (df + x2));
(inner, 0.5 - inner)
} else {
let outer = 0.5 * beta_reg(0.5 * df, 0.5, df / (df + x2));
(0.5 - outer, outer)
}
}
pub fn t_sf(x: f64, df: f64) -> Result<f64, CoreErrors> {
check_positive("df", df)?;
if x.is_infinite() {
return Ok(if x > 0.0 { 0.0 } else { 1.0 });
}
let (inner, outer) = t_half_masses(x, df);
Ok(if x <= 0.0 { 0.5 + inner } else { outer })
}
pub fn t_cdf(x: f64, df: f64) -> Result<f64, CoreErrors> {
t_sf(-x, df)
}
pub fn t_pval_two_sided(x: f64, df: f64) -> Result<f64, CoreErrors> {
Ok((2.0 * t_sf(x.abs(), df)?).min(1.0))
}
pub fn f_sf(x: f64, df1: f64, df2: f64) -> Result<f64, CoreErrors> {
check_positive("df1", df1)?;
check_positive("df2", df2)?;
if x <= 0.0 {
return Ok(1.0);
}
if x.is_infinite() {
return Ok(0.0);
}
Ok(beta_reg(0.5 * df2, 0.5 * df1, df2 / (df1 * x + df2)))
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
#[test]
fn test_norm_cdf_matches_r_pnorm() {
assert_relative_eq!(norm_cdf(-3.0), 0.001349898031630095, max_relative = 1e-12);
assert_relative_eq!(norm_cdf(-1.0), 0.1586552539314571, max_relative = 1e-12);
assert_relative_eq!(norm_cdf(0.0), 0.5, max_relative = 1e-15);
assert_relative_eq!(norm_cdf(1.0), 0.8413447460685429, max_relative = 1e-12);
assert_relative_eq!(norm_cdf(3.0), 0.9986501019683699, max_relative = 1e-12);
}
#[test]
fn test_norm_sf_survives_the_far_tail() {
assert_relative_eq!(
norm_sf(37.0),
5.725_571_222_524_577e-300,
max_relative = 1e-8
);
assert!(1.0 - norm_cdf(37.0) == 0.0, "the naive form should flatten");
}
#[test]
fn test_norm_ppf_round_trips() {
for p in [1e-10, 1e-3, 0.25, 0.5, 0.975, 1.0 - 1e-9] {
let z = norm_ppf(p).unwrap();
assert_relative_eq!(norm_cdf(z), p, max_relative = 1e-9);
}
assert!(norm_ppf(-0.1).is_err());
assert!(norm_ppf(1.1).is_err());
}
#[test]
fn test_chisq_sf_matches_r_pchisq() {
assert_relative_eq!(
chisq_sf(0.5, 1.0).unwrap(),
0.4795001221869535,
max_relative = 1e-12
);
assert_relative_eq!(
chisq_sf(3.84, 1.0).unwrap(),
0.05004352124870519,
max_relative = 1e-12
);
assert_relative_eq!(
chisq_sf(20.0, 4.0).unwrap(),
0.000_499_399_227_387_333_6,
max_relative = 1e-12
);
}
#[test]
fn test_chisq_sf_far_tail() {
assert_relative_eq!(
chisq_sf(200.0, 1.0).unwrap(),
2.088_487_583_762_545e-45,
max_relative = 1e-8
);
assert!(chisq_sf(5.0, 0.0).is_err());
assert_eq!(chisq_sf(-1.0, 1.0).unwrap(), 1.0);
}
#[test]
fn test_t_cdf_matches_r_pt() {
assert_relative_eq!(
t_cdf(-2.0, 10.0).unwrap(),
0.036694017385370196,
max_relative = 1e-12
);
assert_relative_eq!(
t_cdf(-0.5, 10.0).unwrap(),
0.31394680287148646,
max_relative = 1e-12
);
assert_relative_eq!(t_cdf(0.0, 10.0).unwrap(), 0.5, max_relative = 1e-14);
assert_relative_eq!(
t_cdf(0.5, 10.0).unwrap(),
0.686_053_197_128_513_5,
max_relative = 1e-12
);
assert_relative_eq!(
t_cdf(2.0, 10.0).unwrap(),
0.963_305_982_614_629_7,
max_relative = 1e-12
);
}
#[test]
fn test_t_sf_far_tail() {
assert_relative_eq!(
t_sf(50.0, 100.0).unwrap(),
7.236_081_839_880_67e-73,
max_relative = 1e-6
);
assert!(t_sf(1.0, 0.0).is_err());
}
#[test]
fn test_t_sf_mirrors_t_cdf() {
for &x in &[-3.0, -1.0, -0.3, 0.0, 0.3, 1.0, 3.0] {
for &df in &[1.0, 5.0, 300.0] {
let total = t_cdf(x, df).unwrap() + t_sf(x, df).unwrap();
assert_relative_eq!(total, 1.0, max_relative = 1e-14);
}
}
}
#[test]
fn test_t_pval_two_sided() {
assert_relative_eq!(
t_pval_two_sided(2.5, 12.0).unwrap(),
0.027915399571325213,
max_relative = 1e-6
);
assert_relative_eq!(
t_pval_two_sided(-2.5, 12.0).unwrap(),
t_pval_two_sided(2.5, 12.0).unwrap(),
max_relative = 1e-15
);
assert_eq!(t_pval_two_sided(0.0, 12.0).unwrap(), 1.0);
}
#[test]
fn test_f_sf_matches_r_pf() {
assert_relative_eq!(
f_sf(1.0, 1.0, 10.0).unwrap(),
0.340_893_132_302_060_1,
max_relative = 1e-12
);
assert_relative_eq!(
f_sf(4.96, 2.0, 10.0).unwrap(),
0.031882570564059055,
max_relative = 1e-10
);
assert_relative_eq!(
f_sf(100.0, 3.0, 10.0).unwrap(),
9.327_525_286_716_801e-8,
max_relative = 1e-6
);
}
#[test]
fn test_f_sf_edges() {
assert_eq!(f_sf(-1.0, 1.0, 1.0).unwrap(), 1.0);
assert_eq!(f_sf(f64::INFINITY, 1.0, 1.0).unwrap(), 0.0);
assert!(f_sf(1.0, 0.0, 1.0).is_err());
assert!(f_sf(1.0, 1.0, -2.0).is_err());
}
#[test]
fn test_gamma_cdf_matches_r() {
let cases = [
(1.0, 2.0, 3.0, 0.044_624_919_234_947_685),
(0.5, 0.3, 2.0, 0.695_545_214_656_659_5),
(10.0, 5.0, 1.0, 0.970_747_311_923_038_9),
(2.5, 7.5, 0.4, 0.359_143_513_422_833_35),
];
for (x, shape, scale, expected) in cases {
assert_relative_eq!(
gamma_cdf(x, shape, scale).unwrap(),
expected,
max_relative = 1e-12
);
}
}
#[test]
fn test_gamma_sf_matches_r() {
let cases = [
(1.0, 2.0, 3.0, 0.955_375_080_765_052_4),
(0.5, 0.3, 2.0, 0.304_454_785_343_340_5),
(10.0, 5.0, 1.0, 0.029_252_688_076_961_08),
(50.0, 2.0, 3.0, 1.020_735_571_764_047_2e-6),
];
for (x, shape, scale, expected) in cases {
assert_relative_eq!(
gamma_sf(x, shape, scale).unwrap(),
expected,
max_relative = 1e-11
);
}
}
#[test]
fn test_gamma_sf_far_tail() {
let sf = gamma_sf(200.0, 2.0, 1.0).unwrap();
assert_relative_eq!(sf, 2.781_632_018_740_842_3e-85, max_relative = 1e-9);
assert_eq!(1.0 - gamma_cdf(200.0, 2.0, 1.0).unwrap(), 0.0);
}
#[test]
fn test_gamma_cdf_sf_complement() {
for &x in &[0.1, 1.0, 3.0, 7.5] {
let cdf = gamma_cdf(x, 2.5, 1.5).unwrap();
let sf = gamma_sf(x, 2.5, 1.5).unwrap();
assert_relative_eq!(cdf + sf, 1.0, max_relative = 1e-14);
}
}
#[test]
fn test_gamma_edges() {
assert_eq!(gamma_cdf(-1.0, 2.0, 1.0).unwrap(), 0.0);
assert_eq!(gamma_sf(-1.0, 2.0, 1.0).unwrap(), 1.0);
assert_eq!(gamma_cdf(f64::INFINITY, 2.0, 1.0).unwrap(), 1.0);
assert_eq!(gamma_sf(f64::INFINITY, 2.0, 1.0).unwrap(), 0.0);
assert!(gamma_cdf(1.0, 0.0, 1.0).is_err());
assert!(gamma_sf(1.0, 1.0, -1.0).is_err());
}
}