1const FRAC_1_SQRT_2PI: f64 = 0.3989422804014326779399460599343818684758586311649;
8
9pub fn standard_normal_cdf(x: f64) -> f64 {
33 if x.is_nan() {
34 return f64::NAN;
35 }
36 if x == f64::INFINITY {
37 return 1.0;
38 }
39 if x == f64::NEG_INFINITY {
40 return 0.0;
41 }
42
43 let abs_x = x.abs();
45 let k = 1.0 / (1.0 + 0.2316419 * abs_x);
46
47 let phi = FRAC_1_SQRT_2PI * (-0.5 * abs_x * abs_x).exp();
49
50 let poly = k
54 * (0.319381530
55 + k * (-0.356563782 + k * (1.781477937 + k * (-1.821255978 + k * 1.330274429))));
56
57 let cdf_abs = 1.0 - phi * poly;
58
59 if x >= 0.0 {
60 cdf_abs
61 } else {
62 1.0 - cdf_abs
63 }
64}
65
66pub fn inverse_normal_cdf(p: f64) -> f64 {
94 if p.is_nan() || !(0.0..=1.0).contains(&p) {
95 return f64::NAN;
96 }
97 if p == 0.0 {
98 return f64::NEG_INFINITY;
99 }
100 if p == 1.0 {
101 return f64::INFINITY;
102 }
103
104 let (q, sign) = if p > 0.5 { (1.0 - p, 1.0) } else { (p, -1.0) };
106
107 let t = (-2.0 * q.ln()).sqrt();
109
110 const C0: f64 = 2.515517;
112 const C1: f64 = 0.802853;
113 const C2: f64 = 0.010328;
114 const D1: f64 = 1.432788;
115 const D2: f64 = 0.189269;
116 const D3: f64 = 0.001308;
117
118 let z = t - (C0 + C1 * t + C2 * t * t) / (1.0 + D1 * t + D2 * t * t + D3 * t * t * t);
119
120 sign * z
121}
122
123pub fn standard_normal_pdf(x: f64) -> f64 {
132 if x.is_nan() {
133 return f64::NAN;
134 }
135 FRAC_1_SQRT_2PI * (-0.5 * x * x).exp()
136}
137
138pub fn ln_gamma(x: f64) -> f64 {
153 #[allow(clippy::excessive_precision)]
154 const COEFFICIENTS: [f64; 9] = [
155 0.99999999999980993,
156 676.5203681218851,
157 -1259.1392167224028,
158 771.32342877765313,
159 -176.61502916214059,
160 12.507343278686905,
161 -0.13857109526572012,
162 9.9843695780195716e-6,
163 1.5056327351493116e-7,
164 ];
165 const G: f64 = 7.0;
166
167 if x < 0.5 {
168 let pi = std::f64::consts::PI;
169 return (pi / (pi * x).sin()).ln() - ln_gamma(1.0 - x);
170 }
171
172 let x = x - 1.0;
173 let mut sum = COEFFICIENTS[0];
174 for (i, &c) in COEFFICIENTS[1..].iter().enumerate() {
175 sum += c / (x + i as f64 + 1.0);
176 }
177
178 let t = x + G + 0.5;
179 0.5 * (2.0 * std::f64::consts::PI).ln() + (x + 0.5) * t.ln() - t + sum.ln()
180}
181
182pub fn gamma(x: f64) -> f64 {
193 ln_gamma(x).exp()
194}
195
196pub fn ln_beta(a: f64, b: f64) -> f64 {
209 ln_gamma(a) + ln_gamma(b) - ln_gamma(a + b)
210}
211
212pub fn regularized_incomplete_beta(x: f64, a: f64, b: f64) -> f64 {
243 if x <= 0.0 {
244 return 0.0;
245 }
246 if x >= 1.0 {
247 return 1.0;
248 }
249
250 if x > (a + 1.0) / (a + b + 2.0) {
252 return 1.0 - regularized_incomplete_beta(1.0 - x, b, a);
253 }
254
255 let ln_prefix = a * x.ln() + b * (1.0 - x).ln() - ln_beta(a, b);
256 let cf = beta_cf(x, a, b);
257 (ln_prefix.exp() / a) * cf
258}
259
260fn beta_cf(x: f64, a: f64, b: f64) -> f64 {
262 const MAX_ITER: usize = 200;
263 const EPS: f64 = 1e-14;
264 const TINY: f64 = 1e-30;
265
266 let mut c = 1.0;
267 let mut d = 1.0 / (1.0 - (a + b) * x / (a + 1.0)).max(TINY);
268 let mut h = d;
269
270 for m in 1..=MAX_ITER {
271 let m_f = m as f64;
272 let num_even = m_f * (b - m_f) * x / ((a + 2.0 * m_f - 1.0) * (a + 2.0 * m_f));
273 d = 1.0 / (1.0 + num_even * d).max(TINY);
274 c = (1.0 + num_even / c).max(TINY);
275 h *= d * c;
276
277 let num_odd = -(a + m_f) * (a + b + m_f) * x / ((a + 2.0 * m_f) * (a + 2.0 * m_f + 1.0));
278 d = 1.0 / (1.0 + num_odd * d).max(TINY);
279 c = (1.0 + num_odd / c).max(TINY);
280 let delta = d * c;
281 h *= delta;
282
283 if (delta - 1.0).abs() < EPS {
284 break;
285 }
286 }
287 h
288}
289
290pub fn regularized_lower_gamma(a: f64, x: f64) -> f64 {
307 if x <= 0.0 {
308 return 0.0;
309 }
310 if x < a + 1.0 {
311 gamma_series(a, x)
312 } else {
313 1.0 - gamma_cf(a, x)
314 }
315}
316
317fn gamma_series(a: f64, x: f64) -> f64 {
319 let mut term = 1.0 / a;
320 let mut sum = term;
321 let mut ap = a;
322 for _ in 0..200 {
323 ap += 1.0;
324 term *= x / ap;
325 sum += term;
326 if term.abs() < sum.abs() * 1e-14 {
327 break;
328 }
329 }
330 sum * (-x + a * x.ln() - ln_gamma(a)).exp()
331}
332
333fn gamma_cf(a: f64, x: f64) -> f64 {
335 let mut b = x + 1.0 - a;
336 let mut c = 1.0 / 1e-30;
337 let mut d = 1.0 / b;
338 let mut h = d;
339 for i in 1..=200 {
340 let an = -(i as f64) * (i as f64 - a);
341 b += 2.0;
342 d = an * d + b;
343 if d.abs() < 1e-30 {
344 d = 1e-30;
345 }
346 c = b + an / c;
347 if c.abs() < 1e-30 {
348 c = 1e-30;
349 }
350 d = 1.0 / d;
351 let delta = d * c;
352 h *= delta;
353 if (delta - 1.0).abs() < 1e-14 {
354 break;
355 }
356 }
357 h * (-x + a * x.ln() - ln_gamma(a)).exp()
358}
359
360pub fn erf(x: f64) -> f64 {
381 if x.is_nan() {
382 return f64::NAN;
383 }
384 let sign = if x >= 0.0 { 1.0 } else { -1.0 };
385 let x = x.abs();
386
387 const P: f64 = 0.3275911;
389 const A1: f64 = 0.254829592;
390 const A2: f64 = -0.284496736;
391 const A3: f64 = 1.421413741;
392 const A4: f64 = -1.453152027;
393 const A5: f64 = 1.061405429;
394
395 let t = 1.0 / (1.0 + P * x);
396 let poly = t * (A1 + t * (A2 + t * (A3 + t * (A4 + t * A5))));
397 sign * (1.0 - poly * (-x * x).exp())
398}
399
400pub fn erfc(x: f64) -> f64 {
411 1.0 - erf(x)
412}
413
414pub fn t_distribution_cdf(t: f64, df: f64) -> f64 {
439 if t.is_nan() || df.is_nan() || df <= 0.0 {
440 return f64::NAN;
441 }
442 if t == 0.0 {
443 return 0.5;
444 }
445 let x = df / (df + t * t);
446 let ib = regularized_incomplete_beta(x, df / 2.0, 0.5);
447 if t >= 0.0 {
448 1.0 - ib / 2.0
449 } else {
450 ib / 2.0
451 }
452}
453
454pub fn t_distribution_pdf(t: f64, df: f64) -> f64 {
461 if t.is_nan() || df.is_nan() || df <= 0.0 {
462 return f64::NAN;
463 }
464 let half_df = df / 2.0;
465 let log_pdf = ln_gamma(half_df + 0.5)
466 - 0.5 * (df * std::f64::consts::PI).ln()
467 - ln_gamma(half_df)
468 - (half_df + 0.5) * (1.0 + t * t / df).ln();
469 log_pdf.exp()
470}
471
472pub fn t_distribution_quantile(p: f64, df: f64) -> f64 {
492 if p.is_nan() || df.is_nan() || df <= 0.0 || p <= 0.0 || p >= 1.0 {
493 return f64::NAN;
494 }
495 if (p - 0.5).abs() < 1e-15 {
496 return 0.0;
497 }
498
499 let mut t = inverse_normal_cdf(p);
501
502 for _ in 0..50 {
504 let cdf = t_distribution_cdf(t, df);
505 let pdf = t_distribution_pdf(t, df);
506 if pdf.abs() < 1e-300 {
507 break;
508 }
509 let delta = (cdf - p) / pdf;
510 t -= delta;
511 if delta.abs() < 1e-12 * t.abs().max(1.0) {
512 break;
513 }
514 }
515 t
516}
517
518pub fn f_distribution_cdf(x: f64, df1: f64, df2: f64) -> f64 {
543 if x.is_nan() || df1.is_nan() || df2.is_nan() || df1 <= 0.0 || df2 <= 0.0 {
544 return f64::NAN;
545 }
546 if x <= 0.0 {
547 return 0.0;
548 }
549 let y = df1 * x / (df1 * x + df2);
550 regularized_incomplete_beta(y, df1 / 2.0, df2 / 2.0)
551}
552
553pub fn f_distribution_quantile(p: f64, df1: f64, df2: f64) -> f64 {
563 if p.is_nan()
564 || df1.is_nan()
565 || df2.is_nan()
566 || df1 <= 0.0
567 || df2 <= 0.0
568 || p <= 0.0
569 || p >= 1.0
570 {
571 return f64::NAN;
572 }
573
574 let mut hi = 2.0;
576 while f_distribution_cdf(hi, df1, df2) < p {
577 hi *= 2.0;
578 if hi > 1e15 {
579 return hi;
580 }
581 }
582 let mut lo = 0.0_f64;
583
584 for _ in 0..200 {
586 let mid = (lo + hi) / 2.0;
587 if hi - lo < 1e-12 * mid.max(1e-15) {
588 break;
589 }
590 if f_distribution_cdf(mid, df1, df2) < p {
591 lo = mid;
592 } else {
593 hi = mid;
594 }
595 }
596 (lo + hi) / 2.0
597}
598
599pub fn chi_squared_cdf(x: f64, k: f64) -> f64 {
624 if x.is_nan() || k.is_nan() || k <= 0.0 {
625 return f64::NAN;
626 }
627 if x <= 0.0 {
628 return 0.0;
629 }
630 regularized_lower_gamma(k / 2.0, x / 2.0)
631}
632
633#[cfg(test)]
634mod tests {
635 use super::*;
636
637 #[test]
640 fn test_cdf_at_zero() {
641 assert!((standard_normal_cdf(0.0) - 0.5).abs() < 1e-7);
642 }
643
644 #[test]
645 fn test_cdf_symmetry() {
646 for &x in &[0.5, 1.0, 1.5, 2.0, 2.5, 3.0] {
647 let sum = standard_normal_cdf(x) + standard_normal_cdf(-x);
648 assert!(
649 (sum - 1.0).abs() < 1e-7,
650 "Φ({x}) + Φ(-{x}) = {sum}, expected 1.0"
651 );
652 }
653 }
654
655 #[test]
656 fn test_cdf_known_values() {
657 assert!((standard_normal_cdf(1.0) - 0.8413).abs() < 0.001);
659 assert!((standard_normal_cdf(2.0) - 0.9772).abs() < 0.001);
660 assert!((standard_normal_cdf(3.0) - 0.9987).abs() < 0.001);
661
662 assert!((standard_normal_cdf(1.645) - 0.95).abs() < 0.001);
664 assert!((standard_normal_cdf(1.96) - 0.975).abs() < 0.001);
665 assert!((standard_normal_cdf(2.576) - 0.995).abs() < 0.001);
666 }
667
668 #[test]
669 fn test_cdf_extremes() {
670 assert_eq!(standard_normal_cdf(f64::INFINITY), 1.0);
671 assert_eq!(standard_normal_cdf(f64::NEG_INFINITY), 0.0);
672 assert!(standard_normal_cdf(f64::NAN).is_nan());
673 }
674
675 #[test]
676 fn test_cdf_monotonic() {
677 let xs: Vec<f64> = (-30..=30).map(|i| i as f64 * 0.1).collect();
678 for w in xs.windows(2) {
679 assert!(
680 standard_normal_cdf(w[0]) <= standard_normal_cdf(w[1]),
681 "CDF not monotonic at x = {}, {}",
682 w[0],
683 w[1]
684 );
685 }
686 }
687
688 #[test]
691 fn test_inverse_cdf_at_half() {
692 assert!(inverse_normal_cdf(0.5).abs() < 1e-4);
693 }
694
695 #[test]
696 fn test_inverse_cdf_known_values() {
697 assert!((inverse_normal_cdf(0.8413) - 1.0).abs() < 0.01);
698 assert!((inverse_normal_cdf(0.975) - 1.96).abs() < 0.01);
699 assert!((inverse_normal_cdf(0.95) - 1.645).abs() < 0.01);
700 }
701
702 #[test]
703 fn test_inverse_cdf_symmetry() {
704 for &p in &[0.1, 0.2, 0.3, 0.4] {
705 let z1 = inverse_normal_cdf(p);
706 let z2 = inverse_normal_cdf(1.0 - p);
707 assert!(
708 (z1 + z2).abs() < 1e-3,
709 "Φ⁻¹({p}) + Φ⁻¹({}) = {}, expected ~0",
710 1.0 - p,
711 z1 + z2
712 );
713 }
714 }
715
716 #[test]
717 fn test_inverse_cdf_extremes() {
718 assert_eq!(inverse_normal_cdf(0.0), f64::NEG_INFINITY);
719 assert_eq!(inverse_normal_cdf(1.0), f64::INFINITY);
720 assert!(inverse_normal_cdf(f64::NAN).is_nan());
721 assert!(inverse_normal_cdf(-0.1).is_nan());
722 assert!(inverse_normal_cdf(1.1).is_nan());
723 }
724
725 #[test]
726 fn test_roundtrip_cdf_inverse() {
727 for &p in &[0.01, 0.05, 0.1, 0.25, 0.5, 0.75, 0.9, 0.95, 0.99] {
728 let z = inverse_normal_cdf(p);
729 let p_back = standard_normal_cdf(z);
730 assert!(
731 (p_back - p).abs() < 0.002,
732 "roundtrip failed: p={p}, z={z}, p_back={p_back}"
733 );
734 }
735 }
736
737 #[test]
740 fn test_pdf_at_zero() {
741 let peak = standard_normal_pdf(0.0);
742 assert!((peak - 0.3989422804014327).abs() < 1e-14);
743 }
744
745 #[test]
746 fn test_pdf_symmetry() {
747 for &x in &[0.5, 1.0, 2.0, 3.0] {
748 let diff = (standard_normal_pdf(x) - standard_normal_pdf(-x)).abs();
749 assert!(diff < 1e-15, "PDF not symmetric at x={x}");
750 }
751 }
752
753 #[test]
756 fn test_ln_gamma_integers() {
757 assert!((ln_gamma(1.0)).abs() < 1e-10); assert!((ln_gamma(2.0)).abs() < 1e-10); assert!((ln_gamma(3.0) - 2.0_f64.ln()).abs() < 1e-10); assert!((ln_gamma(5.0) - 24.0_f64.ln()).abs() < 1e-10); assert!((ln_gamma(7.0) - 720.0_f64.ln()).abs() < 1e-9); }
764
765 #[test]
766 fn test_gamma_half_integers() {
767 let sqrt_pi = std::f64::consts::PI.sqrt();
769 assert!((gamma(0.5) - sqrt_pi).abs() < 1e-10);
770 assert!((gamma(1.5) - sqrt_pi / 2.0).abs() < 1e-10);
772 assert!((gamma(2.5) - 3.0 * sqrt_pi / 4.0).abs() < 1e-10);
774 }
775
776 #[test]
777 fn test_gamma_positive() {
778 for &x in &[0.1, 0.5, 1.0, 2.0, 5.0, 10.0] {
779 assert!(gamma(x) > 0.0, "Γ({x}) should be positive");
780 }
781 }
782
783 #[test]
786 fn test_ln_beta_known() {
787 assert!(ln_beta(1.0, 1.0).abs() < 1e-10);
789 assert!((ln_beta(1.0, 2.0) - (-2.0_f64.ln())).abs() < 1e-10);
791 assert!((ln_beta(3.0, 5.0) - ln_beta(5.0, 3.0)).abs() < 1e-10);
793 }
794
795 #[test]
798 fn test_inc_beta_boundary() {
799 assert_eq!(regularized_incomplete_beta(0.0, 2.0, 3.0), 0.0);
800 assert_eq!(regularized_incomplete_beta(1.0, 2.0, 3.0), 1.0);
801 }
802
803 #[test]
804 fn test_inc_beta_uniform() {
805 for &x in &[0.1, 0.3, 0.5, 0.7, 0.9] {
807 let result = regularized_incomplete_beta(x, 1.0, 1.0);
808 assert!(
809 (result - x).abs() < 1e-10,
810 "I_{x}(1,1) = {result}, expected {x}"
811 );
812 }
813 }
814
815 #[test]
816 fn test_inc_beta_symmetry() {
817 let result = regularized_incomplete_beta(0.5, 3.0, 3.0);
819 assert!((result - 0.5).abs() < 1e-8);
820 }
821
822 #[test]
823 fn test_inc_beta_known_formula() {
824 for &x in &[0.1, 0.5, 0.9] {
826 let result = regularized_incomplete_beta(x, 1.0, 3.0);
827 let expected = 1.0 - (1.0 - x).powi(3);
828 assert!((result - expected).abs() < 1e-10);
829 }
830 }
831
832 #[test]
835 fn test_lower_gamma_exponential() {
836 for &x in &[0.5, 1.0, 2.0, 5.0] {
838 let result = regularized_lower_gamma(1.0, x);
839 let expected = 1.0 - (-x).exp();
840 assert!(
841 (result - expected).abs() < 1e-10,
842 "P(1,{x}) = {result}, expected {expected}"
843 );
844 }
845 }
846
847 #[test]
848 fn test_lower_gamma_boundary() {
849 assert_eq!(regularized_lower_gamma(2.0, 0.0), 0.0);
850 assert_eq!(regularized_lower_gamma(2.0, -1.0), 0.0);
851 }
852
853 #[test]
854 fn test_lower_gamma_large_x() {
855 let result = regularized_lower_gamma(3.0, 100.0);
857 assert!((result - 1.0).abs() < 1e-10);
858 }
859
860 #[test]
863 fn test_erf_known_values() {
864 assert!(erf(0.0).abs() < 1e-7);
865 assert!((erf(1.0) - 0.8427007929).abs() < 1e-6);
866 assert!((erf(10.0) - 1.0).abs() < 1e-7);
868 assert!((erf(1.5) + erf(-1.5)).abs() < 1e-7);
870 }
871
872 #[test]
873 fn test_erf_nan() {
874 assert!(erf(f64::NAN).is_nan());
875 }
876
877 #[test]
878 fn test_erfc_complement() {
879 for &x in &[0.0, 0.5, 1.0, 2.0, 3.0] {
880 let sum = erf(x) + erfc(x);
881 assert!((sum - 1.0).abs() < 1e-7, "erf({x}) + erfc({x}) = {sum}");
882 }
883 }
884
885 #[test]
888 fn test_t_cdf_at_zero() {
889 for &df in &[1.0, 5.0, 10.0, 30.0, 100.0] {
891 assert!(
892 (t_distribution_cdf(0.0, df) - 0.5).abs() < 1e-10,
893 "t_cdf(0, {df}) should be 0.5"
894 );
895 }
896 }
897
898 #[test]
899 fn test_t_cdf_symmetry() {
900 for &df in &[1.0, 5.0, 10.0] {
901 for &t in &[0.5, 1.0, 2.0] {
902 let sum = t_distribution_cdf(t, df) + t_distribution_cdf(-t, df);
903 assert!(
904 (sum - 1.0).abs() < 1e-8,
905 "t_cdf({t},{df}) + t_cdf(-{t},{df}) = {sum}"
906 );
907 }
908 }
909 }
910
911 #[test]
912 fn test_t_cdf_approaches_normal() {
913 let t_val = 1.96;
915 let result = t_distribution_cdf(t_val, 10000.0);
916 let expected = standard_normal_cdf(t_val);
917 assert!((result - expected).abs() < 0.002);
918 }
919
920 #[test]
921 fn test_t_cdf_known_values() {
922 let cdf = t_distribution_cdf(-2.228, 10.0);
924 assert!((cdf - 0.025).abs() < 0.002, "t_cdf(-2.228, 10) = {cdf}");
925 }
926
927 #[test]
928 fn test_t_cdf_nan() {
929 assert!(t_distribution_cdf(1.0, -1.0).is_nan());
930 assert!(t_distribution_cdf(f64::NAN, 5.0).is_nan());
931 }
932
933 #[test]
934 fn test_t_pdf_positive() {
935 for &df in &[1.0, 5.0, 10.0] {
936 for &t in &[-2.0, 0.0, 1.0, 3.0] {
937 assert!(t_distribution_pdf(t, df) > 0.0);
938 }
939 }
940 }
941
942 #[test]
943 fn test_t_pdf_symmetry() {
944 for &df in &[1.0, 5.0, 10.0] {
945 for &t in &[0.5, 1.0, 2.0] {
946 let diff = (t_distribution_pdf(t, df) - t_distribution_pdf(-t, df)).abs();
947 assert!(diff < 1e-12, "t_pdf not symmetric at t={t}, df={df}");
948 }
949 }
950 }
951
952 #[test]
953 fn test_t_quantile_median() {
954 for &df in &[1.0, 5.0, 10.0, 100.0] {
956 assert!(t_distribution_quantile(0.5, df).abs() < 1e-10);
957 }
958 }
959
960 #[test]
961 fn test_t_quantile_roundtrip() {
962 for &df in &[2.0, 5.0, 10.0, 30.0] {
963 for &p in &[0.025, 0.05, 0.1, 0.5, 0.9, 0.95, 0.975] {
964 let t = t_distribution_quantile(p, df);
965 let p_back = t_distribution_cdf(t, df);
966 assert!(
967 (p_back - p).abs() < 1e-6,
968 "roundtrip: p={p}, df={df}, t={t}, p_back={p_back}"
969 );
970 }
971 }
972 }
973
974 #[test]
975 fn test_t_quantile_nan() {
976 assert!(t_distribution_quantile(0.0, 5.0).is_nan());
977 assert!(t_distribution_quantile(1.0, 5.0).is_nan());
978 assert!(t_distribution_quantile(0.5, -1.0).is_nan());
979 }
980
981 #[test]
984 fn test_f_cdf_zero() {
985 assert_eq!(f_distribution_cdf(0.0, 5.0, 10.0), 0.0);
986 assert_eq!(f_distribution_cdf(-1.0, 5.0, 10.0), 0.0);
987 }
988
989 #[test]
990 fn test_f_cdf_known() {
991 let f = f_distribution_cdf(1.0, 10.0, 10.0);
993 assert!((f - 0.5).abs() < 0.05, "F_cdf(1,10,10) = {f}");
994 }
995
996 #[test]
997 fn test_f_cdf_monotonic() {
998 let xs: Vec<f64> = (0..=20).map(|i| i as f64 * 0.5).collect();
999 for w in xs.windows(2) {
1000 let c0 = f_distribution_cdf(w[0], 5.0, 10.0);
1001 let c1 = f_distribution_cdf(w[1], 5.0, 10.0);
1002 assert!(
1003 c1 >= c0 - 1e-10,
1004 "F CDF not monotonic at {}, {}",
1005 w[0],
1006 w[1]
1007 );
1008 }
1009 }
1010
1011 #[test]
1012 fn test_f_cdf_nan() {
1013 assert!(f_distribution_cdf(1.0, -1.0, 5.0).is_nan());
1014 assert!(f_distribution_cdf(1.0, 5.0, -1.0).is_nan());
1015 }
1016
1017 #[test]
1018 fn test_f_quantile_roundtrip() {
1019 for &(df1, df2) in &[(5.0, 10.0), (10.0, 10.0), (3.0, 20.0)] {
1020 for &p in &[0.05, 0.1, 0.5, 0.9, 0.95] {
1021 let x = f_distribution_quantile(p, df1, df2);
1022 let p_back = f_distribution_cdf(x, df1, df2);
1023 assert!(
1024 (p_back - p).abs() < 1e-4,
1025 "F roundtrip: p={p}, df1={df1}, df2={df2}, x={x}, p_back={p_back}"
1026 );
1027 }
1028 }
1029 }
1030
1031 #[test]
1032 fn test_f_quantile_nan() {
1033 assert!(f_distribution_quantile(0.0, 5.0, 10.0).is_nan());
1034 assert!(f_distribution_quantile(1.0, 5.0, 10.0).is_nan());
1035 }
1036
1037 #[test]
1040 fn test_chi2_cdf_zero() {
1041 assert_eq!(chi_squared_cdf(0.0, 5.0), 0.0);
1042 assert_eq!(chi_squared_cdf(-1.0, 5.0), 0.0);
1043 }
1044
1045 #[test]
1046 fn test_chi2_cdf_exponential_special_case() {
1047 for &x in &[1.0, 2.0, 5.0, 10.0] {
1049 let result = chi_squared_cdf(x, 2.0);
1050 let expected = 1.0 - (-x / 2.0).exp();
1051 assert!(
1052 (result - expected).abs() < 1e-8,
1053 "chi2_cdf({x}, 2) = {result}, expected {expected}"
1054 );
1055 }
1056 }
1057
1058 #[test]
1059 fn test_chi2_cdf_known_critical() {
1060 assert!((chi_squared_cdf(3.841, 1.0) - 0.95).abs() < 0.01);
1062 assert!((chi_squared_cdf(5.991, 2.0) - 0.95).abs() < 0.01);
1064 }
1065
1066 #[test]
1067 fn test_chi2_cdf_nan() {
1068 assert!(chi_squared_cdf(1.0, -1.0).is_nan());
1069 assert!(chi_squared_cdf(f64::NAN, 5.0).is_nan());
1070 }
1071}
1072
1073#[cfg(test)]
1074mod proptests {
1075 use super::*;
1076 use proptest::prelude::*;
1077
1078 proptest! {
1079 #![proptest_config(ProptestConfig::with_cases(500))]
1080
1081 #[test]
1082 fn cdf_in_zero_one(x in -6.0_f64..6.0) {
1083 let c = standard_normal_cdf(x);
1084 prop_assert!((0.0..=1.0).contains(&c), "CDF({x}) = {c} out of [0,1]");
1085 }
1086
1087 #[test]
1088 fn cdf_is_monotonic(x1 in -6.0_f64..6.0, x2 in -6.0_f64..6.0) {
1089 let (lo, hi) = if x1 <= x2 { (x1, x2) } else { (x2, x1) };
1090 prop_assert!(
1091 standard_normal_cdf(lo) <= standard_normal_cdf(hi) + 1e-15,
1092 "CDF not monotonic"
1093 );
1094 }
1095
1096 #[test]
1097 fn inverse_roundtrip(p in 0.001_f64..0.999) {
1098 let z = inverse_normal_cdf(p);
1099 let p_back = standard_normal_cdf(z);
1100 let err = (p_back - p).abs();
1101 prop_assert!(err < 0.005, "roundtrip error {} for p={}", err, p);
1102 }
1103
1104 #[test]
1105 fn pdf_is_non_negative(x in -10.0_f64..10.0) {
1106 prop_assert!(standard_normal_pdf(x) >= 0.0);
1107 }
1108
1109 #[test]
1110 fn inc_beta_in_01(x in 0.01_f64..0.99, a in 0.5_f64..10.0, b in 0.5_f64..10.0) {
1111 let result = regularized_incomplete_beta(x, a, b);
1112 prop_assert!(
1113 (0.0..=1.0).contains(&result),
1114 "I_{x}({a},{b}) = {result} out of [0,1]"
1115 );
1116 }
1117
1118 #[test]
1119 fn inc_beta_complementary(x in 0.01_f64..0.99, a in 0.5_f64..10.0, b in 0.5_f64..10.0) {
1120 let ix = regularized_incomplete_beta(x, a, b);
1122 let i1x = regularized_incomplete_beta(1.0 - x, b, a);
1123 prop_assert!(
1124 (ix + i1x - 1.0).abs() < 1e-8,
1125 "complementary: {ix} + {i1x} != 1"
1126 );
1127 }
1128
1129 #[test]
1130 fn t_cdf_in_01(t in -10.0_f64..10.0, df in 1.0_f64..100.0) {
1131 let c = t_distribution_cdf(t, df);
1132 prop_assert!(
1133 (0.0..=1.0).contains(&c),
1134 "t_cdf({t}, {df}) = {c} out of [0,1]"
1135 );
1136 }
1137
1138 #[test]
1139 fn t_cdf_symmetric(t in 0.01_f64..10.0, df in 1.0_f64..50.0) {
1140 let sum = t_distribution_cdf(t, df) + t_distribution_cdf(-t, df);
1141 prop_assert!(
1142 (sum - 1.0).abs() < 1e-6,
1143 "t symmetry: {sum} != 1 for t={t}, df={df}"
1144 );
1145 }
1146
1147 #[test]
1148 fn erf_odd_symmetry(x in 0.01_f64..5.0) {
1149 let sum = erf(x) + erf(-x);
1150 prop_assert!(sum.abs() < 1e-6, "erf odd symmetry: {sum} for x={x}");
1151 }
1152
1153 #[test]
1154 fn chi2_cdf_in_01(x in 0.01_f64..50.0, k in 0.5_f64..20.0) {
1155 let c = chi_squared_cdf(x, k);
1156 prop_assert!(
1157 (0.0..=1.0).contains(&c),
1158 "chi2_cdf({x}, {k}) = {c} out of [0,1]"
1159 );
1160 }
1161 }
1162}