1use std::f64::consts::{PI, SQRT_2};
7
8const FRAC_1_SQRT_PI: f64 = 0.564_189_583_547_756_3;
9
10pub fn erfc(x: f64) -> f64 {
17 if x.is_nan() {
18 return f64::NAN;
19 }
20 if x < 0.0 {
21 return 2.0 - erfc(-x);
22 }
23 if x < 2.0 {
24 let x2 = x * x;
25 let mut term = x;
26 let mut sum = x;
27 let mut n = 0.0;
28 while term > sum * f64::EPSILON {
29 n += 1.0;
30 term *= 2.0 * x2 / (2.0 * n + 1.0);
31 sum += term;
32 }
33 return 1.0 - 2.0 * FRAC_1_SQRT_PI * (-x2).exp() * sum;
34 }
35 if x > 27.3 {
36 return 0.0; }
38 let tiny = 1e-300;
40 let mut f = x;
41 let mut c = x;
42 let mut d = 0.0;
43 for k in 1..1000 {
44 let a = k as f64 / 2.0;
45 d = x + a * d;
46 d = if d.abs() < tiny { 1.0 / tiny } else { 1.0 / d };
47 c = x + a / c;
48 if c.abs() < tiny {
49 c = tiny;
50 }
51 let delta = c * d;
52 f *= delta;
53 if (delta - 1.0).abs() < f64::EPSILON {
54 break;
55 }
56 }
57 FRAC_1_SQRT_PI * (-x * x).exp() / f
58}
59
60pub fn norm_cdf(x: f64) -> f64 {
62 0.5 * erfc(-x / SQRT_2)
63}
64
65pub fn norm_sf(x: f64) -> f64 {
67 0.5 * erfc(x / SQRT_2)
68}
69
70#[allow(clippy::excessive_precision)]
74pub fn norm_ppf(p: f64) -> f64 {
75 const A: [f64; 6] = [
76 -3.969_683_028_665_376e1,
77 2.209_460_984_245_205e2,
78 -2.759_285_104_469_687e2,
79 1.383_577_518_672_69e2,
80 -3.066_479_806_614_716e1,
81 2.506_628_277_459_239,
82 ];
83 const B: [f64; 5] = [
84 -5.447_609_879_822_406e1,
85 1.615_858_368_580_409e2,
86 -1.556_989_798_598_866e2,
87 6.680_131_188_771_972e1,
88 -1.328_068_155_288_572e1,
89 ];
90 const C: [f64; 6] = [
91 -7.784_894_002_430_293e-3,
92 -3.223_964_580_411_365e-1,
93 -2.400_758_277_161_838,
94 -2.549_732_539_343_734,
95 4.374_664_141_464_968,
96 2.938_163_982_698_783,
97 ];
98 const D: [f64; 4] = [
99 7.784_695_709_041_462e-3,
100 3.224_671_290_700_398e-1,
101 2.445_134_137_142_996,
102 3.754_408_661_907_416,
103 ];
104 const P_LOW: f64 = 0.024_25;
105
106 if p == 0.0 {
107 return f64::NEG_INFINITY;
108 }
109 if p == 1.0 {
110 return f64::INFINITY;
111 }
112 if !(p > 0.0 && p < 1.0) {
113 return f64::NAN;
114 }
115 let tail = |q: f64| {
116 (((((C[0] * q + C[1]) * q + C[2]) * q + C[3]) * q + C[4]) * q + C[5])
117 / ((((D[0] * q + D[1]) * q + D[2]) * q + D[3]) * q + 1.0)
118 };
119 let x = if p < P_LOW {
120 tail((-2.0 * p.ln()).sqrt())
121 } else if p > 1.0 - P_LOW {
122 -tail((-2.0 * (1.0 - p).ln()).sqrt())
123 } else {
124 let q = p - 0.5;
125 let r = q * q;
126 (((((A[0] * r + A[1]) * r + A[2]) * r + A[3]) * r + A[4]) * r + A[5]) * q
127 / (((((B[0] * r + B[1]) * r + B[2]) * r + B[3]) * r + B[4]) * r + 1.0)
128 };
129 let e = if p < 0.5 {
131 norm_cdf(x) - p
132 } else {
133 (1.0 - p) - norm_sf(x)
134 };
135 let u = e * (2.0 * PI).sqrt() * (0.5 * x * x).exp();
136 x - u / (1.0 + 0.5 * x * u)
137}
138
139pub(crate) fn quantile_mut(values: &mut [f64], q: f64) -> f64 {
142 values.sort_by(f64::total_cmp);
143 let pos = q * (values.len() - 1) as f64;
144 let lo = pos.floor() as usize;
145 let hi = pos.ceil() as usize;
146 values[lo] + (pos - lo as f64) * (values[hi] - values[lo])
147}
148
149pub(crate) fn median(values: &[f64]) -> f64 {
151 quantile_mut(&mut values.to_vec(), 0.5)
152}
153
154#[cfg(test)]
155mod tests {
156 use super::*;
157
158 fn rel(a: f64, b: f64) -> f64 {
159 ((a - b) / b).abs()
160 }
161
162 #[test]
164 fn cdf_matches_scipy() {
165 assert_eq!(norm_cdf(0.0), 0.5);
166 assert!(rel(norm_cdf(-1.0), 0.158_655_253_931_457_07) < 1e-14);
167 assert!(rel(norm_cdf(1.96), 0.975_002_104_851_779_5) < 1e-14);
168 assert!(rel(norm_cdf(-2.5), 0.006_209_665_325_776_132) < 1e-13);
169 assert!(rel(norm_sf(5.0), 2.866_515_718_791_933e-7) < 1e-13);
170 assert!(rel(norm_sf(10.0), 7.619_853_024_160_47e-24) < 1e-12);
171 }
172
173 #[test]
174 fn ppf_matches_scipy_and_inverts_cdf() {
175 assert!(rel(norm_ppf(0.975), 1.959_963_984_540_054) < 1e-14);
176 assert!(rel(norm_ppf(0.995), 2.575_829_303_548_900_4) < 1e-14);
177 assert!(rel(norm_ppf(1e-10), -6.361_340_902_404_056) < 1e-13);
178 assert_eq!(norm_ppf(0.5), 0.0);
179 for &p in &[1e-6, 0.001, 0.02, 0.3, 0.7, 0.99, 0.999_999] {
180 assert!(rel(norm_cdf(norm_ppf(p)), p) < 1e-13, "p = {p}");
181 }
182 }
183
184 #[test]
185 fn quantile_and_median_match_numpy() {
186 assert_eq!(median(&[3.0, 1.0, 2.0]), 2.0);
187 assert_eq!(median(&[4.0, 1.0, 3.0, 2.0]), 2.5);
188 assert!((quantile_mut(&mut [1.0, 2.0, 3.0, 4.0, 5.0], 0.95) - 4.8).abs() < 1e-12);
189 }
190}