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