#![allow(clippy::excessive_precision)]
use std::time::{Duration, Instant};
use symplex::prelude::*;
use symplex::stats::numdist::{
beta, betainc_regularized_f64, binom, chi2, gamma, norm, poisson, t,
};
fn close(actual: f64, expected: f64, rel: f64, label: &str) {
if expected.abs() < 1e-15 && actual.abs() < 1e-15 {
return;
}
let scale = expected.abs().max(1e-300);
assert!(
(actual - expected).abs() <= rel * scale,
"{label}: got {actual:e}, expected {expected:e} (relative error {:e})",
(actual - expected).abs() / scale
);
}
fn close_subnormal(actual: f64, expected: f64, rel: f64, ulps: f64, label: &str) {
let spacing = f64::from_bits(1);
let tol = (rel * expected.abs()).max(ulps * spacing);
assert!(
(actual - expected).abs() <= tol,
"{label}: got {actual:e}, expected {expected:e} (difference {:e}, tolerance {tol:e})",
(actual - expected).abs()
);
}
fn err_is_invalid<T: std::fmt::Debug>(r: Result<T, SymplexError>, label: &str) {
assert!(
matches!(r, Err(SymplexError::InvalidArgument { .. })),
"{label}: expected an InvalidArgument error, got {r:?}"
);
}
#[test]
fn t_sf_cauchy_far_beyond_the_x_squared_overflow() {
close(
t::sf(1e160, 1.0),
3.1830988618379067e-161,
1e-12,
"t.sf(1e160, 1)",
);
close(
t::sf(1e154, 1.0),
3.1830988618379067e-155,
1e-12,
"t.sf(1e154, 1)",
);
close(
t::sf(1e155, 1.0),
3.1830988618379067e-156,
1e-12,
"t.sf(1e155, 1)",
);
close(
t::sf(1e300, 1.0),
3.1830988618379067e-301,
1e-12,
"t.sf(1e300, 1)",
);
}
#[test]
fn t_sf_half_degree_of_freedom_at_1e200() {
close(
t::sf(1e200, 0.5),
3.207009754142229e-101,
1e-12,
"t.sf(1e200, 0.5)",
);
}
#[test]
fn t_cdf_lower_tail_at_minus_1e170() {
close(
t::cdf(-1e170, 1.5),
3.7708524320162464e-256,
1e-12,
"t.cdf(-1e170, 1.5)",
);
assert_eq!(t::sf(-1e170, 1.5), 1.0);
assert_eq!(t::sf(f64::INFINITY, 1.5), 0.0);
assert_eq!(t::cdf(f64::NEG_INFINITY, 1.5), 0.0);
assert_eq!(t::cdf(f64::INFINITY, 1.5), 1.0);
}
#[test]
fn t_ppf_cauchy_at_1e_minus_200() -> Result<(), SymplexError> {
close(
t::ppf(1e-200, 1.0)?,
-3.1830988618379067e199,
1e-11,
"t.ppf(1e-200, 1)",
);
Ok(())
}
#[test]
fn t_ppf_cauchy_at_1e_minus_160() -> Result<(), SymplexError> {
close(
t::ppf(1e-160, 1.0)?,
-3.1830988618379067e159,
1e-11,
"t.ppf(1e-160, 1)",
);
Ok(())
}
#[test]
fn t_isf_cauchy_at_3e_minus_231() -> Result<(), SymplexError> {
close(
t::isf(3.120729288632323e-231, 1.0)?,
1.0199855762666673e230,
1e-11,
"t.isf(3.12e-231, 1)",
);
Ok(())
}
#[test]
fn t_ppf_half_degree_of_freedom_at_1e_minus_100() -> Result<(), SymplexError> {
let x = t::ppf(1e-100, 0.5)?;
close(x, -1.02849115631634e199, 1e-11, "t.ppf(1e-100, 0.5)");
close(
t::cdf(x, 0.5),
1e-100,
1e-11,
"t.cdf(t.ppf(1e-100, 0.5), 0.5)",
);
Ok(())
}
#[test]
fn t_ppf_beyond_the_largest_double_is_infinite() -> Result<(), SymplexError> {
assert_eq!(t::ppf(1e-50, 0.1)?, f64::NEG_INFINITY);
assert_eq!(t::isf(1e-50, 0.1)?, f64::INFINITY);
assert_eq!(t::ppf(1e-300, 0.5)?, f64::NEG_INFINITY);
assert_eq!(t::isf(1e-300, 0.5)?, f64::INFINITY);
let x = t::ppf(1e-150, 0.5)?;
assert!(x.is_finite() && x < -1e290, "t.ppf(1e-150, 0.5) = {x:e}");
close(
t::cdf(x, 0.5),
1e-150,
1e-11,
"t.cdf(t.ppf(1e-150, 0.5), 0.5)",
);
Ok(())
}
#[test]
fn t_pdf_matches_scipy_and_mpmath() {
close(
t::pdf(0.7, 3.5),
0.2768478555390718,
1e-14,
"t.pdf(0.7, 3.5)",
);
close(
t::pdf(-2.3, 1.0),
0.050605705275642406,
1e-14,
"t.pdf(-2.3, 1)",
);
close(
t::pdf(5.0, 30.0),
3.288890205292232e-05,
1e-13,
"t.pdf(5, 30)",
);
close(t::pdf(0.0, 2.0), 0.3535533905932738, 1e-14, "t.pdf(0, 2)");
close(
t::pdf(1e160, 0.5),
1.6035048770711145e-241,
1e-12,
"t.pdf(1e160, 0.5)",
);
assert_eq!(t::pdf(1e200, 1.0), 0.0);
assert_eq!(t::pdf(f64::INFINITY, 1.0), 0.0);
assert!(t::pdf(f64::NAN, 1.0).is_nan());
}
#[test]
fn gamma_sf_deep_temme_tail_is_positive_and_right() {
let q = gamma::sf(5.027e7, 5e7, 1.0);
assert!(q > 0.0, "gamma.sf(5.027e7, 5e7) = {q:e} must be positive");
close_subnormal(
q,
3.5745492319883549e-318,
1e-12,
8.0,
"gamma.sf(5.027e7, 5e7)",
);
close_subnormal(
chi2::sf(1.0054e8, 1e8),
3.5745492319883549e-318,
1e-12,
8.0,
"chi2.sf(1.0054e8, 1e8)",
);
assert_eq!(gamma::sf(5.0275e7, 5e7, 1.0), 0.0);
}
#[test]
fn gamma_cdf_deep_temme_lower_tail() {
close_subnormal(
gamma::cdf(4.973e7, 5e7, 1.0),
1.8778449525391997e-320,
1e-12,
8.0,
"gamma.cdf(4.973e7, 5e7)",
);
close(
gamma::sf(5.0265e7, 5e7, 1.0),
1.3088239274803528e-306,
1e-11,
"gamma.sf(5.0265e7, 5e7)",
);
close_subnormal(
gamma::cdf(4.9735e7, 5e7, 1.0),
9.1544625064343368e-309,
1e-11,
8.0,
"gamma.cdf(4.9735e7, 5e7)",
);
}
#[test]
fn gamma_cdf_deep_lower_tail_where_scipy_is_wrong() {
close(
gamma::cdf(49929289.32188135, 5e7, 1.0),
7.2687198106979244e-24,
1e-10,
"gamma.cdf(5e7 − 10√5e7, 5e7)",
);
close(
gamma::cdf(999683772.2339832, 1e9, 1.0),
7.5399586583055715e-24,
1e-10,
"gamma.cdf(1e9 − 10√1e9, 1e9)",
);
}
#[test]
fn chi2_and_gamma_isf_at_1e_minus_310() -> Result<(), SymplexError> {
close(
chi2::isf(1e-310, 1e8)?,
100533581.52867371,
1e-12,
"chi2.isf(1e-310, 1e8)",
);
close(
gamma::isf(1e-310, 5e7, 1.0)?,
50266790.764336854,
1e-12,
"gamma.isf(1e-310, 5e7)",
);
Ok(())
}
#[test]
fn norm_cdf_at_minus_37_6_is_subnormal_not_zero() {
close_subnormal(
norm::cdf(-37.6),
1.0748112495871029e-309,
1e-13,
4.0,
"norm.cdf(-37.6)",
);
close_subnormal(
norm::sf(37.6),
1.0748112495871029e-309,
1e-13,
4.0,
"norm.sf(37.6)",
);
}
#[test]
fn norm_cdf_at_minus_38_is_subnormal_not_zero() {
close_subnormal(
norm::cdf(-38.0),
2.8854283600687843e-316,
1e-13,
4.0,
"norm.cdf(-38.0)",
);
close_subnormal(
norm::cdf(-38.4),
6.6015998543264075e-323,
1e-13,
1.0,
"norm.cdf(-38.4)",
);
assert_eq!(norm::cdf(-39.0), 0.0);
assert_eq!(norm::cdf(f64::NEG_INFINITY), 0.0);
}
#[test]
fn norm_cdf_normal_range_around_the_erfc_switch() {
close(
norm::cdf(-37.4),
1.9536815616488883e-306,
1e-13,
"norm.cdf(-37.4)",
);
close(
norm::cdf(-37.5),
4.6053530095819548e-308,
1e-13,
"norm.cdf(-37.5)",
);
close(
norm::cdf(-30.0),
4.9067139271481871e-198,
1e-14,
"norm.cdf(-30)",
);
close(
norm::cdf(-26.0),
2.4760633155033893e-149,
1e-14,
"norm.cdf(-26)",
);
}
#[test]
fn discrete_ppf_above_2_53_is_an_error_not_a_hang() {
let start = Instant::now();
let r = poisson::ppf(0.5, 1e17);
err_is_invalid(r, "poisson::ppf(0.5, 1e17)");
err_is_invalid(poisson::ppf(0.5, 1e300), "poisson::ppf(0.5, 1e300)");
err_is_invalid(binom::ppf(0.5, 1e17, 0.5), "binom::ppf(0.5, 1e17, 0.5)");
err_is_invalid(poisson::isf(0.5, 1e17), "poisson::isf(0.5, 1e17)");
err_is_invalid(binom::isf(0.5, 1e17, 0.5), "binom::isf(0.5, 1e17, 0.5)");
err_is_invalid(
poisson::ppf(0.5, 9_007_199_254_740_994.0),
"poisson::ppf(0.5, 2^53 + 2)",
);
match poisson::ppf(0.5, 1e17) {
Err(SymplexError::InvalidArgument { reason, .. }) => {
assert!(
reason.contains("2^53"),
"message should name the limit: {reason}"
);
}
other => panic!("expected InvalidArgument, got {other:?}"),
}
assert!(
start.elapsed() < Duration::from_secs(2),
"the rejections took {:?}",
start.elapsed()
);
}
#[test]
fn discrete_ppf_on_a_1e15_lattice_terminates_and_matches_mpmath() -> Result<(), SymplexError> {
let start = Instant::now();
assert_eq!(poisson::ppf(0.975, 1e15)?, 1000000061979504.0);
assert_eq!(poisson::ppf(0.5, 1e15)?, 1e15);
assert_eq!(binom::ppf(0.5, 1e15, 0.5)?, 500000000000000.0);
assert_eq!(binom::ppf(0.025, 1e15, 0.3)?, 299999971597424.0);
assert!(poisson::cdf(1000000061979504.0, 1e15) >= 0.975);
assert!(poisson::cdf(1000000061979503.0, 1e15) < 0.975);
assert!(
start.elapsed() < Duration::from_secs(2),
"the four quantiles took {:?}",
start.elapsed()
);
Ok(())
}
#[test]
fn poisson_cdf_at_infinity() {
assert_eq!(poisson::cdf(f64::INFINITY, 3.5), 1.0);
assert_eq!(poisson::sf(f64::INFINITY, 3.5), 0.0);
assert_eq!(poisson::cdf(f64::NEG_INFINITY, 3.5), 0.0);
assert_eq!(poisson::sf(f64::NEG_INFINITY, 3.5), 1.0);
assert_eq!(binom::cdf(f64::INFINITY, 9.0, 0.3), 1.0);
assert_eq!(binom::sf(f64::NEG_INFINITY, 9.0, 0.3), 1.0);
assert!(poisson::cdf(f64::NAN, 3.5).is_nan());
}
#[test]
fn binom_isf_matches_scipy() -> Result<(), SymplexError> {
assert_eq!(binom::isf(0.3, 9.0, 3.0 / 7.0)?, 5.0);
assert_eq!(binom::isf(0.9, 9.0, 3.0 / 7.0)?, 2.0);
assert_eq!(binom::isf(0.5, 1.0, 0.5)?, 0.0);
assert_eq!(binom::isf(0.5, 20.0, 0.5)?, 10.0);
assert_eq!(binom::isf(1e-6, 100.0, 0.2)?, 41.0);
assert_eq!(binom::isf(0.999, 100.0, 0.2)?, 9.0);
assert_eq!(binom::isf(0.05, 1000.0, 0.01)?, 15.0);
assert_eq!(binom::isf(0.5, 0.0, 0.3)?, 0.0);
assert_eq!(binom::isf(0.2, 5.0, 0.0)?, 0.0);
assert_eq!(binom::isf(0.2, 5.0, 1.0)?, 5.0);
assert!(binom::sf(40.0, 100.0, 0.2) > 1e-6 && binom::sf(41.0, 100.0, 0.2) <= 1e-6);
for q in [0.0, 1.0, -0.1, f64::NAN] {
err_is_invalid(binom::isf(q, 9.0, 0.3), "binom::isf level");
}
err_is_invalid(binom::isf(0.3, -1.0, 0.5), "binom::isf n < 0");
err_is_invalid(binom::isf(0.3, 9.0, 1.5), "binom::isf p > 1");
Ok(())
}
#[test]
fn poisson_isf_matches_scipy() -> Result<(), SymplexError> {
assert_eq!(poisson::isf(0.1, 7.0 / 3.0)?, 4.0);
assert_eq!(poisson::isf(0.95, 7.0 / 3.0)?, 0.0);
assert_eq!(poisson::isf(0.5, 7.0 / 3.0)?, 2.0);
assert_eq!(poisson::isf(1e-10, 1e6)?, 1006368.0);
assert_eq!(poisson::isf(1e-6, 0.5)?, 7.0);
assert_eq!(poisson::isf(0.999, 100.0)?, 71.0);
assert_eq!(poisson::isf(0.3, 1e4)?, 10052.0);
assert!(poisson::sf(1006367.0, 1e6) > 1e-10 && poisson::sf(1006368.0, 1e6) <= 1e-10);
for q in [0.0, 1.0, 1.5, f64::NAN] {
err_is_invalid(poisson::isf(q, 2.0), "poisson::isf level");
}
err_is_invalid(poisson::isf(0.3, 0.0), "poisson::isf λ = 0");
match poisson::isf(2.0, 3.0) {
Err(SymplexError::InvalidArgument { operation, .. }) => {
assert_eq!(operation, "numdist::poisson::isf");
}
other => panic!("expected InvalidArgument, got {other:?}"),
}
Ok(())
}
#[test]
fn binom_sf_matches_scipy() {
close(
binom::sf(3.0, 9.0, 3.0 / 7.0),
0.5878990445637241,
1e-14,
"binom.sf(3, 9, 3/7)",
);
close(
binom::sf(0.0, 9.0, 3.0 / 7.0),
0.9935038273034675,
1e-14,
"binom.sf(0, 9, 3/7)",
);
close(
binom::sf(8.0, 9.0, 3.0 / 7.0),
0.0004877630889352714,
1e-14,
"binom.sf(8, 9, 3/7)",
);
close(
binom::sf(50.0, 100.0, 0.2),
5.17989263752495e-12,
1e-13,
"binom.sf(50, 100, 0.2)",
);
close(
binom::sf(150.0, 1000.0, 0.1),
2.774440900996004e-07,
1e-13,
"binom.sf(150, 1000, 0.1)",
);
close(
binom::sf(2.0, 20.0, 0.05),
0.07548367378849626,
1e-14,
"binom.sf(2, 20, 0.05)",
);
close(
binom::sf(10.7, 20.0, 0.5),
0.41190147399902344,
1e-14,
"binom.sf(10.7, 20, 0.5)",
);
assert_eq!(binom::sf(-1.0, 5.0, 0.5), 1.0);
assert_eq!(binom::sf(5.0, 5.0, 0.5), 0.0);
}
#[test]
fn basym_two_large_shapes_near_the_mean() {
close(
betainc_regularized_f64(2e5, 3e5, 0.4),
0.50007677650083536,
1e-12,
"I_0.4(2e5, 3e5)",
);
close(
betainc_regularized_f64(1e6, 1e6, 0.5001),
0.6113512821376899,
1e-12,
"I_0.5001(1e6, 1e6)",
);
close(
beta::cdf(0.4999, 1e6, 1e6),
0.3886487178623101,
1e-12,
"I_0.4999(1e6, 1e6)",
);
}
#[test]
fn basym_small_tails_inside_its_window() {
close(
betainc_regularized_f64(2e5, 3e5, 0.39),
8.6699388658990661e-48,
1e-11,
"I_0.39(2e5, 3e5)",
);
close(
beta::sf(0.405, 2e5, 3e5),
2.8545525000973192e-13,
1e-11,
"1 − I_0.405(2e5, 3e5)",
);
let lower = beta::cdf(0.405, 2e5, 3e5);
close(
lower,
1.0 - 2.8545525000973192e-13,
1e-15,
"I_0.405(2e5, 3e5)",
);
}
#[test]
fn beta_quantile_below_the_smallest_float_is_the_smallest_float() {
let (a, b) = (1.0000000000000002e-3, 1.0000000000000002e-3);
let tiniest = f64::from_bits(1);
assert_eq!(beta::ppf(0.20554351806640625, a, b).unwrap(), tiniest);
assert_eq!(beta::ppf(1e-100, a, b).unwrap(), tiniest);
close(
beta::cdf(tiniest, a, b),
0.23750048582096115,
1e-12,
"F(5e-324)",
);
assert_eq!(beta::isf(0.20554351806640625, a, b).unwrap(), 1.0);
let x = beta::ppf(0.3, a, b).unwrap();
assert!(x > tiniest && x < 1e-100, "{x:e}");
close(beta::cdf(x, a, b), 0.3, 1e-9, "F(ppf(0.3))");
}
#[test]
fn gamma_quantile_below_the_smallest_float_is_the_smallest_float() {
let tiniest = f64::from_bits(1);
assert_eq!(gamma::ppf(8e-4, 0.005, 1.0).unwrap(), tiniest);
assert_eq!(chi2::ppf(8e-4, 0.01).unwrap(), tiniest);
assert_eq!(chi2::isf(1.0 - 8e-4, 0.01).unwrap(), tiniest);
close(
gamma::cdf(tiniest, 0.005, 1.0),
0.024250093812704408,
1e-12,
"F(5e-324)",
);
close(
chi2::cdf(tiniest, 0.01),
0.024166194861712902,
1e-12,
"χ²(0.01) F(5e-324)",
);
let x = gamma::ppf(0.3, 0.005, 1.0).unwrap();
close(gamma::cdf(x, 0.005, 1.0), 0.3, 1e-9, "F(ppf(0.3))");
}
#[test]
fn f_quantile_below_the_smallest_float_is_the_smallest_float() {
use symplex::stats::numdist::f;
let tiniest = f64::from_bits(1);
assert_eq!(f::isf(1.0 - 2.3721549469e-6, 0.01, 0.01).unwrap(), tiniest);
assert_eq!(f::ppf(1e-3, 0.01, 0.01).unwrap(), tiniest);
close(
f::cdf(tiniest, 0.01, 0.01),
0.012090845112971765,
1e-10,
"F(5e-324)",
);
let (d1, d2) = (1.0530057972938501e-2, 1.358019733374666e4);
close(
f::cdf(tiniest, d1, d2),
0.019368514617951588,
1e-10,
"F(5e-324), d2 ≫ d1",
);
assert_eq!(f::isf(0.9999927248020288, d1, d2).unwrap(), tiniest);
}
#[test]
fn beta_quantile_reaches_the_subnormals() {
let (a, b) = (1.0743069978730814e-3, 1.0000000000000002e-3);
let q = 0.7814254760742188;
let x = beta::isf(q, a, b).unwrap();
assert!(x < 1e-300, "{x:e}");
close(x, 1.7090373616895022e-320, 1e-3, "subnormal quantile");
assert!(beta::sf(x.next_down(), a, b) >= q && beta::sf(x.next_up(), a, b) <= q);
}
#[test]
fn f_quantile_in_the_subnormals_brackets_the_level() {
use symplex::stats::numdist::f;
let p = 0.01242828369140625;
let x = f::ppf(p, 0.01, 0.01).unwrap();
assert!(x > 0.0 && x < 1e-300, "{x:e}");
assert!(
f::cdf(x.next_down(), 0.01, 0.01) <= p && f::cdf(x, 0.01, 0.01) >= p,
"cdf around {x:e}: {:e}, {:e}",
f::cdf(x.next_down(), 0.01, 0.01),
f::cdf(x, 0.01, 0.01)
);
}
#[test]
fn f_upper_tail_beyond_the_largest_float() {
use symplex::stats::numdist::f;
let (d1, d2) = (6.331113875958191e3, 1.0000000000000005e-2);
close(
f::sf(2.8391364988509896e304, d1, d2),
0.029340876076892379,
1e-10,
"sf near MAX/6000",
);
close(
f::sf(f64::MAX, d1, d2),
0.028084418152830489,
1e-10,
"sf(f64::MAX)",
);
assert_eq!(f::isf(0.02733612060546875, d1, d2).unwrap(), f64::INFINITY);
let x = f::isf(0.0292, d1, d2).unwrap();
assert!(x.is_finite(), "{x:e}");
close(f::sf(x, d1, d2), 0.0292, 1e-8, "sf(isf(0.0292))");
}
#[test]
fn gamma_quantile_scales_in_logarithms() {
let (k, theta, p) = (9.646949844764864e-3, 6.15390567767807e2, 7.7056884765625e-4);
let x = gamma::ppf(p, k, theta).unwrap();
close(x, 6.7559119122692663e-321, 2e-3, "subnormal gamma quantile");
assert!(gamma::cdf(x.next_down(), k, theta) <= p && gamma::cdf(x.next_up(), k, theta) >= p);
}
#[test]
fn f_quantile_solves_below_the_exp_underflow() {
use symplex::stats::numdist::f;
let (d1, d2, q) = (
1.0116620928845984e-2,
2.977617615047337e3,
9.765701293945313e-1,
);
close(
f::cdf(f64::from_bits(1), d1, d2),
0.022607946088527692,
1e-10,
"F(5e-324)",
);
let x = f::isf(q, d1, d2).unwrap();
close(x, 5.751550438474862e-321, 2e-3, "subnormal F quantile");
assert!(f::sf(x.next_down(), d1, d2) >= q && f::sf(x.next_up(), d1, d2) <= q);
}