use std::f64::consts::PI;
const LN_GAMMA_RECURRENCE_BELOW: f64 = 0.5;
pub fn ln_gamma(x: f64) -> f64 {
if x.is_nan() || x < 0.0 {
return f64::NAN;
}
if x == 0.0 || x.is_infinite() {
return f64::INFINITY;
}
if x < LN_GAMMA_RECURRENCE_BELOW {
return ln_gamma(x + 1.0) - x.ln();
}
const COEFFS: [f64; 9] = [
0.999_999_999_999_809_9,
676.520_368_121_885_1,
-1259.139_216_722_402_8,
771.323_428_777_653_1,
-176.615_029_162_140_6,
12.507_343_278_686_905,
-0.138_571_095_265_720_12,
9.984_369_578_019_572e-6,
1.505_632_735_149_311_6e-7,
];
let x = x - 1.0;
let mut sum = COEFFS[0];
for (i, &c) in COEFFS[1..].iter().enumerate() {
sum += c / (x + i as f64 + 1.0);
}
let t = x + 7.5;
0.5 * (2.0 * PI).ln() + (x + 0.5) * t.ln() - t + sum.ln()
}
pub const UPPER_INC_GAMMA_MIN_A: f64 = 0.5;
pub fn upper_inc_gamma_reg(a: f64, x: f64) -> f64 {
if a < UPPER_INC_GAMMA_MIN_A || x < 0.0 || x.is_nan() || a.is_nan() {
return f64::NAN;
}
if x == 0.0 {
return 1.0;
}
let q = if x < a + 1.0 {
1.0 - lower_gamma_series(a, x)
} else {
upper_gamma_cf(a, x)
};
if !(0.0..=1.0).contains(&q) {
return f64::NAN;
}
q
}
const GAMMA_EPS: f64 = 1e-15;
fn gamma_max_iter(a: f64) -> usize {
200 + (12.0 * a.sqrt()).ceil() as usize
}
#[inline]
fn gamma_prefactor(a: f64, x: f64) -> f64 {
(-x + a * x.ln() - ln_gamma(a)).exp()
}
fn lower_gamma_series(a: f64, x: f64) -> f64 {
let mut sum = 1.0 / a;
let mut term = 1.0 / a;
let mut converged = false;
for n in 1..gamma_max_iter(a) {
term *= x / (a + n as f64);
sum += term;
if term.abs() < GAMMA_EPS * sum.abs() {
converged = true;
break;
}
}
if !converged {
return f64::NAN;
}
sum * gamma_prefactor(a, x)
}
fn upper_gamma_cf(a: f64, x: f64) -> f64 {
const TINY: f64 = 1e-30;
let mut f = TINY;
let mut c = TINY;
let mut d = 0.0_f64;
let mut converged = false;
for n in 0..gamma_max_iter(a) {
let an = if n == 0 {
1.0
} else {
-(n as f64) * (n as f64 - a)
};
let bn = x - a + 1.0 + 2.0 * n as f64;
d = bn + an * d;
if d.abs() < TINY {
d = TINY;
}
c = bn + an / c;
if c.abs() < TINY {
c = TINY;
}
d = 1.0 / d;
let delta = c * d;
f *= delta;
if (delta - 1.0).abs() < GAMMA_EPS {
converged = true;
break;
}
}
if !converged {
return f64::NAN;
}
f * gamma_prefactor(a, x)
}
pub fn chi2_sf(x: f64, k: usize) -> f64 {
if x.is_nan() || k == 0 {
return f64::NAN;
}
if x <= 0.0 {
return 1.0;
}
if x.is_infinite() {
return 0.0;
}
let a = k as f64 / 2.0;
let z = x / 2.0;
upper_inc_gamma_reg(a, z)
}
const PDF_SPLIT_ABOVE: f64 = 2.0;
const TAIL_UNDERFLOWS_ABOVE: f64 = 39.0;
#[inline]
pub fn normal_pdf(x: f64) -> f64 {
let ax = x.abs();
if ax <= PDF_SPLIT_ABOVE {
return (-0.5 * x * x).exp() / (2.0 * PI).sqrt();
}
if ax.is_nan() {
return f64::NAN;
}
if ax >= TAIL_UNDERFLOWS_ABOVE {
return 0.0;
}
let head = (ax * 64.0).trunc() / 64.0;
let rest = (ax - head) * (ax + head);
(-0.5 * head * head).exp() * (-0.5 * rest).exp() / (2.0 * PI).sqrt()
}
const SERIES_SWITCH: f64 = 1.75;
const SERIES_REL_EPS: f64 = 1e-18;
const SERIES_MAX_TERMS: usize = 100;
fn erf_series_sum(t: f64) -> f64 {
let t_sq = t * t;
let mut term = 1.0_f64;
let mut sum = 1.0_f64;
let mut correction = 0.0_f64;
let mut converged = false;
for n in 0..SERIES_MAX_TERMS {
term *= t_sq / (2 * n + 3) as f64;
let adjusted = term - correction;
let raised = sum + adjusted;
correction = (raised - sum) - adjusted;
sum = raised;
if term < SERIES_REL_EPS * sum {
converged = true;
break;
}
}
debug_assert!(
converged,
"the error-function series ran out of terms at t^2 = {t_sq}"
);
sum
}
#[inline]
fn cf_depth(t: f64) -> usize {
debug_assert!(
t >= SERIES_SWITCH,
"cf_depth is only calibrated from the handover outward, got {t}"
);
12 + (360.0 / (t * t) + 35.0 / t).ceil() as usize
}
fn upper_tail_cf(t: f64, depth: usize) -> f64 {
let mut fraction = 0.0_f64;
for level in (1..=depth).rev() {
fraction = level as f64 / (t + fraction);
}
normal_pdf(t) / (t + fraction)
}
fn upper_tail(t: f64) -> f64 {
debug_assert!(t >= 0.0 || t.is_nan(), "upper_tail takes t >= 0, got {t}");
if t >= TAIL_UNDERFLOWS_ABOVE {
return 0.0;
}
if t < SERIES_SWITCH {
return 0.5 - t * normal_pdf(t) * erf_series_sum(t);
}
upper_tail_cf(t, cf_depth(t))
}
fn central_mass(t: f64) -> f64 {
debug_assert!(t >= 0.0 || t.is_nan(), "central_mass takes t >= 0, got {t}");
if t < SERIES_SWITCH {
t * normal_pdf(t) * erf_series_sum(t)
} else if t >= TAIL_UNDERFLOWS_ABOVE {
0.5
} else {
0.5 - upper_tail_cf(t, cf_depth(t))
}
}
pub fn normal_cdf(x: f64) -> f64 {
if x.is_nan() {
return f64::NAN;
}
if x <= 0.0 {
upper_tail(-x)
} else {
1.0 - upper_tail(x)
}
}
pub fn normal_sf(x: f64) -> f64 {
if x.is_nan() {
return f64::NAN;
}
if x >= 0.0 {
upper_tail(x)
} else {
1.0 - upper_tail(-x)
}
}
pub fn normal_cdf_difference(hi: f64, lo: f64) -> f64 {
if hi < lo {
return -normal_cdf_difference(lo, hi);
}
if hi.is_nan() || lo.is_nan() {
return f64::NAN;
}
if lo > 0.0 {
upper_tail(lo) - upper_tail(hi)
} else if hi < 0.0 {
upper_tail(-hi) - upper_tail(-lo)
} else {
central_mass(hi) + central_mass(-lo)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ln_gamma_integer_values() {
let factorials = [1.0_f64, 1.0, 2.0, 6.0, 24.0, 120.0, 720.0, 5040.0];
for (n, &f) in factorials.iter().enumerate() {
let expected = f.ln();
let got = ln_gamma(n as f64 + 1.0);
assert!(
(got - expected).abs() < 1e-12,
"n={n}: got {got}, expected {expected}"
);
}
}
#[test]
fn ln_gamma_half_integer() {
assert!((ln_gamma(0.5) - PI.sqrt().ln()).abs() < 1e-12);
assert!((ln_gamma(1.5) - (0.5 * PI.sqrt()).ln()).abs() < 1e-12);
assert!((ln_gamma(2.5) - (0.75 * PI.sqrt()).ln()).abs() < 1e-12);
}
#[test]
fn ln_gamma_large_argument() {
for &x in &[10.0_f64, 50.0, 100.0, 1000.0] {
let stirling = (x - 0.5) * x.ln() - x + 0.5 * (2.0 * PI).ln();
let got = ln_gamma(x);
let rel = (got - stirling).abs() / stirling.abs();
assert!(
rel < 1e-3,
"x={x}: got {got}, stirling {stirling}, rel {rel}"
);
}
}
#[test]
fn upper_inc_gamma_reg_boundary() {
for &a in &[0.5_f64, 1.0, 2.0, 5.0] {
assert!((upper_inc_gamma_reg(a, 0.0) - 1.0).abs() < 1e-12);
}
}
#[test]
fn upper_inc_gamma_reg_a_equals_one() {
for &x in &[0.1_f64, 1.0, 5.0, 10.0] {
let expected = (-x).exp();
let got = upper_inc_gamma_reg(1.0, x);
assert!(
(got - expected).abs() < 1e-12,
"x={x}: got {got}, expected {expected}"
);
}
}
#[test]
fn upper_inc_gamma_reg_large_x() {
for &a in &[0.5_f64, 1.0, 2.0] {
assert!(upper_inc_gamma_reg(a, 100.0) < 1e-30);
}
}
#[test]
fn upper_inc_gamma_reg_invalid_inputs() {
assert!(upper_inc_gamma_reg(-1.0, 1.0).is_nan());
assert!(upper_inc_gamma_reg(1.0, -1.0).is_nan());
assert!(upper_inc_gamma_reg(f64::NAN, 1.0).is_nan());
assert!(upper_inc_gamma_reg(1.0, f64::NAN).is_nan());
}
#[test]
fn chi2_sf_at_zero() {
for &k in &[1_usize, 2, 6] {
assert!((chi2_sf(0.0, k) - 1.0).abs() < 1e-12);
}
}
#[test]
fn chi2_sf_k_equals_two_is_exponential() {
for &x in &[1.0_f64, 3.0, 10.0] {
let expected = (-x / 2.0).exp();
let got = chi2_sf(x, 2);
assert!(
(got - expected).abs() < 1e-12,
"x={x}: got {got}, expected {expected}"
);
}
}
#[test]
fn chi2_sf_large_x_underflows_smoothly() {
for &k in &[1_usize, 6] {
for &x in &[100.0_f64, 200.0] {
let sf = chi2_sf(x, k);
assert!(sf >= 0.0);
assert!(sf < 1e-15);
}
}
}
#[test]
fn chi2_sf_at_infinity_is_zero_and_not_a_non_number() {
for &k in &[1_usize, 2, 6, 15] {
assert_eq!(chi2_sf(f64::INFINITY, k), 0.0, "k = {k}");
}
assert_eq!(normal_sf(f64::INFINITY), 0.0);
}
#[test]
fn ln_gamma_is_finite_at_small_arguments() {
let cases = [
(1e-300_f64, 690.775_527_898_213_7),
(1e-100, 230.258_509_299_404_58),
(5.551_115_123_125_783e-17, 37.429_947_750_237_05),
(1e-16, 36.841_361_487_904_734),
(1e-8, 18.420_680_738_180_21),
];
for (x, expected) in cases {
let got = ln_gamma(x);
assert!(got.is_finite(), "ln Γ({x}) returned {got}");
let rel = (got - expected).abs() / expected;
assert!(
rel < 1e-14,
"ln Γ({x}) = {got}, expected {expected}, rel {rel}"
);
}
assert!((ln_gamma(0.5) - PI.sqrt().ln()).abs() < 1e-15);
assert!((ln_gamma(1.5) - (0.5 * PI.sqrt()).ln()).abs() < 1e-15);
}
#[test]
fn ln_gamma_edges_are_named() {
assert_eq!(ln_gamma(0.0), f64::INFINITY);
assert_eq!(ln_gamma(f64::INFINITY), f64::INFINITY);
assert!(ln_gamma(-1.0).is_nan());
assert!(ln_gamma(-0.5).is_nan());
assert!(ln_gamma(f64::NAN).is_nan());
}
#[test]
fn a_small_shape_parameter_is_refused_and_not_approximated() {
for &a in &[1e-16_f64, 1e-12, 1e-8, 1e-4, 0.01, 0.1, 0.25, 0.4999] {
for &x in &[1e-6_f64, 0.5, 1.0, 2.0] {
let got = upper_inc_gamma_reg(a, x);
assert!(got.is_nan(), "Q({a}, {x}) returned {got} below the domain");
}
}
assert_eq!(UPPER_INC_GAMMA_MIN_A, 0.5);
for &x in &[0.001_f64, 0.4, 1.4999, 1.50001, 20.0] {
assert!(
upper_inc_gamma_reg(UPPER_INC_GAMMA_MIN_A, x).is_finite(),
"Q(0.5, {x}) should be answered"
);
}
assert!(chi2_sf(1.0, 1).is_finite());
}
#[test]
fn the_branches_are_reached_where_the_tests_believe_they_are() {
for &a in &[0.5_f64, 1.0, 7.5, 100.0, 5000.0] {
let seam = a + 1.0;
let below = seam - seam * f64::EPSILON;
assert!(
below < seam,
"a = {a}: the series probe is not below the seam"
);
let series_side = upper_inc_gamma_reg(a, below);
let fraction_side = upper_inc_gamma_reg(a, seam);
assert!(series_side.is_finite() && fraction_side.is_finite());
let allowed = 1e-14 + 4.0 * f64::EPSILON * seam.max(a * seam.ln());
let step = (series_side - fraction_side).abs() / fraction_side;
assert!(
step < allowed,
"a = {a}: the branches step by {step:e} against an allowance of {allowed:e}"
);
}
}
#[test]
fn the_iteration_ceiling_follows_the_shape_parameter() {
for &a in &[2500.0_f64, 5000.0, 25000.0, 50000.0] {
let got = upper_inc_gamma_reg(a, a);
assert!(got.is_finite(), "Q({a}, {a}) = {got}");
assert!(
(0.49..0.5).contains(&got),
"Q({a}, {a}) = {got}, which is not just under one half"
);
}
for (a, series, fraction) in [
(100.0_f64, 89_usize, 39_usize),
(5000.0, 484, 152),
(50000.0, 1101, 328),
(1e6, 1724, 894),
] {
let ceiling = gamma_max_iter(a);
assert!(
ceiling > 2 * series.max(fraction),
"at a = {a} the ceiling {ceiling} is under twice the {} needed",
series.max(fraction)
);
}
}
#[test]
fn the_upper_edge_is_bounded_by_its_stated_mechanism_then_refused() {
for &a in &[1e3_f64, 1e5, 1e6, 1e8] {
let got = upper_inc_gamma_reg(a, a);
assert!(
(0.0..0.5).contains(&got),
"Q({a:e}, {a:e}) = {got}, which is not a probability under one half"
);
}
for &a in &[1e10_f64, 1e12, 1e14] {
let got = upper_inc_gamma_reg(a, a);
assert!(
(0.0..=1.0).contains(&got),
"Q({a:e}, {a:e}) = {got} left the unit interval without being refused"
);
assert!(
got > 0.5,
"Q({a:e}, {a:e}) = {got} is no longer demonstrably wrong; re-measure the \
accuracy table if the routine improved"
);
let bound = f64::EPSILON * a.max(a * a.ln());
let excess = (got - 0.5) / 0.5;
assert!(
excess <= bound,
"Q({a:e}, {a:e}) = {got} exceeds one half by {excess:e}, over the \
published bound {bound:e}"
);
}
assert!((upper_inc_gamma_reg(1e10, 1e10) - 0.5) / 0.5 > 1e-6);
assert!((upper_inc_gamma_reg(1e14, 1e14) - 0.5) / 0.5 > 0.1);
let broken = upper_inc_gamma_reg(1e15, 1e15);
assert!(
broken.is_nan(),
"Q(1e15, 1e15) = {broken} should be refused"
);
for &k in &[1_usize, 2, 6, 15] {
for &x in &[0.5_f64, 1.0, 12.59, 100.0, 1000.0] {
assert!(chi2_sf(x, k).is_finite(), "chi2_sf({x}, {k})");
}
}
}
#[test]
fn no_argument_on_the_documented_domain_exhausts_a_ceiling() {
let mut checked = 0_usize;
let mut a = UPPER_INC_GAMMA_MIN_A;
while a <= 60_000.0 {
let mut x = 1e-4_f64;
while x < 8.0 * a + 800.0 {
let q = upper_inc_gamma_reg(a, x);
assert!(
q.is_finite() && (0.0..=1.0).contains(&q),
"Q({a}, {x}) = {q}"
);
checked += 1;
x *= 1.35;
}
for &x in &[
a + 1.0 - f64::EPSILON * a,
a + 1.0,
a + 1.0 + f64::EPSILON * a,
] {
assert!(upper_inc_gamma_reg(a, x).is_finite(), "seam at a = {a}");
}
a = if a < 100.0 { a + 0.5 } else { a * 1.6 };
}
assert!(
checked > 5_000,
"the sweep covered only {checked} arguments"
);
}
#[test]
fn chi2_sf_invalid_inputs() {
assert!(chi2_sf(f64::NAN, 1).is_nan());
assert!(chi2_sf(1.0, 0).is_nan());
}
#[test]
fn normal_pdf_at_zero() {
let expected = 1.0 / (2.0 * PI).sqrt();
assert!((normal_pdf(0.0) - expected).abs() < 1e-15);
}
#[test]
fn normal_pdf_symmetric() {
for &x in &[0.5_f64, 1.0, 2.5, 5.0, 12.5, 30.0] {
assert_eq!(normal_pdf(x), normal_pdf(-x));
}
}
#[test]
fn normal_pdf_known_values() {
assert!((normal_pdf(1.0) - 0.241_970_724_519_143_37).abs() < 1e-15);
assert!((normal_pdf(2.0) - 0.053_990_966_513_188_06).abs() < 1e-15);
}
#[test]
fn the_two_density_branches_agree_at_their_seam() {
let below = normal_pdf(PDF_SPLIT_ABOVE);
let above = normal_pdf(f64::from_bits(PDF_SPLIT_ABOVE.to_bits() + 1));
let step = (below - above).abs() / below;
assert!(step < 4e-15, "the density steps by {step:e} at its seam");
}
#[test]
fn normal_pdf_handles_non_finite_and_absurd_arguments() {
assert!(normal_pdf(f64::NAN).is_nan());
assert_eq!(normal_pdf(f64::INFINITY), 0.0);
assert_eq!(normal_pdf(f64::NEG_INFINITY), 0.0);
for &x in &[40.0_f64, 1e100, 1e300, -1e300] {
assert_eq!(normal_pdf(x), 0.0, "φ({x}) should underflow to zero");
}
}
fn abramowitz_stegun_26_2_17(x: f64) -> f64 {
if x < -8.0 {
return 0.0;
}
if x > 8.0 {
return 1.0;
}
let sign = if x >= 0.0 { 1.0 } else { -1.0 };
let ax = x.abs();
let t = 1.0 / (1.0 + 0.231_641_9 * ax);
let t2 = t * t;
let t3 = t2 * t;
let t4 = t3 * t;
let t5 = t4 * t;
let poly = 0.319_381_530 * t - 0.356_563_782 * t2 + 1.781_477_937 * t3 - 1.821_255_978 * t4
+ 1.330_274_429 * t5;
let cdf_abs = 1.0 - normal_pdf(ax) * poly;
0.5 + sign * 0.5 * (2.0 * cdf_abs - 1.0)
}
#[test]
fn the_replaced_approximation_was_wrong_in_the_tail_by_these_amounts() {
let cases = [
(4.0_f64, 3.167_124_183_311_998e-5, 4e-4),
(6.0, 9.865_876_450_376_98e-10, 3e-3),
(8.0, 6.220_960_574_271_784e-16, 7e-2),
];
for (sigma, reference, at_least) in cases {
let old = abramowitz_stegun_26_2_17(-sigma);
let old_rel = (old - reference).abs() / reference;
assert!(
old_rel > at_least,
"at {sigma}σ the A&S form is only off by {old_rel:e}; \
this test exists because it is off by more"
);
let new_rel = (normal_cdf(-sigma) - reference).abs() / reference;
assert!(
new_rel < 1e-14,
"at {sigma}σ the replacement is off by {new_rel:e}"
);
println!("{sigma}σ: A&S relative error {old_rel:e}, now {new_rel:e}");
}
}
#[test]
fn the_replaced_approximation_clamped_a_representable_tail_to_zero() {
for &x in &[-8.01_f64, -10.0, -20.0, -37.0] {
assert_eq!(
abramowitz_stegun_26_2_17(x),
0.0,
"the A&S form is supposed to clamp at {x}"
);
assert!(
normal_cdf(x) > 0.0,
"Φ({x}) is representable and must not be clamped"
);
}
let tail = 6.220_960_574_271_784e-16;
let jump = (abramowitz_stegun_26_2_17(-7.999_999_999)
- abramowitz_stegun_26_2_17(-8.000_000_001))
/ tail;
assert!(
jump > 0.9,
"the clamp should drop the whole tail, dropped {jump:e}"
);
let step = (normal_cdf(-7.999_999_999) - normal_cdf(-8.000_000_001)) / tail;
assert!(step < 1e-7, "the replacement steps by {step:e} of the tail");
println!("across the old clamp: A&S drops {jump:e} of the tail, this drops {step:e}");
}
#[test]
fn the_cdf_and_the_survival_function_are_exact_at_the_origin() {
assert_eq!(normal_cdf(0.0), 0.5);
assert_eq!(normal_sf(0.0), 0.5);
assert_eq!(normal_cdf(-0.0), 0.5);
assert_eq!(normal_sf(-0.0), 0.5);
}
#[test]
fn the_survival_function_is_the_reflected_cdf_bit_for_bit() {
let mut x = -38.5_f64;
while x <= 38.5 {
assert_eq!(
normal_sf(x).to_bits(),
normal_cdf(-x).to_bits(),
"Q({x}) and Φ({}) disagree",
-x
);
x += 0.031_25;
}
}
#[test]
fn the_cdf_and_its_reflection_sum_to_one() {
for &x in &[0.125_f64, 0.5, 1.0, 1.749, 1.75, 2.0, 3.0, 5.0, 8.0, 20.0] {
let sum = normal_cdf(x) + normal_cdf(-x);
assert!(
(sum - 1.0).abs() <= f64::EPSILON,
"Φ({x}) + Φ(-{x}) = {sum}"
);
}
}
#[test]
fn sigma_coverage_matches_the_textbook_figures() {
let coverage = [
(1.0_f64, 0.682_689_492_137_085_9),
(2.0, 0.954_499_736_103_641_6),
(3.0, 0.997_300_203_936_740_1),
];
for (k, expected) in coverage {
let p = normal_cdf(k) - normal_cdf(-k);
assert!(
(p - expected).abs() < 1e-15,
"{k}σ coverage {p}, expected {expected}"
);
}
}
#[test]
fn the_cdf_is_non_decreasing_across_the_whole_range() {
let mut previous = 0.0_f64;
for step in 0..80_000_u32 {
let x = -40.0 + f64::from(step) * 0.001;
let value = normal_cdf(x);
assert!(
value >= previous,
"Φ dropped from {previous:e} to {value:e} at x = {x}"
);
previous = value;
}
assert_eq!(previous, 1.0);
}
#[test]
fn monotonicity_between_neighbouring_doubles_has_two_answers() {
fn walk(start: f64) -> (usize, f64) {
let mut x = start;
let mut previous = normal_cdf(x);
let mut backward = 0_usize;
let mut worst = 0.0_f64;
for _ in 0..20_000 {
x = f64::from_bits(if x < 0.0 {
x.to_bits() - 1
} else {
x.to_bits() + 1
});
let value = normal_cdf(x);
if value < previous {
backward += 1;
worst = worst.max((previous - value) / previous);
}
previous = value;
}
(backward, worst)
}
const FRACTION_ALLOWANCE: usize = 20;
for &start in &[
-1.8_f64, -2.0, -3.0, -6.0, -20.0, -37.0, 1.75, 2.0, 3.0, 4.0, 6.0,
] {
let (backward, _) = walk(start);
println!("fraction branch from {start}: {backward} backward steps in 20000");
assert!(
backward <= FRACTION_ALLOWANCE,
"the fraction branch stepped backward {backward} times from {start}, \
over an allowance of {FRACTION_ALLOWANCE}"
);
}
const SERIES_TOLERANCE: f64 = 0.02;
let series = [
(-1.75_f64, 5_611_usize),
(-1.5, 4_586),
(-1.0, 2_459),
(-0.75, 1_019),
];
for (start, recorded) in series {
let (backward, worst) = walk(start);
println!("{start}: {backward} backward steps in 20000, worst {worst:e}");
let drift = (backward as f64 - recorded as f64).abs() / recorded as f64;
assert!(
drift <= SERIES_TOLERANCE,
"backward steps from {start} moved from {recorded} to {backward}, \
a drift of {drift:.4} over the {SERIES_TOLERANCE} allowed"
);
assert!(
(1_000..10_000).contains(&backward),
"backward steps from {start} came to {backward}, outside the \
documented order of magnitude"
);
assert!(
worst < 1e-14,
"a backward step of {worst:e} at {start} exceeds the accuracy bound"
);
}
}
#[test]
fn the_continued_fraction_is_converged_at_the_shipped_depth() {
let mut worst = (0.0_f64, 0.0_f64);
let mut t = SERIES_SWITCH;
while t < TAIL_UNDERFLOWS_ABOVE {
let shipped = upper_tail_cf(t, cf_depth(t));
let deeper = upper_tail_cf(t, 4 * cf_depth(t) + 40);
let rel = (shipped - deeper).abs() / deeper;
if rel > worst.0 {
worst = (rel, t);
}
t += 0.001_25;
}
assert!(
worst.0 < 1e-15,
"the shipped depth is short by {:e} at t = {}",
worst.0,
worst.1
);
println!(
"worst shipped-vs-deep disagreement {:e} at t = {}",
worst.0, worst.1
);
}
#[test]
fn the_two_branches_agree_where_they_overlap() {
let mut worst = (0.0_f64, 0.0_f64);
let mut t = 0.4_f64;
while t < SERIES_SWITCH {
let series = 0.5 - t * normal_pdf(t) * erf_series_sum(t);
let fraction = upper_tail_cf(t, 4_000);
let rel = (series - fraction).abs() / fraction;
if rel > worst.0 {
worst = (rel, t);
}
t += 0.000_5;
}
assert!(
worst.0 < 1e-14,
"the branches part company by {:e} at t = {}",
worst.0,
worst.1
);
println!(
"worst branch disagreement {:e} at t = {} (a floor, not a bound)",
worst.0, worst.1
);
}
#[test]
fn every_reachable_split_head_squares_exactly() {
let granularity = 64_u64;
let highest = (TAIL_UNDERFLOWS_ABOVE as u64) * granularity;
for k in 1..=highest {
let head = k as f64 / granularity as f64;
let exact = (k * k) as f64 / (granularity * granularity) as f64;
assert_eq!(
head * head,
exact,
"head = {head} squares inexactly at k = {k}"
);
assert_eq!(-0.5 * head * head, -(exact / 2.0));
}
for &x in &[2.000_1_f64, 7.3, 19.999, 33.953_051_231_955_506, 38.9] {
let head = (x * 64.0).trunc() / 64.0;
assert_eq!(head * 64.0, (head * 64.0).trunc());
assert!(x - head < 1.0 / 64.0);
}
}
#[test]
fn the_series_and_the_continued_fraction_agree_at_their_seam() {
let series_side =
0.5 - SERIES_SWITCH * normal_pdf(SERIES_SWITCH) * erf_series_sum(SERIES_SWITCH);
let fraction_side = upper_tail_cf(SERIES_SWITCH, cf_depth(SERIES_SWITCH));
let rel = (series_side - fraction_side).abs() / fraction_side;
assert!(rel < 1e-14, "the branches disagree by {rel:e} at the seam");
}
#[test]
fn the_tail_runs_to_the_edge_of_the_representable_range() {
let deep = [
(10.0_f64, 7.619_853_024_160_525e-24),
(20.0, 2.753_624_118_606_233_7e-89),
(30.0, 4.906_713_927_148_187e-198),
(37.0, 5.725_571_222_524_577e-300),
(38.0, 2.885_428_35e-316),
];
for (x, expected) in deep {
let got = normal_sf(x);
let rel = (got - expected).abs() / expected;
let bound = if x >= 37.5 { 1e-7 } else { 1e-14 };
assert!(rel < bound, "Q({x}) = {got:e}, expected {expected:e}");
}
assert!(normal_sf(38.48) > 0.0);
assert_eq!(normal_sf(38.6), 0.0);
assert_eq!(normal_sf(f64::INFINITY), 0.0);
assert_eq!(normal_cdf(f64::NEG_INFINITY), 0.0);
assert_eq!(normal_cdf(f64::INFINITY), 1.0);
assert_eq!(normal_sf(f64::NEG_INFINITY), 1.0);
}
#[test]
fn non_numbers_propagate() {
assert!(normal_cdf(f64::NAN).is_nan());
assert!(normal_sf(f64::NAN).is_nan());
}
#[test]
fn the_survival_function_outlives_the_complement_of_the_cdf() {
assert_eq!(1.0 - normal_cdf(9.0), 0.0);
assert!((normal_sf(9.0) / 1.128_588_405_953_840_5e-19 - 1.0).abs() < 1e-14);
}
}