pub fn erf(x: f64) -> f64 {
if x.is_nan() {
return f64::NAN;
}
if x < 0.0 {
-(1.0 - erfc(-x))
} else {
1.0 - erfc(x)
}
}
pub fn erfc(x: f64) -> f64 {
if x < 0.0 {
return 2.0 - erfc(-x);
}
let z = x.abs();
let t = 2.0 / (2.0 + z);
let ty = 4.0 * t - 2.0;
const C: [f64; 28] = [
-1.3026537197817094, 6.419_697_923_564_902e-1, 1.9476473204185836e-2,
-9.561_514_786_808_63e-3, -9.46595344482036e-4, 3.66839497852761e-4,
4.2523324806907e-5, -2.0278578112534e-5, -1.624290004647e-6,
1.303655835580e-6, 1.5626441722e-8, -8.5238095915e-8,
6.529054439e-9, 5.059343495e-9, -9.91364156e-10,
-2.27365122e-10, 9.6467911e-11, 2.394038e-12,
-6.886027e-12, 8.94487e-13, 3.13092e-13,
-1.12708e-13, 3.81e-16, 7.106e-15,
-1.523e-15, -9.4e-17, 1.21e-16, -2.8e-17,
];
let mut d = 0.0f64;
let mut dd = 0.0f64;
for j in (1..C.len()).rev() {
let tmp = d;
d = ty * d - dd + C[j];
dd = tmp;
}
t * (-z * z + 0.5 * (C[0] + ty * d) - dd).exp()
}
pub fn normal_cdf(z: f64) -> f64 {
0.5 * erfc(-z / std::f64::consts::SQRT_2)
}
pub fn ln_gamma(x: f64) -> f64 {
if !x.is_finite() || x <= 0.0 {
return f64::NAN;
}
const G: f64 = 7.0;
const COEF: [f64; 9] = [
0.999_999_999_999_809_9,
676.520_368_121_885_1,
-1_259.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,
];
if x < 0.5 {
let pi = std::f64::consts::PI;
return (pi / (pi * x).sin()).ln() - ln_gamma(1.0 - x);
}
let x = x - 1.0;
let mut acc = COEF[0];
for (i, c) in COEF.iter().enumerate().skip(1) {
acc += c / (x + i as f64);
}
let t = x + G + 0.5;
0.5 * (2.0 * std::f64::consts::PI).ln() + (x + 0.5) * t.ln() - t + acc.ln()
}
pub fn ln_beta(a: f64, b: f64) -> f64 {
ln_gamma(a) + ln_gamma(b) - ln_gamma(a + b)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn erf_matches_published_reference_values() {
for (x, want) in [(0.0, 0.0), (0.5, 0.5204998778), (1.0, 0.8427007929),
(2.0, 0.9953222650), (3.0, 0.9999779095)] {
assert!((erf(x) - want).abs() < 1e-9, "erf({x}) = {} want {want}", erf(x));
}
}
#[test]
fn erf_is_odd_and_bounded() {
for x in [0.3, 1.7, 4.2, 9.0] {
assert!((erf(-x) + erf(x)).abs() < 1e-12);
assert!(erf(x) <= 1.0 && erf(x) >= -1.0);
}
}
#[test]
fn normal_cdf_hits_known_points() {
assert!((normal_cdf(0.0) - 0.5).abs() < 1e-12);
assert!((normal_cdf(1.959_963_984_540_054) - 0.975).abs() < 1e-9);
assert!((normal_cdf(-6.0)).abs() < 1e-8);
}
#[test]
fn erf_matches_libm_cross_check_over_a_range() {
let cases = [
(0.0, 0.0), (0.25, 0.2763263901682369), (0.5, 0.5204998778130465),
(0.75, 0.7111556336535152), (1.0, 0.8427007929497148), (1.25, 0.9229001282564582),
(1.5, 0.9661051464753108), (1.75, 0.9866716712191824), (2.0, 0.9953222650189527),
(2.25, 0.9985372834133188), (2.5, 0.999593047982555), (2.75, 0.9998993780778804),
(3.0, 0.9999779095030015), (3.25, 0.9999956972205364), (3.5, 0.9999992569016276),
(3.75, 0.9999998862727435), (4.0, 0.9999999845827421), (4.25, 0.9999999981494259),
(4.5, 0.9999999998033839), (4.75, 0.999999999981515), (5.0, 0.9999999999984626),
(5.25, 0.999999999999887), (5.5, 0.9999999999999927), (5.75, 0.9999999999999996),
(6.0, 1.0),
];
let mut worst: f64 = 0.0;
for (x, want) in cases {
let got = erf(x);
let diff = (got - want).abs();
worst = worst.max(diff);
assert!(diff < 1e-12, "erf({x}) = {got:.17e} want {want:.17e} diff {diff:.3e}");
}
assert!(worst < 1e-12, "worst diff over the sweep was {worst:.3e}");
}
#[test]
fn normal_cdf_tail_is_real_not_flushed_to_zero() {
let far_tail = normal_cdf(-6.0);
assert!(far_tail > 0.0, "normal_cdf(-6.0) must be a real positive number, got {far_tail}");
assert_ne!(far_tail, 0.0, "tail was flushed to exactly 0.0");
assert!(
(far_tail - 9.865876450377014e-10).abs() < 1e-16,
"normal_cdf(-6.0) = {far_tail:.17e}, want ~9.865876450377014e-10"
);
let mut prev = erfc(0.0);
assert!((prev - 1.0).abs() < 1e-14, "erfc(0.0) = {prev:.17}, want ~1.0");
for i in 1..=32 {
let x = i as f64 * 0.25;
let v = erfc(x);
assert!(v > 0.0, "erfc({x}) = {v} is not strictly positive");
assert!(v < prev, "erfc not strictly decreasing at x={x}: prev={prev:e} v={v:e}");
prev = v;
}
}
#[test]
fn erf_and_normal_cdf_are_monotonic_and_bounded_over_a_sweep() {
let xs = [
-5.0, -4.0, -3.0, -2.5, -2.0, -1.5, -1.0, -0.75, -0.5, -0.25, -0.1,
0.0,
0.1, 0.25, 0.5, 0.75, 1.0, 1.5, 2.0, 2.5, 3.0, 4.0, 5.0,
];
let mut prev_erf = f64::NEG_INFINITY;
let mut prev_cdf = f64::NEG_INFINITY;
for x in xs {
let e = erf(x);
let c = normal_cdf(x);
assert!((-1.0..=1.0).contains(&e), "erf({x}) = {e} out of [-1, 1]");
assert!((0.0..=1.0).contains(&c), "normal_cdf({x}) = {c} out of [0, 1]");
assert!(e > prev_erf, "erf not strictly increasing at x={x}: prev={prev_erf} now={e}");
assert!(c > prev_cdf, "normal_cdf not strictly increasing at x={x}: prev={prev_cdf} now={c}");
prev_erf = e;
prev_cdf = c;
}
for x in [10.0, 20.0, 50.0, 1.0e10] {
let e = erf(x);
let ne = erf(-x);
let c = normal_cdf(x);
let nc = normal_cdf(-x);
assert_eq!(e, 1.0, "erf({x}) should have saturated to exactly 1.0");
assert_eq!(ne, -1.0, "erf(-{x}) should have saturated to exactly -1.0");
assert_eq!(c, 1.0, "normal_cdf({x}) should have saturated to exactly 1.0");
assert!((0.0..=1.0).contains(&nc), "normal_cdf(-{x}) = {nc} out of [0, 1]");
assert!(!nc.is_nan());
}
}
#[test]
fn normal_cdf_symmetry_around_zero() {
for z in [0.0, 0.05, 0.1, 0.5, 1.0, 1.959_963_984_540_054, 2.0, 3.0, 4.5, 6.0, 100.0] {
let sum = normal_cdf(z) + normal_cdf(-z);
assert!(
(sum - 1.0).abs() < 1e-12,
"normal_cdf({z}) + normal_cdf(-{z}) = {sum:.17}, want 1.0"
);
}
}
#[test]
fn non_finite_inputs_are_well_defined() {
assert_eq!(erf(f64::INFINITY), 1.0);
assert_eq!(erf(f64::NEG_INFINITY), -1.0);
assert!(erf(f64::NAN).is_nan());
assert_eq!(erfc(f64::INFINITY), 0.0);
assert_eq!(erfc(f64::NEG_INFINITY), 2.0);
assert!(erfc(f64::NAN).is_nan());
assert_eq!(normal_cdf(f64::INFINITY), 1.0);
assert_eq!(normal_cdf(f64::NEG_INFINITY), 0.0);
assert!(normal_cdf(f64::NAN).is_nan());
}
#[test]
fn ln_gamma_matches_published_reference_values() {
let cases = [
(0.5_f64, 0.572_364_942_924_700_1_f64), (1.0, 0.0),
(1.5, -0.120_782_237_635_245_22),
(2.0, 0.0),
(3.0, std::f64::consts::LN_2), (10.0, 12.801_827_480_081_469), (100.0, 359.134_205_369_575_4), (0.1, 2.252_712_651_734_206), ];
for (x, expected) in cases {
let got = ln_gamma(x);
assert!(
(got - expected).abs() < 1e-12,
"ln_gamma({x}) = {got}, expected {expected}"
);
}
}
#[test]
fn ln_gamma_rejects_nonpositive_and_nonfinite() {
assert!(ln_gamma(0.0).is_nan());
assert!(ln_gamma(-1.0).is_nan());
assert!(ln_gamma(f64::NAN).is_nan());
assert!(ln_gamma(f64::INFINITY).is_nan());
}
#[test]
fn ln_gamma_satisfies_the_recurrence_identity() {
for x in [0.3_f64, 0.7, 1.2, 4.5, 25.0, 170.0] {
let lhs = ln_gamma(x + 1.0);
let rhs = ln_gamma(x) + x.ln();
assert!((lhs - rhs).abs() < 1e-10, "recurrence failed at x={x}: {lhs} vs {rhs}");
}
}
#[test]
fn ln_beta_is_the_gamma_combination_and_is_symmetric() {
assert!((ln_beta(1.0, 1.0) - 0.0).abs() < 1e-14);
assert!((ln_beta(2.0, 3.0) - (-(12.0_f64).ln())).abs() < 1e-12);
assert!((ln_beta(7.3, 0.4) - ln_beta(0.4, 7.3)).abs() < 1e-13);
assert!(ln_beta(0.0, 1.0).is_nan());
}
}