use std::error::Error as StdError;
use std::fmt;
use std::ops::Bound;
use super::sum::exact_sum;
use crate::error::{Interval, ValueError};
use crate::numeric;
const INVERSE_SQRT_TWO_PI: f64 = 0.398_942_280_401_432_7;
const SQRT_TWO_PI: f64 = 2.506_628_274_631_000_7;
const LOG_SQRT_TWO_PI: f64 = 0.918_938_533_204_672_8;
const CENTER_LIMIT: f64 = 1.5;
const CONTINUED_FRACTION_DEPTH: u32 = 256;
const CENTER_ITERATION_LIMIT: u32 = 128;
const INVERSE_ITERATION_LIMIT: u32 = 256;
const UNIT_ROUNDOFF: f64 = f64::EPSILON / 2.0;
const QUARTER_OVERFLOW_OFFSET: f64 = f64::from_bits((1023 + 968) << 52);
const PROBABILITY_DOMAIN: Interval = Interval::new(Bound::Excluded(0.0), Bound::Excluded(1.0));
const PROBABILITY_RESULT: Interval = Interval::new(Bound::Included(0.0), Bound::Included(1.0));
const DENSITY_RESULT: Interval = Interval::new(Bound::Included(0.0), Bound::Unbounded);
const LOG_PROBABILITY_RESULT: Interval = Interval::new(Bound::Unbounded, Bound::Included(0.0));
pub fn standard_normal_pdf(x: f64) -> Result<f64, NormalError> {
let x = numeric::finite("normal variate", x)?;
Ok(numeric::result_within(
"normal density",
standard_density(x),
DENSITY_RESULT,
)?)
}
pub fn standard_normal_cdf(x: f64) -> Result<f64, NormalError> {
let x = numeric::finite("normal variate", x)?;
cumulative_probability(x)
}
pub fn standard_normal_survival(x: f64) -> Result<f64, NormalError> {
let x = numeric::finite("normal variate", x)?;
cumulative_probability(-x)
}
pub fn standard_normal_log_cdf(x: f64) -> Result<f64, NormalError> {
let x = numeric::finite("normal variate", x)?;
cumulative_logarithm(x)
}
pub fn standard_normal_log_survival(x: f64) -> Result<f64, NormalError> {
let x = numeric::finite("normal variate", x)?;
cumulative_logarithm(-x)
}
#[expect(
clippy::float_cmp,
reason = "the exactly supplied median is an identity"
)]
pub fn standard_normal_inverse_cdf(probability: f64) -> Result<f64, NormalError> {
let probability = numeric::within("normal probability", probability, PROBABILITY_DOMAIN)?;
if probability == 0.5 {
return Ok(0.0);
}
let below_median = probability < 0.5;
let magnitude = if (0.25..=0.75).contains(&probability) {
inverse_center((probability - 0.5).abs())?
} else {
let tail = if below_median {
probability
} else {
1.0 - probability
};
inverse_tail(tail.ln())?
};
Ok(numeric::finite_result(
"normal quantile",
if below_median { -magnitude } else { magnitude },
)?)
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct NormalDistribution {
mean: f64,
standard_deviation: f64,
}
impl NormalDistribution {
pub fn new(mean: f64, standard_deviation: f64) -> Result<Self, NormalError> {
let mean = numeric::finite("normal mean", mean)?;
let standard_deviation = numeric::within(
"normal standard deviation",
standard_deviation,
Interval::greater_than(0.0),
)?;
Ok(Self {
mean: canonical_zero(mean),
standard_deviation,
})
}
#[must_use]
pub const fn mean(self) -> f64 {
self.mean
}
#[must_use]
pub const fn standard_deviation(self) -> f64 {
self.standard_deviation
}
pub fn pdf(self, x: f64) -> Result<f64, NormalError> {
let x = numeric::finite("normal variate", x)?;
let z = standardized(x, self.mean, self.standard_deviation);
let square_term = (-0.5 * z) * z;
if square_term.is_infinite() {
return Ok(0.0);
}
let log_scale = self.standard_deviation.ln();
let logarithm = (square_term - log_scale) - LOG_SQRT_TWO_PI;
let uncertainty_scale = 1.0 + square_term.abs() + log_scale.abs() + LOG_SQRT_TWO_PI;
if (logarithm - f64::MAX.ln()).abs() <= 8.0 * UNIT_ROUNDOFF * uncertainty_scale {
return Err(NormalError::NumericalBoundary {
quantity: "normal density",
logarithm,
});
}
Ok(canonical_zero(numeric::result_within(
"normal density",
logarithm.exp(),
DENSITY_RESULT,
)?))
}
pub fn cdf(self, x: f64) -> Result<f64, NormalError> {
let x = numeric::finite("normal variate", x)?;
cumulative_probability(standardized(x, self.mean, self.standard_deviation))
}
pub fn survival(self, x: f64) -> Result<f64, NormalError> {
let x = numeric::finite("normal variate", x)?;
cumulative_probability(-standardized(x, self.mean, self.standard_deviation))
}
pub fn log_cdf(self, x: f64) -> Result<f64, NormalError> {
let x = numeric::finite("normal variate", x)?;
cumulative_logarithm(standardized(x, self.mean, self.standard_deviation))
}
pub fn log_survival(self, x: f64) -> Result<f64, NormalError> {
let x = numeric::finite("normal variate", x)?;
cumulative_logarithm(-standardized(x, self.mean, self.standard_deviation))
}
pub fn inverse_cdf(self, probability: f64) -> Result<f64, NormalError> {
let z = standard_normal_inverse_cdf(probability)?;
if z == 0.0 {
return Ok(self.mean);
}
let value = self.standard_deviation.mul_add(z, self.mean);
guard_affine_quantile(probability, z, self.mean, self.standard_deviation, value)?;
Ok(canonical_zero(numeric::finite_result(
"normal quantile",
value,
)?))
}
}
fn canonical_zero(value: f64) -> f64 {
if value == 0.0 { 0.0 } else { value }
}
fn guard_affine_quantile(
probability: f64,
z: f64,
mean: f64,
scale: f64,
value: f64,
) -> Result<(), NormalError> {
let quarter_z = z * 0.25;
let product = scale * quarter_z;
if !product.is_finite() {
return Ok(());
}
let residual = scale.mul_add(quarter_z, -product);
let sign = if value.is_sign_negative() { -1.0 } else { 1.0 };
let gap = exact_sum(&[
product,
residual,
mean * 0.25,
-sign * (f64::MAX * 0.25),
-sign * QUARTER_OVERFLOW_OFFSET,
]);
let Ok(gap) = gap else {
return Ok(());
};
let magnitude = z.abs();
let standard_allowance = if (0.25..=0.75).contains(&probability) {
16.0 * UNIT_ROUNDOFF * magnitude
} else {
let log_tail = probability.min(1.0 - probability).ln().abs();
8.0 * UNIT_ROUNDOFF * (1.0 + magnitude + log_tail / magnitude)
};
let allowance = (scale * 0.25) * standard_allowance
+ 2.0 * UNIT_ROUNDOFF * gap.abs()
+ 2.0 * f64::from_bits(1);
if gap.abs() <= allowance {
let quarter_value = scale.mul_add(quarter_z, mean * 0.25);
return Err(NormalError::NumericalBoundary {
quantity: "normal quantile",
logarithm: quarter_value.abs().ln() + std::f64::consts::LN_2 * 2.0,
});
}
Ok(())
}
fn standardized(x: f64, mean: f64, scale: f64) -> f64 {
let difference = x - mean;
if difference.is_finite() {
difference / scale
} else {
(x / scale) - (mean / scale)
}
}
fn standard_density(x: f64) -> f64 {
((-0.5 * x) * x).exp() * INVERSE_SQRT_TWO_PI
}
fn centered_probability(x: f64) -> Result<f64, NormalError> {
if x == 0.0 {
return Ok(0.0);
}
let square = x * x;
let mut term = x;
let mut sum = x;
let mut correction = 0.0;
for iteration in 1..=CENTER_ITERATION_LIMIT {
let divisor = f64::from(2 * iteration + 1);
term *= square / divisor;
let next = sum + term;
correction += if sum.abs() >= term.abs() {
(sum - next) + term
} else {
(term - next) + sum
};
sum = next;
let remaining_ratio = square / f64::from(2 * iteration + 3);
let remaining_bound = term * (remaining_ratio / (1.0 - remaining_ratio));
if remaining_ratio < 1.0 && remaining_bound <= (UNIT_ROUNDOFF / 8.0) * (sum + correction) {
return Ok(standard_density(x) * (sum + correction));
}
}
Err(NormalError::Convergence {
quantity: "normal centered probability",
iterations: CENTER_ITERATION_LIMIT,
})
}
fn mills_ratio(x: f64) -> f64 {
let mut denominator_tail = 0.0;
for numerator in (1..=CONTINUED_FRACTION_DEPTH).rev() {
denominator_tail = f64::from(numerator) / (x + denominator_tail);
}
1.0 / (x + denominator_tail)
}
fn smaller_tail(x: f64) -> Result<f64, NormalError> {
if x < CENTER_LIMIT {
Ok(0.5 - centered_probability(x)?)
} else if x >= 40.0 {
Ok(0.0)
} else {
Ok(standard_density(x) * mills_ratio(x))
}
}
fn cumulative_probability(x: f64) -> Result<f64, NormalError> {
let tail = smaller_tail(x.abs())?;
Ok(numeric::result_within(
"normal probability",
if x <= 0.0 { tail } else { 1.0 - tail },
PROBABILITY_RESULT,
)?)
}
fn smaller_log_tail(x: f64) -> Result<f64, NormalError> {
if x < CENTER_LIMIT {
Ok((0.5 - centered_probability(x)?).ln())
} else {
let logarithm = (((-0.5 * x) * x) - LOG_SQRT_TWO_PI) + mills_ratio(x).ln();
Ok(numeric::finite_result(
"normal logarithmic tail",
logarithm,
)?)
}
}
fn cumulative_logarithm(x: f64) -> Result<f64, NormalError> {
let logarithm = if x <= 0.0 {
smaller_log_tail(x.abs())?
} else {
(-smaller_tail(x)?).ln_1p()
};
Ok(canonical_zero(numeric::result_within(
"normal logarithmic probability",
logarithm,
LOG_PROBABILITY_RESULT,
)?))
}
#[expect(
clippy::float_cmp,
reason = "equal iterates identify the binary64 precision limit"
)]
fn inverse_center(mass: f64) -> Result<f64, NormalError> {
let mut lower = 0.0;
let mut upper = 1.0;
let mut x = mass * SQRT_TWO_PI;
for _ in 0..INVERSE_ITERATION_LIMIT {
let residual = centered_probability(x)? - mass;
if residual == 0.0 {
return Ok(x);
}
if residual < 0.0 {
lower = x;
} else {
upper = x;
}
let proposed = x - residual / standard_density(x);
if proposed == x {
return Ok(x);
}
let next = if proposed > lower && proposed < upper {
proposed
} else {
lower + (upper - lower) * 0.5
};
if next == lower || next == upper {
return Ok(x);
}
x = next;
}
Err(NormalError::Convergence {
quantity: "normal inverse probability",
iterations: INVERSE_ITERATION_LIMIT,
})
}
#[expect(
clippy::float_cmp,
reason = "equal iterates identify the binary64 precision limit"
)]
fn inverse_tail(log_probability: f64) -> Result<f64, NormalError> {
let mut lower = 0.0;
let mut upper = 40.0;
let mut x = (-2.0 * log_probability).sqrt();
for _ in 0..INVERSE_ITERATION_LIMIT {
let residual = smaller_log_tail(x)? - log_probability;
if residual == 0.0 {
return Ok(x);
}
if residual > 0.0 {
lower = x;
} else {
upper = x;
}
let ratio = if x < CENTER_LIMIT {
smaller_tail(x)? / standard_density(x)
} else {
mills_ratio(x)
};
let proposed = residual.mul_add(ratio, x);
if proposed == x {
return Ok(x);
}
let next = if proposed > lower && proposed < upper {
proposed
} else {
lower + (upper - lower) * 0.5
};
if next == lower || next == upper {
return Ok(x);
}
x = next;
}
Err(NormalError::Convergence {
quantity: "normal inverse probability",
iterations: INVERSE_ITERATION_LIMIT,
})
}
#[derive(Clone, Debug, PartialEq)]
#[non_exhaustive]
pub enum NormalError {
Value(ValueError),
#[non_exhaustive]
NumericalBoundary {
quantity: &'static str,
logarithm: f64,
},
#[non_exhaustive]
Convergence {
quantity: &'static str,
iterations: u32,
},
}
impl NormalError {
#[must_use]
pub const fn kind(&self) -> &'static str {
match self {
Self::Value(_) => "normal.value",
Self::NumericalBoundary { .. } => "normal.numerical_boundary",
Self::Convergence { .. } => "normal.convergence",
}
}
}
impl fmt::Display for NormalError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Value(error) => error.fmt(f),
Self::NumericalBoundary {
quantity,
logarithm,
} => write!(
f,
"{quantity} overflow is unresolved near a binary64 boundary (logarithm {logarithm})"
),
Self::Convergence {
quantity,
iterations,
} => write!(
f,
"{quantity} did not converge within {iterations} iterations"
),
}
}
}
impl StdError for NormalError {
fn source(&self) -> Option<&(dyn StdError + 'static)> {
match self {
Self::Value(error) => Some(error),
_ => None,
}
}
}
impl From<ValueError> for NormalError {
fn from(value: ValueError) -> Self {
Self::Value(value)
}
}