1use crate::special;
27
28#[derive(Debug, Clone, PartialEq)]
30pub enum DistributionError {
31 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#[derive(Debug, Clone, PartialEq)]
59pub struct Uniform {
60 min: f64,
61 max: f64,
62}
63
64impl Uniform {
65 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 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 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 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#[derive(Debug, Clone, PartialEq)]
141pub struct Triangular {
142 min: f64,
143 mode: f64,
144 max: f64,
145}
146
147impl Triangular {
148 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 pub fn mean(&self) -> f64 {
180 (self.min + self.mode + self.max) / 3.0
181 }
182
183 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 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 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 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); 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#[derive(Debug, Clone, PartialEq)]
260pub struct Normal {
261 mu: f64,
262 sigma: f64,
263}
264
265impl Normal {
266 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 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 pub fn cdf(&self, x: f64) -> f64 {
307 let z = (x - self.mu) / self.sigma;
308 special::standard_normal_cdf(z)
309 }
310
311 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#[derive(Debug, Clone, PartialEq)]
337pub struct LogNormal {
338 mu: f64,
339 sigma: f64,
340}
341
342impl LogNormal {
343 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 pub fn mean(&self) -> f64 {
368 (self.mu + self.sigma * self.sigma / 2.0).exp()
369 }
370
371 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 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 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 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#[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 pub fn new(min: f64, mode: f64, max: f64) -> Result<Self, DistributionError> {
457 Self::with_shape(min, mode, max, 4.0)
458 }
459
460 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 pub fn mean(&self) -> f64 {
528 let ab = self.alpha + self.beta;
529 self.min + (self.alpha / ab) * (self.max - self.min)
531 }
532
533 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 pub fn std_dev(&self) -> f64 {
546 self.variance().sqrt()
547 }
548
549 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 pub fn quantile(&self, p: f64) -> Option<f64> {
582 if !(0.0..=1.0).contains(&p) {
583 return None;
584 }
585 let standard = BetaDistribution::new(self.alpha, self.beta)
586 .expect("PERT shape parameters are positive by construction")
587 .quantile(p)?;
588 Some(self.min + (self.max - self.min) * standard)
589 }
590}
591
592#[derive(Debug, Clone, PartialEq)]
615pub struct Weibull {
616 shape: f64, scale: f64, }
619
620impl Weibull {
621 pub fn new(shape: f64, scale: f64) -> Result<Self, DistributionError> {
626 if !shape.is_finite() || !scale.is_finite() || shape <= 0.0 || scale <= 0.0 {
627 return Err(DistributionError::InvalidParameters(format!(
628 "Weibull requires shape > 0 and scale > 0, got shape={shape}, scale={scale}"
629 )));
630 }
631 Ok(Self { shape, scale })
632 }
633
634 pub fn shape(&self) -> f64 {
636 self.shape
637 }
638
639 pub fn scale(&self) -> f64 {
641 self.scale
642 }
643
644 pub fn mean(&self) -> f64 {
646 self.scale * special::gamma(1.0 + 1.0 / self.shape)
647 }
648
649 pub fn variance(&self) -> f64 {
651 let g1 = special::gamma(1.0 + 1.0 / self.shape);
652 let g2 = special::gamma(1.0 + 2.0 / self.shape);
653 self.scale * self.scale * (g2 - g1 * g1)
654 }
655
656 pub fn pdf(&self, t: f64) -> f64 {
658 if t < 0.0 {
659 return 0.0;
660 }
661 if t == 0.0 {
662 return if self.shape < 1.0 {
663 f64::INFINITY
664 } else if (self.shape - 1.0).abs() < f64::EPSILON {
665 1.0 / self.scale
666 } else {
667 0.0
668 };
669 }
670 let z = t / self.scale;
671 (self.shape / self.scale) * z.powf(self.shape - 1.0) * (-z.powf(self.shape)).exp()
672 }
673
674 pub fn cdf(&self, t: f64) -> f64 {
676 if t <= 0.0 {
677 return 0.0;
678 }
679 let z = t / self.scale;
680 1.0 - (-z.powf(self.shape)).exp()
681 }
682
683 pub fn quantile(&self, p: f64) -> Option<f64> {
687 if !(0.0..1.0).contains(&p) {
688 return None;
689 }
690 if p == 0.0 {
691 return Some(0.0);
692 }
693 Some(self.scale * (-(1.0 - p).ln()).powf(1.0 / self.shape))
694 }
695
696 pub fn hazard_rate(&self, t: f64) -> f64 {
702 if t <= 0.0 {
703 return if t == 0.0 && self.shape >= 1.0 {
704 if (self.shape - 1.0).abs() < f64::EPSILON {
705 1.0 / self.scale
706 } else {
707 0.0
708 }
709 } else {
710 0.0
711 };
712 }
713 let z = t / self.scale;
714 (self.shape / self.scale) * z.powf(self.shape - 1.0)
715 }
716
717 pub fn reliability(&self, t: f64) -> f64 {
719 1.0 - self.cdf(t)
720 }
721}
722
723#[derive(Debug, Clone, Copy)]
742pub struct Exponential {
743 rate: f64,
744}
745
746impl Exponential {
747 pub fn new(rate: f64) -> Result<Self, DistributionError> {
751 if !rate.is_finite() || rate <= 0.0 {
752 return Err(DistributionError::InvalidParameters(format!(
753 "Exponential rate must be positive and finite, got {rate}"
754 )));
755 }
756 Ok(Self { rate })
757 }
758
759 pub fn rate(&self) -> f64 {
761 self.rate
762 }
763
764 pub fn mean(&self) -> f64 {
766 1.0 / self.rate
767 }
768
769 pub fn variance(&self) -> f64 {
771 1.0 / (self.rate * self.rate)
772 }
773
774 pub fn pdf(&self, x: f64) -> f64 {
776 if x < 0.0 {
777 0.0
778 } else {
779 self.rate * (-self.rate * x).exp()
780 }
781 }
782
783 pub fn cdf(&self, x: f64) -> f64 {
785 if x < 0.0 {
786 0.0
787 } else {
788 1.0 - (-self.rate * x).exp()
789 }
790 }
791
792 pub fn quantile(&self, p: f64) -> Option<f64> {
796 if !(0.0..=1.0).contains(&p) {
797 return None;
798 }
799 if p == 1.0 {
800 return Some(f64::INFINITY);
801 }
802 Some(-(1.0 - p).ln() / self.rate)
803 }
804
805 pub fn hazard_rate(&self) -> f64 {
807 self.rate
808 }
809
810 pub fn survival(&self, x: f64) -> f64 {
812 1.0 - self.cdf(x)
813 }
814}
815
816#[derive(Debug, Clone, Copy)]
843pub struct GammaDistribution {
844 shape: f64, rate: f64, }
847
848impl GammaDistribution {
849 pub fn new(shape: f64, rate: f64) -> Result<Self, DistributionError> {
853 if !shape.is_finite() || shape <= 0.0 || !rate.is_finite() || rate <= 0.0 {
854 return Err(DistributionError::InvalidParameters(format!(
855 "Gamma shape and rate must be positive and finite, got shape={shape}, rate={rate}"
856 )));
857 }
858 Ok(Self { shape, rate })
859 }
860
861 pub fn from_shape_scale(shape: f64, scale: f64) -> Result<Self, DistributionError> {
863 if !scale.is_finite() || scale <= 0.0 {
864 return Err(DistributionError::InvalidParameters(format!(
865 "Gamma scale must be positive and finite, got {scale}"
866 )));
867 }
868 Self::new(shape, 1.0 / scale)
869 }
870
871 pub fn shape(&self) -> f64 {
873 self.shape
874 }
875
876 pub fn rate(&self) -> f64 {
878 self.rate
879 }
880
881 pub fn scale(&self) -> f64 {
883 1.0 / self.rate
884 }
885
886 pub fn mean(&self) -> f64 {
888 self.shape / self.rate
889 }
890
891 pub fn variance(&self) -> f64 {
893 self.shape / (self.rate * self.rate)
894 }
895
896 pub fn mode(&self) -> f64 {
898 if self.shape >= 1.0 {
899 (self.shape - 1.0) / self.rate
900 } else {
901 0.0
902 }
903 }
904
905 pub fn pdf(&self, x: f64) -> f64 {
907 if x <= 0.0 {
908 if x == 0.0 && self.shape < 1.0 {
909 return f64::INFINITY;
910 }
911 return 0.0;
912 }
913 let log_pdf = self.shape * self.rate.ln() - special::ln_gamma(self.shape)
914 + (self.shape - 1.0) * x.ln()
915 - self.rate * x;
916 log_pdf.exp()
917 }
918
919 pub fn cdf(&self, x: f64) -> f64 {
923 if x <= 0.0 {
924 return 0.0;
925 }
926 special::regularized_lower_gamma(self.shape, self.rate * x)
927 }
928
929 pub fn quantile(&self, p: f64) -> Option<f64> {
933 if !(0.0..=1.0).contains(&p) {
934 return None;
935 }
936 if p == 0.0 {
937 return Some(0.0);
938 }
939 if p == 1.0 {
940 return Some(f64::INFINITY);
941 }
942
943 let z = crate::special::inverse_normal_cdf(p);
945 let v = 1.0 / (9.0 * self.shape);
946 let chi2_approx = self.shape * (1.0 - v + z * v.sqrt()).powi(3).max(0.001);
947 let mut x = chi2_approx / self.rate;
948
949 for _ in 0..50 {
951 let f = self.cdf(x) - p;
952 let fp = self.pdf(x);
953 if fp < 1e-300 {
954 break;
955 }
956 let step = f / fp;
957 x = (x - step).max(1e-15);
958 if step.abs() < 1e-12 * x {
959 break;
960 }
961 }
962
963 Some(x)
964 }
965}
966
967#[derive(Debug, Clone)]
996pub struct BetaDistribution {
997 pub alpha: f64,
999 pub beta: f64,
1001}
1002
1003impl BetaDistribution {
1004 pub fn new(alpha: f64, beta: f64) -> Result<Self, DistributionError> {
1009 if alpha <= 0.0 || !alpha.is_finite() {
1010 return Err(DistributionError::InvalidParameters(format!(
1011 "alpha must be positive and finite, got {alpha}"
1012 )));
1013 }
1014 if beta <= 0.0 || !beta.is_finite() {
1015 return Err(DistributionError::InvalidParameters(format!(
1016 "beta must be positive and finite, got {beta}"
1017 )));
1018 }
1019 Ok(Self { alpha, beta })
1020 }
1021
1022 pub fn mean(&self) -> f64 {
1024 self.alpha / (self.alpha + self.beta)
1025 }
1026
1027 pub fn variance(&self) -> f64 {
1029 let ab = self.alpha + self.beta;
1030 self.alpha * self.beta / (ab * ab * (ab + 1.0))
1031 }
1032
1033 pub fn mode(&self) -> Option<f64> {
1038 if self.alpha > 1.0 && self.beta > 1.0 {
1039 Some((self.alpha - 1.0) / (self.alpha + self.beta - 2.0))
1040 } else {
1041 None
1042 }
1043 }
1044
1045 pub fn pdf(&self, x: f64) -> f64 {
1047 if x <= 0.0 || x >= 1.0 {
1048 return 0.0;
1049 }
1050 let ln_pdf = (self.alpha - 1.0) * x.ln() + (self.beta - 1.0) * (1.0 - x).ln()
1051 - special::ln_beta(self.alpha, self.beta);
1052 ln_pdf.exp()
1053 }
1054
1055 pub fn cdf(&self, x: f64) -> f64 {
1057 if x <= 0.0 {
1058 return 0.0;
1059 }
1060 if x >= 1.0 {
1061 return 1.0;
1062 }
1063 special::regularized_incomplete_beta(x, self.alpha, self.beta)
1064 }
1065
1066 pub fn quantile(&self, p: f64) -> Option<f64> {
1071 if !(0.0..=1.0).contains(&p) {
1072 return None;
1073 }
1074 if p == 0.0 {
1075 return Some(0.0);
1076 }
1077 if (p - 1.0).abs() < 1e-15 {
1078 return Some(1.0);
1079 }
1080
1081 let mut lo = 0.0_f64;
1083 let mut hi = 1.0_f64;
1084
1085 for _ in 0..100 {
1086 let mid = (lo + hi) / 2.0;
1087 if hi - lo < 1e-14 {
1088 break;
1089 }
1090 if self.cdf(mid) < p {
1091 lo = mid;
1092 } else {
1093 hi = mid;
1094 }
1095 }
1096
1097 Some((lo + hi) / 2.0)
1098 }
1099}
1100
1101#[derive(Debug, Clone)]
1132pub struct ChiSquared {
1133 pub k: f64,
1135}
1136
1137impl ChiSquared {
1138 pub fn new(k: f64) -> Result<Self, DistributionError> {
1143 if k <= 0.0 || !k.is_finite() {
1144 return Err(DistributionError::InvalidParameters(format!(
1145 "k must be positive and finite, got {k}"
1146 )));
1147 }
1148 Ok(Self { k })
1149 }
1150
1151 pub fn mean(&self) -> f64 {
1153 self.k
1154 }
1155
1156 pub fn variance(&self) -> f64 {
1158 2.0 * self.k
1159 }
1160
1161 pub fn pdf(&self, x: f64) -> f64 {
1163 if x <= 0.0 {
1164 return 0.0;
1165 }
1166 let half_k = self.k / 2.0;
1167 let ln_pdf =
1168 (half_k - 1.0) * x.ln() - x / 2.0 - half_k * 2.0_f64.ln() - special::ln_gamma(half_k);
1169 ln_pdf.exp()
1170 }
1171
1172 pub fn cdf(&self, x: f64) -> f64 {
1174 if x <= 0.0 {
1175 return 0.0;
1176 }
1177 special::regularized_lower_gamma(self.k / 2.0, x / 2.0)
1178 }
1179
1180 pub fn quantile(&self, p: f64) -> Option<f64> {
1184 if !(0.0..=1.0).contains(&p) {
1185 return None;
1186 }
1187 if p == 0.0 {
1188 return Some(0.0);
1189 }
1190 if (p - 1.0).abs() < 1e-15 {
1191 return Some(f64::INFINITY);
1192 }
1193
1194 let gamma = GammaDistribution {
1196 shape: self.k / 2.0,
1197 rate: 0.5,
1198 };
1199 gamma.quantile(p)
1200 }
1201}
1202
1203#[cfg(test)]
1208mod tests {
1209 use super::*;
1210
1211 #[test]
1214 fn test_uniform_basic() {
1215 let u = Uniform::new(0.0, 10.0).unwrap();
1216 assert!((u.mean() - 5.0).abs() < 1e-15);
1217 assert!((u.variance() - 100.0 / 12.0).abs() < 1e-10);
1218 }
1219
1220 #[test]
1221 fn test_uniform_cdf() {
1222 let u = Uniform::new(0.0, 10.0).unwrap();
1223 assert_eq!(u.cdf(-1.0), 0.0);
1224 assert!((u.cdf(5.0) - 0.5).abs() < 1e-15);
1225 assert_eq!(u.cdf(11.0), 1.0);
1226 }
1227
1228 #[test]
1229 fn test_uniform_quantile() {
1230 let u = Uniform::new(2.0, 8.0).unwrap();
1231 assert_eq!(u.quantile(0.0), Some(2.0));
1232 assert_eq!(u.quantile(1.0), Some(8.0));
1233 assert!((u.quantile(0.5).unwrap() - 5.0).abs() < 1e-15);
1234 }
1235
1236 #[test]
1237 fn test_uniform_pdf() {
1238 let u = Uniform::new(0.0, 5.0).unwrap();
1239 assert!((u.pdf(2.5) - 0.2).abs() < 1e-15);
1240 assert_eq!(u.pdf(-1.0), 0.0);
1241 }
1242
1243 #[test]
1244 fn test_uniform_invalid() {
1245 assert!(Uniform::new(5.0, 5.0).is_err());
1246 assert!(Uniform::new(5.0, 3.0).is_err());
1247 assert!(Uniform::new(f64::NAN, 5.0).is_err());
1248 }
1249
1250 #[test]
1253 fn test_triangular_mean() {
1254 let t = Triangular::new(0.0, 3.0, 6.0).unwrap();
1255 assert!((t.mean() - 3.0).abs() < 1e-15);
1256 }
1257
1258 #[test]
1259 fn test_triangular_symmetric_variance() {
1260 let t = Triangular::new(0.0, 5.0, 10.0).unwrap();
1261 let expected = (0.0 + 25.0 + 100.0 - 0.0 - 0.0 - 50.0) / 18.0;
1263 assert!((t.variance() - expected).abs() < 1e-10);
1264 }
1265
1266 #[test]
1267 fn test_triangular_cdf() {
1268 let t = Triangular::new(0.0, 5.0, 10.0).unwrap();
1269 assert!((t.cdf(0.0)).abs() < 1e-15);
1270 assert!((t.cdf(10.0) - 1.0).abs() < 1e-15);
1271 assert!((t.cdf(5.0) - 0.5).abs() < 1e-15);
1273 }
1274
1275 #[test]
1276 fn test_triangular_quantile() {
1277 let t = Triangular::new(0.0, 5.0, 10.0).unwrap();
1278 assert!((t.quantile(0.0).unwrap() - 0.0).abs() < 1e-15);
1279 assert!((t.quantile(1.0).unwrap() - 10.0).abs() < 1e-15);
1280 assert!((t.quantile(0.5).unwrap() - 5.0).abs() < 1e-10);
1281 }
1282
1283 #[test]
1284 fn test_triangular_invalid() {
1285 assert!(Triangular::new(5.0, 3.0, 10.0).is_err()); assert!(Triangular::new(0.0, 11.0, 10.0).is_err()); assert!(Triangular::new(5.0, 5.0, 5.0).is_err()); }
1289
1290 #[test]
1293 fn test_normal_standard() {
1294 let n = Normal::new(0.0, 1.0).unwrap();
1295 assert!((n.mean()).abs() < 1e-15);
1296 assert!((n.variance() - 1.0).abs() < 1e-15);
1297 assert!((n.cdf(0.0) - 0.5).abs() < 1e-7);
1298 }
1299
1300 #[test]
1301 fn test_normal_shifted() {
1302 let n = Normal::new(10.0, 2.0).unwrap();
1303 assert!((n.mean() - 10.0).abs() < 1e-15);
1304 assert!((n.variance() - 4.0).abs() < 1e-15);
1305 assert!((n.cdf(10.0) - 0.5).abs() < 1e-7);
1306 }
1307
1308 #[test]
1309 fn test_normal_quantile() {
1310 let n = Normal::new(0.0, 1.0).unwrap();
1311 assert!((n.quantile(0.5).unwrap()).abs() < 0.01);
1312 assert!((n.quantile(0.975).unwrap() - 1.96).abs() < 0.01);
1313 }
1314
1315 #[test]
1316 fn test_normal_invalid() {
1317 assert!(Normal::new(0.0, 0.0).is_err());
1318 assert!(Normal::new(0.0, -1.0).is_err());
1319 }
1320
1321 #[test]
1324 fn test_lognormal_mean() {
1325 let ln = LogNormal::new(0.0, 1.0).unwrap();
1326 let expected = (0.5_f64).exp(); assert!((ln.mean() - expected).abs() < 1e-10);
1328 }
1329
1330 #[test]
1331 fn test_lognormal_cdf() {
1332 let ln = LogNormal::new(0.0, 1.0).unwrap();
1333 assert_eq!(ln.cdf(0.0), 0.0);
1334 assert!((ln.cdf(1.0) - 0.5).abs() < 0.001);
1336 }
1337
1338 #[test]
1339 fn test_lognormal_quantile() {
1340 let ln = LogNormal::new(0.0, 1.0).unwrap();
1341 let q50 = ln.quantile(0.5).unwrap();
1343 assert!((q50 - 1.0).abs() < 0.01);
1344 }
1345
1346 #[test]
1349 fn test_pert_mean() {
1350 let p = Pert::new(1.0, 4.0, 7.0).unwrap();
1351 assert!((p.mean() - 4.0).abs() < 1e-10);
1353 }
1354
1355 #[test]
1356 fn test_pert_symmetric_variance() {
1357 let p = Pert::new(0.0, 5.0, 10.0).unwrap();
1358 let expected = 9.0 / (36.0 * 7.0) * 100.0;
1361 assert!(
1362 (p.variance() - expected).abs() < 1e-10,
1363 "PERT variance: {} vs expected: {}",
1364 p.variance(),
1365 expected
1366 );
1367 }
1368
1369 #[test]
1370 fn test_pert_shape_params() {
1371 let p = Pert::new(0.0, 5.0, 10.0).unwrap();
1372 assert!((p.alpha() - 3.0).abs() < 1e-15);
1375 assert!((p.beta_param() - 3.0).abs() < 1e-15);
1376 }
1377
1378 #[test]
1379 fn test_pert_cdf_bounds() {
1380 let p = Pert::new(1.0, 4.0, 7.0).unwrap();
1381 assert_eq!(p.cdf(0.0), 0.0);
1382 assert_eq!(p.cdf(8.0), 1.0);
1383 let p_sym = Pert::new(0.0, 5.0, 10.0).unwrap();
1385 assert!((p_sym.cdf(5.0) - 0.5).abs() < 0.01);
1386 }
1387
1388 #[test]
1389 fn test_pert_cdf_monotonic() {
1390 let p = Pert::new(0.0, 3.0, 10.0).unwrap();
1391 let mut prev = 0.0;
1392 for i in 0..=100 {
1393 let x = i as f64 * 0.1;
1394 let c = p.cdf(x);
1395 assert!(c >= prev - 1e-15, "CDF not monotonic at x={x}");
1396 prev = c;
1397 }
1398 }
1399
1400 #[test]
1401 fn test_pert_quantile_symmetric_median_is_the_mode() {
1402 let p = Pert::new(0.0, 5.0, 10.0).unwrap();
1403 assert!((p.quantile(0.5).unwrap() - 5.0).abs() < 1e-9);
1404 assert_eq!(p.quantile(0.0), Some(0.0));
1405 assert_eq!(p.quantile(1.0), Some(10.0));
1406 assert_eq!(p.quantile(1.5), None);
1407 }
1408
1409 #[test]
1414 fn test_pert_quantile_inverts_the_cdf_when_skewed() {
1415 let p = Pert::new(0.0, 1.0, 10.0).unwrap();
1416 for &q in &[1e-6, 0.01, 0.05, 0.25, 0.5, 0.75, 0.95, 0.99] {
1417 let x = p.quantile(q).unwrap();
1418 assert!((0.0..=10.0).contains(&x));
1419 assert!(
1420 (p.cdf(x) - q).abs() < 1e-9,
1421 "p={q}: x={x}, cdf={}",
1422 p.cdf(x)
1423 );
1424 }
1425 let normal_approx_at_5 = p.mean() + special::inverse_normal_cdf(0.05) * p.std_dev();
1426 assert!((p.quantile(0.05).unwrap() - 0.300).abs() < 1e-3);
1427 assert!((p.quantile(0.95).unwrap() - 5.4207).abs() < 1e-3);
1428 assert!(
1429 normal_approx_at_5 < 0.0,
1430 "the approximation leaves the support"
1431 );
1432 }
1433
1434 #[test]
1435 fn test_pert_invalid() {
1436 assert!(Pert::new(5.0, 3.0, 10.0).is_err());
1437 assert!(Pert::new(5.0, 5.0, 5.0).is_err());
1438 }
1439
1440 #[test]
1441 fn test_pert_with_lambda() {
1442 let p4 = Pert::with_shape(0.0, 5.0, 10.0, 4.0).unwrap();
1444 let p8 = Pert::with_shape(0.0, 5.0, 10.0, 8.0).unwrap();
1445 assert!(
1446 p8.variance() < p4.variance(),
1447 "higher λ should give lower variance"
1448 );
1449 }
1450
1451 #[test]
1454 fn test_regularized_beta_bounds() {
1455 assert_eq!(special::regularized_incomplete_beta(0.0, 2.0, 3.0), 0.0);
1456 assert_eq!(special::regularized_incomplete_beta(1.0, 2.0, 3.0), 1.0);
1457 }
1458
1459 #[test]
1460 fn test_regularized_beta_symmetric() {
1461 let result = special::regularized_incomplete_beta(0.5, 3.0, 3.0);
1463 assert!(
1464 (result - 0.5).abs() < 1e-8,
1465 "I_0.5(3,3) = {result}, expected 0.5"
1466 );
1467 }
1468
1469 #[test]
1470 fn test_regularized_beta_known_values() {
1471 for &x in &[0.1, 0.3, 0.5, 0.7, 0.9] {
1473 let result = special::regularized_incomplete_beta(x, 1.0, 1.0);
1474 assert!(
1475 (result - x).abs() < 1e-10,
1476 "I_{x}(1,1) = {result}, expected {x}"
1477 );
1478 }
1479
1480 for &x in &[0.1, 0.5, 0.9] {
1482 let result = special::regularized_incomplete_beta(x, 1.0, 3.0);
1483 let expected = 1.0 - (1.0 - x).powi(3);
1484 assert!(
1485 (result - expected).abs() < 1e-10,
1486 "I_{x}(1,3) = {result}, expected {expected}"
1487 );
1488 }
1489 }
1490
1491 #[test]
1494 fn test_ln_gamma_known() {
1495 assert!((special::ln_gamma(1.0)).abs() < 1e-10);
1497 assert!((special::ln_gamma(2.0)).abs() < 1e-10);
1499 assert!((special::ln_gamma(3.0) - 2.0_f64.ln()).abs() < 1e-10);
1501 assert!((special::ln_gamma(5.0) - 24.0_f64.ln()).abs() < 1e-10);
1503 assert!(
1505 (special::ln_gamma(0.5) - std::f64::consts::PI.sqrt().ln()).abs() < 1e-10,
1506 "ln Γ(0.5) = {}, expected {}",
1507 special::ln_gamma(0.5),
1508 std::f64::consts::PI.sqrt().ln()
1509 );
1510 }
1511
1512 #[test]
1515 fn test_weibull_exponential_special_case() {
1516 let w = Weibull::new(1.0, 5.0).unwrap();
1518 assert!((w.mean() - 5.0).abs() < 1e-10);
1519 assert!((w.variance() - 25.0).abs() < 1e-8);
1520 }
1521
1522 #[test]
1523 fn test_weibull_rayleigh_special_case() {
1524 let w = Weibull::new(2.0, 1.0).unwrap();
1526 let expected_mean = std::f64::consts::PI.sqrt() / 2.0;
1527 assert!(
1528 (w.mean() - expected_mean).abs() < 1e-10,
1529 "Weibull(2,1) mean = {}, expected {}",
1530 w.mean(),
1531 expected_mean
1532 );
1533 }
1534
1535 #[test]
1536 fn test_weibull_cdf_known_values() {
1537 let w = Weibull::new(2.0, 10.0).unwrap();
1539 assert!((w.cdf(10.0) - (1.0 - (-1.0_f64).exp())).abs() < 1e-10);
1541 assert_eq!(w.cdf(0.0), 0.0);
1543 assert_eq!(w.cdf(-1.0), 0.0);
1545 }
1546
1547 #[test]
1548 fn test_weibull_pdf_basic() {
1549 let w = Weibull::new(1.0, 1.0).unwrap();
1551 assert!((w.pdf(0.0) - 1.0).abs() < 1e-10);
1552 assert!((w.pdf(1.0) - (-1.0_f64).exp()).abs() < 1e-10);
1553 assert!((w.pdf(2.0) - (-2.0_f64).exp()).abs() < 1e-10);
1554 }
1555
1556 #[test]
1557 fn test_weibull_pdf_negative() {
1558 let w = Weibull::new(2.0, 5.0).unwrap();
1559 assert_eq!(w.pdf(-1.0), 0.0);
1560 }
1561
1562 #[test]
1563 fn test_weibull_quantile_roundtrip() {
1564 let w = Weibull::new(2.5, 100.0).unwrap();
1565 for &p in &[0.1, 0.25, 0.5, 0.75, 0.9] {
1566 let t = w.quantile(p).unwrap();
1567 let p_back = w.cdf(t);
1568 assert!(
1569 (p_back - p).abs() < 1e-10,
1570 "roundtrip: p={p} -> t={t} -> p_back={p_back}"
1571 );
1572 }
1573 }
1574
1575 #[test]
1576 fn test_weibull_quantile_edge_cases() {
1577 let w = Weibull::new(2.0, 10.0).unwrap();
1578 assert_eq!(w.quantile(0.0), Some(0.0));
1579 assert_eq!(w.quantile(1.0), None);
1580 assert_eq!(w.quantile(-0.1), None);
1581 }
1582
1583 #[test]
1584 fn test_weibull_reliability() {
1585 let w = Weibull::new(2.0, 10.0).unwrap();
1586 for &t in &[1.0, 5.0, 10.0, 20.0] {
1588 let r = w.reliability(t);
1589 let f = w.cdf(t);
1590 assert!((r + f - 1.0).abs() < 1e-14);
1591 }
1592 }
1593
1594 #[test]
1595 fn test_weibull_hazard_rate() {
1596 let w = Weibull::new(1.0, 5.0).unwrap();
1598 for &t in &[1.0, 5.0, 10.0] {
1599 assert!((w.hazard_rate(t) - 0.2).abs() < 1e-10);
1600 }
1601 let w2 = Weibull::new(2.0, 10.0).unwrap();
1603 assert!((w2.hazard_rate(5.0) - 2.0 * 5.0 / 100.0).abs() < 1e-10);
1604 }
1605
1606 #[test]
1607 fn test_weibull_invalid() {
1608 assert!(Weibull::new(0.0, 1.0).is_err());
1609 assert!(Weibull::new(-1.0, 1.0).is_err());
1610 assert!(Weibull::new(1.0, 0.0).is_err());
1611 assert!(Weibull::new(1.0, -1.0).is_err());
1612 assert!(Weibull::new(f64::NAN, 1.0).is_err());
1613 assert!(Weibull::new(1.0, f64::INFINITY).is_err());
1614 }
1615
1616 #[test]
1619 fn test_exponential_basic() {
1620 let e = Exponential::new(0.5).unwrap();
1621 assert!((e.rate() - 0.5).abs() < 1e-10);
1622 assert!((e.mean() - 2.0).abs() < 1e-10);
1623 assert!((e.variance() - 4.0).abs() < 1e-10);
1624 }
1625
1626 #[test]
1627 fn test_exponential_pdf() {
1628 let e = Exponential::new(1.0).unwrap();
1629 assert!((e.pdf(0.0) - 1.0).abs() < 1e-10);
1630 assert!((e.pdf(1.0) - (-1.0_f64).exp()).abs() < 1e-10);
1631 assert_eq!(e.pdf(-1.0), 0.0);
1632 }
1633
1634 #[test]
1635 fn test_exponential_cdf() {
1636 let e = Exponential::new(1.0).unwrap();
1637 assert_eq!(e.cdf(-1.0), 0.0);
1638 assert_eq!(e.cdf(0.0), 0.0);
1639 assert!((e.cdf(1.0) - (1.0 - (-1.0_f64).exp())).abs() < 1e-10);
1640 }
1641
1642 #[test]
1643 fn test_exponential_quantile() {
1644 let e = Exponential::new(2.0).unwrap();
1645 assert_eq!(e.quantile(0.0), Some(0.0));
1646 assert_eq!(e.quantile(1.0), Some(f64::INFINITY));
1647 let q = e.quantile(0.5).unwrap();
1648 assert!((q - 2.0_f64.ln() / 2.0).abs() < 1e-10);
1650 assert!(e.quantile(-0.1).is_none());
1651 assert!(e.quantile(1.1).is_none());
1652 }
1653
1654 #[test]
1655 fn test_exponential_quantile_roundtrip() {
1656 let e = Exponential::new(3.0).unwrap();
1657 for &p in &[0.01, 0.1, 0.25, 0.5, 0.75, 0.9, 0.99] {
1658 let x = e.quantile(p).unwrap();
1659 let p_back = e.cdf(x);
1660 assert!((p_back - p).abs() < 1e-10, "p={p}, x={x}, p_back={p_back}");
1661 }
1662 }
1663
1664 #[test]
1665 fn test_exponential_memoryless() {
1666 let e = Exponential::new(1.5).unwrap();
1668 let s = 2.0;
1669 let t = 3.0;
1670 let lhs = e.survival(s + t) / e.survival(s);
1671 let rhs = e.survival(t);
1672 assert!((lhs - rhs).abs() < 1e-10);
1673 }
1674
1675 #[test]
1676 fn test_exponential_invalid() {
1677 assert!(Exponential::new(0.0).is_err());
1678 assert!(Exponential::new(-1.0).is_err());
1679 assert!(Exponential::new(f64::NAN).is_err());
1680 assert!(Exponential::new(f64::INFINITY).is_err());
1681 }
1682
1683 #[test]
1686 fn test_gamma_basic() {
1687 let g = GammaDistribution::new(2.0, 1.0).unwrap();
1688 assert!((g.shape() - 2.0).abs() < 1e-10);
1689 assert!((g.rate() - 1.0).abs() < 1e-10);
1690 assert!((g.scale() - 1.0).abs() < 1e-10);
1691 assert!((g.mean() - 2.0).abs() < 1e-10);
1692 assert!((g.variance() - 2.0).abs() < 1e-10);
1693 assert!((g.mode() - 1.0).abs() < 1e-10);
1694 }
1695
1696 #[test]
1697 fn test_gamma_from_shape_scale() {
1698 let g = GammaDistribution::from_shape_scale(3.0, 2.0).unwrap();
1699 assert!((g.shape() - 3.0).abs() < 1e-10);
1700 assert!((g.rate() - 0.5).abs() < 1e-10);
1701 assert!((g.mean() - 6.0).abs() < 1e-10); }
1703
1704 #[test]
1705 fn test_gamma_pdf_integral() {
1706 let g = GammaDistribution::new(3.0, 2.0).unwrap();
1708 let n = 20_000;
1709 let dt = 20.0 / n as f64;
1710 let integral: f64 = (0..n)
1711 .map(|i| {
1712 let x = (i as f64 + 0.5) * dt;
1713 g.pdf(x) * dt
1714 })
1715 .sum();
1716 assert!((integral - 1.0).abs() < 0.01, "PDF integral = {integral}");
1717 }
1718
1719 #[test]
1720 fn test_gamma_cdf_known() {
1721 let g = GammaDistribution::new(1.0, 1.0).unwrap();
1723 let expected = 1.0 - (-1.0_f64).exp();
1724 assert!((g.cdf(1.0) - expected).abs() < 1e-10);
1725 }
1726
1727 #[test]
1728 fn test_gamma_cdf_chi2() {
1729 let g = GammaDistribution::new(1.0, 0.5).unwrap(); let x = 4.0;
1733 let expected = 1.0 - (-0.5_f64 * x).exp();
1734 assert!(
1735 (g.cdf(x) - expected).abs() < 1e-8,
1736 "CDF({x}) = {} vs {expected}",
1737 g.cdf(x)
1738 );
1739 }
1740
1741 #[test]
1742 fn test_gamma_quantile_roundtrip() {
1743 let g = GammaDistribution::new(5.0, 2.0).unwrap();
1744 for &p in &[0.01, 0.1, 0.25, 0.5, 0.75, 0.9, 0.99] {
1745 let x = g.quantile(p).unwrap();
1746 let p_back = g.cdf(x);
1747 assert!((p_back - p).abs() < 1e-6, "p={p}, x={x}, p_back={p_back}");
1748 }
1749 }
1750
1751 #[test]
1752 fn test_gamma_mode_small_shape() {
1753 let g = GammaDistribution::new(0.5, 1.0).unwrap();
1754 assert_eq!(g.mode(), 0.0); }
1756
1757 #[test]
1758 fn test_gamma_invalid() {
1759 assert!(GammaDistribution::new(0.0, 1.0).is_err());
1760 assert!(GammaDistribution::new(-1.0, 1.0).is_err());
1761 assert!(GammaDistribution::new(1.0, 0.0).is_err());
1762 assert!(GammaDistribution::new(1.0, -1.0).is_err());
1763 assert!(GammaDistribution::new(f64::NAN, 1.0).is_err());
1764 }
1765
1766 #[test]
1769 fn test_beta_mean_variance() {
1770 let b = BetaDistribution::new(2.0, 5.0).unwrap();
1771 assert!((b.mean() - 2.0 / 7.0).abs() < 1e-10);
1773 let expected_var = 2.0 * 5.0 / (7.0 * 7.0 * 8.0);
1775 assert!((b.variance() - expected_var).abs() < 1e-10);
1776 }
1777
1778 #[test]
1779 fn test_beta_symmetric() {
1780 let b = BetaDistribution::new(3.0, 3.0).unwrap();
1782 assert!((b.mean() - 0.5).abs() < 1e-10);
1783 assert!((b.mode().unwrap() - 0.5).abs() < 1e-10);
1784 assert!((b.pdf(0.3) - b.pdf(0.7)).abs() < 1e-10);
1786 }
1787
1788 #[test]
1789 fn test_beta_uniform_special_case() {
1790 let b = BetaDistribution::new(1.0, 1.0).unwrap();
1792 assert!((b.mean() - 0.5).abs() < 1e-10);
1793 assert!((b.cdf(0.5) - 0.5).abs() < 1e-10);
1794 assert!((b.cdf(0.25) - 0.25).abs() < 1e-10);
1795 }
1796
1797 #[test]
1798 fn test_beta_cdf_boundaries() {
1799 let b = BetaDistribution::new(2.0, 3.0).unwrap();
1800 assert_eq!(b.cdf(0.0), 0.0);
1801 assert_eq!(b.cdf(-1.0), 0.0);
1802 assert_eq!(b.cdf(1.0), 1.0);
1803 assert_eq!(b.cdf(2.0), 1.0);
1804 }
1805
1806 #[test]
1807 fn test_beta_pdf_integral() {
1808 let b = BetaDistribution::new(2.0, 5.0).unwrap();
1809 let n = 10_000;
1810 let dt = 1.0 / n as f64;
1811 let integral: f64 = (0..n)
1812 .map(|i| {
1813 let x = (i as f64 + 0.5) * dt;
1814 b.pdf(x) * dt
1815 })
1816 .sum();
1817 assert!((integral - 1.0).abs() < 0.01, "PDF integral = {integral}");
1818 }
1819
1820 #[test]
1821 fn test_beta_quantile_roundtrip() {
1822 let b = BetaDistribution::new(2.0, 5.0).unwrap();
1823 for &p in &[0.01, 0.1, 0.25, 0.5, 0.75, 0.9, 0.99] {
1824 let x = b.quantile(p).unwrap();
1825 let p_back = b.cdf(x);
1826 assert!((p_back - p).abs() < 1e-6, "p={p}, x={x}, p_back={p_back}");
1827 }
1828 }
1829
1830 #[test]
1831 fn test_beta_mode() {
1832 let b = BetaDistribution::new(5.0, 3.0).unwrap();
1833 assert!((b.mode().unwrap() - 2.0 / 3.0).abs() < 1e-10);
1835 }
1836
1837 #[test]
1838 fn test_beta_mode_undefined() {
1839 let b = BetaDistribution::new(0.5, 0.5).unwrap();
1841 assert!(b.mode().is_none());
1842 }
1843
1844 #[test]
1845 fn test_beta_invalid() {
1846 assert!(BetaDistribution::new(0.0, 1.0).is_err());
1847 assert!(BetaDistribution::new(-1.0, 1.0).is_err());
1848 assert!(BetaDistribution::new(1.0, 0.0).is_err());
1849 assert!(BetaDistribution::new(f64::NAN, 1.0).is_err());
1850 }
1851
1852 #[test]
1855 fn test_chi2_mean_variance() {
1856 let chi2 = ChiSquared::new(5.0).unwrap();
1857 assert!((chi2.mean() - 5.0).abs() < 1e-10);
1858 assert!((chi2.variance() - 10.0).abs() < 1e-10);
1859 }
1860
1861 #[test]
1862 fn test_chi2_exp_special_case() {
1863 let chi2 = ChiSquared::new(2.0).unwrap();
1865 let x = 4.0;
1866 let expected = 1.0 - (-x / 2.0_f64).exp();
1867 assert!(
1868 (chi2.cdf(x) - expected).abs() < 1e-8,
1869 "CDF({x}) = {} vs {expected}",
1870 chi2.cdf(x)
1871 );
1872 }
1873
1874 #[test]
1875 fn test_chi2_cdf_boundaries() {
1876 let chi2 = ChiSquared::new(3.0).unwrap();
1877 assert_eq!(chi2.cdf(0.0), 0.0);
1878 assert_eq!(chi2.cdf(-1.0), 0.0);
1879 assert!(chi2.cdf(100.0) > 0.999);
1881 }
1882
1883 #[test]
1884 fn test_chi2_pdf_integral() {
1885 let chi2 = ChiSquared::new(4.0).unwrap();
1886 let n = 20_000;
1887 let dt = 30.0 / n as f64;
1888 let integral: f64 = (0..n)
1889 .map(|i| {
1890 let x = (i as f64 + 0.5) * dt;
1891 chi2.pdf(x) * dt
1892 })
1893 .sum();
1894 assert!((integral - 1.0).abs() < 0.01, "PDF integral = {integral}");
1895 }
1896
1897 #[test]
1898 fn test_chi2_quantile_roundtrip() {
1899 let chi2 = ChiSquared::new(5.0).unwrap();
1900 for &p in &[0.01, 0.05, 0.1, 0.25, 0.5, 0.75, 0.9, 0.95, 0.99] {
1901 let x = chi2.quantile(p).unwrap();
1902 let p_back = chi2.cdf(x);
1903 assert!((p_back - p).abs() < 1e-6, "p={p}, x={x}, p_back={p_back}");
1904 }
1905 }
1906
1907 #[test]
1908 fn test_chi2_known_quantiles() {
1909 let chi2 = ChiSquared::new(5.0).unwrap();
1912 let q05 = chi2.quantile(0.05).unwrap();
1913 assert!((q05 - 1.1455).abs() < 0.01, "q(0.05) = {q05}");
1914 let q95 = chi2.quantile(0.95).unwrap();
1915 assert!((q95 - 11.0705).abs() < 0.01, "q(0.95) = {q95}");
1916 }
1917
1918 #[test]
1919 fn test_chi2_invalid() {
1920 assert!(ChiSquared::new(0.0).is_err());
1921 assert!(ChiSquared::new(-1.0).is_err());
1922 assert!(ChiSquared::new(f64::NAN).is_err());
1923 }
1924
1925 #[test]
1926 fn test_chi2_gamma_consistency() {
1927 let k = 6.0;
1929 let chi2 = ChiSquared::new(k).unwrap();
1930 let gamma = GammaDistribution::new(k / 2.0, 0.5).unwrap();
1931
1932 let x = 5.0;
1933 assert!(
1934 (chi2.cdf(x) - gamma.cdf(x)).abs() < 1e-10,
1935 "Chi2 and Gamma CDF should match"
1936 );
1937 }
1938
1939 #[test]
1940 fn test_weibull_mean_variance_known() {
1941 let w = Weibull::new(3.6, 1000.0).unwrap();
1944 let mean = w.mean();
1945 assert!(mean > 800.0 && mean < 1000.0, "mean={mean}");
1946 assert!(w.variance() > 0.0);
1947 }
1948
1949 #[test]
1950 fn test_weibull_pdf_integral_approx() {
1951 let w = Weibull::new(2.0, 10.0).unwrap();
1953 let n = 10_000;
1954 let dt = 50.0 / n as f64;
1955 let integral: f64 = (0..n)
1956 .map(|i| {
1957 let t = (i as f64 + 0.5) * dt;
1958 w.pdf(t) * dt
1959 })
1960 .sum();
1961 assert!(
1962 (integral - 1.0).abs() < 0.01,
1963 "PDF integral = {integral}, expected ≈ 1.0"
1964 );
1965 }
1966
1967 #[test]
1976 fn test_normal_cdf_reference_values() {
1977 assert!(
1979 (special::standard_normal_cdf(0.0) - 0.5).abs() < 1e-7,
1980 "Φ(0) error"
1981 );
1982 assert!(
1984 (special::standard_normal_cdf(1.96) - 0.975002).abs() < 1e-6,
1985 "Φ(1.96) = {}, expected 0.975002",
1986 special::standard_normal_cdf(1.96)
1987 );
1988 assert!(
1990 (special::standard_normal_cdf(-1.96) - 0.024998).abs() < 1e-6,
1991 "Φ(-1.96) = {}, expected 0.024998",
1992 special::standard_normal_cdf(-1.96)
1993 );
1994 assert!(
1996 (special::standard_normal_cdf(3.0) - 0.998650).abs() < 1e-6,
1997 "Φ(3.0) = {}, expected 0.998650",
1998 special::standard_normal_cdf(3.0)
1999 );
2000 for &x in &[0.5, 1.0, 1.96, 2.5, 3.0] {
2002 let sum = special::standard_normal_cdf(x) + special::standard_normal_cdf(-x);
2003 assert!(
2004 (sum - 1.0).abs() < 1e-14,
2005 "Symmetry violated at x={x}: sum={sum}"
2006 );
2007 }
2008 }
2009
2010 #[test]
2014 fn test_normal_quantile_reference_values() {
2015 assert!(
2017 special::inverse_normal_cdf(0.5).abs() < 5e-4,
2018 "Φ⁻¹(0.5) = {}, expected 0.0",
2019 special::inverse_normal_cdf(0.5)
2020 );
2021 let q975 = special::inverse_normal_cdf(0.975);
2023 assert!(
2024 (q975 - 1.95996).abs() < 5e-4,
2025 "Φ⁻¹(0.975) = {q975}, expected ≈ 1.95996"
2026 );
2027 let q025 = special::inverse_normal_cdf(0.025);
2029 assert!(
2030 (q025 + 1.95996).abs() < 5e-4,
2031 "Φ⁻¹(0.025) = {q025}, expected ≈ -1.95996"
2032 );
2033 for &p in &[0.01, 0.05, 0.1, 0.25, 0.5, 0.75, 0.9, 0.95, 0.99] {
2035 let z = special::inverse_normal_cdf(p);
2036 let p_back = special::standard_normal_cdf(z);
2037 assert!(
2038 (p_back - p).abs() < 1e-3,
2039 "Roundtrip failed: p={p}, z={z}, p_back={p_back}"
2040 );
2041 }
2042 }
2043
2044 #[test]
2052 fn test_gamma_reference_values() {
2053 let sqrt_pi = std::f64::consts::PI.sqrt();
2055 assert!(
2056 (special::gamma(0.5) - sqrt_pi).abs() < 1e-7,
2057 "Γ(0.5) = {}, expected √π = {}",
2058 special::gamma(0.5),
2059 sqrt_pi
2060 );
2061 assert!(
2063 (special::gamma(1.0) - 1.0).abs() < 1e-14,
2064 "Γ(1.0) = {}",
2065 special::gamma(1.0)
2066 );
2067 let gamma_1_5 = sqrt_pi / 2.0;
2069 assert!(
2070 (special::gamma(1.5) - gamma_1_5).abs() < 1e-7,
2071 "Γ(1.5) = {}, expected {}",
2072 special::gamma(1.5),
2073 gamma_1_5
2074 );
2075 assert!(
2077 (special::gamma(2.0) - 1.0).abs() < 1e-14,
2078 "Γ(2.0) = {}",
2079 special::gamma(2.0)
2080 );
2081 assert!(
2083 (special::gamma(3.0) - 2.0).abs() < 1e-13,
2084 "Γ(3.0) = {}",
2085 special::gamma(3.0)
2086 );
2087 assert!(
2089 (special::gamma(4.0) - 6.0).abs() < 1e-12,
2090 "Γ(4.0) = {}",
2091 special::gamma(4.0)
2092 );
2093 assert!(
2095 (special::gamma(5.0) - 24.0).abs() < 1e-8,
2096 "Γ(5.0) = {}, expected 24.0",
2097 special::gamma(5.0)
2098 );
2099 }
2100
2101 #[test]
2110 fn test_gamma_pdf_reference_value() {
2111 let g = GammaDistribution::new(2.0, 1.0).unwrap();
2112 let expected = (-1.0_f64).exp(); assert!(
2114 (g.pdf(1.0) - expected).abs() < 1e-5,
2115 "Gamma(2,1).pdf(1) = {}, expected e⁻¹ = {}",
2116 g.pdf(1.0),
2117 expected
2118 );
2119 }
2120}
2121
2122#[cfg(test)]
2123mod proptests {
2124 use super::*;
2125 use proptest::prelude::*;
2126
2127 proptest! {
2128 #![proptest_config(ProptestConfig::with_cases(300))]
2129
2130 #[test]
2133 fn uniform_cdf_in_01(
2134 min in -100.0_f64..0.0,
2135 max in 1.0_f64..100.0,
2136 x in -200.0_f64..200.0,
2137 ) {
2138 let u = Uniform::new(min, max).unwrap();
2139 let c = u.cdf(x);
2140 prop_assert!((0.0..=1.0).contains(&c));
2141 }
2142
2143 #[test]
2144 fn uniform_quantile_roundtrip(
2145 min in -100.0_f64..0.0,
2146 max in 1.0_f64..100.0,
2147 p in 0.0_f64..=1.0,
2148 ) {
2149 let u = Uniform::new(min, max).unwrap();
2150 let x = u.quantile(p).unwrap();
2151 let p_back = u.cdf(x);
2152 prop_assert!((p_back - p).abs() < 1e-12, "roundtrip: p={p} -> x={x} -> p_back={p_back}");
2153 }
2154
2155 #[test]
2158 fn triangular_cdf_in_01(
2159 min in -100.0_f64..-1.0,
2160 mode_frac in 0.0_f64..=1.0,
2161 range in 1.0_f64..100.0,
2162 x in -200.0_f64..200.0,
2163 ) {
2164 let max = min + range;
2165 let mode = min + mode_frac * range;
2166 let t = Triangular::new(min, mode, max).unwrap();
2167 let c = t.cdf(x);
2168 prop_assert!((0.0..=1.0).contains(&c));
2169 }
2170
2171 #[test]
2172 fn triangular_quantile_roundtrip(
2173 min in -50.0_f64..0.0,
2174 mode_frac in 0.01_f64..0.99,
2175 range in 1.0_f64..50.0,
2176 p in 0.001_f64..0.999,
2177 ) {
2178 let max = min + range;
2179 let mode = min + mode_frac * range;
2180 let t = Triangular::new(min, mode, max).unwrap();
2181 let x = t.quantile(p).unwrap();
2182 let p_back = t.cdf(x);
2183 prop_assert!(
2184 (p_back - p).abs() < 1e-8,
2185 "roundtrip: p={p} -> x={x} -> p_back={p_back}"
2186 );
2187 }
2188
2189 #[test]
2192 fn pert_mean_formula(
2193 min in -50.0_f64..0.0,
2194 mode_frac in 0.01_f64..0.99,
2195 range in 1.0_f64..50.0,
2196 ) {
2197 let max = min + range;
2198 let mode = min + mode_frac * range;
2199 let p = Pert::new(min, mode, max).unwrap();
2200 let expected = (min + 4.0 * mode + max) / 6.0;
2201 prop_assert!(
2202 (p.mean() - expected).abs() < 1e-10,
2203 "PERT mean: {} vs expected: {}",
2204 p.mean(),
2205 expected
2206 );
2207 }
2208
2209 #[test]
2210 fn pert_variance_positive(
2211 min in -50.0_f64..0.0,
2212 mode_frac in 0.01_f64..0.99,
2213 range in 1.0_f64..50.0,
2214 ) {
2215 let max = min + range;
2216 let mode = min + mode_frac * range;
2217 let p = Pert::new(min, mode, max).unwrap();
2218 prop_assert!(p.variance() > 0.0);
2219 }
2220
2221 #[test]
2222 fn pert_cdf_monotonic(
2223 min in -50.0_f64..0.0,
2224 mode_frac in 0.05_f64..0.95,
2225 range in 2.0_f64..50.0,
2226 ) {
2227 let max = min + range;
2228 let mode = min + mode_frac * range;
2229 let p = Pert::new(min, mode, max).unwrap();
2230 let mut prev = 0.0;
2231 for i in 0..=20 {
2232 let x = min + (i as f64 / 20.0) * range;
2233 let c = p.cdf(x);
2234 prop_assert!(c >= prev - 1e-10, "CDF not monotonic at x={x}");
2235 prev = c;
2236 }
2237 }
2238
2239 #[test]
2242 fn weibull_cdf_in_01(
2243 shape in 0.1_f64..10.0,
2244 scale in 0.1_f64..100.0,
2245 t in 0.0_f64..200.0,
2246 ) {
2247 let w = Weibull::new(shape, scale).unwrap();
2248 let c = w.cdf(t);
2249 prop_assert!((0.0..=1.0).contains(&c), "CDF({t}) = {c} out of [0,1]");
2250 }
2251
2252 #[test]
2253 fn weibull_quantile_roundtrip(
2254 shape in 0.5_f64..10.0,
2255 scale in 1.0_f64..100.0,
2256 p in 0.001_f64..0.999,
2257 ) {
2258 let w = Weibull::new(shape, scale).unwrap();
2259 let t = w.quantile(p).unwrap();
2260 let p_back = w.cdf(t);
2261 prop_assert!(
2262 (p_back - p).abs() < 1e-8,
2263 "roundtrip: p={p} -> t={t} -> p_back={p_back}"
2264 );
2265 }
2266
2267 #[test]
2268 fn weibull_pdf_non_negative(
2269 shape in 0.1_f64..10.0,
2270 scale in 0.1_f64..100.0,
2271 t in 0.0_f64..200.0,
2272 ) {
2273 let w = Weibull::new(shape, scale).unwrap();
2274 prop_assert!(w.pdf(t) >= 0.0, "PDF({t}) must be >= 0");
2275 }
2276
2277 #[test]
2278 fn weibull_reliability_plus_cdf_is_one(
2279 shape in 0.5_f64..5.0,
2280 scale in 1.0_f64..50.0,
2281 t in 0.001_f64..100.0,
2282 ) {
2283 let w = Weibull::new(shape, scale).unwrap();
2284 let sum = w.cdf(t) + w.reliability(t);
2285 prop_assert!(
2286 (sum - 1.0).abs() < 1e-12,
2287 "CDF + R should = 1, got {sum}"
2288 );
2289 }
2290
2291 #[test]
2292 fn weibull_variance_positive(
2293 shape in 0.1_f64..10.0,
2294 scale in 0.1_f64..100.0,
2295 ) {
2296 let w = Weibull::new(shape, scale).unwrap();
2297 prop_assert!(w.variance() > 0.0, "variance must be positive");
2298 }
2299
2300 #[test]
2303 fn exponential_cdf_in_01(
2304 rate in 0.01_f64..10.0,
2305 x in 0.0_f64..100.0,
2306 ) {
2307 let e = Exponential::new(rate).unwrap();
2308 let c = e.cdf(x);
2309 prop_assert!((0.0..=1.0).contains(&c), "CDF({x}) = {c}");
2310 }
2311
2312 #[test]
2313 fn exponential_quantile_roundtrip(
2314 rate in 0.01_f64..10.0,
2315 p in 0.001_f64..0.999,
2316 ) {
2317 let e = Exponential::new(rate).unwrap();
2318 let x = e.quantile(p).unwrap();
2319 let p_back = e.cdf(x);
2320 prop_assert!((p_back - p).abs() < 1e-10);
2321 }
2322
2323 #[test]
2326 fn gamma_cdf_in_01(
2327 shape in 0.5_f64..10.0,
2328 rate in 0.1_f64..10.0,
2329 x in 0.001_f64..50.0,
2330 ) {
2331 let g = GammaDistribution::new(shape, rate).unwrap();
2332 let c = g.cdf(x);
2333 prop_assert!((0.0..=1.0).contains(&c), "CDF({x}) = {c}");
2334 }
2335
2336 #[test]
2337 fn gamma_variance_positive(
2338 shape in 0.1_f64..10.0,
2339 rate in 0.1_f64..10.0,
2340 ) {
2341 let g = GammaDistribution::new(shape, rate).unwrap();
2342 prop_assert!(g.variance() > 0.0, "variance must be positive");
2343 }
2344
2345 #[test]
2348 fn beta_cdf_in_01(
2349 alpha in 0.5_f64..10.0,
2350 beta_param in 0.5_f64..10.0,
2351 x in 0.001_f64..0.999,
2352 ) {
2353 let b = BetaDistribution::new(alpha, beta_param).unwrap();
2354 let c = b.cdf(x);
2355 prop_assert!((0.0..=1.0).contains(&c), "CDF({x}) = {c}");
2356 }
2357
2358 #[test]
2359 fn beta_quantile_roundtrip(
2360 alpha in 1.0_f64..10.0,
2361 beta_param in 1.0_f64..10.0,
2362 p in 0.01_f64..0.99,
2363 ) {
2364 let b = BetaDistribution::new(alpha, beta_param).unwrap();
2365 let x = b.quantile(p).unwrap();
2366 let p_back = b.cdf(x);
2367 prop_assert!((p_back - p).abs() < 1e-5, "p={p}, p_back={p_back}");
2368 }
2369
2370 #[test]
2373 fn chi2_cdf_in_01(
2374 k in 1.0_f64..20.0,
2375 x in 0.001_f64..50.0,
2376 ) {
2377 let chi2 = ChiSquared::new(k).unwrap();
2378 let c = chi2.cdf(x);
2379 prop_assert!((0.0..=1.0).contains(&c), "CDF({x}) = {c}");
2380 }
2381
2382 #[test]
2383 fn chi2_variance_positive(
2384 k in 0.5_f64..50.0,
2385 ) {
2386 let chi2 = ChiSquared::new(k).unwrap();
2387 prop_assert!(chi2.variance() > 0.0, "variance must be positive");
2388 }
2389 }
2390}