use crate::ad::{dual::DualFwd, expr::FloatExt, scalar::Scalar};
const SQRT_2PI: f64 = 2.506_628_274_631_000_5;
const HART_THRESHOLD: f64 = 7.071_067_811_865_475;
#[must_use]
pub fn norm_cdf<T: NormCDF>(x: T) -> T {
x.norm_cdf()
}
pub trait NormCDF: Scalar + Clone {
#[must_use]
fn norm_cdf(self) -> Self;
}
impl NormCDF for f64 {
fn norm_cdf(self) -> Self {
let z = self.abs();
let cum = if z > 37.0 {
0.0
} else {
let e = (-z * z * 0.5).exp();
if z < HART_THRESHOLD {
let num = 3.526_249_659_989_11e-2_f64
.mul_add(z, 0.700_383_064_443_688)
.mul_add(z, 6.373_962_203_531_65)
.mul_add(z, 33.912_866_078_383)
.mul_add(z, 112.079_291_497_871)
.mul_add(z, 221.213_596_169_931)
.mul_add(z, 220.206_867_912_376);
let den = 8.838_834_764_831_84e-2_f64
.mul_add(z, 1.755_667_163_182_64)
.mul_add(z, 16.064_177_579_207)
.mul_add(z, 86.780_732_202_946_1)
.mul_add(z, 296.564_248_779_674)
.mul_add(z, 637.333_633_378_831)
.mul_add(z, 793.826_512_519_948)
.mul_add(z, 440.413_735_824_752);
e * num / den
} else {
let b = z + 0.65;
let b = z + 4.0 / b;
let b = z + 3.0 / b;
let b = z + 2.0 / b;
let b = z + 1.0 / b;
e / (b * SQRT_2PI)
}
};
if self > 0.0 {
1.0 - cum
} else {
cum
}
}
}
impl NormCDF for DualFwd {
fn norm_cdf(self) -> Self {
let one: Self = 1.0.into();
let z: Self = self.abs();
let zv = z.value();
if zv > 37.0 {
return if self.value() > 0.0 {
1.0.into()
} else {
0.0.into()
};
}
let e: Self = ((-(z * z)) * 0.5).exp().into();
let cum: Self = if zv < HART_THRESHOLD {
let num: Self = (z * 3.526_249_659_989_11e-2 + 0.700_383_064_443_688).into();
let num: Self = (num * z + 6.373_962_203_531_65).into();
let num: Self = (num * z + 33.912_866_078_383).into();
let num: Self = (num * z + 112.079_291_497_871).into();
let num: Self = (num * z + 221.213_596_169_931).into();
let num: Self = (num * z + 220.206_867_912_376).into();
let den: Self = (z * 8.838_834_764_831_84e-2 + 1.755_667_163_182_64).into();
let den: Self = (den * z + 16.064_177_579_207).into();
let den: Self = (den * z + 86.780_732_202_946_1).into();
let den: Self = (den * z + 296.564_248_779_674).into();
let den: Self = (den * z + 637.333_633_378_831).into();
let den: Self = (den * z + 793.826_512_519_948).into();
let den: Self = (den * z + 440.413_735_824_752).into();
(e * num / den).into()
} else {
let b: Self = (z + 0.65).into();
let b: Self = (z + (one * 4.0) / b).into();
let b: Self = (z + (one * 3.0) / b).into();
let b: Self = (z + (one * 2.0) / b).into();
let b: Self = (z + one / b).into();
(e / (b * SQRT_2PI)).into()
};
if self.value() > 0.0 {
(one - cum).into()
} else {
cum
}
}
}
#[cfg(test)]
mod tests {
use super::*;
const REFERENCES: [(f64, f64); 7] = [
(0.0, 0.5),
(0.5, 0.691_462_461_274_013),
(1.0, 0.841_344_746_068_542_9),
(2.0, 0.977_249_868_051_820_8),
(-1.0, 0.158_655_253_931_457_05),
(-5.0, 2.866_515_718_791_945e-7),
(-10.0, 7.619_853_024_160_593e-24),
];
#[test]
fn matches_reference_values_including_deep_tails() {
for (x, expected) in REFERENCES {
let computed = x.norm_cdf();
let ok = (computed - expected).abs() < 1e-15
|| (computed - expected).abs() / expected < 1e-8;
assert!(
ok,
"norm_cdf({x}) = {computed}, expected {expected}"
);
}
}
#[test]
fn is_symmetric_and_monotone() {
let mut prev = 0.0;
for i in -400..=400 {
let x = f64::from(i) * 0.05;
let cdf = x.norm_cdf();
assert!(
(cdf + (-x).norm_cdf() - 1.0).abs() < 1e-15,
"symmetry at {x}"
);
assert!(cdf >= prev, "monotonicity at {x}");
prev = cdf;
}
assert!((37.5_f64).norm_cdf() == 1.0);
assert!((-37.5_f64).norm_cdf() == 0.0);
}
#[test]
fn dual_fwd_value_matches_f64() {
for x in [-10.0, -5.0, -1.0, 0.0, 0.5, 2.0, 8.0, 20.0] {
let dual: DualFwd = DualFwd::from(x).norm_cdf();
let plain = x.norm_cdf();
assert!(
(dual.value() - plain).abs() <= plain.abs().mul_add(1e-14, 1e-300),
"DualFwd norm_cdf({x}) = {} but f64 gives {plain}",
dual.value()
);
}
}
}