Skip to main content

u_numflow/
distributions.rs

1//! Probability distributions.
2//!
3//! Domain-agnostic probability distribution types with analytical
4//! moments (mean, variance) and CDF/inverse-CDF evaluation.
5//!
6//! # Supported Distributions
7//!
8//! | Distribution | Parameters | Mean | Variance |
9//! |---|---|---|---|
10//! | [`Uniform`] | min, max | (a+b)/2 | (b−a)²/12 |
11//! | [`Triangular`] | min, mode, max | (a+b+c)/3 | (a²+b²+c²−ab−ac−bc)/18 |
12//! | [`Normal`] | μ, σ | μ | σ² |
13//! | [`LogNormal`] | μ, σ | exp(μ+σ²/2) | (exp(σ²)−1)·exp(2μ+σ²) |
14//! | [`Pert`] | min, mode, max | (a+4m+b)/6 | see docs |
15//! | [`Weibull`] | shape (β), scale (η) | η·Γ(1+1/β) | η²·[Γ(1+2/β)−Γ(1+1/β)²] |
16//! | [`Exponential`] | rate (λ) | 1/λ | 1/λ² |
17//! | [`GammaDistribution`] | shape (α), rate (β) | α/β | α/β² |
18//! | [`BetaDistribution`] | α, β | α/(α+β) | αβ/((α+β)²(α+β+1)) |
19//! | [`ChiSquared`] | k (degrees of freedom) | k | 2k |
20//!
21//! # Design Notes
22//!
23//! This module is **domain-agnostic**. There is no concept of "duration",
24//! "scheduling", or any consumer domain. Parameters are plain `f64` values.
25
26use crate::special;
27
28/// Error type for invalid distribution parameters.
29#[derive(Debug, Clone, PartialEq)]
30pub enum DistributionError {
31    /// Parameters violate distribution constraints.
32    InvalidParameters(String),
33}
34
35impl std::fmt::Display for DistributionError {
36    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
37        match self {
38            DistributionError::InvalidParameters(msg) => {
39                write!(f, "invalid distribution parameters: {msg}")
40            }
41        }
42    }
43}
44
45impl std::error::Error for DistributionError {}
46
47// ============================================================================
48// Uniform Distribution
49// ============================================================================
50
51/// Continuous uniform distribution on `[min, max]`.
52///
53/// # Mathematical Definition
54/// - PDF: f(x) = 1/(max−min) for x ∈ [min, max]
55/// - CDF: F(x) = (x−min)/(max−min)
56/// - Mean: (min+max)/2
57/// - Variance: (max−min)²/12
58#[derive(Debug, Clone, PartialEq)]
59pub struct Uniform {
60    min: f64,
61    max: f64,
62}
63
64impl Uniform {
65    /// Creates a new uniform distribution on `[min, max]`.
66    ///
67    /// # Errors
68    /// Returns `Err` if `min >= max` or either parameter is not finite.
69    pub fn new(min: f64, max: f64) -> Result<Self, DistributionError> {
70        if !min.is_finite() || !max.is_finite() || min >= max {
71            return Err(DistributionError::InvalidParameters(format!(
72                "Uniform requires min < max, got min={min}, max={max}"
73            )));
74        }
75        Ok(Self { min, max })
76    }
77
78    pub fn min(&self) -> f64 {
79        self.min
80    }
81
82    pub fn max(&self) -> f64 {
83        self.max
84    }
85
86    pub fn mean(&self) -> f64 {
87        (self.min + self.max) / 2.0
88    }
89
90    pub fn variance(&self) -> f64 {
91        let range = self.max - self.min;
92        range * range / 12.0
93    }
94
95    /// CDF: F(x) = (x−min)/(max−min), clamped to [0, 1].
96    pub fn cdf(&self, x: f64) -> f64 {
97        if x <= self.min {
98            0.0
99        } else if x >= self.max {
100            1.0
101        } else {
102            (x - self.min) / (self.max - self.min)
103        }
104    }
105
106    /// Inverse CDF (quantile function): x = min + p·(max−min).
107    ///
108    /// Returns `None` if `p` is outside `[0, 1]`.
109    pub fn quantile(&self, p: f64) -> Option<f64> {
110        if !(0.0..=1.0).contains(&p) {
111            return None;
112        }
113        Some(self.min + p * (self.max - self.min))
114    }
115
116    /// PDF: f(x) = 1/(max−min) for x ∈ [min, max], 0 otherwise.
117    pub fn pdf(&self, x: f64) -> f64 {
118        if x >= self.min && x <= self.max {
119            1.0 / (self.max - self.min)
120        } else {
121            0.0
122        }
123    }
124}
125
126// ============================================================================
127// Triangular Distribution
128// ============================================================================
129
130/// Triangular distribution with parameters `[min, mode, max]`.
131///
132/// # Mathematical Definition
133/// - PDF: piecewise linear, peaking at mode
134/// - CDF: piecewise quadratic
135/// - Mean: (min + mode + max) / 3
136/// - Variance: (a² + b² + c² − ab − ac − bc) / 18
137///
138/// Reference: Johnson, Kotz & Balakrishnan (1995), *Continuous Univariate
139/// Distributions*, Vol. 2, Chapter 26.
140#[derive(Debug, Clone, PartialEq)]
141pub struct Triangular {
142    min: f64,
143    mode: f64,
144    max: f64,
145}
146
147impl Triangular {
148    /// Creates a new triangular distribution.
149    ///
150    /// # Errors
151    /// Returns `Err` if `min >= max` or `mode` is outside `[min, max]`.
152    pub fn new(min: f64, mode: f64, max: f64) -> Result<Self, DistributionError> {
153        if !min.is_finite() || !mode.is_finite() || !max.is_finite() {
154            return Err(DistributionError::InvalidParameters(
155                "Triangular parameters must be finite".into(),
156            ));
157        }
158        if min > mode || mode > max || min >= max {
159            return Err(DistributionError::InvalidParameters(format!(
160                "Triangular requires min ≤ mode ≤ max and min < max, got {min}, {mode}, {max}"
161            )));
162        }
163        Ok(Self { min, mode, max })
164    }
165
166    pub fn min(&self) -> f64 {
167        self.min
168    }
169
170    pub fn mode(&self) -> f64 {
171        self.mode
172    }
173
174    pub fn max(&self) -> f64 {
175        self.max
176    }
177
178    /// Mean = (min + mode + max) / 3.
179    pub fn mean(&self) -> f64 {
180        (self.min + self.mode + self.max) / 3.0
181    }
182
183    /// Variance = (a² + b² + c² − ab − ac − bc) / 18.
184    pub fn variance(&self) -> f64 {
185        let (a, b, c) = (self.min, self.mode, self.max);
186        (a * a + b * b + c * c - a * b - a * c - b * c) / 18.0
187    }
188
189    /// PDF of the triangular distribution.
190    ///
191    /// ```text
192    /// f(x) = 2(x−a) / ((c−a)(b−a))  for a ≤ x ≤ b
193    ///      = 2(c−x) / ((c−a)(c−b))  for b < x ≤ c
194    ///      = 0                       otherwise
195    /// ```
196    pub fn pdf(&self, x: f64) -> f64 {
197        let (a, b, c) = (self.min, self.mode, self.max);
198        if x < a || x > c {
199            0.0
200        } else if x <= b {
201            2.0 * (x - a) / ((c - a) * (b - a).max(f64::MIN_POSITIVE))
202        } else {
203            2.0 * (c - x) / ((c - a) * (c - b).max(f64::MIN_POSITIVE))
204        }
205    }
206
207    /// CDF of the triangular distribution.
208    ///
209    /// ```text
210    /// F(x) = (x−a)² / ((c−a)(b−a))       for a ≤ x ≤ b
211    ///      = 1 − (c−x)² / ((c−a)(c−b))   for b < x ≤ c
212    /// ```
213    pub fn cdf(&self, x: f64) -> f64 {
214        let (a, b, c) = (self.min, self.mode, self.max);
215        if x <= a {
216            0.0
217        } else if x <= b {
218            (x - a) * (x - a) / ((c - a) * (b - a).max(f64::MIN_POSITIVE))
219        } else if x < c {
220            1.0 - (c - x) * (c - x) / ((c - a) * (c - b).max(f64::MIN_POSITIVE))
221        } else {
222            1.0
223        }
224    }
225
226    /// Inverse CDF (quantile function) of the triangular distribution.
227    ///
228    /// ```text
229    /// F⁻¹(p) = a + √(p·(c−a)·(b−a))                 if p < F(b)
230    ///        = c − √((1−p)·(c−a)·(c−b))              if p ≥ F(b)
231    /// ```
232    ///
233    /// Returns `None` if `p` is outside `[0, 1]`.
234    pub fn quantile(&self, p: f64) -> Option<f64> {
235        if !(0.0..=1.0).contains(&p) {
236            return None;
237        }
238        let (a, b, c) = (self.min, self.mode, self.max);
239        let fc = (b - a) / (c - a); // CDF at the mode
240        if p < fc {
241            Some(a + ((c - a) * (b - a) * p).sqrt())
242        } else {
243            Some(c - ((c - a) * (c - b) * (1.0 - p)).sqrt())
244        }
245    }
246}
247
248// ============================================================================
249// Normal Distribution
250// ============================================================================
251
252/// Normal (Gaussian) distribution N(μ, σ²).
253///
254/// # Mathematical Definition
255/// - PDF: φ(x) = (1/(σ√(2π))) exp(−(x−μ)²/(2σ²))
256/// - CDF: Φ((x−μ)/σ) (via standard normal CDF)
257/// - Mean: μ
258/// - Variance: σ²
259#[derive(Debug, Clone, PartialEq)]
260pub struct Normal {
261    mu: f64,
262    sigma: f64,
263}
264
265impl Normal {
266    /// Creates a new normal distribution N(μ, σ).
267    ///
268    /// # Errors
269    /// Returns `Err` if `sigma ≤ 0` or parameters are not finite.
270    pub fn new(mu: f64, sigma: f64) -> Result<Self, DistributionError> {
271        if !mu.is_finite() || !sigma.is_finite() || sigma <= 0.0 {
272            return Err(DistributionError::InvalidParameters(format!(
273                "Normal requires finite μ and σ > 0, got μ={mu}, σ={sigma}"
274            )));
275        }
276        Ok(Self { mu, sigma })
277    }
278
279    pub fn mu(&self) -> f64 {
280        self.mu
281    }
282
283    pub fn sigma(&self) -> f64 {
284        self.sigma
285    }
286
287    pub fn mean(&self) -> f64 {
288        self.mu
289    }
290
291    pub fn variance(&self) -> f64 {
292        self.sigma * self.sigma
293    }
294
295    pub fn std_dev(&self) -> f64 {
296        self.sigma
297    }
298
299    /// PDF: (1/(σ√(2π))) exp(−(x−μ)²/(2σ²)).
300    pub fn pdf(&self, x: f64) -> f64 {
301        let z = (x - self.mu) / self.sigma;
302        special::standard_normal_pdf(z) / self.sigma
303    }
304
305    /// CDF: Φ((x−μ)/σ).
306    pub fn cdf(&self, x: f64) -> f64 {
307        let z = (x - self.mu) / self.sigma;
308        special::standard_normal_cdf(z)
309    }
310
311    /// Inverse CDF (quantile): μ + σ·Φ⁻¹(p).
312    ///
313    /// Returns `None` if `p` is outside `(0, 1)`.
314    pub fn quantile(&self, p: f64) -> Option<f64> {
315        if p <= 0.0 || p >= 1.0 {
316            return None;
317        }
318        Some(self.mu + self.sigma * special::inverse_normal_cdf(p))
319    }
320}
321
322// ============================================================================
323// LogNormal Distribution
324// ============================================================================
325
326/// Log-normal distribution: if X ~ LogNormal(μ, σ), then ln(X) ~ N(μ, σ²).
327///
328/// # Mathematical Definition
329/// - PDF: (1/(xσ√(2π))) exp(−(ln(x)−μ)²/(2σ²)) for x > 0
330/// - CDF: Φ((ln(x)−μ)/σ)
331/// - Mean: exp(μ + σ²/2)
332/// - Variance: (exp(σ²) − 1) · exp(2μ + σ²)
333///
334/// Reference: Johnson, Kotz & Balakrishnan (1994), *Continuous Univariate
335/// Distributions*, Vol. 1, Chapter 14.
336#[derive(Debug, Clone, PartialEq)]
337pub struct LogNormal {
338    mu: f64,
339    sigma: f64,
340}
341
342impl LogNormal {
343    /// Creates a new log-normal distribution.
344    ///
345    /// Parameters `mu` and `sigma` are the mean and std dev of ln(X).
346    ///
347    /// # Errors
348    /// Returns `Err` if `sigma ≤ 0` or parameters are not finite.
349    pub fn new(mu: f64, sigma: f64) -> Result<Self, DistributionError> {
350        if !mu.is_finite() || !sigma.is_finite() || sigma <= 0.0 {
351            return Err(DistributionError::InvalidParameters(format!(
352                "LogNormal requires finite μ and σ > 0, got μ={mu}, σ={sigma}"
353            )));
354        }
355        Ok(Self { mu, sigma })
356    }
357
358    pub fn mu(&self) -> f64 {
359        self.mu
360    }
361
362    pub fn sigma(&self) -> f64 {
363        self.sigma
364    }
365
366    /// Mean = exp(μ + σ²/2).
367    pub fn mean(&self) -> f64 {
368        (self.mu + self.sigma * self.sigma / 2.0).exp()
369    }
370
371    /// Variance = (exp(σ²) − 1) · exp(2μ + σ²).
372    pub fn variance(&self) -> f64 {
373        let s2 = self.sigma * self.sigma;
374        (s2.exp() - 1.0) * (2.0 * self.mu + s2).exp()
375    }
376
377    /// PDF for x > 0.
378    pub fn pdf(&self, x: f64) -> f64 {
379        if x <= 0.0 {
380            return 0.0;
381        }
382        let ln_x = x.ln();
383        let z = (ln_x - self.mu) / self.sigma;
384        special::standard_normal_pdf(z) / (x * self.sigma)
385    }
386
387    /// CDF: Φ((ln(x)−μ)/σ) for x > 0.
388    pub fn cdf(&self, x: f64) -> f64 {
389        if x <= 0.0 {
390            return 0.0;
391        }
392        let z = (x.ln() - self.mu) / self.sigma;
393        special::standard_normal_cdf(z)
394    }
395
396    /// Inverse CDF: exp(μ + σ·Φ⁻¹(p)).
397    ///
398    /// Returns `None` if `p` is outside `(0, 1)`.
399    pub fn quantile(&self, p: f64) -> Option<f64> {
400        if p <= 0.0 || p >= 1.0 {
401            return None;
402        }
403        Some((self.mu + self.sigma * special::inverse_normal_cdf(p)).exp())
404    }
405}
406
407// ============================================================================
408// PERT Distribution (Modified Beta)
409// ============================================================================
410
411/// PERT distribution (Program Evaluation and Review Technique).
412///
413/// A modified Beta distribution defined by three points: optimistic (min),
414/// most likely (mode), and pessimistic (max).
415///
416/// # Mathematical Definition
417///
418/// Shape parameters (with λ = 4):
419/// ```text
420/// α = 1 + λ · (mode − min) / (max − min)
421/// β = 1 + λ · (max − mode) / (max − min)
422/// ```
423///
424/// The underlying variable Y = (X − min)/(max − min) follows Beta(α, β).
425///
426/// - Mean: (min + λ·mode + max) / (λ + 2) = (min + 4·mode + max) / 6
427/// - Std Dev (simplified): (max − min) / (λ + 2) = (max − min) / 6
428///
429/// # Exact vs Simplified Variance
430///
431/// The simplified variance `((max−min)/6)²` is an approximation. The exact
432/// variance uses the Beta distribution formula:
433/// ```text
434/// Var = α·β / ((α+β)²·(α+β+1)) × (max−min)²
435/// ```
436///
437/// The simplified formula is exact when the distribution is symmetric
438/// (mode = midpoint) and becomes less accurate as skewness increases.
439///
440/// Reference: Malcolm et al. (1959), "Application of a Technique for
441/// Research and Development Program Evaluation", *Operations Research* 7(5).
442#[derive(Debug, Clone, PartialEq)]
443pub struct Pert {
444    min: f64,
445    mode: f64,
446    max: f64,
447    alpha: f64,
448    beta: f64,
449}
450
451impl Pert {
452    /// Creates a standard PERT distribution (λ = 4).
453    ///
454    /// # Errors
455    /// Returns `Err` if `min >= max` or `mode` is outside `[min, max]`.
456    pub fn new(min: f64, mode: f64, max: f64) -> Result<Self, DistributionError> {
457        Self::with_shape(min, mode, max, 4.0)
458    }
459
460    /// Creates a modified PERT distribution with custom shape parameter λ.
461    ///
462    /// λ controls the weight of the mode:
463    /// - λ = 4: standard PERT
464    /// - λ > 4: tighter distribution (more peaked)
465    /// - λ < 4: flatter distribution (less peaked)
466    ///
467    /// # Errors
468    /// Returns `Err` if parameters are invalid.
469    pub fn with_shape(
470        min: f64,
471        mode: f64,
472        max: f64,
473        lambda: f64,
474    ) -> Result<Self, DistributionError> {
475        if !min.is_finite() || !mode.is_finite() || !max.is_finite() || !lambda.is_finite() {
476            return Err(DistributionError::InvalidParameters(
477                "PERT parameters must be finite".into(),
478            ));
479        }
480        if min > mode || mode > max || min >= max {
481            return Err(DistributionError::InvalidParameters(format!(
482                "PERT requires min ≤ mode ≤ max and min < max, got {min}, {mode}, {max}"
483            )));
484        }
485        if lambda <= 0.0 {
486            return Err(DistributionError::InvalidParameters(format!(
487                "PERT λ must be > 0, got {lambda}"
488            )));
489        }
490
491        let range = max - min;
492        let alpha = 1.0 + lambda * (mode - min) / range;
493        let beta = 1.0 + lambda * (max - mode) / range;
494
495        Ok(Self {
496            min,
497            mode,
498            max,
499            alpha,
500            beta,
501        })
502    }
503
504    pub fn min(&self) -> f64 {
505        self.min
506    }
507
508    pub fn mode(&self) -> f64 {
509        self.mode
510    }
511
512    pub fn max(&self) -> f64 {
513        self.max
514    }
515
516    pub fn alpha(&self) -> f64 {
517        self.alpha
518    }
519
520    pub fn beta_param(&self) -> f64 {
521        self.beta
522    }
523
524    /// Mean = (min + 4·mode + max) / 6 for standard PERT (λ=4).
525    ///
526    /// General: (min + λ·mode + max) / (λ + 2).
527    pub fn mean(&self) -> f64 {
528        let ab = self.alpha + self.beta;
529        // Mean of Beta(α,β) on [min,max] = min + (α/(α+β))·(max-min)
530        self.min + (self.alpha / ab) * (self.max - self.min)
531    }
532
533    /// Exact variance using Beta distribution formula.
534    ///
535    /// ```text
536    /// Var = α·β / ((α+β)²·(α+β+1)) × (max−min)²
537    /// ```
538    pub fn variance(&self) -> f64 {
539        let ab = self.alpha + self.beta;
540        let range = self.max - self.min;
541        (self.alpha * self.beta) / (ab * ab * (ab + 1.0)) * range * range
542    }
543
544    /// Standard deviation = √(variance).
545    pub fn std_dev(&self) -> f64 {
546        self.variance().sqrt()
547    }
548
549    /// CDF via regularized incomplete beta function approximation.
550    ///
551    /// Uses a numerical approximation of the regularized incomplete beta
552    /// function I_x(α, β).
553    pub fn cdf(&self, x: f64) -> f64 {
554        if x <= self.min {
555            return 0.0;
556        }
557        if x >= self.max {
558            return 1.0;
559        }
560        let t = (x - self.min) / (self.max - self.min);
561        special::regularized_incomplete_beta(t, self.alpha, self.beta)
562    }
563
564    /// Evaluates the quantile (inverse CDF) at probability `p ∈ [0, 1]`.
565    ///
566    /// A PERT is a Beta(α, β) stretched over `[min, max]`, so its quantile is
567    /// `min + (max − min)·B⁻¹(p; α, β)` -- exact to the tolerance of the Beta
568    /// quantile, and inside `[min, max]` by construction. (A normal
569    /// approximation `μ + σ·Φ⁻¹(p)` is off for any skewed estimate and can land
570    /// outside the support.)
571    ///
572    /// Returns `None` if `p` is outside `[0, 1]`.
573    ///
574    /// # Examples
575    /// ```
576    /// use u_numflow::distributions::Pert;
577    /// let pert = Pert::new(0.0, 1.0, 10.0).unwrap();
578    /// let q = pert.quantile(0.9).unwrap();
579    /// assert!((pert.cdf(q) - 0.9).abs() < 1e-9);
580    /// ```
581    pub fn quantile(&self, p: f64) -> Option<f64> {
582        if !(0.0..=1.0).contains(&p) {
583            return None;
584        }
585        let standard = BetaDistribution::new(self.alpha, self.beta)
586            .expect("PERT shape parameters are positive by construction")
587            .quantile(p)?;
588        Some(self.min + (self.max - self.min) * standard)
589    }
590
591    /// Normal approximation `μ + σ·Φ⁻¹(p)`, clamped to `[min, max]`.
592    ///
593    /// Superseded by the exact [`quantile`](Self::quantile), which this
594    /// differs from on any skewed estimate (it can even fall outside the
595    /// support before the clamp). Kept only so this release stays a patch; it
596    /// is removed in the next minor version.
597    ///
598    /// Returns `None` if `p` is outside `(0, 1)`.
599    #[deprecated(since = "0.6.3", note = "use `Pert::quantile`, which is exact")]
600    pub fn quantile_approx(&self, p: f64) -> Option<f64> {
601        if p <= 0.0 || p >= 1.0 {
602            return None;
603        }
604        let z = special::inverse_normal_cdf(p);
605        let result = self.mean() + z * self.std_dev();
606        Some(result.clamp(self.min, self.max))
607    }
608}
609
610// ============================================================================
611// Weibull Distribution
612// ============================================================================
613
614/// Weibull distribution with shape parameter β (> 0) and scale parameter η (> 0).
615///
616/// Widely used in reliability engineering and survival analysis.
617///
618/// # Mathematical Definition
619/// - PDF: f(t) = (β/η)·(t/η)^(β−1)·exp(−(t/η)^β) for t ≥ 0
620/// - CDF: F(t) = 1 − exp(−(t/η)^β)
621/// - Quantile: t = η·(−ln(1−p))^(1/β)
622/// - Mean: η·Γ(1 + 1/β)
623/// - Variance: η²·[Γ(1 + 2/β) − Γ(1 + 1/β)²]
624///
625/// # Special Cases
626/// - β = 1: Exponential distribution with rate 1/η
627/// - β = 2: Rayleigh distribution
628/// - β ≈ 3.6: Approximates a normal distribution
629///
630/// Reference: Weibull (1951), "A Statistical Distribution Function of Wide
631/// Applicability", *Journal of Applied Mechanics* 18(3), pp. 293–297.
632#[derive(Debug, Clone, PartialEq)]
633pub struct Weibull {
634    shape: f64, // β
635    scale: f64, // η
636}
637
638impl Weibull {
639    /// Creates a new Weibull distribution with the given shape (β) and scale (η).
640    ///
641    /// # Errors
642    /// Returns `Err` if `shape ≤ 0`, `scale ≤ 0`, or either is not finite.
643    pub fn new(shape: f64, scale: f64) -> Result<Self, DistributionError> {
644        if !shape.is_finite() || !scale.is_finite() || shape <= 0.0 || scale <= 0.0 {
645            return Err(DistributionError::InvalidParameters(format!(
646                "Weibull requires shape > 0 and scale > 0, got shape={shape}, scale={scale}"
647            )));
648        }
649        Ok(Self { shape, scale })
650    }
651
652    /// Shape parameter β.
653    pub fn shape(&self) -> f64 {
654        self.shape
655    }
656
657    /// Scale parameter η.
658    pub fn scale(&self) -> f64 {
659        self.scale
660    }
661
662    /// Mean = η·Γ(1 + 1/β).
663    pub fn mean(&self) -> f64 {
664        self.scale * special::gamma(1.0 + 1.0 / self.shape)
665    }
666
667    /// Variance = η²·[Γ(1 + 2/β) − Γ(1 + 1/β)²].
668    pub fn variance(&self) -> f64 {
669        let g1 = special::gamma(1.0 + 1.0 / self.shape);
670        let g2 = special::gamma(1.0 + 2.0 / self.shape);
671        self.scale * self.scale * (g2 - g1 * g1)
672    }
673
674    /// PDF: f(t) = (β/η)·(t/η)^(β−1)·exp(−(t/η)^β) for t ≥ 0.
675    pub fn pdf(&self, t: f64) -> f64 {
676        if t < 0.0 {
677            return 0.0;
678        }
679        if t == 0.0 {
680            return if self.shape < 1.0 {
681                f64::INFINITY
682            } else if (self.shape - 1.0).abs() < f64::EPSILON {
683                1.0 / self.scale
684            } else {
685                0.0
686            };
687        }
688        let z = t / self.scale;
689        (self.shape / self.scale) * z.powf(self.shape - 1.0) * (-z.powf(self.shape)).exp()
690    }
691
692    /// CDF: F(t) = 1 − exp(−(t/η)^β).
693    pub fn cdf(&self, t: f64) -> f64 {
694        if t <= 0.0 {
695            return 0.0;
696        }
697        let z = t / self.scale;
698        1.0 - (-z.powf(self.shape)).exp()
699    }
700
701    /// Quantile (inverse CDF): t = η·(−ln(1−p))^(1/β).
702    ///
703    /// Returns `None` if `p` is outside `[0, 1)`.
704    pub fn quantile(&self, p: f64) -> Option<f64> {
705        if !(0.0..1.0).contains(&p) {
706            return None;
707        }
708        if p == 0.0 {
709            return Some(0.0);
710        }
711        Some(self.scale * (-(1.0 - p).ln()).powf(1.0 / self.shape))
712    }
713
714    /// Hazard rate (failure rate): λ(t) = (β/η)·(t/η)^(β−1).
715    ///
716    /// - β < 1: Decreasing failure rate (infant mortality)
717    /// - β = 1: Constant failure rate (random failures)
718    /// - β > 1: Increasing failure rate (wear-out)
719    pub fn hazard_rate(&self, t: f64) -> f64 {
720        if t <= 0.0 {
721            return if t == 0.0 && self.shape >= 1.0 {
722                if (self.shape - 1.0).abs() < f64::EPSILON {
723                    1.0 / self.scale
724                } else {
725                    0.0
726                }
727            } else {
728                0.0
729            };
730        }
731        let z = t / self.scale;
732        (self.shape / self.scale) * z.powf(self.shape - 1.0)
733    }
734
735    /// Reliability (survival) function: R(t) = 1 − F(t) = exp(−(t/η)^β).
736    pub fn reliability(&self, t: f64) -> f64 {
737        1.0 - self.cdf(t)
738    }
739}
740
741// ============================================================================
742// Exponential Distribution
743// ============================================================================
744
745/// Exponential distribution with rate parameter λ.
746///
747/// Models the time between events in a Poisson process. The rate λ > 0
748/// determines the expected frequency; the mean is 1/λ.
749///
750/// # Examples
751///
752/// ```
753/// use u_numflow::distributions::Exponential;
754///
755/// let exp = Exponential::new(0.5).unwrap();
756/// assert!((exp.mean() - 2.0).abs() < 1e-10);
757/// assert!((exp.cdf(2.0) - (1.0 - (-1.0_f64).exp())).abs() < 1e-10);
758/// ```
759#[derive(Debug, Clone, Copy)]
760pub struct Exponential {
761    rate: f64,
762}
763
764impl Exponential {
765    /// Creates a new Exponential distribution with the given rate λ.
766    ///
767    /// Returns an error if λ ≤ 0 or λ is not finite.
768    pub fn new(rate: f64) -> Result<Self, DistributionError> {
769        if !rate.is_finite() || rate <= 0.0 {
770            return Err(DistributionError::InvalidParameters(format!(
771                "Exponential rate must be positive and finite, got {rate}"
772            )));
773        }
774        Ok(Self { rate })
775    }
776
777    /// Rate parameter λ.
778    pub fn rate(&self) -> f64 {
779        self.rate
780    }
781
782    /// Mean = 1/λ.
783    pub fn mean(&self) -> f64 {
784        1.0 / self.rate
785    }
786
787    /// Variance = 1/λ².
788    pub fn variance(&self) -> f64 {
789        1.0 / (self.rate * self.rate)
790    }
791
792    /// PDF: f(x) = λ exp(−λx) for x ≥ 0, 0 otherwise.
793    pub fn pdf(&self, x: f64) -> f64 {
794        if x < 0.0 {
795            0.0
796        } else {
797            self.rate * (-self.rate * x).exp()
798        }
799    }
800
801    /// CDF: F(x) = 1 − exp(−λx) for x ≥ 0, 0 otherwise.
802    pub fn cdf(&self, x: f64) -> f64 {
803        if x < 0.0 {
804            0.0
805        } else {
806            1.0 - (-self.rate * x).exp()
807        }
808    }
809
810    /// Quantile (inverse CDF): x = −ln(1−p)/λ.
811    ///
812    /// Returns `None` if p is not in [0, 1].
813    pub fn quantile(&self, p: f64) -> Option<f64> {
814        if !(0.0..=1.0).contains(&p) {
815            return None;
816        }
817        if p == 1.0 {
818            return Some(f64::INFINITY);
819        }
820        Some(-(1.0 - p).ln() / self.rate)
821    }
822
823    /// Hazard rate (failure rate): h(x) = λ (constant).
824    pub fn hazard_rate(&self) -> f64 {
825        self.rate
826    }
827
828    /// Survival function: S(x) = exp(−λx).
829    pub fn survival(&self, x: f64) -> f64 {
830        1.0 - self.cdf(x)
831    }
832}
833
834// ============================================================================
835// Gamma Distribution
836// ============================================================================
837
838/// Gamma distribution with shape α and rate β (or equivalently scale θ = 1/β).
839///
840/// Uses the (shape, rate) parametrization: X ~ Gamma(α, β).
841///
842/// # PDF
843///
844/// f(x) = β^α / Γ(α) · x^(α−1) · exp(−βx) for x > 0
845///
846/// # Examples
847///
848/// ```
849/// use u_numflow::distributions::GammaDistribution;
850///
851/// let g = GammaDistribution::new(2.0, 1.0).unwrap();
852/// assert!((g.mean() - 2.0).abs() < 1e-10);
853/// assert!((g.variance() - 2.0).abs() < 1e-10);
854/// ```
855///
856/// # Reference
857///
858/// Johnson, N.L., Kotz, S. & Balakrishnan, N. (1994). *Continuous Univariate
859/// Distributions*, Vol. 1, 2nd ed. Wiley.
860#[derive(Debug, Clone, Copy)]
861pub struct GammaDistribution {
862    shape: f64, // α > 0
863    rate: f64,  // β > 0
864}
865
866impl GammaDistribution {
867    /// Creates a Gamma(α, β) distribution with shape α and rate β.
868    ///
869    /// Returns an error if α ≤ 0, β ≤ 0, or either is not finite.
870    pub fn new(shape: f64, rate: f64) -> Result<Self, DistributionError> {
871        if !shape.is_finite() || shape <= 0.0 || !rate.is_finite() || rate <= 0.0 {
872            return Err(DistributionError::InvalidParameters(format!(
873                "Gamma shape and rate must be positive and finite, got shape={shape}, rate={rate}"
874            )));
875        }
876        Ok(Self { shape, rate })
877    }
878
879    /// Creates a Gamma distribution from shape and scale θ = 1/β.
880    pub fn from_shape_scale(shape: f64, scale: f64) -> Result<Self, DistributionError> {
881        if !scale.is_finite() || scale <= 0.0 {
882            return Err(DistributionError::InvalidParameters(format!(
883                "Gamma scale must be positive and finite, got {scale}"
884            )));
885        }
886        Self::new(shape, 1.0 / scale)
887    }
888
889    /// Shape parameter α.
890    pub fn shape(&self) -> f64 {
891        self.shape
892    }
893
894    /// Rate parameter β.
895    pub fn rate(&self) -> f64 {
896        self.rate
897    }
898
899    /// Scale parameter θ = 1/β.
900    pub fn scale(&self) -> f64 {
901        1.0 / self.rate
902    }
903
904    /// Mean = α/β.
905    pub fn mean(&self) -> f64 {
906        self.shape / self.rate
907    }
908
909    /// Variance = α/β².
910    pub fn variance(&self) -> f64 {
911        self.shape / (self.rate * self.rate)
912    }
913
914    /// Mode = (α−1)/β for α ≥ 1, 0 for α < 1.
915    pub fn mode(&self) -> f64 {
916        if self.shape >= 1.0 {
917            (self.shape - 1.0) / self.rate
918        } else {
919            0.0
920        }
921    }
922
923    /// PDF: f(x) = β^α / Γ(α) · x^(α−1) · exp(−βx) for x > 0.
924    pub fn pdf(&self, x: f64) -> f64 {
925        if x <= 0.0 {
926            if x == 0.0 && self.shape < 1.0 {
927                return f64::INFINITY;
928            }
929            return 0.0;
930        }
931        let log_pdf = self.shape * self.rate.ln() - special::ln_gamma(self.shape)
932            + (self.shape - 1.0) * x.ln()
933            - self.rate * x;
934        log_pdf.exp()
935    }
936
937    /// CDF: F(x) = γ(α, βx) / Γ(α) = regularized lower incomplete gamma.
938    ///
939    /// Uses series expansion for small x or continued fraction for large x.
940    pub fn cdf(&self, x: f64) -> f64 {
941        if x <= 0.0 {
942            return 0.0;
943        }
944        special::regularized_lower_gamma(self.shape, self.rate * x)
945    }
946
947    /// Quantile (inverse CDF) via Newton-Raphson iteration.
948    ///
949    /// Returns `None` if p is not in [0, 1].
950    pub fn quantile(&self, p: f64) -> Option<f64> {
951        if !(0.0..=1.0).contains(&p) {
952            return None;
953        }
954        if p == 0.0 {
955            return Some(0.0);
956        }
957        if p == 1.0 {
958            return Some(f64::INFINITY);
959        }
960
961        // Initial guess: Wilson-Hilferty approximation
962        let z = crate::special::inverse_normal_cdf(p);
963        let v = 1.0 / (9.0 * self.shape);
964        let chi2_approx = self.shape * (1.0 - v + z * v.sqrt()).powi(3).max(0.001);
965        let mut x = chi2_approx / self.rate;
966
967        // Newton-Raphson refinement
968        for _ in 0..50 {
969            let f = self.cdf(x) - p;
970            let fp = self.pdf(x);
971            if fp < 1e-300 {
972                break;
973            }
974            let step = f / fp;
975            x = (x - step).max(1e-15);
976            if step.abs() < 1e-12 * x {
977                break;
978            }
979        }
980
981        Some(x)
982    }
983}
984
985// ============================================================================
986// Beta Distribution
987// ============================================================================
988
989/// Beta distribution on `[0, 1]`.
990///
991/// # Mathematical Definition
992/// - PDF: f(x) = x^(α−1) (1−x)^(β−1) / B(α, β)
993/// - CDF: F(x) = I_x(α, β) (regularized incomplete beta)
994/// - Mean: α / (α + β)
995/// - Variance: αβ / ((α+β)²(α+β+1))
996///
997/// # Examples
998///
999/// ```
1000/// use u_numflow::distributions::BetaDistribution;
1001///
1002/// let beta = BetaDistribution::new(2.0, 5.0).unwrap();
1003/// assert!((beta.mean() - 2.0 / 7.0).abs() < 1e-10);
1004/// assert!(beta.pdf(0.3) > 0.0);
1005/// assert!((beta.cdf(0.0)).abs() < 1e-10);
1006/// assert!((beta.cdf(1.0) - 1.0).abs() < 1e-10);
1007/// ```
1008///
1009/// # References
1010///
1011/// Johnson, N. L., Kotz, S., & Balakrishnan, N. (1995).
1012/// *Continuous Univariate Distributions*, Vol. 2, Chapter 25.
1013#[derive(Debug, Clone)]
1014pub struct BetaDistribution {
1015    /// Shape parameter α > 0.
1016    pub alpha: f64,
1017    /// Shape parameter β > 0.
1018    pub beta: f64,
1019}
1020
1021impl BetaDistribution {
1022    /// Creates a new Beta distribution with shape parameters α and β.
1023    ///
1024    /// # Errors
1025    /// Returns `DistributionError` if α ≤ 0 or β ≤ 0.
1026    pub fn new(alpha: f64, beta: f64) -> Result<Self, DistributionError> {
1027        if alpha <= 0.0 || !alpha.is_finite() {
1028            return Err(DistributionError::InvalidParameters(format!(
1029                "alpha must be positive and finite, got {alpha}"
1030            )));
1031        }
1032        if beta <= 0.0 || !beta.is_finite() {
1033            return Err(DistributionError::InvalidParameters(format!(
1034                "beta must be positive and finite, got {beta}"
1035            )));
1036        }
1037        Ok(Self { alpha, beta })
1038    }
1039
1040    /// Returns the mean: α / (α + β).
1041    pub fn mean(&self) -> f64 {
1042        self.alpha / (self.alpha + self.beta)
1043    }
1044
1045    /// Returns the variance: αβ / ((α+β)²(α+β+1)).
1046    pub fn variance(&self) -> f64 {
1047        let ab = self.alpha + self.beta;
1048        self.alpha * self.beta / (ab * ab * (ab + 1.0))
1049    }
1050
1051    /// Returns the mode.
1052    ///
1053    /// Defined when α > 1 and β > 1: mode = (α−1)/(α+β−2).
1054    /// Returns `None` for other parameter combinations (bimodal or boundary modes).
1055    pub fn mode(&self) -> Option<f64> {
1056        if self.alpha > 1.0 && self.beta > 1.0 {
1057            Some((self.alpha - 1.0) / (self.alpha + self.beta - 2.0))
1058        } else {
1059            None
1060        }
1061    }
1062
1063    /// Evaluates the PDF at x.
1064    pub fn pdf(&self, x: f64) -> f64 {
1065        if x <= 0.0 || x >= 1.0 {
1066            return 0.0;
1067        }
1068        let ln_pdf = (self.alpha - 1.0) * x.ln() + (self.beta - 1.0) * (1.0 - x).ln()
1069            - special::ln_beta(self.alpha, self.beta);
1070        ln_pdf.exp()
1071    }
1072
1073    /// Evaluates the CDF at x: F(x) = I_x(α, β).
1074    pub fn cdf(&self, x: f64) -> f64 {
1075        if x <= 0.0 {
1076            return 0.0;
1077        }
1078        if x >= 1.0 {
1079            return 1.0;
1080        }
1081        special::regularized_incomplete_beta(x, self.alpha, self.beta)
1082    }
1083
1084    /// Evaluates the quantile (inverse CDF) at probability p ∈ [0, 1].
1085    ///
1086    /// Uses Newton-Raphson iteration with an initial approximation
1087    /// from the normal approximation to the beta distribution.
1088    pub fn quantile(&self, p: f64) -> Option<f64> {
1089        if !(0.0..=1.0).contains(&p) {
1090            return None;
1091        }
1092        if p == 0.0 {
1093            return Some(0.0);
1094        }
1095        if (p - 1.0).abs() < 1e-15 {
1096            return Some(1.0);
1097        }
1098
1099        // Bisection method for robust convergence on [0, 1]
1100        let mut lo = 0.0_f64;
1101        let mut hi = 1.0_f64;
1102
1103        for _ in 0..100 {
1104            let mid = (lo + hi) / 2.0;
1105            if hi - lo < 1e-14 {
1106                break;
1107            }
1108            if self.cdf(mid) < p {
1109                lo = mid;
1110            } else {
1111                hi = mid;
1112            }
1113        }
1114
1115        Some((lo + hi) / 2.0)
1116    }
1117}
1118
1119// ============================================================================
1120// Chi-Squared Distribution
1121// ============================================================================
1122
1123/// Chi-squared distribution with k degrees of freedom.
1124///
1125/// The chi-squared distribution is a special case of the Gamma distribution
1126/// with shape = k/2 and rate = 1/2.
1127///
1128/// # Mathematical Definition
1129/// - PDF: f(x) = x^(k/2−1) exp(−x/2) / (2^(k/2) Γ(k/2))
1130/// - CDF: F(x) = P(k/2, x/2) (regularized lower incomplete gamma)
1131/// - Mean: k
1132/// - Variance: 2k
1133///
1134/// # Examples
1135///
1136/// ```
1137/// use u_numflow::distributions::ChiSquared;
1138///
1139/// let chi2 = ChiSquared::new(3.0).unwrap();
1140/// assert!((chi2.mean() - 3.0).abs() < 1e-10);
1141/// assert!((chi2.variance() - 6.0).abs() < 1e-10);
1142/// assert!((chi2.cdf(0.0)).abs() < 1e-10);
1143/// ```
1144///
1145/// # References
1146///
1147/// Johnson, N. L., Kotz, S., & Balakrishnan, N. (1994).
1148/// *Continuous Univariate Distributions*, Vol. 1, Chapter 18.
1149#[derive(Debug, Clone)]
1150pub struct ChiSquared {
1151    /// Degrees of freedom k > 0.
1152    pub k: f64,
1153}
1154
1155impl ChiSquared {
1156    /// Creates a new Chi-squared distribution with k degrees of freedom.
1157    ///
1158    /// # Errors
1159    /// Returns `DistributionError` if k ≤ 0.
1160    pub fn new(k: f64) -> Result<Self, DistributionError> {
1161        if k <= 0.0 || !k.is_finite() {
1162            return Err(DistributionError::InvalidParameters(format!(
1163                "k must be positive and finite, got {k}"
1164            )));
1165        }
1166        Ok(Self { k })
1167    }
1168
1169    /// Returns the mean: k.
1170    pub fn mean(&self) -> f64 {
1171        self.k
1172    }
1173
1174    /// Returns the variance: 2k.
1175    pub fn variance(&self) -> f64 {
1176        2.0 * self.k
1177    }
1178
1179    /// Evaluates the PDF at x.
1180    pub fn pdf(&self, x: f64) -> f64 {
1181        if x <= 0.0 {
1182            return 0.0;
1183        }
1184        let half_k = self.k / 2.0;
1185        let ln_pdf =
1186            (half_k - 1.0) * x.ln() - x / 2.0 - half_k * 2.0_f64.ln() - special::ln_gamma(half_k);
1187        ln_pdf.exp()
1188    }
1189
1190    /// Evaluates the CDF at x: F(x) = P(k/2, x/2).
1191    pub fn cdf(&self, x: f64) -> f64 {
1192        if x <= 0.0 {
1193            return 0.0;
1194        }
1195        special::regularized_lower_gamma(self.k / 2.0, x / 2.0)
1196    }
1197
1198    /// Evaluates the quantile (inverse CDF) at probability p ∈ [0, 1].
1199    ///
1200    /// Delegates to Gamma(k/2, 1/2) quantile since Chi2(k) = Gamma(k/2, 1/2).
1201    pub fn quantile(&self, p: f64) -> Option<f64> {
1202        if !(0.0..=1.0).contains(&p) {
1203            return None;
1204        }
1205        if p == 0.0 {
1206            return Some(0.0);
1207        }
1208        if (p - 1.0).abs() < 1e-15 {
1209            return Some(f64::INFINITY);
1210        }
1211
1212        // Chi2(k) = Gamma(k/2, rate=1/2)
1213        let gamma = GammaDistribution {
1214            shape: self.k / 2.0,
1215            rate: 0.5,
1216        };
1217        gamma.quantile(p)
1218    }
1219}
1220
1221// ============================================================================
1222// Tests
1223// ============================================================================
1224
1225#[cfg(test)]
1226mod tests {
1227    use super::*;
1228
1229    // --- Uniform ---
1230
1231    #[test]
1232    fn test_uniform_basic() {
1233        let u = Uniform::new(0.0, 10.0).unwrap();
1234        assert!((u.mean() - 5.0).abs() < 1e-15);
1235        assert!((u.variance() - 100.0 / 12.0).abs() < 1e-10);
1236    }
1237
1238    #[test]
1239    fn test_uniform_cdf() {
1240        let u = Uniform::new(0.0, 10.0).unwrap();
1241        assert_eq!(u.cdf(-1.0), 0.0);
1242        assert!((u.cdf(5.0) - 0.5).abs() < 1e-15);
1243        assert_eq!(u.cdf(11.0), 1.0);
1244    }
1245
1246    #[test]
1247    fn test_uniform_quantile() {
1248        let u = Uniform::new(2.0, 8.0).unwrap();
1249        assert_eq!(u.quantile(0.0), Some(2.0));
1250        assert_eq!(u.quantile(1.0), Some(8.0));
1251        assert!((u.quantile(0.5).unwrap() - 5.0).abs() < 1e-15);
1252    }
1253
1254    #[test]
1255    fn test_uniform_pdf() {
1256        let u = Uniform::new(0.0, 5.0).unwrap();
1257        assert!((u.pdf(2.5) - 0.2).abs() < 1e-15);
1258        assert_eq!(u.pdf(-1.0), 0.0);
1259    }
1260
1261    #[test]
1262    fn test_uniform_invalid() {
1263        assert!(Uniform::new(5.0, 5.0).is_err());
1264        assert!(Uniform::new(5.0, 3.0).is_err());
1265        assert!(Uniform::new(f64::NAN, 5.0).is_err());
1266    }
1267
1268    // --- Triangular ---
1269
1270    #[test]
1271    fn test_triangular_mean() {
1272        let t = Triangular::new(0.0, 3.0, 6.0).unwrap();
1273        assert!((t.mean() - 3.0).abs() < 1e-15);
1274    }
1275
1276    #[test]
1277    fn test_triangular_symmetric_variance() {
1278        let t = Triangular::new(0.0, 5.0, 10.0).unwrap();
1279        // Var = (0+100+25-0-0-50)/18 = 75/18 ≈ 4.1667
1280        let expected = (0.0 + 25.0 + 100.0 - 0.0 - 0.0 - 50.0) / 18.0;
1281        assert!((t.variance() - expected).abs() < 1e-10);
1282    }
1283
1284    #[test]
1285    fn test_triangular_cdf() {
1286        let t = Triangular::new(0.0, 5.0, 10.0).unwrap();
1287        assert!((t.cdf(0.0)).abs() < 1e-15);
1288        assert!((t.cdf(10.0) - 1.0).abs() < 1e-15);
1289        // At mode: F(5) = (5-0)²/((10-0)*(5-0)) = 25/50 = 0.5
1290        assert!((t.cdf(5.0) - 0.5).abs() < 1e-15);
1291    }
1292
1293    #[test]
1294    fn test_triangular_quantile() {
1295        let t = Triangular::new(0.0, 5.0, 10.0).unwrap();
1296        assert!((t.quantile(0.0).unwrap() - 0.0).abs() < 1e-15);
1297        assert!((t.quantile(1.0).unwrap() - 10.0).abs() < 1e-15);
1298        assert!((t.quantile(0.5).unwrap() - 5.0).abs() < 1e-10);
1299    }
1300
1301    #[test]
1302    fn test_triangular_invalid() {
1303        assert!(Triangular::new(5.0, 3.0, 10.0).is_err()); // mode < min
1304        assert!(Triangular::new(0.0, 11.0, 10.0).is_err()); // mode > max
1305        assert!(Triangular::new(5.0, 5.0, 5.0).is_err()); // min == max
1306    }
1307
1308    // --- Normal ---
1309
1310    #[test]
1311    fn test_normal_standard() {
1312        let n = Normal::new(0.0, 1.0).unwrap();
1313        assert!((n.mean()).abs() < 1e-15);
1314        assert!((n.variance() - 1.0).abs() < 1e-15);
1315        assert!((n.cdf(0.0) - 0.5).abs() < 1e-7);
1316    }
1317
1318    #[test]
1319    fn test_normal_shifted() {
1320        let n = Normal::new(10.0, 2.0).unwrap();
1321        assert!((n.mean() - 10.0).abs() < 1e-15);
1322        assert!((n.variance() - 4.0).abs() < 1e-15);
1323        assert!((n.cdf(10.0) - 0.5).abs() < 1e-7);
1324    }
1325
1326    #[test]
1327    fn test_normal_quantile() {
1328        let n = Normal::new(0.0, 1.0).unwrap();
1329        assert!((n.quantile(0.5).unwrap()).abs() < 0.01);
1330        assert!((n.quantile(0.975).unwrap() - 1.96).abs() < 0.01);
1331    }
1332
1333    #[test]
1334    fn test_normal_invalid() {
1335        assert!(Normal::new(0.0, 0.0).is_err());
1336        assert!(Normal::new(0.0, -1.0).is_err());
1337    }
1338
1339    // --- LogNormal ---
1340
1341    #[test]
1342    fn test_lognormal_mean() {
1343        let ln = LogNormal::new(0.0, 1.0).unwrap();
1344        let expected = (0.5_f64).exp(); // exp(0 + 1/2)
1345        assert!((ln.mean() - expected).abs() < 1e-10);
1346    }
1347
1348    #[test]
1349    fn test_lognormal_cdf() {
1350        let ln = LogNormal::new(0.0, 1.0).unwrap();
1351        assert_eq!(ln.cdf(0.0), 0.0);
1352        // Median of LogNormal(0,1) = exp(0) = 1.0
1353        assert!((ln.cdf(1.0) - 0.5).abs() < 0.001);
1354    }
1355
1356    #[test]
1357    fn test_lognormal_quantile() {
1358        let ln = LogNormal::new(0.0, 1.0).unwrap();
1359        // Median = exp(μ) = 1.0
1360        let q50 = ln.quantile(0.5).unwrap();
1361        assert!((q50 - 1.0).abs() < 0.01);
1362    }
1363
1364    // --- PERT ---
1365
1366    #[test]
1367    fn test_pert_mean() {
1368        let p = Pert::new(1.0, 4.0, 7.0).unwrap();
1369        // Mean = (1 + 4*4 + 7) / 6 = 24/6 = 4.0
1370        assert!((p.mean() - 4.0).abs() < 1e-10);
1371    }
1372
1373    #[test]
1374    fn test_pert_symmetric_variance() {
1375        let p = Pert::new(0.0, 5.0, 10.0).unwrap();
1376        // For symmetric PERT: α = β = 3, range = 10
1377        // Var = 3*3/(6*6*7) * 100 = 900/2520 * 100 ≈ 3.571
1378        let expected = 9.0 / (36.0 * 7.0) * 100.0;
1379        assert!(
1380            (p.variance() - expected).abs() < 1e-10,
1381            "PERT variance: {} vs expected: {}",
1382            p.variance(),
1383            expected
1384        );
1385    }
1386
1387    #[test]
1388    fn test_pert_shape_params() {
1389        let p = Pert::new(0.0, 5.0, 10.0).unwrap();
1390        // α = 1 + 4*(5-0)/(10-0) = 1 + 2 = 3
1391        // β = 1 + 4*(10-5)/(10-0) = 1 + 2 = 3
1392        assert!((p.alpha() - 3.0).abs() < 1e-15);
1393        assert!((p.beta_param() - 3.0).abs() < 1e-15);
1394    }
1395
1396    #[test]
1397    fn test_pert_cdf_bounds() {
1398        let p = Pert::new(1.0, 4.0, 7.0).unwrap();
1399        assert_eq!(p.cdf(0.0), 0.0);
1400        assert_eq!(p.cdf(8.0), 1.0);
1401        // CDF at mean should be close to 0.5 for symmetric PERT
1402        let p_sym = Pert::new(0.0, 5.0, 10.0).unwrap();
1403        assert!((p_sym.cdf(5.0) - 0.5).abs() < 0.01);
1404    }
1405
1406    #[test]
1407    fn test_pert_cdf_monotonic() {
1408        let p = Pert::new(0.0, 3.0, 10.0).unwrap();
1409        let mut prev = 0.0;
1410        for i in 0..=100 {
1411            let x = i as f64 * 0.1;
1412            let c = p.cdf(x);
1413            assert!(c >= prev - 1e-15, "CDF not monotonic at x={x}");
1414            prev = c;
1415        }
1416    }
1417
1418    #[test]
1419    fn test_pert_quantile_symmetric_median_is_the_mode() {
1420        let p = Pert::new(0.0, 5.0, 10.0).unwrap();
1421        assert!((p.quantile(0.5).unwrap() - 5.0).abs() < 1e-9);
1422        assert_eq!(p.quantile(0.0), Some(0.0));
1423        assert_eq!(p.quantile(1.0), Some(10.0));
1424        assert_eq!(p.quantile(1.5), None);
1425    }
1426
1427    /// The quantile inverts the CDF on a strongly skewed estimate, where the
1428    /// normal approximation this replaced was off: at p = 0.05 it gave −0.30
1429    /// (below the minimum, then clamped to 0) where the exact value is 0.300,
1430    /// and at p = 0.95 it gave 4.96 where the exact value is 5.42.
1431    #[test]
1432    fn test_pert_quantile_inverts_the_cdf_when_skewed() {
1433        let p = Pert::new(0.0, 1.0, 10.0).unwrap();
1434        for &q in &[1e-6, 0.01, 0.05, 0.25, 0.5, 0.75, 0.95, 0.99] {
1435            let x = p.quantile(q).unwrap();
1436            assert!((0.0..=10.0).contains(&x));
1437            assert!(
1438                (p.cdf(x) - q).abs() < 1e-9,
1439                "p={q}: x={x}, cdf={}",
1440                p.cdf(x)
1441            );
1442        }
1443        let normal_approx_at_5 = p.mean() + special::inverse_normal_cdf(0.05) * p.std_dev();
1444        assert!((p.quantile(0.05).unwrap() - 0.300).abs() < 1e-3);
1445        assert!((p.quantile(0.95).unwrap() - 5.4207).abs() < 1e-3);
1446        assert!(
1447            normal_approx_at_5 < 0.0,
1448            "the approximation leaves the support"
1449        );
1450    }
1451
1452    #[test]
1453    fn test_pert_invalid() {
1454        assert!(Pert::new(5.0, 3.0, 10.0).is_err());
1455        assert!(Pert::new(5.0, 5.0, 5.0).is_err());
1456    }
1457
1458    #[test]
1459    fn test_pert_with_lambda() {
1460        // Higher lambda = more peaked
1461        let p4 = Pert::with_shape(0.0, 5.0, 10.0, 4.0).unwrap();
1462        let p8 = Pert::with_shape(0.0, 5.0, 10.0, 8.0).unwrap();
1463        assert!(
1464            p8.variance() < p4.variance(),
1465            "higher λ should give lower variance"
1466        );
1467    }
1468
1469    // --- Regularized Incomplete Beta Function ---
1470
1471    #[test]
1472    fn test_regularized_beta_bounds() {
1473        assert_eq!(special::regularized_incomplete_beta(0.0, 2.0, 3.0), 0.0);
1474        assert_eq!(special::regularized_incomplete_beta(1.0, 2.0, 3.0), 1.0);
1475    }
1476
1477    #[test]
1478    fn test_regularized_beta_symmetric() {
1479        // For Beta(a,a), I_{0.5}(a,a) = 0.5 by symmetry
1480        let result = special::regularized_incomplete_beta(0.5, 3.0, 3.0);
1481        assert!(
1482            (result - 0.5).abs() < 1e-8,
1483            "I_0.5(3,3) = {result}, expected 0.5"
1484        );
1485    }
1486
1487    #[test]
1488    fn test_regularized_beta_known_values() {
1489        // I_x(1,1) = x (Uniform)
1490        for &x in &[0.1, 0.3, 0.5, 0.7, 0.9] {
1491            let result = special::regularized_incomplete_beta(x, 1.0, 1.0);
1492            assert!(
1493                (result - x).abs() < 1e-10,
1494                "I_{x}(1,1) = {result}, expected {x}"
1495            );
1496        }
1497
1498        // I_x(1,b) = 1 - (1-x)^b
1499        for &x in &[0.1, 0.5, 0.9] {
1500            let result = special::regularized_incomplete_beta(x, 1.0, 3.0);
1501            let expected = 1.0 - (1.0 - x).powi(3);
1502            assert!(
1503                (result - expected).abs() < 1e-10,
1504                "I_{x}(1,3) = {result}, expected {expected}"
1505            );
1506        }
1507    }
1508
1509    // --- ln_gamma ---
1510
1511    #[test]
1512    fn test_ln_gamma_known() {
1513        // Γ(1) = 1, ln(1) = 0
1514        assert!((special::ln_gamma(1.0)).abs() < 1e-10);
1515        // Γ(2) = 1, ln(1) = 0
1516        assert!((special::ln_gamma(2.0)).abs() < 1e-10);
1517        // Γ(3) = 2, ln(2) ≈ 0.6931
1518        assert!((special::ln_gamma(3.0) - 2.0_f64.ln()).abs() < 1e-10);
1519        // Γ(5) = 24, ln(24) ≈ 3.1781
1520        assert!((special::ln_gamma(5.0) - 24.0_f64.ln()).abs() < 1e-10);
1521        // Γ(0.5) = √π
1522        assert!(
1523            (special::ln_gamma(0.5) - std::f64::consts::PI.sqrt().ln()).abs() < 1e-10,
1524            "ln Γ(0.5) = {}, expected {}",
1525            special::ln_gamma(0.5),
1526            std::f64::consts::PI.sqrt().ln()
1527        );
1528    }
1529
1530    // --- Weibull ---
1531
1532    #[test]
1533    fn test_weibull_exponential_special_case() {
1534        // β=1 → Exponential(λ=1/η), mean=η, variance=η²
1535        let w = Weibull::new(1.0, 5.0).unwrap();
1536        assert!((w.mean() - 5.0).abs() < 1e-10);
1537        assert!((w.variance() - 25.0).abs() < 1e-8);
1538    }
1539
1540    #[test]
1541    fn test_weibull_rayleigh_special_case() {
1542        // β=2 → Rayleigh, mean = η√(π/4) = η·Γ(1.5) = η·(√π/2)
1543        let w = Weibull::new(2.0, 1.0).unwrap();
1544        let expected_mean = std::f64::consts::PI.sqrt() / 2.0;
1545        assert!(
1546            (w.mean() - expected_mean).abs() < 1e-10,
1547            "Weibull(2,1) mean = {}, expected {}",
1548            w.mean(),
1549            expected_mean
1550        );
1551    }
1552
1553    #[test]
1554    fn test_weibull_cdf_known_values() {
1555        // F(t) = 1 - exp(-(t/η)^β)
1556        let w = Weibull::new(2.0, 10.0).unwrap();
1557        // F(10) = 1 - exp(-1) ≈ 0.6321
1558        assert!((w.cdf(10.0) - (1.0 - (-1.0_f64).exp())).abs() < 1e-10);
1559        // F(0) = 0
1560        assert_eq!(w.cdf(0.0), 0.0);
1561        // F(-1) = 0
1562        assert_eq!(w.cdf(-1.0), 0.0);
1563    }
1564
1565    #[test]
1566    fn test_weibull_pdf_basic() {
1567        // β=1, η=1: f(t) = exp(-t)
1568        let w = Weibull::new(1.0, 1.0).unwrap();
1569        assert!((w.pdf(0.0) - 1.0).abs() < 1e-10);
1570        assert!((w.pdf(1.0) - (-1.0_f64).exp()).abs() < 1e-10);
1571        assert!((w.pdf(2.0) - (-2.0_f64).exp()).abs() < 1e-10);
1572    }
1573
1574    #[test]
1575    fn test_weibull_pdf_negative() {
1576        let w = Weibull::new(2.0, 5.0).unwrap();
1577        assert_eq!(w.pdf(-1.0), 0.0);
1578    }
1579
1580    #[test]
1581    fn test_weibull_quantile_roundtrip() {
1582        let w = Weibull::new(2.5, 100.0).unwrap();
1583        for &p in &[0.1, 0.25, 0.5, 0.75, 0.9] {
1584            let t = w.quantile(p).unwrap();
1585            let p_back = w.cdf(t);
1586            assert!(
1587                (p_back - p).abs() < 1e-10,
1588                "roundtrip: p={p} -> t={t} -> p_back={p_back}"
1589            );
1590        }
1591    }
1592
1593    #[test]
1594    fn test_weibull_quantile_edge_cases() {
1595        let w = Weibull::new(2.0, 10.0).unwrap();
1596        assert_eq!(w.quantile(0.0), Some(0.0));
1597        assert_eq!(w.quantile(1.0), None);
1598        assert_eq!(w.quantile(-0.1), None);
1599    }
1600
1601    #[test]
1602    fn test_weibull_reliability() {
1603        let w = Weibull::new(2.0, 10.0).unwrap();
1604        // R(t) = 1 - F(t)
1605        for &t in &[1.0, 5.0, 10.0, 20.0] {
1606            let r = w.reliability(t);
1607            let f = w.cdf(t);
1608            assert!((r + f - 1.0).abs() < 1e-14);
1609        }
1610    }
1611
1612    #[test]
1613    fn test_weibull_hazard_rate() {
1614        // β=1 → constant hazard rate = 1/η
1615        let w = Weibull::new(1.0, 5.0).unwrap();
1616        for &t in &[1.0, 5.0, 10.0] {
1617            assert!((w.hazard_rate(t) - 0.2).abs() < 1e-10);
1618        }
1619        // β=2 → h(t) = 2t/η², linearly increasing
1620        let w2 = Weibull::new(2.0, 10.0).unwrap();
1621        assert!((w2.hazard_rate(5.0) - 2.0 * 5.0 / 100.0).abs() < 1e-10);
1622    }
1623
1624    #[test]
1625    fn test_weibull_invalid() {
1626        assert!(Weibull::new(0.0, 1.0).is_err());
1627        assert!(Weibull::new(-1.0, 1.0).is_err());
1628        assert!(Weibull::new(1.0, 0.0).is_err());
1629        assert!(Weibull::new(1.0, -1.0).is_err());
1630        assert!(Weibull::new(f64::NAN, 1.0).is_err());
1631        assert!(Weibull::new(1.0, f64::INFINITY).is_err());
1632    }
1633
1634    // --- Exponential ---
1635
1636    #[test]
1637    fn test_exponential_basic() {
1638        let e = Exponential::new(0.5).unwrap();
1639        assert!((e.rate() - 0.5).abs() < 1e-10);
1640        assert!((e.mean() - 2.0).abs() < 1e-10);
1641        assert!((e.variance() - 4.0).abs() < 1e-10);
1642    }
1643
1644    #[test]
1645    fn test_exponential_pdf() {
1646        let e = Exponential::new(1.0).unwrap();
1647        assert!((e.pdf(0.0) - 1.0).abs() < 1e-10);
1648        assert!((e.pdf(1.0) - (-1.0_f64).exp()).abs() < 1e-10);
1649        assert_eq!(e.pdf(-1.0), 0.0);
1650    }
1651
1652    #[test]
1653    fn test_exponential_cdf() {
1654        let e = Exponential::new(1.0).unwrap();
1655        assert_eq!(e.cdf(-1.0), 0.0);
1656        assert_eq!(e.cdf(0.0), 0.0);
1657        assert!((e.cdf(1.0) - (1.0 - (-1.0_f64).exp())).abs() < 1e-10);
1658    }
1659
1660    #[test]
1661    fn test_exponential_quantile() {
1662        let e = Exponential::new(2.0).unwrap();
1663        assert_eq!(e.quantile(0.0), Some(0.0));
1664        assert_eq!(e.quantile(1.0), Some(f64::INFINITY));
1665        let q = e.quantile(0.5).unwrap();
1666        // Median = ln(2)/λ = ln(2)/2
1667        assert!((q - 2.0_f64.ln() / 2.0).abs() < 1e-10);
1668        assert!(e.quantile(-0.1).is_none());
1669        assert!(e.quantile(1.1).is_none());
1670    }
1671
1672    #[test]
1673    fn test_exponential_quantile_roundtrip() {
1674        let e = Exponential::new(3.0).unwrap();
1675        for &p in &[0.01, 0.1, 0.25, 0.5, 0.75, 0.9, 0.99] {
1676            let x = e.quantile(p).unwrap();
1677            let p_back = e.cdf(x);
1678            assert!((p_back - p).abs() < 1e-10, "p={p}, x={x}, p_back={p_back}");
1679        }
1680    }
1681
1682    #[test]
1683    fn test_exponential_memoryless() {
1684        // P(X > s+t | X > s) = P(X > t)
1685        let e = Exponential::new(1.5).unwrap();
1686        let s = 2.0;
1687        let t = 3.0;
1688        let lhs = e.survival(s + t) / e.survival(s);
1689        let rhs = e.survival(t);
1690        assert!((lhs - rhs).abs() < 1e-10);
1691    }
1692
1693    #[test]
1694    fn test_exponential_invalid() {
1695        assert!(Exponential::new(0.0).is_err());
1696        assert!(Exponential::new(-1.0).is_err());
1697        assert!(Exponential::new(f64::NAN).is_err());
1698        assert!(Exponential::new(f64::INFINITY).is_err());
1699    }
1700
1701    // --- Gamma Distribution ---
1702
1703    #[test]
1704    fn test_gamma_basic() {
1705        let g = GammaDistribution::new(2.0, 1.0).unwrap();
1706        assert!((g.shape() - 2.0).abs() < 1e-10);
1707        assert!((g.rate() - 1.0).abs() < 1e-10);
1708        assert!((g.scale() - 1.0).abs() < 1e-10);
1709        assert!((g.mean() - 2.0).abs() < 1e-10);
1710        assert!((g.variance() - 2.0).abs() < 1e-10);
1711        assert!((g.mode() - 1.0).abs() < 1e-10);
1712    }
1713
1714    #[test]
1715    fn test_gamma_from_shape_scale() {
1716        let g = GammaDistribution::from_shape_scale(3.0, 2.0).unwrap();
1717        assert!((g.shape() - 3.0).abs() < 1e-10);
1718        assert!((g.rate() - 0.5).abs() < 1e-10);
1719        assert!((g.mean() - 6.0).abs() < 1e-10); // α/β = 3/0.5 = 6
1720    }
1721
1722    #[test]
1723    fn test_gamma_pdf_integral() {
1724        // Numerical integration should ≈ 1
1725        let g = GammaDistribution::new(3.0, 2.0).unwrap();
1726        let n = 20_000;
1727        let dt = 20.0 / n as f64;
1728        let integral: f64 = (0..n)
1729            .map(|i| {
1730                let x = (i as f64 + 0.5) * dt;
1731                g.pdf(x) * dt
1732            })
1733            .sum();
1734        assert!((integral - 1.0).abs() < 0.01, "PDF integral = {integral}");
1735    }
1736
1737    #[test]
1738    fn test_gamma_cdf_known() {
1739        // Gamma(1, 1) = Exponential(1): CDF(1) = 1 - e^{-1}
1740        let g = GammaDistribution::new(1.0, 1.0).unwrap();
1741        let expected = 1.0 - (-1.0_f64).exp();
1742        assert!((g.cdf(1.0) - expected).abs() < 1e-10);
1743    }
1744
1745    #[test]
1746    fn test_gamma_cdf_chi2() {
1747        // Chi-squared(k) = Gamma(k/2, 1/2)
1748        // For k=2, CDF(x) = 1 - exp(-x/2)
1749        let g = GammaDistribution::new(1.0, 0.5).unwrap(); // Gamma(1, 0.5) = Exp(0.5)
1750        let x = 4.0;
1751        let expected = 1.0 - (-0.5_f64 * x).exp();
1752        assert!(
1753            (g.cdf(x) - expected).abs() < 1e-8,
1754            "CDF({x}) = {} vs {expected}",
1755            g.cdf(x)
1756        );
1757    }
1758
1759    #[test]
1760    fn test_gamma_quantile_roundtrip() {
1761        let g = GammaDistribution::new(5.0, 2.0).unwrap();
1762        for &p in &[0.01, 0.1, 0.25, 0.5, 0.75, 0.9, 0.99] {
1763            let x = g.quantile(p).unwrap();
1764            let p_back = g.cdf(x);
1765            assert!((p_back - p).abs() < 1e-6, "p={p}, x={x}, p_back={p_back}");
1766        }
1767    }
1768
1769    #[test]
1770    fn test_gamma_mode_small_shape() {
1771        let g = GammaDistribution::new(0.5, 1.0).unwrap();
1772        assert_eq!(g.mode(), 0.0); // α < 1
1773    }
1774
1775    #[test]
1776    fn test_gamma_invalid() {
1777        assert!(GammaDistribution::new(0.0, 1.0).is_err());
1778        assert!(GammaDistribution::new(-1.0, 1.0).is_err());
1779        assert!(GammaDistribution::new(1.0, 0.0).is_err());
1780        assert!(GammaDistribution::new(1.0, -1.0).is_err());
1781        assert!(GammaDistribution::new(f64::NAN, 1.0).is_err());
1782    }
1783
1784    // --- Beta Distribution ---
1785
1786    #[test]
1787    fn test_beta_mean_variance() {
1788        let b = BetaDistribution::new(2.0, 5.0).unwrap();
1789        // Mean = 2/7
1790        assert!((b.mean() - 2.0 / 7.0).abs() < 1e-10);
1791        // Variance = 2*5 / (49 * 8) = 10/392
1792        let expected_var = 2.0 * 5.0 / (7.0 * 7.0 * 8.0);
1793        assert!((b.variance() - expected_var).abs() < 1e-10);
1794    }
1795
1796    #[test]
1797    fn test_beta_symmetric() {
1798        // Beta(3, 3) is symmetric around 0.5
1799        let b = BetaDistribution::new(3.0, 3.0).unwrap();
1800        assert!((b.mean() - 0.5).abs() < 1e-10);
1801        assert!((b.mode().unwrap() - 0.5).abs() < 1e-10);
1802        // PDF symmetric: f(0.3) = f(0.7)
1803        assert!((b.pdf(0.3) - b.pdf(0.7)).abs() < 1e-10);
1804    }
1805
1806    #[test]
1807    fn test_beta_uniform_special_case() {
1808        // Beta(1, 1) = Uniform(0, 1)
1809        let b = BetaDistribution::new(1.0, 1.0).unwrap();
1810        assert!((b.mean() - 0.5).abs() < 1e-10);
1811        assert!((b.cdf(0.5) - 0.5).abs() < 1e-10);
1812        assert!((b.cdf(0.25) - 0.25).abs() < 1e-10);
1813    }
1814
1815    #[test]
1816    fn test_beta_cdf_boundaries() {
1817        let b = BetaDistribution::new(2.0, 3.0).unwrap();
1818        assert_eq!(b.cdf(0.0), 0.0);
1819        assert_eq!(b.cdf(-1.0), 0.0);
1820        assert_eq!(b.cdf(1.0), 1.0);
1821        assert_eq!(b.cdf(2.0), 1.0);
1822    }
1823
1824    #[test]
1825    fn test_beta_pdf_integral() {
1826        let b = BetaDistribution::new(2.0, 5.0).unwrap();
1827        let n = 10_000;
1828        let dt = 1.0 / n as f64;
1829        let integral: f64 = (0..n)
1830            .map(|i| {
1831                let x = (i as f64 + 0.5) * dt;
1832                b.pdf(x) * dt
1833            })
1834            .sum();
1835        assert!((integral - 1.0).abs() < 0.01, "PDF integral = {integral}");
1836    }
1837
1838    #[test]
1839    fn test_beta_quantile_roundtrip() {
1840        let b = BetaDistribution::new(2.0, 5.0).unwrap();
1841        for &p in &[0.01, 0.1, 0.25, 0.5, 0.75, 0.9, 0.99] {
1842            let x = b.quantile(p).unwrap();
1843            let p_back = b.cdf(x);
1844            assert!((p_back - p).abs() < 1e-6, "p={p}, x={x}, p_back={p_back}");
1845        }
1846    }
1847
1848    #[test]
1849    fn test_beta_mode() {
1850        let b = BetaDistribution::new(5.0, 3.0).unwrap();
1851        // mode = (5-1)/(5+3-2) = 4/6 = 2/3
1852        assert!((b.mode().unwrap() - 2.0 / 3.0).abs() < 1e-10);
1853    }
1854
1855    #[test]
1856    fn test_beta_mode_undefined() {
1857        // Beta(0.5, 0.5) — U-shaped, no interior mode
1858        let b = BetaDistribution::new(0.5, 0.5).unwrap();
1859        assert!(b.mode().is_none());
1860    }
1861
1862    #[test]
1863    fn test_beta_invalid() {
1864        assert!(BetaDistribution::new(0.0, 1.0).is_err());
1865        assert!(BetaDistribution::new(-1.0, 1.0).is_err());
1866        assert!(BetaDistribution::new(1.0, 0.0).is_err());
1867        assert!(BetaDistribution::new(f64::NAN, 1.0).is_err());
1868    }
1869
1870    // --- Chi-Squared Distribution ---
1871
1872    #[test]
1873    fn test_chi2_mean_variance() {
1874        let chi2 = ChiSquared::new(5.0).unwrap();
1875        assert!((chi2.mean() - 5.0).abs() < 1e-10);
1876        assert!((chi2.variance() - 10.0).abs() < 1e-10);
1877    }
1878
1879    #[test]
1880    fn test_chi2_exp_special_case() {
1881        // Chi2(2) = Exp(1/2): CDF(x) = 1 - exp(-x/2)
1882        let chi2 = ChiSquared::new(2.0).unwrap();
1883        let x = 4.0;
1884        let expected = 1.0 - (-x / 2.0_f64).exp();
1885        assert!(
1886            (chi2.cdf(x) - expected).abs() < 1e-8,
1887            "CDF({x}) = {} vs {expected}",
1888            chi2.cdf(x)
1889        );
1890    }
1891
1892    #[test]
1893    fn test_chi2_cdf_boundaries() {
1894        let chi2 = ChiSquared::new(3.0).unwrap();
1895        assert_eq!(chi2.cdf(0.0), 0.0);
1896        assert_eq!(chi2.cdf(-1.0), 0.0);
1897        // CDF should approach 1 for large x
1898        assert!(chi2.cdf(100.0) > 0.999);
1899    }
1900
1901    #[test]
1902    fn test_chi2_pdf_integral() {
1903        let chi2 = ChiSquared::new(4.0).unwrap();
1904        let n = 20_000;
1905        let dt = 30.0 / n as f64;
1906        let integral: f64 = (0..n)
1907            .map(|i| {
1908                let x = (i as f64 + 0.5) * dt;
1909                chi2.pdf(x) * dt
1910            })
1911            .sum();
1912        assert!((integral - 1.0).abs() < 0.01, "PDF integral = {integral}");
1913    }
1914
1915    #[test]
1916    fn test_chi2_quantile_roundtrip() {
1917        let chi2 = ChiSquared::new(5.0).unwrap();
1918        for &p in &[0.01, 0.05, 0.1, 0.25, 0.5, 0.75, 0.9, 0.95, 0.99] {
1919            let x = chi2.quantile(p).unwrap();
1920            let p_back = chi2.cdf(x);
1921            assert!((p_back - p).abs() < 1e-6, "p={p}, x={x}, p_back={p_back}");
1922        }
1923    }
1924
1925    #[test]
1926    fn test_chi2_known_quantiles() {
1927        // Common chi-squared critical values (k=5)
1928        // p=0.05 → 1.1455, p=0.95 → 11.0705 (from tables)
1929        let chi2 = ChiSquared::new(5.0).unwrap();
1930        let q05 = chi2.quantile(0.05).unwrap();
1931        assert!((q05 - 1.1455).abs() < 0.01, "q(0.05) = {q05}");
1932        let q95 = chi2.quantile(0.95).unwrap();
1933        assert!((q95 - 11.0705).abs() < 0.01, "q(0.95) = {q95}");
1934    }
1935
1936    #[test]
1937    fn test_chi2_invalid() {
1938        assert!(ChiSquared::new(0.0).is_err());
1939        assert!(ChiSquared::new(-1.0).is_err());
1940        assert!(ChiSquared::new(f64::NAN).is_err());
1941    }
1942
1943    #[test]
1944    fn test_chi2_gamma_consistency() {
1945        // Chi2(k) = Gamma(k/2, 1/2)
1946        let k = 6.0;
1947        let chi2 = ChiSquared::new(k).unwrap();
1948        let gamma = GammaDistribution::new(k / 2.0, 0.5).unwrap();
1949
1950        let x = 5.0;
1951        assert!(
1952            (chi2.cdf(x) - gamma.cdf(x)).abs() < 1e-10,
1953            "Chi2 and Gamma CDF should match"
1954        );
1955    }
1956
1957    #[test]
1958    fn test_weibull_mean_variance_known() {
1959        // β=3.6, η=1000: known engineering example
1960        // Γ(1 + 1/3.6) = Γ(1.2778) ≈ 0.8946
1961        let w = Weibull::new(3.6, 1000.0).unwrap();
1962        let mean = w.mean();
1963        assert!(mean > 800.0 && mean < 1000.0, "mean={mean}");
1964        assert!(w.variance() > 0.0);
1965    }
1966
1967    #[test]
1968    fn test_weibull_pdf_integral_approx() {
1969        // Numerical integration of PDF should ≈ 1
1970        let w = Weibull::new(2.0, 10.0).unwrap();
1971        let n = 10_000;
1972        let dt = 50.0 / n as f64;
1973        let integral: f64 = (0..n)
1974            .map(|i| {
1975                let t = (i as f64 + 0.5) * dt;
1976                w.pdf(t) * dt
1977            })
1978            .sum();
1979        assert!(
1980            (integral - 1.0).abs() < 0.01,
1981            "PDF integral = {integral}, expected ≈ 1.0"
1982        );
1983    }
1984
1985    // -----------------------------------------------------------------------
1986    // Precision validation: Normal CDF/quantile reference values
1987    // -----------------------------------------------------------------------
1988
1989    /// Validates `standard_normal_cdf` against tabulated reference values.
1990    ///
1991    /// Reference: Abramowitz & Stegun (1964), Table 26.1.
1992    /// Algorithm 26.2.17 guarantees max absolute error < 7.5 × 10⁻⁸.
1993    #[test]
1994    fn test_normal_cdf_reference_values() {
1995        // Φ(0) = 0.5 (exact by symmetry)
1996        assert!(
1997            (special::standard_normal_cdf(0.0) - 0.5).abs() < 1e-7,
1998            "Φ(0) error"
1999        );
2000        // Φ(1.96) = 0.975002 (reference: A&S Table 26.1)
2001        assert!(
2002            (special::standard_normal_cdf(1.96) - 0.975002).abs() < 1e-6,
2003            "Φ(1.96) = {}, expected 0.975002",
2004            special::standard_normal_cdf(1.96)
2005        );
2006        // Φ(-1.96) = 0.024998
2007        assert!(
2008            (special::standard_normal_cdf(-1.96) - 0.024998).abs() < 1e-6,
2009            "Φ(-1.96) = {}, expected 0.024998",
2010            special::standard_normal_cdf(-1.96)
2011        );
2012        // Φ(3.0) = 0.998650 (reference)
2013        assert!(
2014            (special::standard_normal_cdf(3.0) - 0.998650).abs() < 1e-6,
2015            "Φ(3.0) = {}, expected 0.998650",
2016            special::standard_normal_cdf(3.0)
2017        );
2018        // Symmetry: Φ(x) + Φ(-x) = 1
2019        for &x in &[0.5, 1.0, 1.96, 2.5, 3.0] {
2020            let sum = special::standard_normal_cdf(x) + special::standard_normal_cdf(-x);
2021            assert!(
2022                (sum - 1.0).abs() < 1e-14,
2023                "Symmetry violated at x={x}: sum={sum}"
2024            );
2025        }
2026    }
2027
2028    /// Validates `inverse_normal_cdf` against reference values.
2029    ///
2030    /// A&S 26.2.23 has max absolute error < 4.5 × 10⁻⁴.
2031    #[test]
2032    fn test_normal_quantile_reference_values() {
2033        // Φ⁻¹(0.5) = 0.0 (A&S 26.2.23: max error < 4.5 × 10⁻⁴, typical much smaller)
2034        assert!(
2035            special::inverse_normal_cdf(0.5).abs() < 5e-4,
2036            "Φ⁻¹(0.5) = {}, expected 0.0",
2037            special::inverse_normal_cdf(0.5)
2038        );
2039        // Φ⁻¹(0.975) ≈ 1.95996 (exact: 1.959964...)
2040        let q975 = special::inverse_normal_cdf(0.975);
2041        assert!(
2042            (q975 - 1.95996).abs() < 5e-4,
2043            "Φ⁻¹(0.975) = {q975}, expected ≈ 1.95996"
2044        );
2045        // Φ⁻¹(0.025) ≈ -1.95996 (antisymmetry)
2046        let q025 = special::inverse_normal_cdf(0.025);
2047        assert!(
2048            (q025 + 1.95996).abs() < 5e-4,
2049            "Φ⁻¹(0.025) = {q025}, expected ≈ -1.95996"
2050        );
2051        // Roundtrip: Φ(Φ⁻¹(p)) = p (within CDF accuracy)
2052        for &p in &[0.01, 0.05, 0.1, 0.25, 0.5, 0.75, 0.9, 0.95, 0.99] {
2053            let z = special::inverse_normal_cdf(p);
2054            let p_back = special::standard_normal_cdf(z);
2055            assert!(
2056                (p_back - p).abs() < 1e-3,
2057                "Roundtrip failed: p={p}, z={z}, p_back={p_back}"
2058            );
2059        }
2060    }
2061
2062    // -----------------------------------------------------------------------
2063    // Precision validation: Gamma function reference values
2064    // -----------------------------------------------------------------------
2065
2066    /// Validates `gamma` against exact reference values.
2067    ///
2068    /// The Lanczos approximation has relative error < 2 × 10⁻¹⁰.
2069    #[test]
2070    fn test_gamma_reference_values() {
2071        // Γ(0.5) = √π ≈ 1.77245385090552
2072        let sqrt_pi = std::f64::consts::PI.sqrt();
2073        assert!(
2074            (special::gamma(0.5) - sqrt_pi).abs() < 1e-7,
2075            "Γ(0.5) = {}, expected √π = {}",
2076            special::gamma(0.5),
2077            sqrt_pi
2078        );
2079        // Γ(1.0) = 1 (exact)
2080        assert!(
2081            (special::gamma(1.0) - 1.0).abs() < 1e-14,
2082            "Γ(1.0) = {}",
2083            special::gamma(1.0)
2084        );
2085        // Γ(1.5) = (1/2)·Γ(0.5) = √π/2 ≈ 0.88622693
2086        let gamma_1_5 = sqrt_pi / 2.0;
2087        assert!(
2088            (special::gamma(1.5) - gamma_1_5).abs() < 1e-7,
2089            "Γ(1.5) = {}, expected {}",
2090            special::gamma(1.5),
2091            gamma_1_5
2092        );
2093        // Γ(2.0) = 1! = 1 (exact)
2094        assert!(
2095            (special::gamma(2.0) - 1.0).abs() < 1e-14,
2096            "Γ(2.0) = {}",
2097            special::gamma(2.0)
2098        );
2099        // Γ(3.0) = 2! = 2 (exact)
2100        assert!(
2101            (special::gamma(3.0) - 2.0).abs() < 1e-13,
2102            "Γ(3.0) = {}",
2103            special::gamma(3.0)
2104        );
2105        // Γ(4.0) = 3! = 6 (exact)
2106        assert!(
2107            (special::gamma(4.0) - 6.0).abs() < 1e-12,
2108            "Γ(4.0) = {}",
2109            special::gamma(4.0)
2110        );
2111        // Γ(5.0) = 4! = 24 (exact)
2112        assert!(
2113            (special::gamma(5.0) - 24.0).abs() < 1e-8,
2114            "Γ(5.0) = {}, expected 24.0",
2115            special::gamma(5.0)
2116        );
2117    }
2118
2119    // -----------------------------------------------------------------------
2120    // Precision validation: Gamma distribution PDF at known point
2121    // -----------------------------------------------------------------------
2122
2123    /// Gamma(α=2, β=1) PDF at x=1: f(1) = x^(α-1)·exp(-βx)/Γ(α) = e⁻¹ ≈ 0.36788.
2124    ///
2125    /// Reference: f(x; α, β) = β^α·x^(α-1)·exp(-βx)/Γ(α)
2126    /// f(1; 2, 1) = 1^2·1^1·exp(-1)/Γ(2) = exp(-1)/1 = e⁻¹ ≈ 0.36788
2127    #[test]
2128    fn test_gamma_pdf_reference_value() {
2129        let g = GammaDistribution::new(2.0, 1.0).unwrap();
2130        let expected = (-1.0_f64).exp(); // e⁻¹ ≈ 0.36787944
2131        assert!(
2132            (g.pdf(1.0) - expected).abs() < 1e-5,
2133            "Gamma(2,1).pdf(1) = {}, expected e⁻¹ = {}",
2134            g.pdf(1.0),
2135            expected
2136        );
2137    }
2138}
2139
2140#[cfg(test)]
2141mod proptests {
2142    use super::*;
2143    use proptest::prelude::*;
2144
2145    proptest! {
2146        #![proptest_config(ProptestConfig::with_cases(300))]
2147
2148        // --- Uniform ---
2149
2150        #[test]
2151        fn uniform_cdf_in_01(
2152            min in -100.0_f64..0.0,
2153            max in 1.0_f64..100.0,
2154            x in -200.0_f64..200.0,
2155        ) {
2156            let u = Uniform::new(min, max).unwrap();
2157            let c = u.cdf(x);
2158            prop_assert!((0.0..=1.0).contains(&c));
2159        }
2160
2161        #[test]
2162        fn uniform_quantile_roundtrip(
2163            min in -100.0_f64..0.0,
2164            max in 1.0_f64..100.0,
2165            p in 0.0_f64..=1.0,
2166        ) {
2167            let u = Uniform::new(min, max).unwrap();
2168            let x = u.quantile(p).unwrap();
2169            let p_back = u.cdf(x);
2170            prop_assert!((p_back - p).abs() < 1e-12, "roundtrip: p={p} -> x={x} -> p_back={p_back}");
2171        }
2172
2173        // --- Triangular ---
2174
2175        #[test]
2176        fn triangular_cdf_in_01(
2177            min in -100.0_f64..-1.0,
2178            mode_frac in 0.0_f64..=1.0,
2179            range in 1.0_f64..100.0,
2180            x in -200.0_f64..200.0,
2181        ) {
2182            let max = min + range;
2183            let mode = min + mode_frac * range;
2184            let t = Triangular::new(min, mode, max).unwrap();
2185            let c = t.cdf(x);
2186            prop_assert!((0.0..=1.0).contains(&c));
2187        }
2188
2189        #[test]
2190        fn triangular_quantile_roundtrip(
2191            min in -50.0_f64..0.0,
2192            mode_frac in 0.01_f64..0.99,
2193            range in 1.0_f64..50.0,
2194            p in 0.001_f64..0.999,
2195        ) {
2196            let max = min + range;
2197            let mode = min + mode_frac * range;
2198            let t = Triangular::new(min, mode, max).unwrap();
2199            let x = t.quantile(p).unwrap();
2200            let p_back = t.cdf(x);
2201            prop_assert!(
2202                (p_back - p).abs() < 1e-8,
2203                "roundtrip: p={p} -> x={x} -> p_back={p_back}"
2204            );
2205        }
2206
2207        // --- PERT ---
2208
2209        #[test]
2210        fn pert_mean_formula(
2211            min in -50.0_f64..0.0,
2212            mode_frac in 0.01_f64..0.99,
2213            range in 1.0_f64..50.0,
2214        ) {
2215            let max = min + range;
2216            let mode = min + mode_frac * range;
2217            let p = Pert::new(min, mode, max).unwrap();
2218            let expected = (min + 4.0 * mode + max) / 6.0;
2219            prop_assert!(
2220                (p.mean() - expected).abs() < 1e-10,
2221                "PERT mean: {} vs expected: {}",
2222                p.mean(),
2223                expected
2224            );
2225        }
2226
2227        #[test]
2228        fn pert_variance_positive(
2229            min in -50.0_f64..0.0,
2230            mode_frac in 0.01_f64..0.99,
2231            range in 1.0_f64..50.0,
2232        ) {
2233            let max = min + range;
2234            let mode = min + mode_frac * range;
2235            let p = Pert::new(min, mode, max).unwrap();
2236            prop_assert!(p.variance() > 0.0);
2237        }
2238
2239        #[test]
2240        fn pert_cdf_monotonic(
2241            min in -50.0_f64..0.0,
2242            mode_frac in 0.05_f64..0.95,
2243            range in 2.0_f64..50.0,
2244        ) {
2245            let max = min + range;
2246            let mode = min + mode_frac * range;
2247            let p = Pert::new(min, mode, max).unwrap();
2248            let mut prev = 0.0;
2249            for i in 0..=20 {
2250                let x = min + (i as f64 / 20.0) * range;
2251                let c = p.cdf(x);
2252                prop_assert!(c >= prev - 1e-10, "CDF not monotonic at x={x}");
2253                prev = c;
2254            }
2255        }
2256
2257        // --- Weibull ---
2258
2259        #[test]
2260        fn weibull_cdf_in_01(
2261            shape in 0.1_f64..10.0,
2262            scale in 0.1_f64..100.0,
2263            t in 0.0_f64..200.0,
2264        ) {
2265            let w = Weibull::new(shape, scale).unwrap();
2266            let c = w.cdf(t);
2267            prop_assert!((0.0..=1.0).contains(&c), "CDF({t}) = {c} out of [0,1]");
2268        }
2269
2270        #[test]
2271        fn weibull_quantile_roundtrip(
2272            shape in 0.5_f64..10.0,
2273            scale in 1.0_f64..100.0,
2274            p in 0.001_f64..0.999,
2275        ) {
2276            let w = Weibull::new(shape, scale).unwrap();
2277            let t = w.quantile(p).unwrap();
2278            let p_back = w.cdf(t);
2279            prop_assert!(
2280                (p_back - p).abs() < 1e-8,
2281                "roundtrip: p={p} -> t={t} -> p_back={p_back}"
2282            );
2283        }
2284
2285        #[test]
2286        fn weibull_pdf_non_negative(
2287            shape in 0.1_f64..10.0,
2288            scale in 0.1_f64..100.0,
2289            t in 0.0_f64..200.0,
2290        ) {
2291            let w = Weibull::new(shape, scale).unwrap();
2292            prop_assert!(w.pdf(t) >= 0.0, "PDF({t}) must be >= 0");
2293        }
2294
2295        #[test]
2296        fn weibull_reliability_plus_cdf_is_one(
2297            shape in 0.5_f64..5.0,
2298            scale in 1.0_f64..50.0,
2299            t in 0.001_f64..100.0,
2300        ) {
2301            let w = Weibull::new(shape, scale).unwrap();
2302            let sum = w.cdf(t) + w.reliability(t);
2303            prop_assert!(
2304                (sum - 1.0).abs() < 1e-12,
2305                "CDF + R should = 1, got {sum}"
2306            );
2307        }
2308
2309        #[test]
2310        fn weibull_variance_positive(
2311            shape in 0.1_f64..10.0,
2312            scale in 0.1_f64..100.0,
2313        ) {
2314            let w = Weibull::new(shape, scale).unwrap();
2315            prop_assert!(w.variance() > 0.0, "variance must be positive");
2316        }
2317
2318        // --- Exponential ---
2319
2320        #[test]
2321        fn exponential_cdf_in_01(
2322            rate in 0.01_f64..10.0,
2323            x in 0.0_f64..100.0,
2324        ) {
2325            let e = Exponential::new(rate).unwrap();
2326            let c = e.cdf(x);
2327            prop_assert!((0.0..=1.0).contains(&c), "CDF({x}) = {c}");
2328        }
2329
2330        #[test]
2331        fn exponential_quantile_roundtrip(
2332            rate in 0.01_f64..10.0,
2333            p in 0.001_f64..0.999,
2334        ) {
2335            let e = Exponential::new(rate).unwrap();
2336            let x = e.quantile(p).unwrap();
2337            let p_back = e.cdf(x);
2338            prop_assert!((p_back - p).abs() < 1e-10);
2339        }
2340
2341        // --- Gamma ---
2342
2343        #[test]
2344        fn gamma_cdf_in_01(
2345            shape in 0.5_f64..10.0,
2346            rate in 0.1_f64..10.0,
2347            x in 0.001_f64..50.0,
2348        ) {
2349            let g = GammaDistribution::new(shape, rate).unwrap();
2350            let c = g.cdf(x);
2351            prop_assert!((0.0..=1.0).contains(&c), "CDF({x}) = {c}");
2352        }
2353
2354        #[test]
2355        fn gamma_variance_positive(
2356            shape in 0.1_f64..10.0,
2357            rate in 0.1_f64..10.0,
2358        ) {
2359            let g = GammaDistribution::new(shape, rate).unwrap();
2360            prop_assert!(g.variance() > 0.0, "variance must be positive");
2361        }
2362
2363        // --- Beta ---
2364
2365        #[test]
2366        fn beta_cdf_in_01(
2367            alpha in 0.5_f64..10.0,
2368            beta_param in 0.5_f64..10.0,
2369            x in 0.001_f64..0.999,
2370        ) {
2371            let b = BetaDistribution::new(alpha, beta_param).unwrap();
2372            let c = b.cdf(x);
2373            prop_assert!((0.0..=1.0).contains(&c), "CDF({x}) = {c}");
2374        }
2375
2376        #[test]
2377        fn beta_quantile_roundtrip(
2378            alpha in 1.0_f64..10.0,
2379            beta_param in 1.0_f64..10.0,
2380            p in 0.01_f64..0.99,
2381        ) {
2382            let b = BetaDistribution::new(alpha, beta_param).unwrap();
2383            let x = b.quantile(p).unwrap();
2384            let p_back = b.cdf(x);
2385            prop_assert!((p_back - p).abs() < 1e-5, "p={p}, p_back={p_back}");
2386        }
2387
2388        // --- Chi-Squared ---
2389
2390        #[test]
2391        fn chi2_cdf_in_01(
2392            k in 1.0_f64..20.0,
2393            x in 0.001_f64..50.0,
2394        ) {
2395            let chi2 = ChiSquared::new(k).unwrap();
2396            let c = chi2.cdf(x);
2397            prop_assert!((0.0..=1.0).contains(&c), "CDF({x}) = {c}");
2398        }
2399
2400        #[test]
2401        fn chi2_variance_positive(
2402            k in 0.5_f64..50.0,
2403        ) {
2404            let chi2 = ChiSquared::new(k).unwrap();
2405            prop_assert!(chi2.variance() > 0.0, "variance must be positive");
2406        }
2407    }
2408}