numeris 0.5.13

Pure-Rust numerical algorithms library — high performance with SIMD support while also supporting no-std for embedded and WASM targets.
Documentation
use super::{normal_quantile_standard, ContinuousDistribution, StatsError};
use crate::special::{erf, erfc};
use crate::FloatScalar;

/// Normal (Gaussian) distribution N(μ, σ²).
///
/// # Example
///
/// ```
/// use numeris::stats::{Normal, ContinuousDistribution};
///
/// let n = Normal::new(0.0_f64, 1.0).unwrap();
/// assert!((n.cdf(0.0) - 0.5).abs() < 1e-14);
/// assert!((n.quantile(0.975) - 1.96).abs() < 0.01);
/// ```
#[derive(Debug, Clone, Copy)]
pub struct Normal<T> {
    mu: T,
    sigma: T,
    sigma_sqrt_two_pi: T, // σ·√(2π), cached at construction
    ln_norm: T,           // −ln σ − ln(2π)/2, cached at construction
}

impl<T: FloatScalar> Normal<T> {
    /// Create a normal distribution with mean `mu` and standard deviation `sigma`.
    ///
    /// Requires `sigma > 0`.
    pub fn new(mu: T, sigma: T) -> Result<Self, StatsError> {
        if sigma <= T::zero() {
            return Err(StatsError::InvalidParameter);
        }
        let two = T::one() + T::one();
        let pi = T::from(core::f64::consts::PI).unwrap();
        Ok(Self {
            mu,
            sigma,
            sigma_sqrt_two_pi: sigma * (two * pi).sqrt(),
            ln_norm: -sigma.ln() - (two * pi).ln() / two,
        })
    }
}

impl<T: FloatScalar> Normal<T> {
    /// Draw a random sample from this distribution.
    ///
    /// # Example
    ///
    /// ```
    /// use numeris::stats::{Normal, Rng};
    ///
    /// let n = Normal::new(0.0_f64, 1.0).unwrap();
    /// let mut rng = Rng::new(42);
    /// let x = n.sample(&mut rng);
    /// // x is drawn from N(0, 1)
    /// ```
    pub fn sample(&self, rng: &mut super::Rng) -> T {
        self.mu + self.sigma * rng.next_normal::<T>()
    }

    impl_sample_array!(T, T::zero());
}

impl<T: FloatScalar> ContinuousDistribution<T> for Normal<T> {
    fn pdf(&self, x: T) -> T {
        let two = T::one() + T::one();
        let z = (x - self.mu) / self.sigma;
        (-(z * z) / two).exp() / self.sigma_sqrt_two_pi
    }

    fn ln_pdf(&self, x: T) -> T {
        let two = T::one() + T::one();
        let z = (x - self.mu) / self.sigma;
        self.ln_norm - z * z / two
    }

    fn cdf(&self, x: T) -> T {
        let half = T::from(0.5).unwrap();
        let sqrt2 = T::from(core::f64::consts::SQRT_2).unwrap();
        let z = (x - self.mu) / (self.sigma * sqrt2);
        if z >= T::zero() {
            half * (T::one() + erf(z))
        } else {
            half * erfc(-z)
        }
    }

    fn quantile(&self, p: T) -> T {
        self.mu + self.sigma * normal_quantile_standard(p)
    }

    fn mean(&self) -> T {
        self.mu
    }

    fn variance(&self) -> T {
        self.sigma * self.sigma
    }
}