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