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 #[deprecated(since = "0.6.3", note = "use `Pert::quantile`, which is exact")]
600 pub fn quantile_approx(&self, p: f64) -> Option<f64> {
601 if p <= 0.0 || p >= 1.0 {
602 return None;
603 }
604 let z = special::inverse_normal_cdf(p);
605 let result = self.mean() + z * self.std_dev();
606 Some(result.clamp(self.min, self.max))
607 }
608}
609
610#[derive(Debug, Clone, PartialEq)]
633pub struct Weibull {
634 shape: f64, scale: f64, }
637
638impl Weibull {
639 pub fn new(shape: f64, scale: f64) -> Result<Self, DistributionError> {
644 if !shape.is_finite() || !scale.is_finite() || shape <= 0.0 || scale <= 0.0 {
645 return Err(DistributionError::InvalidParameters(format!(
646 "Weibull requires shape > 0 and scale > 0, got shape={shape}, scale={scale}"
647 )));
648 }
649 Ok(Self { shape, scale })
650 }
651
652 pub fn shape(&self) -> f64 {
654 self.shape
655 }
656
657 pub fn scale(&self) -> f64 {
659 self.scale
660 }
661
662 pub fn mean(&self) -> f64 {
664 self.scale * special::gamma(1.0 + 1.0 / self.shape)
665 }
666
667 pub fn variance(&self) -> f64 {
669 let g1 = special::gamma(1.0 + 1.0 / self.shape);
670 let g2 = special::gamma(1.0 + 2.0 / self.shape);
671 self.scale * self.scale * (g2 - g1 * g1)
672 }
673
674 pub fn pdf(&self, t: f64) -> f64 {
676 if t < 0.0 {
677 return 0.0;
678 }
679 if t == 0.0 {
680 return if self.shape < 1.0 {
681 f64::INFINITY
682 } else if (self.shape - 1.0).abs() < f64::EPSILON {
683 1.0 / self.scale
684 } else {
685 0.0
686 };
687 }
688 let z = t / self.scale;
689 (self.shape / self.scale) * z.powf(self.shape - 1.0) * (-z.powf(self.shape)).exp()
690 }
691
692 pub fn cdf(&self, t: f64) -> f64 {
694 if t <= 0.0 {
695 return 0.0;
696 }
697 let z = t / self.scale;
698 1.0 - (-z.powf(self.shape)).exp()
699 }
700
701 pub fn quantile(&self, p: f64) -> Option<f64> {
705 if !(0.0..1.0).contains(&p) {
706 return None;
707 }
708 if p == 0.0 {
709 return Some(0.0);
710 }
711 Some(self.scale * (-(1.0 - p).ln()).powf(1.0 / self.shape))
712 }
713
714 pub fn hazard_rate(&self, t: f64) -> f64 {
720 if t <= 0.0 {
721 return if t == 0.0 && self.shape >= 1.0 {
722 if (self.shape - 1.0).abs() < f64::EPSILON {
723 1.0 / self.scale
724 } else {
725 0.0
726 }
727 } else {
728 0.0
729 };
730 }
731 let z = t / self.scale;
732 (self.shape / self.scale) * z.powf(self.shape - 1.0)
733 }
734
735 pub fn reliability(&self, t: f64) -> f64 {
737 1.0 - self.cdf(t)
738 }
739}
740
741#[derive(Debug, Clone, Copy)]
760pub struct Exponential {
761 rate: f64,
762}
763
764impl Exponential {
765 pub fn new(rate: f64) -> Result<Self, DistributionError> {
769 if !rate.is_finite() || rate <= 0.0 {
770 return Err(DistributionError::InvalidParameters(format!(
771 "Exponential rate must be positive and finite, got {rate}"
772 )));
773 }
774 Ok(Self { rate })
775 }
776
777 pub fn rate(&self) -> f64 {
779 self.rate
780 }
781
782 pub fn mean(&self) -> f64 {
784 1.0 / self.rate
785 }
786
787 pub fn variance(&self) -> f64 {
789 1.0 / (self.rate * self.rate)
790 }
791
792 pub fn pdf(&self, x: f64) -> f64 {
794 if x < 0.0 {
795 0.0
796 } else {
797 self.rate * (-self.rate * x).exp()
798 }
799 }
800
801 pub fn cdf(&self, x: f64) -> f64 {
803 if x < 0.0 {
804 0.0
805 } else {
806 1.0 - (-self.rate * x).exp()
807 }
808 }
809
810 pub fn quantile(&self, p: f64) -> Option<f64> {
814 if !(0.0..=1.0).contains(&p) {
815 return None;
816 }
817 if p == 1.0 {
818 return Some(f64::INFINITY);
819 }
820 Some(-(1.0 - p).ln() / self.rate)
821 }
822
823 pub fn hazard_rate(&self) -> f64 {
825 self.rate
826 }
827
828 pub fn survival(&self, x: f64) -> f64 {
830 1.0 - self.cdf(x)
831 }
832}
833
834#[derive(Debug, Clone, Copy)]
861pub struct GammaDistribution {
862 shape: f64, rate: f64, }
865
866impl GammaDistribution {
867 pub fn new(shape: f64, rate: f64) -> Result<Self, DistributionError> {
871 if !shape.is_finite() || shape <= 0.0 || !rate.is_finite() || rate <= 0.0 {
872 return Err(DistributionError::InvalidParameters(format!(
873 "Gamma shape and rate must be positive and finite, got shape={shape}, rate={rate}"
874 )));
875 }
876 Ok(Self { shape, rate })
877 }
878
879 pub fn from_shape_scale(shape: f64, scale: f64) -> Result<Self, DistributionError> {
881 if !scale.is_finite() || scale <= 0.0 {
882 return Err(DistributionError::InvalidParameters(format!(
883 "Gamma scale must be positive and finite, got {scale}"
884 )));
885 }
886 Self::new(shape, 1.0 / scale)
887 }
888
889 pub fn shape(&self) -> f64 {
891 self.shape
892 }
893
894 pub fn rate(&self) -> f64 {
896 self.rate
897 }
898
899 pub fn scale(&self) -> f64 {
901 1.0 / self.rate
902 }
903
904 pub fn mean(&self) -> f64 {
906 self.shape / self.rate
907 }
908
909 pub fn variance(&self) -> f64 {
911 self.shape / (self.rate * self.rate)
912 }
913
914 pub fn mode(&self) -> f64 {
916 if self.shape >= 1.0 {
917 (self.shape - 1.0) / self.rate
918 } else {
919 0.0
920 }
921 }
922
923 pub fn pdf(&self, x: f64) -> f64 {
925 if x <= 0.0 {
926 if x == 0.0 && self.shape < 1.0 {
927 return f64::INFINITY;
928 }
929 return 0.0;
930 }
931 let log_pdf = self.shape * self.rate.ln() - special::ln_gamma(self.shape)
932 + (self.shape - 1.0) * x.ln()
933 - self.rate * x;
934 log_pdf.exp()
935 }
936
937 pub fn cdf(&self, x: f64) -> f64 {
941 if x <= 0.0 {
942 return 0.0;
943 }
944 special::regularized_lower_gamma(self.shape, self.rate * x)
945 }
946
947 pub fn quantile(&self, p: f64) -> Option<f64> {
951 if !(0.0..=1.0).contains(&p) {
952 return None;
953 }
954 if p == 0.0 {
955 return Some(0.0);
956 }
957 if p == 1.0 {
958 return Some(f64::INFINITY);
959 }
960
961 let z = crate::special::inverse_normal_cdf(p);
963 let v = 1.0 / (9.0 * self.shape);
964 let chi2_approx = self.shape * (1.0 - v + z * v.sqrt()).powi(3).max(0.001);
965 let mut x = chi2_approx / self.rate;
966
967 for _ in 0..50 {
969 let f = self.cdf(x) - p;
970 let fp = self.pdf(x);
971 if fp < 1e-300 {
972 break;
973 }
974 let step = f / fp;
975 x = (x - step).max(1e-15);
976 if step.abs() < 1e-12 * x {
977 break;
978 }
979 }
980
981 Some(x)
982 }
983}
984
985#[derive(Debug, Clone)]
1014pub struct BetaDistribution {
1015 pub alpha: f64,
1017 pub beta: f64,
1019}
1020
1021impl BetaDistribution {
1022 pub fn new(alpha: f64, beta: f64) -> Result<Self, DistributionError> {
1027 if alpha <= 0.0 || !alpha.is_finite() {
1028 return Err(DistributionError::InvalidParameters(format!(
1029 "alpha must be positive and finite, got {alpha}"
1030 )));
1031 }
1032 if beta <= 0.0 || !beta.is_finite() {
1033 return Err(DistributionError::InvalidParameters(format!(
1034 "beta must be positive and finite, got {beta}"
1035 )));
1036 }
1037 Ok(Self { alpha, beta })
1038 }
1039
1040 pub fn mean(&self) -> f64 {
1042 self.alpha / (self.alpha + self.beta)
1043 }
1044
1045 pub fn variance(&self) -> f64 {
1047 let ab = self.alpha + self.beta;
1048 self.alpha * self.beta / (ab * ab * (ab + 1.0))
1049 }
1050
1051 pub fn mode(&self) -> Option<f64> {
1056 if self.alpha > 1.0 && self.beta > 1.0 {
1057 Some((self.alpha - 1.0) / (self.alpha + self.beta - 2.0))
1058 } else {
1059 None
1060 }
1061 }
1062
1063 pub fn pdf(&self, x: f64) -> f64 {
1065 if x <= 0.0 || x >= 1.0 {
1066 return 0.0;
1067 }
1068 let ln_pdf = (self.alpha - 1.0) * x.ln() + (self.beta - 1.0) * (1.0 - x).ln()
1069 - special::ln_beta(self.alpha, self.beta);
1070 ln_pdf.exp()
1071 }
1072
1073 pub fn cdf(&self, x: f64) -> f64 {
1075 if x <= 0.0 {
1076 return 0.0;
1077 }
1078 if x >= 1.0 {
1079 return 1.0;
1080 }
1081 special::regularized_incomplete_beta(x, self.alpha, self.beta)
1082 }
1083
1084 pub fn quantile(&self, p: f64) -> Option<f64> {
1089 if !(0.0..=1.0).contains(&p) {
1090 return None;
1091 }
1092 if p == 0.0 {
1093 return Some(0.0);
1094 }
1095 if (p - 1.0).abs() < 1e-15 {
1096 return Some(1.0);
1097 }
1098
1099 let mut lo = 0.0_f64;
1101 let mut hi = 1.0_f64;
1102
1103 for _ in 0..100 {
1104 let mid = (lo + hi) / 2.0;
1105 if hi - lo < 1e-14 {
1106 break;
1107 }
1108 if self.cdf(mid) < p {
1109 lo = mid;
1110 } else {
1111 hi = mid;
1112 }
1113 }
1114
1115 Some((lo + hi) / 2.0)
1116 }
1117}
1118
1119#[derive(Debug, Clone)]
1150pub struct ChiSquared {
1151 pub k: f64,
1153}
1154
1155impl ChiSquared {
1156 pub fn new(k: f64) -> Result<Self, DistributionError> {
1161 if k <= 0.0 || !k.is_finite() {
1162 return Err(DistributionError::InvalidParameters(format!(
1163 "k must be positive and finite, got {k}"
1164 )));
1165 }
1166 Ok(Self { k })
1167 }
1168
1169 pub fn mean(&self) -> f64 {
1171 self.k
1172 }
1173
1174 pub fn variance(&self) -> f64 {
1176 2.0 * self.k
1177 }
1178
1179 pub fn pdf(&self, x: f64) -> f64 {
1181 if x <= 0.0 {
1182 return 0.0;
1183 }
1184 let half_k = self.k / 2.0;
1185 let ln_pdf =
1186 (half_k - 1.0) * x.ln() - x / 2.0 - half_k * 2.0_f64.ln() - special::ln_gamma(half_k);
1187 ln_pdf.exp()
1188 }
1189
1190 pub fn cdf(&self, x: f64) -> f64 {
1192 if x <= 0.0 {
1193 return 0.0;
1194 }
1195 special::regularized_lower_gamma(self.k / 2.0, x / 2.0)
1196 }
1197
1198 pub fn quantile(&self, p: f64) -> Option<f64> {
1202 if !(0.0..=1.0).contains(&p) {
1203 return None;
1204 }
1205 if p == 0.0 {
1206 return Some(0.0);
1207 }
1208 if (p - 1.0).abs() < 1e-15 {
1209 return Some(f64::INFINITY);
1210 }
1211
1212 let gamma = GammaDistribution {
1214 shape: self.k / 2.0,
1215 rate: 0.5,
1216 };
1217 gamma.quantile(p)
1218 }
1219}
1220
1221#[cfg(test)]
1226mod tests {
1227 use super::*;
1228
1229 #[test]
1232 fn test_uniform_basic() {
1233 let u = Uniform::new(0.0, 10.0).unwrap();
1234 assert!((u.mean() - 5.0).abs() < 1e-15);
1235 assert!((u.variance() - 100.0 / 12.0).abs() < 1e-10);
1236 }
1237
1238 #[test]
1239 fn test_uniform_cdf() {
1240 let u = Uniform::new(0.0, 10.0).unwrap();
1241 assert_eq!(u.cdf(-1.0), 0.0);
1242 assert!((u.cdf(5.0) - 0.5).abs() < 1e-15);
1243 assert_eq!(u.cdf(11.0), 1.0);
1244 }
1245
1246 #[test]
1247 fn test_uniform_quantile() {
1248 let u = Uniform::new(2.0, 8.0).unwrap();
1249 assert_eq!(u.quantile(0.0), Some(2.0));
1250 assert_eq!(u.quantile(1.0), Some(8.0));
1251 assert!((u.quantile(0.5).unwrap() - 5.0).abs() < 1e-15);
1252 }
1253
1254 #[test]
1255 fn test_uniform_pdf() {
1256 let u = Uniform::new(0.0, 5.0).unwrap();
1257 assert!((u.pdf(2.5) - 0.2).abs() < 1e-15);
1258 assert_eq!(u.pdf(-1.0), 0.0);
1259 }
1260
1261 #[test]
1262 fn test_uniform_invalid() {
1263 assert!(Uniform::new(5.0, 5.0).is_err());
1264 assert!(Uniform::new(5.0, 3.0).is_err());
1265 assert!(Uniform::new(f64::NAN, 5.0).is_err());
1266 }
1267
1268 #[test]
1271 fn test_triangular_mean() {
1272 let t = Triangular::new(0.0, 3.0, 6.0).unwrap();
1273 assert!((t.mean() - 3.0).abs() < 1e-15);
1274 }
1275
1276 #[test]
1277 fn test_triangular_symmetric_variance() {
1278 let t = Triangular::new(0.0, 5.0, 10.0).unwrap();
1279 let expected = (0.0 + 25.0 + 100.0 - 0.0 - 0.0 - 50.0) / 18.0;
1281 assert!((t.variance() - expected).abs() < 1e-10);
1282 }
1283
1284 #[test]
1285 fn test_triangular_cdf() {
1286 let t = Triangular::new(0.0, 5.0, 10.0).unwrap();
1287 assert!((t.cdf(0.0)).abs() < 1e-15);
1288 assert!((t.cdf(10.0) - 1.0).abs() < 1e-15);
1289 assert!((t.cdf(5.0) - 0.5).abs() < 1e-15);
1291 }
1292
1293 #[test]
1294 fn test_triangular_quantile() {
1295 let t = Triangular::new(0.0, 5.0, 10.0).unwrap();
1296 assert!((t.quantile(0.0).unwrap() - 0.0).abs() < 1e-15);
1297 assert!((t.quantile(1.0).unwrap() - 10.0).abs() < 1e-15);
1298 assert!((t.quantile(0.5).unwrap() - 5.0).abs() < 1e-10);
1299 }
1300
1301 #[test]
1302 fn test_triangular_invalid() {
1303 assert!(Triangular::new(5.0, 3.0, 10.0).is_err()); assert!(Triangular::new(0.0, 11.0, 10.0).is_err()); assert!(Triangular::new(5.0, 5.0, 5.0).is_err()); }
1307
1308 #[test]
1311 fn test_normal_standard() {
1312 let n = Normal::new(0.0, 1.0).unwrap();
1313 assert!((n.mean()).abs() < 1e-15);
1314 assert!((n.variance() - 1.0).abs() < 1e-15);
1315 assert!((n.cdf(0.0) - 0.5).abs() < 1e-7);
1316 }
1317
1318 #[test]
1319 fn test_normal_shifted() {
1320 let n = Normal::new(10.0, 2.0).unwrap();
1321 assert!((n.mean() - 10.0).abs() < 1e-15);
1322 assert!((n.variance() - 4.0).abs() < 1e-15);
1323 assert!((n.cdf(10.0) - 0.5).abs() < 1e-7);
1324 }
1325
1326 #[test]
1327 fn test_normal_quantile() {
1328 let n = Normal::new(0.0, 1.0).unwrap();
1329 assert!((n.quantile(0.5).unwrap()).abs() < 0.01);
1330 assert!((n.quantile(0.975).unwrap() - 1.96).abs() < 0.01);
1331 }
1332
1333 #[test]
1334 fn test_normal_invalid() {
1335 assert!(Normal::new(0.0, 0.0).is_err());
1336 assert!(Normal::new(0.0, -1.0).is_err());
1337 }
1338
1339 #[test]
1342 fn test_lognormal_mean() {
1343 let ln = LogNormal::new(0.0, 1.0).unwrap();
1344 let expected = (0.5_f64).exp(); assert!((ln.mean() - expected).abs() < 1e-10);
1346 }
1347
1348 #[test]
1349 fn test_lognormal_cdf() {
1350 let ln = LogNormal::new(0.0, 1.0).unwrap();
1351 assert_eq!(ln.cdf(0.0), 0.0);
1352 assert!((ln.cdf(1.0) - 0.5).abs() < 0.001);
1354 }
1355
1356 #[test]
1357 fn test_lognormal_quantile() {
1358 let ln = LogNormal::new(0.0, 1.0).unwrap();
1359 let q50 = ln.quantile(0.5).unwrap();
1361 assert!((q50 - 1.0).abs() < 0.01);
1362 }
1363
1364 #[test]
1367 fn test_pert_mean() {
1368 let p = Pert::new(1.0, 4.0, 7.0).unwrap();
1369 assert!((p.mean() - 4.0).abs() < 1e-10);
1371 }
1372
1373 #[test]
1374 fn test_pert_symmetric_variance() {
1375 let p = Pert::new(0.0, 5.0, 10.0).unwrap();
1376 let expected = 9.0 / (36.0 * 7.0) * 100.0;
1379 assert!(
1380 (p.variance() - expected).abs() < 1e-10,
1381 "PERT variance: {} vs expected: {}",
1382 p.variance(),
1383 expected
1384 );
1385 }
1386
1387 #[test]
1388 fn test_pert_shape_params() {
1389 let p = Pert::new(0.0, 5.0, 10.0).unwrap();
1390 assert!((p.alpha() - 3.0).abs() < 1e-15);
1393 assert!((p.beta_param() - 3.0).abs() < 1e-15);
1394 }
1395
1396 #[test]
1397 fn test_pert_cdf_bounds() {
1398 let p = Pert::new(1.0, 4.0, 7.0).unwrap();
1399 assert_eq!(p.cdf(0.0), 0.0);
1400 assert_eq!(p.cdf(8.0), 1.0);
1401 let p_sym = Pert::new(0.0, 5.0, 10.0).unwrap();
1403 assert!((p_sym.cdf(5.0) - 0.5).abs() < 0.01);
1404 }
1405
1406 #[test]
1407 fn test_pert_cdf_monotonic() {
1408 let p = Pert::new(0.0, 3.0, 10.0).unwrap();
1409 let mut prev = 0.0;
1410 for i in 0..=100 {
1411 let x = i as f64 * 0.1;
1412 let c = p.cdf(x);
1413 assert!(c >= prev - 1e-15, "CDF not monotonic at x={x}");
1414 prev = c;
1415 }
1416 }
1417
1418 #[test]
1419 fn test_pert_quantile_symmetric_median_is_the_mode() {
1420 let p = Pert::new(0.0, 5.0, 10.0).unwrap();
1421 assert!((p.quantile(0.5).unwrap() - 5.0).abs() < 1e-9);
1422 assert_eq!(p.quantile(0.0), Some(0.0));
1423 assert_eq!(p.quantile(1.0), Some(10.0));
1424 assert_eq!(p.quantile(1.5), None);
1425 }
1426
1427 #[test]
1432 fn test_pert_quantile_inverts_the_cdf_when_skewed() {
1433 let p = Pert::new(0.0, 1.0, 10.0).unwrap();
1434 for &q in &[1e-6, 0.01, 0.05, 0.25, 0.5, 0.75, 0.95, 0.99] {
1435 let x = p.quantile(q).unwrap();
1436 assert!((0.0..=10.0).contains(&x));
1437 assert!(
1438 (p.cdf(x) - q).abs() < 1e-9,
1439 "p={q}: x={x}, cdf={}",
1440 p.cdf(x)
1441 );
1442 }
1443 let normal_approx_at_5 = p.mean() + special::inverse_normal_cdf(0.05) * p.std_dev();
1444 assert!((p.quantile(0.05).unwrap() - 0.300).abs() < 1e-3);
1445 assert!((p.quantile(0.95).unwrap() - 5.4207).abs() < 1e-3);
1446 assert!(
1447 normal_approx_at_5 < 0.0,
1448 "the approximation leaves the support"
1449 );
1450 }
1451
1452 #[test]
1453 fn test_pert_invalid() {
1454 assert!(Pert::new(5.0, 3.0, 10.0).is_err());
1455 assert!(Pert::new(5.0, 5.0, 5.0).is_err());
1456 }
1457
1458 #[test]
1459 fn test_pert_with_lambda() {
1460 let p4 = Pert::with_shape(0.0, 5.0, 10.0, 4.0).unwrap();
1462 let p8 = Pert::with_shape(0.0, 5.0, 10.0, 8.0).unwrap();
1463 assert!(
1464 p8.variance() < p4.variance(),
1465 "higher λ should give lower variance"
1466 );
1467 }
1468
1469 #[test]
1472 fn test_regularized_beta_bounds() {
1473 assert_eq!(special::regularized_incomplete_beta(0.0, 2.0, 3.0), 0.0);
1474 assert_eq!(special::regularized_incomplete_beta(1.0, 2.0, 3.0), 1.0);
1475 }
1476
1477 #[test]
1478 fn test_regularized_beta_symmetric() {
1479 let result = special::regularized_incomplete_beta(0.5, 3.0, 3.0);
1481 assert!(
1482 (result - 0.5).abs() < 1e-8,
1483 "I_0.5(3,3) = {result}, expected 0.5"
1484 );
1485 }
1486
1487 #[test]
1488 fn test_regularized_beta_known_values() {
1489 for &x in &[0.1, 0.3, 0.5, 0.7, 0.9] {
1491 let result = special::regularized_incomplete_beta(x, 1.0, 1.0);
1492 assert!(
1493 (result - x).abs() < 1e-10,
1494 "I_{x}(1,1) = {result}, expected {x}"
1495 );
1496 }
1497
1498 for &x in &[0.1, 0.5, 0.9] {
1500 let result = special::regularized_incomplete_beta(x, 1.0, 3.0);
1501 let expected = 1.0 - (1.0 - x).powi(3);
1502 assert!(
1503 (result - expected).abs() < 1e-10,
1504 "I_{x}(1,3) = {result}, expected {expected}"
1505 );
1506 }
1507 }
1508
1509 #[test]
1512 fn test_ln_gamma_known() {
1513 assert!((special::ln_gamma(1.0)).abs() < 1e-10);
1515 assert!((special::ln_gamma(2.0)).abs() < 1e-10);
1517 assert!((special::ln_gamma(3.0) - 2.0_f64.ln()).abs() < 1e-10);
1519 assert!((special::ln_gamma(5.0) - 24.0_f64.ln()).abs() < 1e-10);
1521 assert!(
1523 (special::ln_gamma(0.5) - std::f64::consts::PI.sqrt().ln()).abs() < 1e-10,
1524 "ln Γ(0.5) = {}, expected {}",
1525 special::ln_gamma(0.5),
1526 std::f64::consts::PI.sqrt().ln()
1527 );
1528 }
1529
1530 #[test]
1533 fn test_weibull_exponential_special_case() {
1534 let w = Weibull::new(1.0, 5.0).unwrap();
1536 assert!((w.mean() - 5.0).abs() < 1e-10);
1537 assert!((w.variance() - 25.0).abs() < 1e-8);
1538 }
1539
1540 #[test]
1541 fn test_weibull_rayleigh_special_case() {
1542 let w = Weibull::new(2.0, 1.0).unwrap();
1544 let expected_mean = std::f64::consts::PI.sqrt() / 2.0;
1545 assert!(
1546 (w.mean() - expected_mean).abs() < 1e-10,
1547 "Weibull(2,1) mean = {}, expected {}",
1548 w.mean(),
1549 expected_mean
1550 );
1551 }
1552
1553 #[test]
1554 fn test_weibull_cdf_known_values() {
1555 let w = Weibull::new(2.0, 10.0).unwrap();
1557 assert!((w.cdf(10.0) - (1.0 - (-1.0_f64).exp())).abs() < 1e-10);
1559 assert_eq!(w.cdf(0.0), 0.0);
1561 assert_eq!(w.cdf(-1.0), 0.0);
1563 }
1564
1565 #[test]
1566 fn test_weibull_pdf_basic() {
1567 let w = Weibull::new(1.0, 1.0).unwrap();
1569 assert!((w.pdf(0.0) - 1.0).abs() < 1e-10);
1570 assert!((w.pdf(1.0) - (-1.0_f64).exp()).abs() < 1e-10);
1571 assert!((w.pdf(2.0) - (-2.0_f64).exp()).abs() < 1e-10);
1572 }
1573
1574 #[test]
1575 fn test_weibull_pdf_negative() {
1576 let w = Weibull::new(2.0, 5.0).unwrap();
1577 assert_eq!(w.pdf(-1.0), 0.0);
1578 }
1579
1580 #[test]
1581 fn test_weibull_quantile_roundtrip() {
1582 let w = Weibull::new(2.5, 100.0).unwrap();
1583 for &p in &[0.1, 0.25, 0.5, 0.75, 0.9] {
1584 let t = w.quantile(p).unwrap();
1585 let p_back = w.cdf(t);
1586 assert!(
1587 (p_back - p).abs() < 1e-10,
1588 "roundtrip: p={p} -> t={t} -> p_back={p_back}"
1589 );
1590 }
1591 }
1592
1593 #[test]
1594 fn test_weibull_quantile_edge_cases() {
1595 let w = Weibull::new(2.0, 10.0).unwrap();
1596 assert_eq!(w.quantile(0.0), Some(0.0));
1597 assert_eq!(w.quantile(1.0), None);
1598 assert_eq!(w.quantile(-0.1), None);
1599 }
1600
1601 #[test]
1602 fn test_weibull_reliability() {
1603 let w = Weibull::new(2.0, 10.0).unwrap();
1604 for &t in &[1.0, 5.0, 10.0, 20.0] {
1606 let r = w.reliability(t);
1607 let f = w.cdf(t);
1608 assert!((r + f - 1.0).abs() < 1e-14);
1609 }
1610 }
1611
1612 #[test]
1613 fn test_weibull_hazard_rate() {
1614 let w = Weibull::new(1.0, 5.0).unwrap();
1616 for &t in &[1.0, 5.0, 10.0] {
1617 assert!((w.hazard_rate(t) - 0.2).abs() < 1e-10);
1618 }
1619 let w2 = Weibull::new(2.0, 10.0).unwrap();
1621 assert!((w2.hazard_rate(5.0) - 2.0 * 5.0 / 100.0).abs() < 1e-10);
1622 }
1623
1624 #[test]
1625 fn test_weibull_invalid() {
1626 assert!(Weibull::new(0.0, 1.0).is_err());
1627 assert!(Weibull::new(-1.0, 1.0).is_err());
1628 assert!(Weibull::new(1.0, 0.0).is_err());
1629 assert!(Weibull::new(1.0, -1.0).is_err());
1630 assert!(Weibull::new(f64::NAN, 1.0).is_err());
1631 assert!(Weibull::new(1.0, f64::INFINITY).is_err());
1632 }
1633
1634 #[test]
1637 fn test_exponential_basic() {
1638 let e = Exponential::new(0.5).unwrap();
1639 assert!((e.rate() - 0.5).abs() < 1e-10);
1640 assert!((e.mean() - 2.0).abs() < 1e-10);
1641 assert!((e.variance() - 4.0).abs() < 1e-10);
1642 }
1643
1644 #[test]
1645 fn test_exponential_pdf() {
1646 let e = Exponential::new(1.0).unwrap();
1647 assert!((e.pdf(0.0) - 1.0).abs() < 1e-10);
1648 assert!((e.pdf(1.0) - (-1.0_f64).exp()).abs() < 1e-10);
1649 assert_eq!(e.pdf(-1.0), 0.0);
1650 }
1651
1652 #[test]
1653 fn test_exponential_cdf() {
1654 let e = Exponential::new(1.0).unwrap();
1655 assert_eq!(e.cdf(-1.0), 0.0);
1656 assert_eq!(e.cdf(0.0), 0.0);
1657 assert!((e.cdf(1.0) - (1.0 - (-1.0_f64).exp())).abs() < 1e-10);
1658 }
1659
1660 #[test]
1661 fn test_exponential_quantile() {
1662 let e = Exponential::new(2.0).unwrap();
1663 assert_eq!(e.quantile(0.0), Some(0.0));
1664 assert_eq!(e.quantile(1.0), Some(f64::INFINITY));
1665 let q = e.quantile(0.5).unwrap();
1666 assert!((q - 2.0_f64.ln() / 2.0).abs() < 1e-10);
1668 assert!(e.quantile(-0.1).is_none());
1669 assert!(e.quantile(1.1).is_none());
1670 }
1671
1672 #[test]
1673 fn test_exponential_quantile_roundtrip() {
1674 let e = Exponential::new(3.0).unwrap();
1675 for &p in &[0.01, 0.1, 0.25, 0.5, 0.75, 0.9, 0.99] {
1676 let x = e.quantile(p).unwrap();
1677 let p_back = e.cdf(x);
1678 assert!((p_back - p).abs() < 1e-10, "p={p}, x={x}, p_back={p_back}");
1679 }
1680 }
1681
1682 #[test]
1683 fn test_exponential_memoryless() {
1684 let e = Exponential::new(1.5).unwrap();
1686 let s = 2.0;
1687 let t = 3.0;
1688 let lhs = e.survival(s + t) / e.survival(s);
1689 let rhs = e.survival(t);
1690 assert!((lhs - rhs).abs() < 1e-10);
1691 }
1692
1693 #[test]
1694 fn test_exponential_invalid() {
1695 assert!(Exponential::new(0.0).is_err());
1696 assert!(Exponential::new(-1.0).is_err());
1697 assert!(Exponential::new(f64::NAN).is_err());
1698 assert!(Exponential::new(f64::INFINITY).is_err());
1699 }
1700
1701 #[test]
1704 fn test_gamma_basic() {
1705 let g = GammaDistribution::new(2.0, 1.0).unwrap();
1706 assert!((g.shape() - 2.0).abs() < 1e-10);
1707 assert!((g.rate() - 1.0).abs() < 1e-10);
1708 assert!((g.scale() - 1.0).abs() < 1e-10);
1709 assert!((g.mean() - 2.0).abs() < 1e-10);
1710 assert!((g.variance() - 2.0).abs() < 1e-10);
1711 assert!((g.mode() - 1.0).abs() < 1e-10);
1712 }
1713
1714 #[test]
1715 fn test_gamma_from_shape_scale() {
1716 let g = GammaDistribution::from_shape_scale(3.0, 2.0).unwrap();
1717 assert!((g.shape() - 3.0).abs() < 1e-10);
1718 assert!((g.rate() - 0.5).abs() < 1e-10);
1719 assert!((g.mean() - 6.0).abs() < 1e-10); }
1721
1722 #[test]
1723 fn test_gamma_pdf_integral() {
1724 let g = GammaDistribution::new(3.0, 2.0).unwrap();
1726 let n = 20_000;
1727 let dt = 20.0 / n as f64;
1728 let integral: f64 = (0..n)
1729 .map(|i| {
1730 let x = (i as f64 + 0.5) * dt;
1731 g.pdf(x) * dt
1732 })
1733 .sum();
1734 assert!((integral - 1.0).abs() < 0.01, "PDF integral = {integral}");
1735 }
1736
1737 #[test]
1738 fn test_gamma_cdf_known() {
1739 let g = GammaDistribution::new(1.0, 1.0).unwrap();
1741 let expected = 1.0 - (-1.0_f64).exp();
1742 assert!((g.cdf(1.0) - expected).abs() < 1e-10);
1743 }
1744
1745 #[test]
1746 fn test_gamma_cdf_chi2() {
1747 let g = GammaDistribution::new(1.0, 0.5).unwrap(); let x = 4.0;
1751 let expected = 1.0 - (-0.5_f64 * x).exp();
1752 assert!(
1753 (g.cdf(x) - expected).abs() < 1e-8,
1754 "CDF({x}) = {} vs {expected}",
1755 g.cdf(x)
1756 );
1757 }
1758
1759 #[test]
1760 fn test_gamma_quantile_roundtrip() {
1761 let g = GammaDistribution::new(5.0, 2.0).unwrap();
1762 for &p in &[0.01, 0.1, 0.25, 0.5, 0.75, 0.9, 0.99] {
1763 let x = g.quantile(p).unwrap();
1764 let p_back = g.cdf(x);
1765 assert!((p_back - p).abs() < 1e-6, "p={p}, x={x}, p_back={p_back}");
1766 }
1767 }
1768
1769 #[test]
1770 fn test_gamma_mode_small_shape() {
1771 let g = GammaDistribution::new(0.5, 1.0).unwrap();
1772 assert_eq!(g.mode(), 0.0); }
1774
1775 #[test]
1776 fn test_gamma_invalid() {
1777 assert!(GammaDistribution::new(0.0, 1.0).is_err());
1778 assert!(GammaDistribution::new(-1.0, 1.0).is_err());
1779 assert!(GammaDistribution::new(1.0, 0.0).is_err());
1780 assert!(GammaDistribution::new(1.0, -1.0).is_err());
1781 assert!(GammaDistribution::new(f64::NAN, 1.0).is_err());
1782 }
1783
1784 #[test]
1787 fn test_beta_mean_variance() {
1788 let b = BetaDistribution::new(2.0, 5.0).unwrap();
1789 assert!((b.mean() - 2.0 / 7.0).abs() < 1e-10);
1791 let expected_var = 2.0 * 5.0 / (7.0 * 7.0 * 8.0);
1793 assert!((b.variance() - expected_var).abs() < 1e-10);
1794 }
1795
1796 #[test]
1797 fn test_beta_symmetric() {
1798 let b = BetaDistribution::new(3.0, 3.0).unwrap();
1800 assert!((b.mean() - 0.5).abs() < 1e-10);
1801 assert!((b.mode().unwrap() - 0.5).abs() < 1e-10);
1802 assert!((b.pdf(0.3) - b.pdf(0.7)).abs() < 1e-10);
1804 }
1805
1806 #[test]
1807 fn test_beta_uniform_special_case() {
1808 let b = BetaDistribution::new(1.0, 1.0).unwrap();
1810 assert!((b.mean() - 0.5).abs() < 1e-10);
1811 assert!((b.cdf(0.5) - 0.5).abs() < 1e-10);
1812 assert!((b.cdf(0.25) - 0.25).abs() < 1e-10);
1813 }
1814
1815 #[test]
1816 fn test_beta_cdf_boundaries() {
1817 let b = BetaDistribution::new(2.0, 3.0).unwrap();
1818 assert_eq!(b.cdf(0.0), 0.0);
1819 assert_eq!(b.cdf(-1.0), 0.0);
1820 assert_eq!(b.cdf(1.0), 1.0);
1821 assert_eq!(b.cdf(2.0), 1.0);
1822 }
1823
1824 #[test]
1825 fn test_beta_pdf_integral() {
1826 let b = BetaDistribution::new(2.0, 5.0).unwrap();
1827 let n = 10_000;
1828 let dt = 1.0 / n as f64;
1829 let integral: f64 = (0..n)
1830 .map(|i| {
1831 let x = (i as f64 + 0.5) * dt;
1832 b.pdf(x) * dt
1833 })
1834 .sum();
1835 assert!((integral - 1.0).abs() < 0.01, "PDF integral = {integral}");
1836 }
1837
1838 #[test]
1839 fn test_beta_quantile_roundtrip() {
1840 let b = BetaDistribution::new(2.0, 5.0).unwrap();
1841 for &p in &[0.01, 0.1, 0.25, 0.5, 0.75, 0.9, 0.99] {
1842 let x = b.quantile(p).unwrap();
1843 let p_back = b.cdf(x);
1844 assert!((p_back - p).abs() < 1e-6, "p={p}, x={x}, p_back={p_back}");
1845 }
1846 }
1847
1848 #[test]
1849 fn test_beta_mode() {
1850 let b = BetaDistribution::new(5.0, 3.0).unwrap();
1851 assert!((b.mode().unwrap() - 2.0 / 3.0).abs() < 1e-10);
1853 }
1854
1855 #[test]
1856 fn test_beta_mode_undefined() {
1857 let b = BetaDistribution::new(0.5, 0.5).unwrap();
1859 assert!(b.mode().is_none());
1860 }
1861
1862 #[test]
1863 fn test_beta_invalid() {
1864 assert!(BetaDistribution::new(0.0, 1.0).is_err());
1865 assert!(BetaDistribution::new(-1.0, 1.0).is_err());
1866 assert!(BetaDistribution::new(1.0, 0.0).is_err());
1867 assert!(BetaDistribution::new(f64::NAN, 1.0).is_err());
1868 }
1869
1870 #[test]
1873 fn test_chi2_mean_variance() {
1874 let chi2 = ChiSquared::new(5.0).unwrap();
1875 assert!((chi2.mean() - 5.0).abs() < 1e-10);
1876 assert!((chi2.variance() - 10.0).abs() < 1e-10);
1877 }
1878
1879 #[test]
1880 fn test_chi2_exp_special_case() {
1881 let chi2 = ChiSquared::new(2.0).unwrap();
1883 let x = 4.0;
1884 let expected = 1.0 - (-x / 2.0_f64).exp();
1885 assert!(
1886 (chi2.cdf(x) - expected).abs() < 1e-8,
1887 "CDF({x}) = {} vs {expected}",
1888 chi2.cdf(x)
1889 );
1890 }
1891
1892 #[test]
1893 fn test_chi2_cdf_boundaries() {
1894 let chi2 = ChiSquared::new(3.0).unwrap();
1895 assert_eq!(chi2.cdf(0.0), 0.0);
1896 assert_eq!(chi2.cdf(-1.0), 0.0);
1897 assert!(chi2.cdf(100.0) > 0.999);
1899 }
1900
1901 #[test]
1902 fn test_chi2_pdf_integral() {
1903 let chi2 = ChiSquared::new(4.0).unwrap();
1904 let n = 20_000;
1905 let dt = 30.0 / n as f64;
1906 let integral: f64 = (0..n)
1907 .map(|i| {
1908 let x = (i as f64 + 0.5) * dt;
1909 chi2.pdf(x) * dt
1910 })
1911 .sum();
1912 assert!((integral - 1.0).abs() < 0.01, "PDF integral = {integral}");
1913 }
1914
1915 #[test]
1916 fn test_chi2_quantile_roundtrip() {
1917 let chi2 = ChiSquared::new(5.0).unwrap();
1918 for &p in &[0.01, 0.05, 0.1, 0.25, 0.5, 0.75, 0.9, 0.95, 0.99] {
1919 let x = chi2.quantile(p).unwrap();
1920 let p_back = chi2.cdf(x);
1921 assert!((p_back - p).abs() < 1e-6, "p={p}, x={x}, p_back={p_back}");
1922 }
1923 }
1924
1925 #[test]
1926 fn test_chi2_known_quantiles() {
1927 let chi2 = ChiSquared::new(5.0).unwrap();
1930 let q05 = chi2.quantile(0.05).unwrap();
1931 assert!((q05 - 1.1455).abs() < 0.01, "q(0.05) = {q05}");
1932 let q95 = chi2.quantile(0.95).unwrap();
1933 assert!((q95 - 11.0705).abs() < 0.01, "q(0.95) = {q95}");
1934 }
1935
1936 #[test]
1937 fn test_chi2_invalid() {
1938 assert!(ChiSquared::new(0.0).is_err());
1939 assert!(ChiSquared::new(-1.0).is_err());
1940 assert!(ChiSquared::new(f64::NAN).is_err());
1941 }
1942
1943 #[test]
1944 fn test_chi2_gamma_consistency() {
1945 let k = 6.0;
1947 let chi2 = ChiSquared::new(k).unwrap();
1948 let gamma = GammaDistribution::new(k / 2.0, 0.5).unwrap();
1949
1950 let x = 5.0;
1951 assert!(
1952 (chi2.cdf(x) - gamma.cdf(x)).abs() < 1e-10,
1953 "Chi2 and Gamma CDF should match"
1954 );
1955 }
1956
1957 #[test]
1958 fn test_weibull_mean_variance_known() {
1959 let w = Weibull::new(3.6, 1000.0).unwrap();
1962 let mean = w.mean();
1963 assert!(mean > 800.0 && mean < 1000.0, "mean={mean}");
1964 assert!(w.variance() > 0.0);
1965 }
1966
1967 #[test]
1968 fn test_weibull_pdf_integral_approx() {
1969 let w = Weibull::new(2.0, 10.0).unwrap();
1971 let n = 10_000;
1972 let dt = 50.0 / n as f64;
1973 let integral: f64 = (0..n)
1974 .map(|i| {
1975 let t = (i as f64 + 0.5) * dt;
1976 w.pdf(t) * dt
1977 })
1978 .sum();
1979 assert!(
1980 (integral - 1.0).abs() < 0.01,
1981 "PDF integral = {integral}, expected ≈ 1.0"
1982 );
1983 }
1984
1985 #[test]
1994 fn test_normal_cdf_reference_values() {
1995 assert!(
1997 (special::standard_normal_cdf(0.0) - 0.5).abs() < 1e-7,
1998 "Φ(0) error"
1999 );
2000 assert!(
2002 (special::standard_normal_cdf(1.96) - 0.975002).abs() < 1e-6,
2003 "Φ(1.96) = {}, expected 0.975002",
2004 special::standard_normal_cdf(1.96)
2005 );
2006 assert!(
2008 (special::standard_normal_cdf(-1.96) - 0.024998).abs() < 1e-6,
2009 "Φ(-1.96) = {}, expected 0.024998",
2010 special::standard_normal_cdf(-1.96)
2011 );
2012 assert!(
2014 (special::standard_normal_cdf(3.0) - 0.998650).abs() < 1e-6,
2015 "Φ(3.0) = {}, expected 0.998650",
2016 special::standard_normal_cdf(3.0)
2017 );
2018 for &x in &[0.5, 1.0, 1.96, 2.5, 3.0] {
2020 let sum = special::standard_normal_cdf(x) + special::standard_normal_cdf(-x);
2021 assert!(
2022 (sum - 1.0).abs() < 1e-14,
2023 "Symmetry violated at x={x}: sum={sum}"
2024 );
2025 }
2026 }
2027
2028 #[test]
2032 fn test_normal_quantile_reference_values() {
2033 assert!(
2035 special::inverse_normal_cdf(0.5).abs() < 5e-4,
2036 "Φ⁻¹(0.5) = {}, expected 0.0",
2037 special::inverse_normal_cdf(0.5)
2038 );
2039 let q975 = special::inverse_normal_cdf(0.975);
2041 assert!(
2042 (q975 - 1.95996).abs() < 5e-4,
2043 "Φ⁻¹(0.975) = {q975}, expected ≈ 1.95996"
2044 );
2045 let q025 = special::inverse_normal_cdf(0.025);
2047 assert!(
2048 (q025 + 1.95996).abs() < 5e-4,
2049 "Φ⁻¹(0.025) = {q025}, expected ≈ -1.95996"
2050 );
2051 for &p in &[0.01, 0.05, 0.1, 0.25, 0.5, 0.75, 0.9, 0.95, 0.99] {
2053 let z = special::inverse_normal_cdf(p);
2054 let p_back = special::standard_normal_cdf(z);
2055 assert!(
2056 (p_back - p).abs() < 1e-3,
2057 "Roundtrip failed: p={p}, z={z}, p_back={p_back}"
2058 );
2059 }
2060 }
2061
2062 #[test]
2070 fn test_gamma_reference_values() {
2071 let sqrt_pi = std::f64::consts::PI.sqrt();
2073 assert!(
2074 (special::gamma(0.5) - sqrt_pi).abs() < 1e-7,
2075 "Γ(0.5) = {}, expected √π = {}",
2076 special::gamma(0.5),
2077 sqrt_pi
2078 );
2079 assert!(
2081 (special::gamma(1.0) - 1.0).abs() < 1e-14,
2082 "Γ(1.0) = {}",
2083 special::gamma(1.0)
2084 );
2085 let gamma_1_5 = sqrt_pi / 2.0;
2087 assert!(
2088 (special::gamma(1.5) - gamma_1_5).abs() < 1e-7,
2089 "Γ(1.5) = {}, expected {}",
2090 special::gamma(1.5),
2091 gamma_1_5
2092 );
2093 assert!(
2095 (special::gamma(2.0) - 1.0).abs() < 1e-14,
2096 "Γ(2.0) = {}",
2097 special::gamma(2.0)
2098 );
2099 assert!(
2101 (special::gamma(3.0) - 2.0).abs() < 1e-13,
2102 "Γ(3.0) = {}",
2103 special::gamma(3.0)
2104 );
2105 assert!(
2107 (special::gamma(4.0) - 6.0).abs() < 1e-12,
2108 "Γ(4.0) = {}",
2109 special::gamma(4.0)
2110 );
2111 assert!(
2113 (special::gamma(5.0) - 24.0).abs() < 1e-8,
2114 "Γ(5.0) = {}, expected 24.0",
2115 special::gamma(5.0)
2116 );
2117 }
2118
2119 #[test]
2128 fn test_gamma_pdf_reference_value() {
2129 let g = GammaDistribution::new(2.0, 1.0).unwrap();
2130 let expected = (-1.0_f64).exp(); assert!(
2132 (g.pdf(1.0) - expected).abs() < 1e-5,
2133 "Gamma(2,1).pdf(1) = {}, expected e⁻¹ = {}",
2134 g.pdf(1.0),
2135 expected
2136 );
2137 }
2138}
2139
2140#[cfg(test)]
2141mod proptests {
2142 use super::*;
2143 use proptest::prelude::*;
2144
2145 proptest! {
2146 #![proptest_config(ProptestConfig::with_cases(300))]
2147
2148 #[test]
2151 fn uniform_cdf_in_01(
2152 min in -100.0_f64..0.0,
2153 max in 1.0_f64..100.0,
2154 x in -200.0_f64..200.0,
2155 ) {
2156 let u = Uniform::new(min, max).unwrap();
2157 let c = u.cdf(x);
2158 prop_assert!((0.0..=1.0).contains(&c));
2159 }
2160
2161 #[test]
2162 fn uniform_quantile_roundtrip(
2163 min in -100.0_f64..0.0,
2164 max in 1.0_f64..100.0,
2165 p in 0.0_f64..=1.0,
2166 ) {
2167 let u = Uniform::new(min, max).unwrap();
2168 let x = u.quantile(p).unwrap();
2169 let p_back = u.cdf(x);
2170 prop_assert!((p_back - p).abs() < 1e-12, "roundtrip: p={p} -> x={x} -> p_back={p_back}");
2171 }
2172
2173 #[test]
2176 fn triangular_cdf_in_01(
2177 min in -100.0_f64..-1.0,
2178 mode_frac in 0.0_f64..=1.0,
2179 range in 1.0_f64..100.0,
2180 x in -200.0_f64..200.0,
2181 ) {
2182 let max = min + range;
2183 let mode = min + mode_frac * range;
2184 let t = Triangular::new(min, mode, max).unwrap();
2185 let c = t.cdf(x);
2186 prop_assert!((0.0..=1.0).contains(&c));
2187 }
2188
2189 #[test]
2190 fn triangular_quantile_roundtrip(
2191 min in -50.0_f64..0.0,
2192 mode_frac in 0.01_f64..0.99,
2193 range in 1.0_f64..50.0,
2194 p in 0.001_f64..0.999,
2195 ) {
2196 let max = min + range;
2197 let mode = min + mode_frac * range;
2198 let t = Triangular::new(min, mode, max).unwrap();
2199 let x = t.quantile(p).unwrap();
2200 let p_back = t.cdf(x);
2201 prop_assert!(
2202 (p_back - p).abs() < 1e-8,
2203 "roundtrip: p={p} -> x={x} -> p_back={p_back}"
2204 );
2205 }
2206
2207 #[test]
2210 fn pert_mean_formula(
2211 min in -50.0_f64..0.0,
2212 mode_frac in 0.01_f64..0.99,
2213 range in 1.0_f64..50.0,
2214 ) {
2215 let max = min + range;
2216 let mode = min + mode_frac * range;
2217 let p = Pert::new(min, mode, max).unwrap();
2218 let expected = (min + 4.0 * mode + max) / 6.0;
2219 prop_assert!(
2220 (p.mean() - expected).abs() < 1e-10,
2221 "PERT mean: {} vs expected: {}",
2222 p.mean(),
2223 expected
2224 );
2225 }
2226
2227 #[test]
2228 fn pert_variance_positive(
2229 min in -50.0_f64..0.0,
2230 mode_frac in 0.01_f64..0.99,
2231 range in 1.0_f64..50.0,
2232 ) {
2233 let max = min + range;
2234 let mode = min + mode_frac * range;
2235 let p = Pert::new(min, mode, max).unwrap();
2236 prop_assert!(p.variance() > 0.0);
2237 }
2238
2239 #[test]
2240 fn pert_cdf_monotonic(
2241 min in -50.0_f64..0.0,
2242 mode_frac in 0.05_f64..0.95,
2243 range in 2.0_f64..50.0,
2244 ) {
2245 let max = min + range;
2246 let mode = min + mode_frac * range;
2247 let p = Pert::new(min, mode, max).unwrap();
2248 let mut prev = 0.0;
2249 for i in 0..=20 {
2250 let x = min + (i as f64 / 20.0) * range;
2251 let c = p.cdf(x);
2252 prop_assert!(c >= prev - 1e-10, "CDF not monotonic at x={x}");
2253 prev = c;
2254 }
2255 }
2256
2257 #[test]
2260 fn weibull_cdf_in_01(
2261 shape in 0.1_f64..10.0,
2262 scale in 0.1_f64..100.0,
2263 t in 0.0_f64..200.0,
2264 ) {
2265 let w = Weibull::new(shape, scale).unwrap();
2266 let c = w.cdf(t);
2267 prop_assert!((0.0..=1.0).contains(&c), "CDF({t}) = {c} out of [0,1]");
2268 }
2269
2270 #[test]
2271 fn weibull_quantile_roundtrip(
2272 shape in 0.5_f64..10.0,
2273 scale in 1.0_f64..100.0,
2274 p in 0.001_f64..0.999,
2275 ) {
2276 let w = Weibull::new(shape, scale).unwrap();
2277 let t = w.quantile(p).unwrap();
2278 let p_back = w.cdf(t);
2279 prop_assert!(
2280 (p_back - p).abs() < 1e-8,
2281 "roundtrip: p={p} -> t={t} -> p_back={p_back}"
2282 );
2283 }
2284
2285 #[test]
2286 fn weibull_pdf_non_negative(
2287 shape in 0.1_f64..10.0,
2288 scale in 0.1_f64..100.0,
2289 t in 0.0_f64..200.0,
2290 ) {
2291 let w = Weibull::new(shape, scale).unwrap();
2292 prop_assert!(w.pdf(t) >= 0.0, "PDF({t}) must be >= 0");
2293 }
2294
2295 #[test]
2296 fn weibull_reliability_plus_cdf_is_one(
2297 shape in 0.5_f64..5.0,
2298 scale in 1.0_f64..50.0,
2299 t in 0.001_f64..100.0,
2300 ) {
2301 let w = Weibull::new(shape, scale).unwrap();
2302 let sum = w.cdf(t) + w.reliability(t);
2303 prop_assert!(
2304 (sum - 1.0).abs() < 1e-12,
2305 "CDF + R should = 1, got {sum}"
2306 );
2307 }
2308
2309 #[test]
2310 fn weibull_variance_positive(
2311 shape in 0.1_f64..10.0,
2312 scale in 0.1_f64..100.0,
2313 ) {
2314 let w = Weibull::new(shape, scale).unwrap();
2315 prop_assert!(w.variance() > 0.0, "variance must be positive");
2316 }
2317
2318 #[test]
2321 fn exponential_cdf_in_01(
2322 rate in 0.01_f64..10.0,
2323 x in 0.0_f64..100.0,
2324 ) {
2325 let e = Exponential::new(rate).unwrap();
2326 let c = e.cdf(x);
2327 prop_assert!((0.0..=1.0).contains(&c), "CDF({x}) = {c}");
2328 }
2329
2330 #[test]
2331 fn exponential_quantile_roundtrip(
2332 rate in 0.01_f64..10.0,
2333 p in 0.001_f64..0.999,
2334 ) {
2335 let e = Exponential::new(rate).unwrap();
2336 let x = e.quantile(p).unwrap();
2337 let p_back = e.cdf(x);
2338 prop_assert!((p_back - p).abs() < 1e-10);
2339 }
2340
2341 #[test]
2344 fn gamma_cdf_in_01(
2345 shape in 0.5_f64..10.0,
2346 rate in 0.1_f64..10.0,
2347 x in 0.001_f64..50.0,
2348 ) {
2349 let g = GammaDistribution::new(shape, rate).unwrap();
2350 let c = g.cdf(x);
2351 prop_assert!((0.0..=1.0).contains(&c), "CDF({x}) = {c}");
2352 }
2353
2354 #[test]
2355 fn gamma_variance_positive(
2356 shape in 0.1_f64..10.0,
2357 rate in 0.1_f64..10.0,
2358 ) {
2359 let g = GammaDistribution::new(shape, rate).unwrap();
2360 prop_assert!(g.variance() > 0.0, "variance must be positive");
2361 }
2362
2363 #[test]
2366 fn beta_cdf_in_01(
2367 alpha in 0.5_f64..10.0,
2368 beta_param in 0.5_f64..10.0,
2369 x in 0.001_f64..0.999,
2370 ) {
2371 let b = BetaDistribution::new(alpha, beta_param).unwrap();
2372 let c = b.cdf(x);
2373 prop_assert!((0.0..=1.0).contains(&c), "CDF({x}) = {c}");
2374 }
2375
2376 #[test]
2377 fn beta_quantile_roundtrip(
2378 alpha in 1.0_f64..10.0,
2379 beta_param in 1.0_f64..10.0,
2380 p in 0.01_f64..0.99,
2381 ) {
2382 let b = BetaDistribution::new(alpha, beta_param).unwrap();
2383 let x = b.quantile(p).unwrap();
2384 let p_back = b.cdf(x);
2385 prop_assert!((p_back - p).abs() < 1e-5, "p={p}, p_back={p_back}");
2386 }
2387
2388 #[test]
2391 fn chi2_cdf_in_01(
2392 k in 1.0_f64..20.0,
2393 x in 0.001_f64..50.0,
2394 ) {
2395 let chi2 = ChiSquared::new(k).unwrap();
2396 let c = chi2.cdf(x);
2397 prop_assert!((0.0..=1.0).contains(&c), "CDF({x}) = {c}");
2398 }
2399
2400 #[test]
2401 fn chi2_variance_positive(
2402 k in 0.5_f64..50.0,
2403 ) {
2404 let chi2 = ChiSquared::new(k).unwrap();
2405 prop_assert!(chi2.variance() > 0.0, "variance must be positive");
2406 }
2407 }
2408}