#![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)",
);
}