#![allow(clippy::excessive_precision)]
use crate::errors::QlResult;
use crate::fail;
use crate::math::errorfunction::erf;
use crate::types::Real;
const M_SQRT_2: Real = std::f64::consts::FRAC_1_SQRT_2;
const M_1_SQRTPI: Real = 0.564189583547756286948;
fn validate_params(average: Real, sigma: Real) -> QlResult<()> {
if !average.is_finite() {
fail!("average must be a finite number ({average} given)");
}
if !sigma.is_finite() || sigma <= 0.0 {
fail!("sigma must be a finite positive number ({sigma} given)");
}
Ok(())
}
#[derive(Clone, Copy, Debug)]
pub struct NormalDistribution {
average: Real,
normalization_factor: Real,
denominator: Real,
der_normalization_factor: Real,
}
impl NormalDistribution {
pub fn new(average: Real, sigma: Real) -> QlResult<Self> {
validate_params(average, sigma)?;
let normalization_factor = M_SQRT_2 * M_1_SQRTPI / sigma;
let der_normalization_factor = sigma * sigma;
Ok(NormalDistribution {
average,
normalization_factor,
denominator: 2.0 * der_normalization_factor,
der_normalization_factor,
})
}
pub fn standard() -> Self {
NormalDistribution::new(0.0, 1.0).expect("standard normal (0, 1) is valid")
}
pub fn value(&self, x: Real) -> Real {
let deltax = x - self.average;
let exponent = -(deltax * deltax) / self.denominator;
if exponent <= -690.0 {
0.0
} else {
self.normalization_factor * exponent.exp()
}
}
pub fn derivative(&self, x: Real) -> Real {
(self.value(x) * (self.average - x)) / self.der_normalization_factor
}
}
impl Default for NormalDistribution {
fn default() -> Self {
NormalDistribution::standard()
}
}
#[derive(Clone, Copy, Debug)]
pub struct CumulativeNormalDistribution {
average: Real,
sigma: Real,
gaussian: NormalDistribution,
}
impl CumulativeNormalDistribution {
pub fn new(average: Real, sigma: Real) -> QlResult<Self> {
validate_params(average, sigma)?;
Ok(CumulativeNormalDistribution {
average,
sigma,
gaussian: NormalDistribution::standard(),
})
}
pub fn standard() -> Self {
CumulativeNormalDistribution {
average: 0.0,
sigma: 1.0,
gaussian: NormalDistribution::standard(),
}
}
pub fn value(&self, x: Real) -> Real {
let z = (x - self.average) / self.sigma;
let result = 0.5 * (1.0 + erf(z * M_SQRT_2));
if result <= 1e-8 {
let zsqr = z * z;
let mut sum = 1.0;
let mut i = 1.0;
let mut g = 1.0;
let mut a = Real::MAX;
loop {
let lasta = a;
let x = (4.0 * i - 3.0) / zsqr;
let y = x * ((4.0 * i - 1.0) / zsqr);
a = g * (x - y);
sum -= a;
g *= y;
i += 1.0;
a = a.abs();
if !(lasta > a && a >= (sum * Real::EPSILON).abs()) {
break;
}
}
-self.gaussian.value(z) / z * sum
} else {
result
}
}
pub fn derivative(&self, x: Real) -> Real {
let xn = (x - self.average) / self.sigma;
self.gaussian.value(xn) / self.sigma
}
}
const A1: Real = -3.969683028665376e+01;
const A2: Real = 2.209460984245205e+02;
const A3: Real = -2.759285104469687e+02;
const A4: Real = 1.383577518672690e+02;
const A5: Real = -3.066479806614716e+01;
const A6: Real = 2.506628277459239e+00;
const B1: Real = -5.447609879822406e+01;
const B2: Real = 1.615858368580409e+02;
const B3: Real = -1.556989798598866e+02;
const B4: Real = 6.680131188771972e+01;
const B5: Real = -1.328068155288572e+01;
const C1: Real = -7.784894002430293e-03;
const C2: Real = -3.223964580411365e-01;
const C3: Real = -2.400758277161838e+00;
const C4: Real = -2.549732539343734e+00;
const C5: Real = 4.374664141464968e+00;
const C6: Real = 2.938163982698783e+00;
const D1: Real = 7.784695709041462e-03;
const D2: Real = 3.224671290700398e-01;
const D3: Real = 2.445134137142996e+00;
const D4: Real = 3.754408661907416e+00;
const X_LOW: Real = 0.02425;
const X_HIGH: Real = 1.0 - X_LOW;
#[derive(Clone, Copy, Debug)]
pub struct InverseCumulativeNormal {
average: Real,
sigma: Real,
}
impl InverseCumulativeNormal {
pub fn new(average: Real, sigma: Real) -> QlResult<Self> {
validate_params(average, sigma)?;
Ok(InverseCumulativeNormal { average, sigma })
}
pub fn standard() -> Self {
InverseCumulativeNormal {
average: 0.0,
sigma: 1.0,
}
}
pub fn value(&self, x: Real) -> QlResult<Real> {
Ok(self.average + self.sigma * Self::standard_value(x)?)
}
pub fn standard_value(x: Real) -> QlResult<Real> {
if !(X_LOW..=X_HIGH).contains(&x) {
Self::tail_value(x)
} else {
let z = x - 0.5;
let r = z * z;
Ok(
(((((A1 * r + A2) * r + A3) * r + A4) * r + A5) * r + A6) * z
/ (((((B1 * r + B2) * r + B3) * r + B4) * r + B5) * r + 1.0),
)
}
}
fn tail_value(x: Real) -> QlResult<Real> {
if !x.is_finite() || x <= 0.0 || x >= 1.0 {
if (x - 1.0).abs() <= 42.0 * Real::EPSILON {
return Ok(Real::MAX);
} else if x.abs() < Real::EPSILON {
return Ok(Real::MIN);
}
fail!("InverseCumulativeNormal({x}) undefined: must be 0 < x < 1");
}
Ok(if x < X_LOW {
let z = (-2.0 * x.ln()).sqrt();
(((((C1 * z + C2) * z + C3) * z + C4) * z + C5) * z + C6)
/ ((((D1 * z + D2) * z + D3) * z + D4) * z + 1.0)
} else {
let z = (-2.0 * (1.0 - x).ln()).sqrt();
-(((((C1 * z + C2) * z + C3) * z + C4) * z + C5) * z + C6)
/ ((((D1 * z + D2) * z + D3) * z + D4) * z + 1.0)
})
}
}
#[cfg(test)]
mod tests {
use super::*;
const TOL: Real = 1e-15;
#[test]
fn density_matches_analytic_gaussian() {
let n = NormalDistribution::standard();
assert!((n.value(0.0) - 0.398_942_280_401_432_7).abs() < TOL);
assert!((n.value(1.0) - 0.241_970_724_519_143_37).abs() < TOL);
assert!((n.value(-2.3) - n.value(2.3)).abs() < TOL);
}
#[test]
fn cdf_known_values() {
let c = CumulativeNormalDistribution::standard();
assert!((c.value(0.0) - 0.5).abs() < TOL);
assert!((c.value(1.0) - 0.841_344_746_068_542_9).abs() < TOL);
assert!((c.value(-1.0) - 0.158_655_253_931_457_07).abs() < TOL);
assert!((c.value(1.96) - 0.975_002_104_851_779_5).abs() < TOL);
}
#[test]
fn cdf_asymptotic_tail_for_very_negative() {
let c = CumulativeNormalDistribution::standard();
let phi_m6 = 9.865_876_450_376_946e-10;
assert!(((c.value(-6.0) - phi_m6) / phi_m6).abs() < 1e-7);
}
#[test]
fn cdf_derivative_is_density() {
let c = CumulativeNormalDistribution::new(0.3, 1.7).unwrap();
let n = NormalDistribution::new(0.3, 1.7).unwrap();
for &x in &[-2.0, -0.5, 0.0, 1.1, 3.0] {
assert!((c.derivative(x) - n.value(x)).abs() < TOL);
}
}
#[test]
fn inverse_of_one_half_is_zero() {
let inv = InverseCumulativeNormal::standard();
assert_eq!(inv.value(0.5).unwrap(), 0.0);
}
#[test]
fn inverse_round_trips_with_cdf() {
let cum = CumulativeNormalDistribution::new(0.1, 1.3).unwrap();
let inv = InverseCumulativeNormal::new(0.1, 1.3).unwrap();
let (lo, hi) = (0.1 - 6.0 * 1.3, 0.1 + 6.0 * 1.3);
let n = 2000;
let mut max_err: Real = 0.0;
for i in 0..=n {
let x = lo + (hi - lo) * (i as Real) / (n as Real);
let round = inv.value(cum.value(x)).unwrap();
max_err = max_err.max((round - x).abs());
}
assert!(max_err < 1e-7, "round-trip error {max_err} exceeds 1e-7");
}
#[test]
fn invalid_sigma_is_rejected() {
for s in [0.0, -1.0, Real::NAN, Real::INFINITY, Real::NEG_INFINITY] {
assert!(
NormalDistribution::new(0.0, s).is_err(),
"density sigma={s}"
);
assert!(
CumulativeNormalDistribution::new(0.0, s).is_err(),
"cumulative sigma={s}"
);
assert!(
InverseCumulativeNormal::new(0.0, s).is_err(),
"inverse sigma={s}"
);
}
}
#[test]
fn non_finite_average_is_rejected() {
for a in [Real::NAN, Real::INFINITY, Real::NEG_INFINITY] {
assert!(
NormalDistribution::new(a, 1.0).is_err(),
"density average={a}"
);
assert!(
CumulativeNormalDistribution::new(a, 1.0).is_err(),
"cumulative average={a}"
);
assert!(
InverseCumulativeNormal::new(a, 1.0).is_err(),
"inverse average={a}"
);
}
}
#[test]
fn inverse_outside_unit_interval_errors() {
assert!(InverseCumulativeNormal::standard_value(-0.1).is_err());
assert!(InverseCumulativeNormal::standard_value(1.5).is_err());
assert!(InverseCumulativeNormal::standard_value(Real::NAN).is_err());
assert!(InverseCumulativeNormal::standard_value(Real::INFINITY).is_err());
}
}