Skip to main content

u_numflow/
distributions.rs

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