use statrs::function::beta::{beta_reg, inv_beta_reg, ln_beta};
use statrs::function::erf::erfc_inv;
use statrs::function::gamma::ln_gamma;
use crate::errors::EdgeErrors;
const SQRT_2: f64 = std::f64::consts::SQRT_2;
const INC_GAMMA_EPS: f64 = 2.220446049250313e-16;
const INC_GAMMA_MAX_ITER: usize = 10_000;
const LENTZ_TINY: f64 = 1e-300;
const INV_BETA_MAX_ITER: usize = 128;
const INV_BETA_REL_TOL: f64 = 1e-15;
const GAMMA_PPF_MAX_ITER: usize = 128;
const T_INNER_OUTER_SWITCH: f64 = 0.7;
const GAMMA_PPF_REL_TOL: f64 = 1e-15;
fn check_positive(name: &str, value: f64) -> Result<(), EdgeErrors> {
if !(value.is_finite() && value > 0.0) {
return Err(EdgeErrors::InvalidArgument(format!(
"'{name}' must be finite and strictly positive; got {value}."
)));
}
Ok(())
}
fn check_probability(name: &str, value: f64) -> Result<(), EdgeErrors> {
if !(value.is_finite() && (0.0..=1.0).contains(&value)) {
return Err(EdgeErrors::InvalidArgument(format!(
"'{name}' must be a probability in [0, 1]; 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)
}
const BETA_CF_MAX_ITER: usize = 300;
const BETA_CF_EPS: f64 = 3e-16;
const MIN_REPRESENTABLE_LN: f64 = -745.0;
const LN_BETA_STIRLING_MIN: f64 = 30.0;
fn stirlerr(z: f64) -> f64 {
let z2 = z * z;
(1.0 / 12.0 - (1.0 / 360.0 - (1.0 / 1260.0 - 1.0 / (1680.0 * z2)) / z2) / z2) / z
}
fn ln_beta_stable(a: f64, b: f64) -> f64 {
let (big, small) = if a >= b { (a, b) } else { (b, a) };
if big < LN_BETA_STIRLING_MIN {
return ln_beta(a, b);
}
let ratio = small * big.ln() + (big + small - 0.5) * (small / big).ln_1p() - small
+ stirlerr(big + small)
- stirlerr(big);
ln_gamma(small) - ratio
}
fn beta_cf(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() < LENTZ_TINY {
d = LENTZ_TINY;
}
d = 1.0 / d;
let mut h = d;
for m in 1..=BETA_CF_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() < LENTZ_TINY {
d = LENTZ_TINY;
}
c = 1.0 + aa / c;
if c.abs() < LENTZ_TINY {
c = LENTZ_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() < LENTZ_TINY {
d = LENTZ_TINY;
}
c = 1.0 + aa / c;
if c.abs() < LENTZ_TINY {
c = LENTZ_TINY;
}
d = 1.0 / d;
let del = d * c;
h *= del;
if (del - 1.0).abs() < BETA_CF_EPS {
break;
}
}
h
}
fn ln_beta_reg(x: f64, a: f64, b: f64) -> f64 {
ln_beta_reg_pair(x, 1.0 - x, a, b)
}
fn ln_beta_reg_pair(x: f64, one_minus_x: f64, a: f64, b: f64) -> f64 {
if x <= 0.0 {
return f64::NEG_INFINITY;
}
if one_minus_x <= 0.0 {
return 0.0;
}
let ln_x = if x > 0.5 {
(-one_minus_x).ln_1p()
} else {
x.ln()
};
let ln_1mx = if one_minus_x > 0.5 {
(-x).ln_1p()
} else {
one_minus_x.ln()
};
if x < (a + 1.0) / (a + b + 2.0) {
let ln_pre = a * ln_x + b * ln_1mx - ln_beta_stable(a, b);
ln_pre + (beta_cf(a, b, x) / a).ln()
} else {
let ln_other =
b * ln_1mx + a * ln_x - ln_beta_stable(b, a) + (beta_cf(b, a, one_minus_x) / b).ln();
(-ln_other.exp()).ln_1p()
}
}
fn inv_beta_reg_lower(a: f64, b: f64, p: f64) -> Result<f64, EdgeErrors> {
debug_assert!(p <= 0.5, "inv_beta_reg_lower solves the lower half only");
if p <= 0.0 {
return Ok(0.0);
}
inv_ln_beta_reg_lower(a, b, p.ln())
}
fn inv_ln_beta_reg_lower(a: f64, b: f64, ln_p: f64) -> Result<f64, EdgeErrors> {
if ln_p == f64::NEG_INFINITY {
return Ok(0.0);
}
let ln_b = ln_beta(a, b);
let g = |t: f64| ln_beta_reg(t.exp().min(1.0), a, b) - ln_p;
let start = if ln_p > MIN_REPRESENTABLE_LN {
inv_beta_reg(a, b, ln_p.exp())
} else {
0.0
};
let mut t = if start > 0.0 && start < 1.0 {
start.ln()
} else {
(ln_p + a.ln() + ln_b) / a
};
let mut lo = f64::NEG_INFINITY;
let mut hi = 0.0_f64;
if t >= hi {
t = -1.0;
}
let mut step = 1.0;
if g(t) < 0.0 {
lo = t;
for _ in 0..INV_BETA_MAX_ITER {
let probe = (t + step).min(0.0);
if g(probe) >= 0.0 {
hi = probe;
break;
}
lo = probe;
step *= 2.0;
}
} else {
hi = t;
for _ in 0..INV_BETA_MAX_ITER {
let probe = t - step;
if g(probe) < 0.0 {
lo = probe;
break;
}
hi = probe;
step *= 2.0;
}
}
for _ in 0..INV_BETA_MAX_ITER {
if !(t > lo && t < hi) {
t = if lo.is_finite() {
0.5 * (lo + hi)
} else {
hi - 1.0
};
}
let x = t.exp();
let ln_i = ln_beta_reg(x, a, b);
let residual = ln_i - ln_p;
if residual < 0.0 {
lo = t;
} else {
hi = t;
}
let tol = INV_BETA_REL_TOL * (1.0 + t.abs());
if lo.is_finite() && hi - lo <= tol {
return Ok(t.exp());
}
let d = (a * t + (b - 1.0) * (-x).ln_1p() - ln_b - ln_i).exp();
let t_next = if d > 0.0 && d.is_finite() {
t - residual / d
} else if lo.is_finite() {
0.5 * (lo + hi)
} else {
hi - 1.0
};
let delta = (t_next - t).abs();
t = t_next;
if delta <= tol {
return Ok(t.clamp(lo, hi).exp());
}
}
Err(EdgeErrors::NoConvergence {
routine: "inv_ln_beta_reg_lower",
iterations: INV_BETA_MAX_ITER,
last_delta: hi - lo,
})
}
fn inv_beta_reg_pair(a: f64, b: f64, p: f64) -> Result<(f64, f64), EdgeErrors> {
if p <= 0.5 {
let x = inv_beta_reg_lower(a, b, p)?;
Ok((x, 1.0 - x))
} else {
let y = inv_beta_reg_lower(b, a, 1.0 - p)?;
Ok((1.0 - y, y))
}
}
pub fn norm_cdf(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_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_ppf(p: f64) -> Result<f64, EdgeErrors> {
check_probability("p", p)?;
Ok(-SQRT_2 * erfc_inv(2.0 * p))
}
pub fn chisq_sf(x: f64, df: f64) -> Result<f64, EdgeErrors> {
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_cdf(x: f64, df: f64) -> Result<f64, EdgeErrors> {
check_positive("df", df)?;
if x.is_infinite() {
return Ok(if x > 0.0 { 1.0 } else { 0.0 });
}
let (inner, outer) = t_half_masses(x, df);
Ok(if x <= 0.0 { outer } else { 0.5 + inner })
}
pub fn t_sf(x: f64, df: f64) -> Result<f64, EdgeErrors> {
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_ppf(p: f64, df: f64) -> Result<f64, EdgeErrors> {
check_probability("p", p)?;
check_positive("df", df)?;
if p == 0.0 {
return Ok(f64::NEG_INFINITY);
}
if p == 1.0 {
return Ok(f64::INFINITY);
}
let upper = p >= 0.5;
let tail = if upper { 1.0 - p } else { p };
let (y, one_minus_y) = inv_beta_reg_pair(0.5 * df, 0.5, 2.0 * tail)?;
let t = (df * one_minus_y / y).sqrt();
Ok(if upper { t } else { -t })
}
pub fn t_sf_log(x: f64, df: f64) -> Result<f64, EdgeErrors> {
check_positive("df", df)?;
if x.is_infinite() {
return Ok(if x > 0.0 { f64::NEG_INFINITY } else { 0.0 });
}
if x <= 0.0 {
return Ok(t_sf(x, df)?.ln());
}
let x2 = x * x;
let denom = df + x2;
Ok(-std::f64::consts::LN_2 + ln_beta_reg_pair(df / denom, x2 / denom, 0.5 * df, 0.5))
}
pub fn t_isf_log(log_p: f64, df: f64) -> Result<f64, EdgeErrors> {
check_positive("df", df)?;
if log_p > 0.0 || log_p.is_nan() {
return Err(EdgeErrors::InvalidArgument(format!(
"log_p must be at most 0; got {log_p}"
)));
}
if log_p == 0.0 {
return Ok(f64::NEG_INFINITY);
}
if log_p == f64::NEG_INFINITY {
return Ok(f64::INFINITY);
}
if log_p > -std::f64::consts::LN_2 {
return t_ppf(1.0 - log_p.exp(), df);
}
let z = inv_ln_beta_reg_lower(0.5 * df, 0.5, log_p + std::f64::consts::LN_2)?;
if z <= 0.0 {
return Ok(f64::INFINITY);
}
Ok((df * (1.0 - z) / z).sqrt())
}
pub fn f_sf(x: f64, df1: f64, df2: f64) -> Result<f64, EdgeErrors> {
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)))
}
pub fn f_ppf(p: f64, df1: f64, df2: f64) -> Result<f64, EdgeErrors> {
check_probability("p", p)?;
check_positive("df1", df1)?;
check_positive("df2", df2)?;
if p == 0.0 {
return Ok(0.0);
}
if p == 1.0 {
return Ok(f64::INFINITY);
}
let (z, one_minus_z) = inv_beta_reg_pair(0.5 * df1, 0.5 * df2, p)?;
Ok(df2 * z / (df1 * one_minus_z))
}
pub fn beta_cdf(x: f64, a: f64, b: f64) -> Result<f64, EdgeErrors> {
check_positive("a", a)?;
check_positive("b", b)?;
if x <= 0.0 {
return Ok(0.0);
}
if x >= 1.0 {
return Ok(1.0);
}
Ok(beta_reg(a, b, x))
}
pub fn beta_sf(x: f64, a: f64, b: f64) -> Result<f64, EdgeErrors> {
check_positive("a", a)?;
check_positive("b", b)?;
if x <= 0.0 {
return Ok(1.0);
}
if x >= 1.0 {
return Ok(0.0);
}
Ok(beta_reg(b, a, 1.0 - x))
}
pub fn beta_ppf(p: f64, a: f64, b: f64) -> Result<f64, EdgeErrors> {
check_probability("p", p)?;
check_positive("a", a)?;
check_positive("b", b)?;
Ok(inv_beta_reg_pair(a, b, p)?.0)
}
pub fn gamma_cdf(x: f64, shape: f64, scale: f64) -> Result<f64, EdgeErrors> {
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_ppf(p: f64, shape: f64, scale: f64) -> Result<f64, EdgeErrors> {
check_probability("p", p)?;
check_positive("shape", shape)?;
check_positive("scale", scale)?;
if p == 0.0 {
return Ok(0.0);
}
if p == 1.0 {
return Ok(f64::INFINITY);
}
Ok(gamma_lr_inv(p, shape)? * scale)
}
fn gamma_ppf_start(p: f64, shape: f64) -> f64 {
let z = -SQRT_2 * erfc_inv(2.0 * p);
let base = 1.0 - 1.0 / (9.0 * shape) + z / (3.0 * shape.sqrt());
if base > 0.0 {
let x = shape * base * base * base;
if x > 0.0 && x.is_finite() {
return x.ln();
}
}
(p.ln() + ln_gamma(shape) + shape.ln()) / shape
}
fn gamma_lr_inv(p: f64, shape: f64) -> Result<f64, EdgeErrors> {
let upper = p > 0.5;
let target = if upper { 1.0 - p } else { p };
let ln_norm = ln_gamma(shape);
let residual = |t: f64| {
let x = t.exp();
if upper {
target - reg_gamma_upper(shape, x)
} else {
reg_gamma_lower(shape, x) - target
}
};
let slope = |t: f64| (shape * t - t.exp() - ln_norm).exp();
let mut t = gamma_ppf_start(p, shape);
let mut lo = t;
let mut hi = t;
if residual(t) < 0.0 {
for _ in 0..GAMMA_PPF_MAX_ITER {
hi += 1.0;
if residual(hi) >= 0.0 {
break;
}
lo = hi;
}
} else {
for _ in 0..GAMMA_PPF_MAX_ITER {
lo -= 1.0;
if residual(lo) < 0.0 {
break;
}
hi = lo;
}
}
for _ in 0..GAMMA_PPF_MAX_ITER {
if !(t > lo && t < hi) {
t = 0.5 * (lo + hi);
}
let f = residual(t);
if f < 0.0 {
lo = t;
} else {
hi = t;
}
let tol = GAMMA_PPF_REL_TOL * (1.0 + t.abs());
if hi - lo <= tol {
return Ok(t.exp());
}
let d = slope(t);
let t_next = if d > 0.0 && d.is_finite() {
t - f / d
} else {
0.5 * (lo + hi)
};
let step = (t_next - t).abs();
t = t_next;
if step <= tol {
return Ok(t.clamp(lo, hi).exp());
}
}
Err(EdgeErrors::NoConvergence {
routine: "gamma_ppf",
iterations: GAMMA_PPF_MAX_ITER,
last_delta: hi - lo,
})
}
pub fn nbinom_ln_pmf(k: f64, size: f64, prob: f64) -> Result<f64, EdgeErrors> {
check_positive("size", size)?;
check_probability("prob", prob)?;
if k < 0.0 {
return Ok(f64::NEG_INFINITY);
}
if prob == 1.0 {
return Ok(if k == 0.0 { 0.0 } else { f64::NEG_INFINITY });
}
if prob == 0.0 {
return Ok(f64::NEG_INFINITY);
}
Ok(ln_gamma(k + size) - ln_gamma(k + 1.0) - ln_gamma(size)
+ size * prob.ln()
+ k * (-prob).ln_1p())
}
pub fn nbinom_cdf(k: f64, size: f64, prob: f64) -> Result<f64, EdgeErrors> {
check_positive("size", size)?;
check_probability("prob", prob)?;
if k < 0.0 {
return Ok(0.0);
}
if k.is_infinite() {
return Ok(1.0);
}
Ok(beta_reg(size, k.floor() + 1.0, prob))
}
#[cfg(test)]
mod tests {
#![allow(clippy::excessive_precision)]
use super::*;
use approx::assert_relative_eq;
const TOL: f64 = 1e-12;
const BETA_REG_TOL: f64 = 1e-11;
#[test]
fn test_norm_cdf_matches_scipy() {
assert_relative_eq!(norm_cdf(-3.0), 0.001349898031630093, max_relative = TOL);
assert_relative_eq!(norm_cdf(0.0), 0.5, max_relative = TOL);
assert_relative_eq!(norm_cdf(1.959963984540054), 0.975, max_relative = TOL);
assert_relative_eq!(norm_cdf(8.0), 0.9999999999999993, max_relative = TOL);
}
#[test]
fn test_norm_sf_holds_the_far_tail() {
assert_relative_eq!(
norm_sf(1.959963984540054),
0.024999999999999998,
max_relative = TOL
);
assert_relative_eq!(norm_sf(5.0), 2.8665157187919344e-07, max_relative = TOL);
assert_relative_eq!(norm_sf(8.0), 6.22096057427174e-16, max_relative = TOL);
assert_relative_eq!(norm_sf(37.0), 5.725571222523923e-300, max_relative = TOL);
assert!(1.0 - norm_cdf(37.0) == 0.0);
}
#[test]
fn test_norm_ppf_matches_scipy() {
assert_relative_eq!(
norm_ppf(0.025).unwrap(),
-1.9599639845400545,
max_relative = TOL
);
assert_eq!(norm_ppf(0.5).unwrap(), 0.0);
assert_relative_eq!(
norm_ppf(0.975).unwrap(),
1.959963984540054,
max_relative = TOL
);
assert_relative_eq!(
norm_ppf(1e-10).unwrap(),
-6.361340902404056,
max_relative = TOL
);
assert_relative_eq!(
norm_ppf(1e-300).unwrap(),
-37.0470962993612,
max_relative = TOL
);
assert_eq!(norm_ppf(0.0).unwrap(), f64::NEG_INFINITY);
assert_eq!(norm_ppf(1.0).unwrap(), f64::INFINITY);
}
#[test]
fn test_norm_sf_ppf_round_trip() {
for &x in &[-8.0_f64, -4.0, -0.5, 0.0] {
let p = norm_cdf(x);
assert_relative_eq!(
norm_ppf(p).unwrap(),
x,
max_relative = 1e-10,
epsilon = 1e-12
);
assert_relative_eq!(norm_sf(-x), p, max_relative = 1e-14);
}
}
#[test]
fn test_norm_sf_beats_scipy_into_the_subnormals() {
assert_relative_eq!(
norm_sf(37.7),
2.483_485_310_277_6e-311,
max_relative = 1e-11
);
assert_relative_eq!(norm_cdf(-38.15), 9.509_03e-319, max_relative = 1e-4);
}
#[test]
fn test_norm_ppf_rejects_out_of_range() {
assert!(matches!(
norm_ppf(-0.1),
Err(EdgeErrors::InvalidArgument(_))
));
assert!(matches!(norm_ppf(1.5), Err(EdgeErrors::InvalidArgument(_))));
assert!(matches!(
norm_ppf(f64::NAN),
Err(EdgeErrors::InvalidArgument(_))
));
}
#[test]
fn test_chisq_sf_matches_scipy() {
assert_relative_eq!(
chisq_sf(6.0485, 1.0).unwrap(),
0.013918115674714049,
max_relative = TOL
);
assert_relative_eq!(
chisq_sf(3.841458820694124, 1.0).unwrap(),
0.04999999999999994,
max_relative = TOL
);
assert_relative_eq!(
chisq_sf(100.0, 10.0).unwrap(),
5.4497019829205215e-17,
max_relative = TOL
);
assert_relative_eq!(
chisq_sf(0.5, 2.5).unwrap(),
0.8638836284585889,
max_relative = TOL
);
}
#[test]
fn test_chisq_sf_far_upper_tail() {
assert_relative_eq!(
chisq_sf(200.0, 1.0).unwrap(),
2.0884875837625688e-45,
max_relative = TOL
);
assert_relative_eq!(
chisq_sf(1000.0, 1.0).unwrap(),
1.7958327848007363e-219,
max_relative = TOL
);
}
#[test]
fn test_chisq_sf_edges_and_errors() {
assert_eq!(chisq_sf(-1.0, 1.0).unwrap(), 1.0);
assert_eq!(chisq_sf(0.0, 3.0).unwrap(), 1.0);
assert_eq!(chisq_sf(f64::INFINITY, 3.0).unwrap(), 0.0);
assert!(matches!(
chisq_sf(1.0, 0.0),
Err(EdgeErrors::InvalidArgument(_))
));
assert!(matches!(
chisq_sf(1.0, -2.0),
Err(EdgeErrors::InvalidArgument(_))
));
}
#[test]
fn test_t_cdf_matches_scipy() {
assert_relative_eq!(
t_cdf(-2.5, 10.0).unwrap(),
0.01572342211830441,
max_relative = TOL
);
assert_relative_eq!(t_cdf(0.0, 5.0).unwrap(), 0.5, max_relative = TOL);
assert_relative_eq!(
t_cdf(1.5, 3.7).unwrap(),
0.8932009153989933,
max_relative = TOL
);
assert_relative_eq!(
t_cdf(2.5, 10.0).unwrap(),
0.9842765778816955,
max_relative = TOL
);
}
const T_SF_LOG: [(f64, f64, f64); 8] = [
(2.5, 10.0, -4.1526038236684464),
(8.0, 5.0, -8.3083379118742755),
(50.0, 100.0, -166.10963191099665),
(300.0, 3.0, -17.013663984295441),
(0.5, 7.0, -1.1513690707642816),
(-2.5, 10.0, -0.015848346341060398),
(40.0, 1e6, -803.96832475034205),
(120.0, 1e5, -6732.1838846408191),
];
#[test]
fn test_t_sf_log_matches_r() {
for &(x, df, want) in &T_SF_LOG {
let got = t_sf_log(x, df).unwrap();
assert_relative_eq!(got, want, max_relative = 1e-15);
}
}
#[test]
fn test_t_sf_log_reaches_past_underflow() {
assert_eq!(t_sf(120.0, 1e5).unwrap(), 0.0);
assert!(t_sf_log(120.0, 1e5).unwrap() < -6000.0);
}
#[test]
fn test_t_isf_log_round_trips() {
for &(x, df, log_p) in &T_SF_LOG {
let back = t_isf_log(log_p, df).unwrap();
assert_relative_eq!(back, x, max_relative = 1e-10);
}
}
#[test]
fn test_t_isf_log_edges() {
assert_eq!(t_isf_log(0.0, 5.0).unwrap(), f64::NEG_INFINITY);
assert_eq!(t_isf_log(f64::NEG_INFINITY, 5.0).unwrap(), f64::INFINITY);
assert!(t_isf_log(0.5, 5.0).is_err());
assert!(t_isf_log(-1.0, 0.0).is_err());
}
#[test]
fn test_t_sf_holds_the_far_tail() {
assert_relative_eq!(
t_sf(2.5, 10.0).unwrap(),
0.01572342211830441,
max_relative = TOL
);
assert_relative_eq!(
t_sf(20.0, 5.0).unwrap(),
2.8877581866120858e-06,
max_relative = TOL
);
assert_relative_eq!(
t_sf(50.0, 100.0).unwrap(),
7.236081839880731e-73,
max_relative = TOL
);
assert_relative_eq!(
t_sf(-2.5, 10.0).unwrap(),
0.9842765778816955,
max_relative = TOL
);
assert!(1.0 - t_cdf(50.0, 100.0).unwrap() == 0.0);
}
#[test]
fn test_t_tails_near_the_median() {
assert_relative_eq!(
t_cdf(1e-4, 100.0).unwrap(),
0.5000397946186266,
max_relative = TOL
);
assert_relative_eq!(
t_sf(1e-4, 100.0).unwrap(),
0.4999602053813734,
max_relative = TOL
);
assert_relative_eq!(
t_cdf(1e-3, 3.0).unwrap(),
0.5003675525152695,
max_relative = TOL
);
assert_relative_eq!(
t_sf(0.5, 12.0).unwrap(),
0.31305873811266205,
max_relative = BETA_REG_TOL
);
}
#[test]
fn test_t_ppf_matches_scipy() {
assert_relative_eq!(
t_ppf(0.975, 10.0).unwrap(),
2.228138851986274,
max_relative = TOL
);
assert_relative_eq!(
t_ppf(0.025, 3.0).unwrap(),
-3.1824463052837086,
max_relative = TOL
);
assert_relative_eq!(
t_ppf(1e-8, 20.0).unwrap(),
-8.942883259935037,
max_relative = TOL
);
assert_relative_eq!(t_ppf(0.5, 7.0).unwrap(), 0.0, epsilon = 1e-14);
assert_eq!(t_ppf(0.0, 7.0).unwrap(), f64::NEG_INFINITY);
assert_eq!(t_ppf(1.0, 7.0).unwrap(), f64::INFINITY);
}
#[test]
fn test_t_ppf_far_tail_beats_as109() {
assert_relative_eq!(
t_ppf(1e-15, 1.0).unwrap(),
-318309886183790.7,
max_relative = TOL
);
assert_relative_eq!(
t_ppf(1e-15, 3.0).unwrap(),
-103311.08359284987,
max_relative = TOL
);
assert_relative_eq!(
t_ppf(1e-30, 5.0).unwrap(),
-1568392.5590979713,
max_relative = TOL
);
}
#[test]
fn test_t_ppf_sf_round_trip() {
for &(p, df) in &[(1e-6, 4.0_f64), (0.01, 12.0), (0.3, 30.0), (1e-12, 8.0)] {
let x = t_ppf(p, df).unwrap();
assert_relative_eq!(t_cdf(x, df).unwrap(), p, max_relative = 1e-11);
let x_up = t_ppf(1.0 - p, df).unwrap();
assert_relative_eq!(t_sf(x_up, df).unwrap(), p, max_relative = 1e-11);
}
}
#[test]
fn test_t_rejects_bad_arguments() {
assert!(matches!(
t_cdf(0.0, 0.0),
Err(EdgeErrors::InvalidArgument(_))
));
assert!(matches!(
t_sf(0.0, -1.0),
Err(EdgeErrors::InvalidArgument(_))
));
assert!(matches!(
t_ppf(0.5, f64::INFINITY),
Err(EdgeErrors::InvalidArgument(_))
));
assert!(matches!(
t_ppf(1.2, 5.0),
Err(EdgeErrors::InvalidArgument(_))
));
}
#[test]
fn test_f_sf_matches_scipy() {
assert_relative_eq!(
f_sf(4.964602743243619, 1.0, 10.0).unwrap(),
0.050000000009265785,
max_relative = TOL
);
assert_relative_eq!(
f_sf(100.0, 3.0, 7.0).unwrap(),
4.130422101383355e-06,
max_relative = TOL
);
assert_relative_eq!(
f_sf(1.0, 5.0, 5.0).unwrap(),
0.5000000000000002,
max_relative = TOL
);
}
#[test]
fn test_f_sf_far_upper_tail() {
assert_relative_eq!(
f_sf(1e6, 2.0, 4.0).unwrap(),
3.999984000048e-12,
max_relative = TOL
);
assert_relative_eq!(
f_sf(500.0, 1.0, 200.0).unwrap(),
2.607870124613832e-56,
max_relative = TOL
);
}
#[test]
fn test_f_ppf_matches_scipy() {
assert_relative_eq!(
f_ppf(0.95, 1.0, 10.0).unwrap(),
4.964602743730711,
max_relative = TOL
);
assert_relative_eq!(
f_ppf(0.5, 3.0, 7.0).unwrap(),
0.870944253187285,
max_relative = TOL
);
assert_relative_eq!(
f_ppf(0.99, 4.0, 20.0).unwrap(),
4.430690161437775,
max_relative = TOL
);
assert_relative_eq!(
f_ppf(0.5, 1.0, 4.0).unwrap(),
0.5486321704130301,
max_relative = TOL
);
assert_eq!(f_ppf(0.0, 1.0, 4.0).unwrap(), 0.0);
assert_eq!(f_ppf(1.0, 1.0, 4.0).unwrap(), f64::INFINITY);
}
#[test]
fn test_f_ppf_far_tail_beats_as109() {
assert_relative_eq!(
f_ppf(1e-10, 1.0, 1.0).unwrap(),
2.4674011002723397e-20,
max_relative = TOL
);
assert_relative_eq!(
f_ppf(1e-6, 2.0, 7.0).unwrap(),
1.0000006428576329e-06,
max_relative = TOL
);
assert_relative_eq!(
f_ppf(1.0 - 1e-9, 3.0, 5.0).unwrap(),
8817.937012202383,
max_relative = TOL
);
}
#[test]
fn test_f_ppf_sf_round_trip() {
for &(p, d1, d2) in &[
(0.05, 1.0_f64, 10.0),
(0.001, 4.0, 20.0),
(1e-8, 2.0, 6.0),
(0.4, 7.0, 7.0),
] {
let x = f_ppf(1.0 - p, d1, d2).unwrap();
assert_relative_eq!(f_sf(x, d1, d2).unwrap(), p, max_relative = 1e-9);
}
}
#[test]
fn test_f_rejects_bad_arguments() {
assert!(matches!(
f_sf(1.0, 0.0, 5.0),
Err(EdgeErrors::InvalidArgument(_))
));
assert!(matches!(
f_sf(1.0, 5.0, -1.0),
Err(EdgeErrors::InvalidArgument(_))
));
assert!(matches!(
f_ppf(-0.5, 5.0, 5.0),
Err(EdgeErrors::InvalidArgument(_))
));
assert!(matches!(
f_ppf(0.5, f64::NAN, 5.0),
Err(EdgeErrors::InvalidArgument(_))
));
}
#[test]
fn test_beta_cdf_matches_scipy() {
assert_relative_eq!(
beta_cdf(0.3, 2.0, 2.0).unwrap(),
0.21599999999999994,
max_relative = TOL
);
assert_relative_eq!(
beta_cdf(0.5, 0.5, 0.5).unwrap(),
0.5000000000000001,
max_relative = TOL
);
assert_relative_eq!(
beta_cdf(0.9, 5.0, 1.0).unwrap(),
0.5904900000000001,
max_relative = TOL
);
assert_relative_eq!(
beta_cdf(0.25, 2.0, 2.0).unwrap(),
0.15625,
max_relative = TOL
);
assert_eq!(beta_cdf(-1.0, 2.0, 2.0).unwrap(), 0.0);
assert_eq!(beta_cdf(2.0, 2.0, 2.0).unwrap(), 1.0);
}
#[test]
fn test_beta_sf_holds_the_far_tail() {
assert_relative_eq!(
beta_sf(0.99, 2.0, 3.0).unwrap(),
3.97000000000001e-06,
max_relative = TOL
);
assert_relative_eq!(
beta_sf(0.999999, 2.0, 3.0).unwrap(),
3.9999970003450675e-18,
max_relative = TOL
);
assert!(1.0 - beta_cdf(0.999999, 2.0, 3.0).unwrap() == 0.0);
assert_relative_eq!(
beta_sf(0.5, 0.5, 0.5).unwrap(),
0.5000000000000001,
max_relative = TOL
);
assert_relative_eq!(beta_sf(1e-6, 3.0, 2.0).unwrap(), 1.0, max_relative = TOL);
assert_eq!(beta_sf(-1.0, 2.0, 2.0).unwrap(), 1.0);
assert_eq!(beta_sf(2.0, 2.0, 2.0).unwrap(), 0.0);
}
#[test]
fn test_beta_ppf_matches_scipy() {
assert_relative_eq!(beta_ppf(0.5, 2.0, 2.0).unwrap(), 0.5, max_relative = TOL);
assert_relative_eq!(
beta_ppf(0.975, 3.0, 5.0).unwrap(),
0.7095791362626572,
max_relative = TOL
);
assert_relative_eq!(
beta_ppf(1e-8, 2.0, 3.0).unwrap(),
4.082594021609242e-05,
max_relative = TOL
);
}
#[test]
fn test_beta_ppf_far_tail_beats_as109() {
assert_relative_eq!(
beta_ppf(1e-12, 0.3, 0.7).unwrap(),
1.6635847967496953e-40,
max_relative = TOL
);
assert_relative_eq!(
beta_ppf(1e-20, 2.0, 0.5).unwrap(),
1.6329931618110077e-10,
max_relative = TOL
);
}
#[test]
fn test_beta_ppf_cdf_round_trip() {
for &(p, a, b) in &[(0.1_f64, 2.0_f64, 3.0), (0.5, 0.7, 4.0), (0.999, 5.0, 5.0)] {
let x = beta_ppf(p, a, b).unwrap();
assert_relative_eq!(beta_cdf(x, a, b).unwrap(), p, max_relative = 1e-10);
}
}
#[test]
fn test_solvers_converge_across_the_grid() {
for &shape in &[1e-2_f64, 0.5, 1.0, 13.0, 1e3] {
for &b in &[1e-2_f64, 0.5, 1.0, 13.0, 1e3] {
for &p in &[1e-30_f64, 1e-8, 0.1, 0.5, 0.9, 1.0 - 1e-8] {
assert!(beta_ppf(p, shape, b).is_ok(), "beta {p} {shape} {b}");
assert!(f_ppf(p, shape, b).is_ok(), "f {p} {shape} {b}");
}
}
for &p in &[1e-30_f64, 1e-8, 0.1, 0.5, 0.9, 1.0 - 1e-8] {
assert!(gamma_ppf(p, shape, 1.0).is_ok(), "gamma {p} {shape}");
assert!(t_ppf(p, shape).is_ok(), "t {p} {shape}");
}
}
}
#[test]
fn test_beta_rejects_bad_arguments() {
assert!(matches!(
beta_cdf(0.5, 0.0, 2.0),
Err(EdgeErrors::InvalidArgument(_))
));
assert!(matches!(
beta_sf(0.5, 2.0, -1.0),
Err(EdgeErrors::InvalidArgument(_))
));
assert!(matches!(
beta_ppf(1.5, 2.0, 2.0),
Err(EdgeErrors::InvalidArgument(_))
));
assert!(matches!(
beta_ppf(0.5, f64::INFINITY, 2.0),
Err(EdgeErrors::InvalidArgument(_))
));
}
#[test]
fn test_gamma_cdf_matches_scipy() {
assert_relative_eq!(
gamma_cdf(2.0, 3.0, 1.0).unwrap(),
0.3233235838169364,
max_relative = TOL
);
assert_relative_eq!(
gamma_cdf(0.5, 0.1, 2.0).unwrap(),
0.8955592438575553,
max_relative = TOL
);
assert_relative_eq!(
gamma_cdf(10.0, 2.0, 1.5).unwrap(),
0.9902431408563948,
max_relative = TOL
);
assert_relative_eq!(
gamma_cdf(1e-3, 4.0, 1.0).unwrap(),
4.163334721825485e-14,
max_relative = TOL
);
assert_eq!(gamma_cdf(-1.0, 2.0, 1.0).unwrap(), 0.0);
assert_eq!(gamma_cdf(f64::INFINITY, 2.0, 1.0).unwrap(), 1.0);
}
#[test]
fn test_gamma_ppf_matches_scipy() {
assert_relative_eq!(
gamma_ppf(0.5, 3.0, 1.0).unwrap(),
2.6740603137235617,
max_relative = TOL
);
assert_relative_eq!(
gamma_ppf(0.975, 2.5, 1.5).unwrap(),
9.624376495522519,
max_relative = TOL
);
assert_relative_eq!(
gamma_ppf(1e-6, 0.5, 2.0).unwrap(),
1.5707963267957187e-12,
max_relative = TOL
);
assert_relative_eq!(
gamma_ppf(0.999999, 10.0, 0.5).unwrap(),
16.355170258742405,
max_relative = TOL
);
assert_relative_eq!(
gamma_ppf(0.3, 0.05, 3.0).unwrap(),
6.11369156619802e-11,
max_relative = TOL
);
assert_eq!(gamma_ppf(0.0, 2.0, 1.0).unwrap(), 0.0);
assert_eq!(gamma_ppf(1.0, 2.0, 1.0).unwrap(), f64::INFINITY);
}
#[test]
fn test_gamma_ppf_cdf_round_trip() {
for &(p, shape, scale) in &[
(1e-9_f64, 0.3_f64, 2.0_f64),
(0.2, 1.0, 1.0),
(0.5, 7.5, 0.25),
(0.99, 50.0, 3.0),
(0.999999999, 2.0, 1.0),
] {
let x = gamma_ppf(p, shape, scale).unwrap();
assert_relative_eq!(gamma_cdf(x, shape, scale).unwrap(), p, max_relative = 1e-11);
}
}
#[test]
fn test_gamma_rejects_bad_arguments() {
assert!(matches!(
gamma_cdf(1.0, 0.0, 1.0),
Err(EdgeErrors::InvalidArgument(_))
));
assert!(matches!(
gamma_cdf(1.0, 1.0, -2.0),
Err(EdgeErrors::InvalidArgument(_))
));
assert!(matches!(
gamma_ppf(1.5, 1.0, 1.0),
Err(EdgeErrors::InvalidArgument(_))
));
assert!(matches!(
gamma_ppf(0.5, -1.0, 1.0),
Err(EdgeErrors::InvalidArgument(_))
));
assert!(matches!(
gamma_ppf(0.5, 1.0, 0.0),
Err(EdgeErrors::InvalidArgument(_))
));
}
#[test]
fn test_nbinom_ln_pmf_matches_scipy_size_5_prob_03() {
assert_relative_eq!(
nbinom_ln_pmf(0.0, 5.0, 0.3).unwrap(),
-6.01986402162968,
max_relative = TOL
);
assert_relative_eq!(
nbinom_ln_pmf(1.0, 5.0, 0.3).unwrap(),
-4.767101053134312,
max_relative = TOL
);
assert_relative_eq!(
nbinom_ln_pmf(10.0, 5.0, 0.3).unwrap(),
-2.6778586817017827,
max_relative = TOL
);
assert_relative_eq!(
nbinom_ln_pmf(500.0, 5.0, 0.3).unwrap(),
-162.65701716239616,
max_relative = TOL
);
}
#[test]
fn test_nbinom_ln_pmf_matches_scipy_size_25_prob_07() {
assert_relative_eq!(
nbinom_ln_pmf(0.0, 2.5, 0.7).unwrap(),
-0.8916873598468311,
max_relative = TOL
);
assert_relative_eq!(
nbinom_ln_pmf(1.0, 2.5, 0.7).unwrap(),
-1.179369432298612,
max_relative = TOL
);
assert_relative_eq!(
nbinom_ln_pmf(10.0, 2.5, 0.7).unwrap(),
-9.586163334718176,
max_relative = TOL
);
assert_relative_eq!(
nbinom_ln_pmf(500.0, 2.5, 0.7).unwrap(),
-593.8371152363,
max_relative = TOL
);
}
#[test]
fn test_nbinom_ln_pmf_uses_the_successes_convention() {
let (size, mu) = (3.0_f64, 4.0_f64);
let prob = size / (size + mu);
let mean: f64 = (0..4000)
.map(|k| {
let k = k as f64;
k * nbinom_ln_pmf(k, size, prob).unwrap().exp()
})
.sum();
assert_relative_eq!(mean, mu, max_relative = 1e-9);
}
#[test]
fn test_nbinom_ln_pmf_sums_to_one() {
let total: f64 = (0..20000)
.map(|k| nbinom_ln_pmf(k as f64, 2.5, 0.7).unwrap().exp())
.sum();
assert_relative_eq!(total, 1.0, max_relative = 1e-12);
}
#[test]
fn test_nbinom_cdf_matches_scipy() {
assert_relative_eq!(
nbinom_cdf(0.0, 5.0, 0.3).unwrap(),
0.0024299999999999994,
max_relative = TOL
);
assert_relative_eq!(
nbinom_cdf(10.0, 5.0, 0.3).unwrap(),
0.48450894077315665,
max_relative = TOL
);
assert_relative_eq!(
nbinom_cdf(100.0, 5.0, 0.3).unwrap(),
0.9999999999903741,
max_relative = TOL
);
assert_relative_eq!(
nbinom_cdf(3.0, 2.5, 0.7).unwrap(),
0.9514994588636262,
max_relative = TOL
);
assert_relative_eq!(
nbinom_cdf(500.0, 2.5, 0.7).unwrap(),
1.0,
max_relative = TOL
);
assert_eq!(nbinom_cdf(-1.0, 5.0, 0.3).unwrap(), 0.0);
}
#[test]
fn test_nbinom_cdf_agrees_with_summed_pmf() {
let (size, prob) = (4.0_f64, 0.35_f64);
let summed: f64 = (0..=12)
.map(|k| nbinom_ln_pmf(k as f64, size, prob).unwrap().exp())
.sum();
assert_relative_eq!(
nbinom_cdf(12.0, size, prob).unwrap(),
summed,
max_relative = 1e-12
);
}
#[test]
fn test_nbinom_degenerate_probabilities() {
assert_eq!(nbinom_ln_pmf(0.0, 3.0, 1.0).unwrap(), 0.0);
assert_eq!(nbinom_ln_pmf(2.0, 3.0, 1.0).unwrap(), f64::NEG_INFINITY);
assert_eq!(nbinom_ln_pmf(0.0, 3.0, 0.0).unwrap(), f64::NEG_INFINITY);
assert_eq!(nbinom_ln_pmf(-1.0, 3.0, 0.5).unwrap(), f64::NEG_INFINITY);
}
#[test]
fn test_nbinom_rejects_bad_arguments() {
assert!(matches!(
nbinom_ln_pmf(1.0, 0.0, 0.5),
Err(EdgeErrors::InvalidArgument(_))
));
assert!(matches!(
nbinom_ln_pmf(1.0, 5.0, 1.5),
Err(EdgeErrors::InvalidArgument(_))
));
assert!(matches!(
nbinom_cdf(1.0, -5.0, 0.5),
Err(EdgeErrors::InvalidArgument(_))
));
assert!(matches!(
nbinom_cdf(1.0, 5.0, -0.1),
Err(EdgeErrors::InvalidArgument(_))
));
}
}