1const FRAC_1_SQRT_2PI: f64 = 0.3989422804014326779399460599343818684758586311649;
8
9pub fn standard_normal_cdf(x: f64) -> f64 {
28 if x.is_nan() {
29 return f64::NAN;
30 }
31 if x == f64::INFINITY {
32 return 1.0;
33 }
34 if x == f64::NEG_INFINITY {
35 return 0.0;
36 }
37
38 let abs_x = x.abs();
40 let k = 1.0 / (1.0 + 0.2316419 * abs_x);
41
42 let phi = FRAC_1_SQRT_2PI * (-0.5 * abs_x * abs_x).exp();
44
45 let poly = k
49 * (0.319381530
50 + k * (-0.356563782 + k * (1.781477937 + k * (-1.821255978 + k * 1.330274429))));
51
52 let cdf_abs = 1.0 - phi * poly;
53
54 if x >= 0.0 {
55 cdf_abs
56 } else {
57 1.0 - cdf_abs
58 }
59}
60
61pub fn inverse_normal_cdf(p: f64) -> f64 {
86 if p.is_nan() || !(0.0..=1.0).contains(&p) {
87 return f64::NAN;
88 }
89 if p == 0.0 {
90 return f64::NEG_INFINITY;
91 }
92 if p == 1.0 {
93 return f64::INFINITY;
94 }
95
96 let (q, sign) = if p > 0.5 { (1.0 - p, 1.0) } else { (p, -1.0) };
98
99 let t = (-2.0 * q.ln()).sqrt();
101
102 const C0: f64 = 2.515517;
104 const C1: f64 = 0.802853;
105 const C2: f64 = 0.010328;
106 const D1: f64 = 1.432788;
107 const D2: f64 = 0.189269;
108 const D3: f64 = 0.001308;
109
110 let z = t - (C0 + C1 * t + C2 * t * t) / (1.0 + D1 * t + D2 * t * t + D3 * t * t * t);
111
112 sign * z
113}
114
115pub fn standard_normal_pdf(x: f64) -> f64 {
124 if x.is_nan() {
125 return f64::NAN;
126 }
127 FRAC_1_SQRT_2PI * (-0.5 * x * x).exp()
128}
129
130pub fn ln_gamma(x: f64) -> f64 {
145 #[allow(clippy::excessive_precision)]
146 const COEFFICIENTS: [f64; 9] = [
147 0.99999999999980993,
148 676.5203681218851,
149 -1259.1392167224028,
150 771.32342877765313,
151 -176.61502916214059,
152 12.507343278686905,
153 -0.13857109526572012,
154 9.9843695780195716e-6,
155 1.5056327351493116e-7,
156 ];
157 const G: f64 = 7.0;
158
159 if x < 0.5 {
160 let pi = std::f64::consts::PI;
161 return (pi / (pi * x).sin()).ln() - ln_gamma(1.0 - x);
162 }
163
164 let x = x - 1.0;
165 let mut sum = COEFFICIENTS[0];
166 for (i, &c) in COEFFICIENTS[1..].iter().enumerate() {
167 sum += c / (x + i as f64 + 1.0);
168 }
169
170 let t = x + G + 0.5;
171 0.5 * (2.0 * std::f64::consts::PI).ln() + (x + 0.5) * t.ln() - t + sum.ln()
172}
173
174pub fn gamma(x: f64) -> f64 {
185 ln_gamma(x).exp()
186}
187
188pub fn ln_beta(a: f64, b: f64) -> f64 {
201 ln_gamma(a) + ln_gamma(b) - ln_gamma(a + b)
202}
203
204pub fn regularized_incomplete_beta(x: f64, a: f64, b: f64) -> f64 {
235 if x <= 0.0 {
236 return 0.0;
237 }
238 if x >= 1.0 {
239 return 1.0;
240 }
241
242 if x > (a + 1.0) / (a + b + 2.0) {
244 return 1.0 - regularized_incomplete_beta(1.0 - x, b, a);
245 }
246
247 let ln_prefix = a * x.ln() + b * (1.0 - x).ln() - ln_beta(a, b);
248 let cf = beta_cf(x, a, b);
249 (ln_prefix.exp() / a) * cf
250}
251
252fn beta_cf(x: f64, a: f64, b: f64) -> f64 {
254 const MAX_ITER: usize = 200;
255 const EPS: f64 = 1e-14;
256 const TINY: f64 = 1e-30;
257
258 let mut c = 1.0;
259 let mut d = 1.0 / (1.0 - (a + b) * x / (a + 1.0)).max(TINY);
260 let mut h = d;
261
262 for m in 1..=MAX_ITER {
263 let m_f = m as f64;
264 let num_even = m_f * (b - m_f) * x / ((a + 2.0 * m_f - 1.0) * (a + 2.0 * m_f));
265 d = 1.0 / (1.0 + num_even * d).max(TINY);
266 c = (1.0 + num_even / c).max(TINY);
267 h *= d * c;
268
269 let num_odd = -(a + m_f) * (a + b + m_f) * x / ((a + 2.0 * m_f) * (a + 2.0 * m_f + 1.0));
270 d = 1.0 / (1.0 + num_odd * d).max(TINY);
271 c = (1.0 + num_odd / c).max(TINY);
272 let delta = d * c;
273 h *= delta;
274
275 if (delta - 1.0).abs() < EPS {
276 break;
277 }
278 }
279 h
280}
281
282pub fn regularized_lower_gamma(a: f64, x: f64) -> f64 {
299 if x <= 0.0 {
300 return 0.0;
301 }
302 if x < a + 1.0 {
303 gamma_series(a, x)
304 } else {
305 1.0 - gamma_cf(a, x)
306 }
307}
308
309fn gamma_series(a: f64, x: f64) -> f64 {
311 let mut term = 1.0 / a;
312 let mut sum = term;
313 let mut ap = a;
314 for _ in 0..200 {
315 ap += 1.0;
316 term *= x / ap;
317 sum += term;
318 if term.abs() < sum.abs() * 1e-14 {
319 break;
320 }
321 }
322 sum * (-x + a * x.ln() - ln_gamma(a)).exp()
323}
324
325fn gamma_cf(a: f64, x: f64) -> f64 {
327 let mut b = x + 1.0 - a;
328 let mut c = 1.0 / 1e-30;
329 let mut d = 1.0 / b;
330 let mut h = d;
331 for i in 1..=200 {
332 let an = -(i as f64) * (i as f64 - a);
333 b += 2.0;
334 d = an * d + b;
335 if d.abs() < 1e-30 {
336 d = 1e-30;
337 }
338 c = b + an / c;
339 if c.abs() < 1e-30 {
340 c = 1e-30;
341 }
342 d = 1.0 / d;
343 let delta = d * c;
344 h *= delta;
345 if (delta - 1.0).abs() < 1e-14 {
346 break;
347 }
348 }
349 h * (-x + a * x.ln() - ln_gamma(a)).exp()
350}
351
352pub fn erf(x: f64) -> f64 {
373 if x.is_nan() {
374 return f64::NAN;
375 }
376 let sign = if x >= 0.0 { 1.0 } else { -1.0 };
377 let x = x.abs();
378
379 const P: f64 = 0.3275911;
381 const A1: f64 = 0.254829592;
382 const A2: f64 = -0.284496736;
383 const A3: f64 = 1.421413741;
384 const A4: f64 = -1.453152027;
385 const A5: f64 = 1.061405429;
386
387 let t = 1.0 / (1.0 + P * x);
388 let poly = t * (A1 + t * (A2 + t * (A3 + t * (A4 + t * A5))));
389 sign * (1.0 - poly * (-x * x).exp())
390}
391
392pub fn erfc(x: f64) -> f64 {
403 1.0 - erf(x)
404}
405
406pub fn t_distribution_cdf(t: f64, df: f64) -> f64 {
431 if t.is_nan() || df.is_nan() || df <= 0.0 {
432 return f64::NAN;
433 }
434 if t == 0.0 {
435 return 0.5;
436 }
437 let x = df / (df + t * t);
438 let ib = regularized_incomplete_beta(x, df / 2.0, 0.5);
439 if t >= 0.0 {
440 1.0 - ib / 2.0
441 } else {
442 ib / 2.0
443 }
444}
445
446pub fn t_distribution_pdf(t: f64, df: f64) -> f64 {
453 if t.is_nan() || df.is_nan() || df <= 0.0 {
454 return f64::NAN;
455 }
456 let half_df = df / 2.0;
457 let log_pdf = ln_gamma(half_df + 0.5)
458 - 0.5 * (df * std::f64::consts::PI).ln()
459 - ln_gamma(half_df)
460 - (half_df + 0.5) * (1.0 + t * t / df).ln();
461 log_pdf.exp()
462}
463
464pub fn t_distribution_quantile(p: f64, df: f64) -> f64 {
484 if p.is_nan() || df.is_nan() || df <= 0.0 || p <= 0.0 || p >= 1.0 {
485 return f64::NAN;
486 }
487 if (p - 0.5).abs() < 1e-15 {
488 return 0.0;
489 }
490
491 let mut t = inverse_normal_cdf(p);
493
494 for _ in 0..50 {
496 let cdf = t_distribution_cdf(t, df);
497 let pdf = t_distribution_pdf(t, df);
498 if pdf.abs() < 1e-300 {
499 break;
500 }
501 let delta = (cdf - p) / pdf;
502 t -= delta;
503 if delta.abs() < 1e-12 * t.abs().max(1.0) {
504 break;
505 }
506 }
507 t
508}
509
510pub fn f_distribution_cdf(x: f64, df1: f64, df2: f64) -> f64 {
535 if x.is_nan() || df1.is_nan() || df2.is_nan() || df1 <= 0.0 || df2 <= 0.0 {
536 return f64::NAN;
537 }
538 if x <= 0.0 {
539 return 0.0;
540 }
541 let y = df1 * x / (df1 * x + df2);
542 regularized_incomplete_beta(y, df1 / 2.0, df2 / 2.0)
543}
544
545pub fn f_distribution_quantile(p: f64, df1: f64, df2: f64) -> f64 {
555 if p.is_nan()
556 || df1.is_nan()
557 || df2.is_nan()
558 || df1 <= 0.0
559 || df2 <= 0.0
560 || p <= 0.0
561 || p >= 1.0
562 {
563 return f64::NAN;
564 }
565
566 let mut hi = 2.0;
568 while f_distribution_cdf(hi, df1, df2) < p {
569 hi *= 2.0;
570 if hi > 1e15 {
571 return hi;
572 }
573 }
574 let mut lo = 0.0_f64;
575
576 for _ in 0..200 {
578 let mid = (lo + hi) / 2.0;
579 if hi - lo < 1e-12 * mid.max(1e-15) {
580 break;
581 }
582 if f_distribution_cdf(mid, df1, df2) < p {
583 lo = mid;
584 } else {
585 hi = mid;
586 }
587 }
588 (lo + hi) / 2.0
589}
590
591pub fn chi_squared_cdf(x: f64, k: f64) -> f64 {
616 if x.is_nan() || k.is_nan() || k <= 0.0 {
617 return f64::NAN;
618 }
619 if x <= 0.0 {
620 return 0.0;
621 }
622 regularized_lower_gamma(k / 2.0, x / 2.0)
623}
624
625#[cfg(test)]
626mod tests {
627 use super::*;
628
629 #[test]
632 fn test_cdf_at_zero() {
633 assert!((standard_normal_cdf(0.0) - 0.5).abs() < 1e-7);
634 }
635
636 #[test]
637 fn test_cdf_symmetry() {
638 for &x in &[0.5, 1.0, 1.5, 2.0, 2.5, 3.0] {
639 let sum = standard_normal_cdf(x) + standard_normal_cdf(-x);
640 assert!(
641 (sum - 1.0).abs() < 1e-7,
642 "Φ({x}) + Φ(-{x}) = {sum}, expected 1.0"
643 );
644 }
645 }
646
647 #[test]
648 fn test_cdf_known_values() {
649 assert!((standard_normal_cdf(1.0) - 0.8413).abs() < 0.001);
651 assert!((standard_normal_cdf(2.0) - 0.9772).abs() < 0.001);
652 assert!((standard_normal_cdf(3.0) - 0.9987).abs() < 0.001);
653
654 assert!((standard_normal_cdf(1.645) - 0.95).abs() < 0.001);
656 assert!((standard_normal_cdf(1.96) - 0.975).abs() < 0.001);
657 assert!((standard_normal_cdf(2.576) - 0.995).abs() < 0.001);
658 }
659
660 #[test]
661 fn test_cdf_extremes() {
662 assert_eq!(standard_normal_cdf(f64::INFINITY), 1.0);
663 assert_eq!(standard_normal_cdf(f64::NEG_INFINITY), 0.0);
664 assert!(standard_normal_cdf(f64::NAN).is_nan());
665 }
666
667 #[test]
668 fn test_cdf_monotonic() {
669 let xs: Vec<f64> = (-30..=30).map(|i| i as f64 * 0.1).collect();
670 for w in xs.windows(2) {
671 assert!(
672 standard_normal_cdf(w[0]) <= standard_normal_cdf(w[1]),
673 "CDF not monotonic at x = {}, {}",
674 w[0],
675 w[1]
676 );
677 }
678 }
679
680 #[test]
683 fn test_inverse_cdf_at_half() {
684 assert!(inverse_normal_cdf(0.5).abs() < 1e-4);
685 }
686
687 #[test]
688 fn test_inverse_cdf_known_values() {
689 assert!((inverse_normal_cdf(0.8413) - 1.0).abs() < 0.01);
690 assert!((inverse_normal_cdf(0.975) - 1.96).abs() < 0.01);
691 assert!((inverse_normal_cdf(0.95) - 1.645).abs() < 0.01);
692 }
693
694 #[test]
695 fn test_inverse_cdf_symmetry() {
696 for &p in &[0.1, 0.2, 0.3, 0.4] {
697 let z1 = inverse_normal_cdf(p);
698 let z2 = inverse_normal_cdf(1.0 - p);
699 assert!(
700 (z1 + z2).abs() < 1e-3,
701 "Φ⁻¹({p}) + Φ⁻¹({}) = {}, expected ~0",
702 1.0 - p,
703 z1 + z2
704 );
705 }
706 }
707
708 #[test]
709 fn test_inverse_cdf_extremes() {
710 assert_eq!(inverse_normal_cdf(0.0), f64::NEG_INFINITY);
711 assert_eq!(inverse_normal_cdf(1.0), f64::INFINITY);
712 assert!(inverse_normal_cdf(f64::NAN).is_nan());
713 assert!(inverse_normal_cdf(-0.1).is_nan());
714 assert!(inverse_normal_cdf(1.1).is_nan());
715 }
716
717 #[test]
718 fn test_roundtrip_cdf_inverse() {
719 for &p in &[0.01, 0.05, 0.1, 0.25, 0.5, 0.75, 0.9, 0.95, 0.99] {
720 let z = inverse_normal_cdf(p);
721 let p_back = standard_normal_cdf(z);
722 assert!(
723 (p_back - p).abs() < 0.002,
724 "roundtrip failed: p={p}, z={z}, p_back={p_back}"
725 );
726 }
727 }
728
729 #[test]
732 fn test_pdf_at_zero() {
733 let peak = standard_normal_pdf(0.0);
734 assert!((peak - 0.3989422804014327).abs() < 1e-14);
735 }
736
737 #[test]
738 fn test_pdf_symmetry() {
739 for &x in &[0.5, 1.0, 2.0, 3.0] {
740 let diff = (standard_normal_pdf(x) - standard_normal_pdf(-x)).abs();
741 assert!(diff < 1e-15, "PDF not symmetric at x={x}");
742 }
743 }
744
745 #[test]
748 fn test_ln_gamma_integers() {
749 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); }
756
757 #[test]
758 fn test_gamma_half_integers() {
759 let sqrt_pi = std::f64::consts::PI.sqrt();
761 assert!((gamma(0.5) - sqrt_pi).abs() < 1e-10);
762 assert!((gamma(1.5) - sqrt_pi / 2.0).abs() < 1e-10);
764 assert!((gamma(2.5) - 3.0 * sqrt_pi / 4.0).abs() < 1e-10);
766 }
767
768 #[test]
769 fn test_gamma_positive() {
770 for &x in &[0.1, 0.5, 1.0, 2.0, 5.0, 10.0] {
771 assert!(gamma(x) > 0.0, "Γ({x}) should be positive");
772 }
773 }
774
775 #[test]
778 fn test_ln_beta_known() {
779 assert!(ln_beta(1.0, 1.0).abs() < 1e-10);
781 assert!((ln_beta(1.0, 2.0) - (-2.0_f64.ln())).abs() < 1e-10);
783 assert!((ln_beta(3.0, 5.0) - ln_beta(5.0, 3.0)).abs() < 1e-10);
785 }
786
787 #[test]
790 fn test_inc_beta_boundary() {
791 assert_eq!(regularized_incomplete_beta(0.0, 2.0, 3.0), 0.0);
792 assert_eq!(regularized_incomplete_beta(1.0, 2.0, 3.0), 1.0);
793 }
794
795 #[test]
796 fn test_inc_beta_uniform() {
797 for &x in &[0.1, 0.3, 0.5, 0.7, 0.9] {
799 let result = regularized_incomplete_beta(x, 1.0, 1.0);
800 assert!(
801 (result - x).abs() < 1e-10,
802 "I_{x}(1,1) = {result}, expected {x}"
803 );
804 }
805 }
806
807 #[test]
808 fn test_inc_beta_symmetry() {
809 let result = regularized_incomplete_beta(0.5, 3.0, 3.0);
811 assert!((result - 0.5).abs() < 1e-8);
812 }
813
814 #[test]
815 fn test_inc_beta_known_formula() {
816 for &x in &[0.1, 0.5, 0.9] {
818 let result = regularized_incomplete_beta(x, 1.0, 3.0);
819 let expected = 1.0 - (1.0 - x).powi(3);
820 assert!((result - expected).abs() < 1e-10);
821 }
822 }
823
824 #[test]
827 fn test_lower_gamma_exponential() {
828 for &x in &[0.5, 1.0, 2.0, 5.0] {
830 let result = regularized_lower_gamma(1.0, x);
831 let expected = 1.0 - (-x).exp();
832 assert!(
833 (result - expected).abs() < 1e-10,
834 "P(1,{x}) = {result}, expected {expected}"
835 );
836 }
837 }
838
839 #[test]
840 fn test_lower_gamma_boundary() {
841 assert_eq!(regularized_lower_gamma(2.0, 0.0), 0.0);
842 assert_eq!(regularized_lower_gamma(2.0, -1.0), 0.0);
843 }
844
845 #[test]
846 fn test_lower_gamma_large_x() {
847 let result = regularized_lower_gamma(3.0, 100.0);
849 assert!((result - 1.0).abs() < 1e-10);
850 }
851
852 #[test]
855 fn test_erf_known_values() {
856 assert!(erf(0.0).abs() < 1e-7);
857 assert!((erf(1.0) - 0.8427007929).abs() < 1e-6);
858 assert!((erf(10.0) - 1.0).abs() < 1e-7);
860 assert!((erf(1.5) + erf(-1.5)).abs() < 1e-7);
862 }
863
864 #[test]
865 fn test_erf_nan() {
866 assert!(erf(f64::NAN).is_nan());
867 }
868
869 #[test]
870 fn test_erfc_complement() {
871 for &x in &[0.0, 0.5, 1.0, 2.0, 3.0] {
872 let sum = erf(x) + erfc(x);
873 assert!((sum - 1.0).abs() < 1e-7, "erf({x}) + erfc({x}) = {sum}");
874 }
875 }
876
877 #[test]
880 fn test_t_cdf_at_zero() {
881 for &df in &[1.0, 5.0, 10.0, 30.0, 100.0] {
883 assert!(
884 (t_distribution_cdf(0.0, df) - 0.5).abs() < 1e-10,
885 "t_cdf(0, {df}) should be 0.5"
886 );
887 }
888 }
889
890 #[test]
891 fn test_t_cdf_symmetry() {
892 for &df in &[1.0, 5.0, 10.0] {
893 for &t in &[0.5, 1.0, 2.0] {
894 let sum = t_distribution_cdf(t, df) + t_distribution_cdf(-t, df);
895 assert!(
896 (sum - 1.0).abs() < 1e-8,
897 "t_cdf({t},{df}) + t_cdf(-{t},{df}) = {sum}"
898 );
899 }
900 }
901 }
902
903 #[test]
904 fn test_t_cdf_approaches_normal() {
905 let t_val = 1.96;
907 let result = t_distribution_cdf(t_val, 10000.0);
908 let expected = standard_normal_cdf(t_val);
909 assert!((result - expected).abs() < 0.002);
910 }
911
912 #[test]
913 fn test_t_cdf_known_values() {
914 let cdf = t_distribution_cdf(-2.228, 10.0);
916 assert!((cdf - 0.025).abs() < 0.002, "t_cdf(-2.228, 10) = {cdf}");
917 }
918
919 #[test]
920 fn test_t_cdf_nan() {
921 assert!(t_distribution_cdf(1.0, -1.0).is_nan());
922 assert!(t_distribution_cdf(f64::NAN, 5.0).is_nan());
923 }
924
925 #[test]
926 fn test_t_pdf_positive() {
927 for &df in &[1.0, 5.0, 10.0] {
928 for &t in &[-2.0, 0.0, 1.0, 3.0] {
929 assert!(t_distribution_pdf(t, df) > 0.0);
930 }
931 }
932 }
933
934 #[test]
935 fn test_t_pdf_symmetry() {
936 for &df in &[1.0, 5.0, 10.0] {
937 for &t in &[0.5, 1.0, 2.0] {
938 let diff = (t_distribution_pdf(t, df) - t_distribution_pdf(-t, df)).abs();
939 assert!(diff < 1e-12, "t_pdf not symmetric at t={t}, df={df}");
940 }
941 }
942 }
943
944 #[test]
945 fn test_t_quantile_median() {
946 for &df in &[1.0, 5.0, 10.0, 100.0] {
948 assert!(t_distribution_quantile(0.5, df).abs() < 1e-10);
949 }
950 }
951
952 #[test]
953 fn test_t_quantile_roundtrip() {
954 for &df in &[2.0, 5.0, 10.0, 30.0] {
955 for &p in &[0.025, 0.05, 0.1, 0.5, 0.9, 0.95, 0.975] {
956 let t = t_distribution_quantile(p, df);
957 let p_back = t_distribution_cdf(t, df);
958 assert!(
959 (p_back - p).abs() < 1e-6,
960 "roundtrip: p={p}, df={df}, t={t}, p_back={p_back}"
961 );
962 }
963 }
964 }
965
966 #[test]
967 fn test_t_quantile_nan() {
968 assert!(t_distribution_quantile(0.0, 5.0).is_nan());
969 assert!(t_distribution_quantile(1.0, 5.0).is_nan());
970 assert!(t_distribution_quantile(0.5, -1.0).is_nan());
971 }
972
973 #[test]
976 fn test_f_cdf_zero() {
977 assert_eq!(f_distribution_cdf(0.0, 5.0, 10.0), 0.0);
978 assert_eq!(f_distribution_cdf(-1.0, 5.0, 10.0), 0.0);
979 }
980
981 #[test]
982 fn test_f_cdf_known() {
983 let f = f_distribution_cdf(1.0, 10.0, 10.0);
985 assert!((f - 0.5).abs() < 0.05, "F_cdf(1,10,10) = {f}");
986 }
987
988 #[test]
989 fn test_f_cdf_monotonic() {
990 let xs: Vec<f64> = (0..=20).map(|i| i as f64 * 0.5).collect();
991 for w in xs.windows(2) {
992 let c0 = f_distribution_cdf(w[0], 5.0, 10.0);
993 let c1 = f_distribution_cdf(w[1], 5.0, 10.0);
994 assert!(
995 c1 >= c0 - 1e-10,
996 "F CDF not monotonic at {}, {}",
997 w[0],
998 w[1]
999 );
1000 }
1001 }
1002
1003 #[test]
1004 fn test_f_cdf_nan() {
1005 assert!(f_distribution_cdf(1.0, -1.0, 5.0).is_nan());
1006 assert!(f_distribution_cdf(1.0, 5.0, -1.0).is_nan());
1007 }
1008
1009 #[test]
1010 fn test_f_quantile_roundtrip() {
1011 for &(df1, df2) in &[(5.0, 10.0), (10.0, 10.0), (3.0, 20.0)] {
1012 for &p in &[0.05, 0.1, 0.5, 0.9, 0.95] {
1013 let x = f_distribution_quantile(p, df1, df2);
1014 let p_back = f_distribution_cdf(x, df1, df2);
1015 assert!(
1016 (p_back - p).abs() < 1e-4,
1017 "F roundtrip: p={p}, df1={df1}, df2={df2}, x={x}, p_back={p_back}"
1018 );
1019 }
1020 }
1021 }
1022
1023 #[test]
1024 fn test_f_quantile_nan() {
1025 assert!(f_distribution_quantile(0.0, 5.0, 10.0).is_nan());
1026 assert!(f_distribution_quantile(1.0, 5.0, 10.0).is_nan());
1027 }
1028
1029 #[test]
1032 fn test_chi2_cdf_zero() {
1033 assert_eq!(chi_squared_cdf(0.0, 5.0), 0.0);
1034 assert_eq!(chi_squared_cdf(-1.0, 5.0), 0.0);
1035 }
1036
1037 #[test]
1038 fn test_chi2_cdf_exponential_special_case() {
1039 for &x in &[1.0, 2.0, 5.0, 10.0] {
1041 let result = chi_squared_cdf(x, 2.0);
1042 let expected = 1.0 - (-x / 2.0).exp();
1043 assert!(
1044 (result - expected).abs() < 1e-8,
1045 "chi2_cdf({x}, 2) = {result}, expected {expected}"
1046 );
1047 }
1048 }
1049
1050 #[test]
1051 fn test_chi2_cdf_known_critical() {
1052 assert!((chi_squared_cdf(3.841, 1.0) - 0.95).abs() < 0.01);
1054 assert!((chi_squared_cdf(5.991, 2.0) - 0.95).abs() < 0.01);
1056 }
1057
1058 #[test]
1059 fn test_chi2_cdf_nan() {
1060 assert!(chi_squared_cdf(1.0, -1.0).is_nan());
1061 assert!(chi_squared_cdf(f64::NAN, 5.0).is_nan());
1062 }
1063}
1064
1065#[cfg(test)]
1066mod proptests {
1067 use super::*;
1068 use proptest::prelude::*;
1069
1070 proptest! {
1071 #![proptest_config(ProptestConfig::with_cases(500))]
1072
1073 #[test]
1074 fn cdf_in_zero_one(x in -6.0_f64..6.0) {
1075 let c = standard_normal_cdf(x);
1076 prop_assert!((0.0..=1.0).contains(&c), "CDF({x}) = {c} out of [0,1]");
1077 }
1078
1079 #[test]
1080 fn cdf_is_monotonic(x1 in -6.0_f64..6.0, x2 in -6.0_f64..6.0) {
1081 let (lo, hi) = if x1 <= x2 { (x1, x2) } else { (x2, x1) };
1082 prop_assert!(
1083 standard_normal_cdf(lo) <= standard_normal_cdf(hi) + 1e-15,
1084 "CDF not monotonic"
1085 );
1086 }
1087
1088 #[test]
1089 fn inverse_roundtrip(p in 0.001_f64..0.999) {
1090 let z = inverse_normal_cdf(p);
1091 let p_back = standard_normal_cdf(z);
1092 let err = (p_back - p).abs();
1093 prop_assert!(err < 0.005, "roundtrip error {} for p={}", err, p);
1094 }
1095
1096 #[test]
1097 fn pdf_is_non_negative(x in -10.0_f64..10.0) {
1098 prop_assert!(standard_normal_pdf(x) >= 0.0);
1099 }
1100
1101 #[test]
1102 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) {
1103 let result = regularized_incomplete_beta(x, a, b);
1104 prop_assert!(
1105 (0.0..=1.0).contains(&result),
1106 "I_{x}({a},{b}) = {result} out of [0,1]"
1107 );
1108 }
1109
1110 #[test]
1111 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) {
1112 let ix = regularized_incomplete_beta(x, a, b);
1114 let i1x = regularized_incomplete_beta(1.0 - x, b, a);
1115 prop_assert!(
1116 (ix + i1x - 1.0).abs() < 1e-8,
1117 "complementary: {ix} + {i1x} != 1"
1118 );
1119 }
1120
1121 #[test]
1122 fn t_cdf_in_01(t in -10.0_f64..10.0, df in 1.0_f64..100.0) {
1123 let c = t_distribution_cdf(t, df);
1124 prop_assert!(
1125 (0.0..=1.0).contains(&c),
1126 "t_cdf({t}, {df}) = {c} out of [0,1]"
1127 );
1128 }
1129
1130 #[test]
1131 fn t_cdf_symmetric(t in 0.01_f64..10.0, df in 1.0_f64..50.0) {
1132 let sum = t_distribution_cdf(t, df) + t_distribution_cdf(-t, df);
1133 prop_assert!(
1134 (sum - 1.0).abs() < 1e-6,
1135 "t symmetry: {sum} != 1 for t={t}, df={df}"
1136 );
1137 }
1138
1139 #[test]
1140 fn erf_odd_symmetry(x in 0.01_f64..5.0) {
1141 let sum = erf(x) + erf(-x);
1142 prop_assert!(sum.abs() < 1e-6, "erf odd symmetry: {sum} for x={x}");
1143 }
1144
1145 #[test]
1146 fn chi2_cdf_in_01(x in 0.01_f64..50.0, k in 0.5_f64..20.0) {
1147 let c = chi_squared_cdf(x, k);
1148 prop_assert!(
1149 (0.0..=1.0).contains(&c),
1150 "chi2_cdf({x}, {k}) = {c} out of [0,1]"
1151 );
1152 }
1153 }
1154}