1#![allow(clippy::excessive_precision)]
17
18use std::f64::consts::PI;
19
20fn poly(c: &[f64], x: f64) -> f64 {
22 c.iter().rev().fold(0.0, |acc, &k| acc * x + k)
23}
24
25pub fn qnorm(p: f64) -> f64 {
27 if p.is_nan() || !(0.0..=1.0).contains(&p) {
28 return f64::NAN;
29 }
30 if p == 0.0 {
31 return f64::NEG_INFINITY;
32 }
33 if p == 1.0 {
34 return f64::INFINITY;
35 }
36 const A: [f64; 8] = [
37 3.387_132_872_796_366_608,
38 133.141_667_891_784_377_45,
39 1_971.590_950_306_551_442_7,
40 13_731.693_765_509_461_125,
41 45_921.953_931_549_871_457,
42 67_265.770_927_008_700_853,
43 33_430.575_583_588_128_105,
44 2_509.080_928_730_122_672_7,
45 ];
46 const B: [f64; 8] = [
47 1.0,
48 42.313_330_701_600_911_252,
49 687.187_007_492_057_908_3,
50 5_394.196_021_424_751_107_7,
51 21_213.794_301_586_595_867,
52 39_307.895_800_092_710_61,
53 28_729.085_735_721_942_674,
54 5_226.495_278_852_854_561,
55 ];
56 const C: [f64; 8] = [
57 1.423_437_110_749_683_577_34,
58 4.630_337_846_156_545_295_9,
59 5.769_497_221_460_691_405_5,
60 3.647_848_324_763_204_605_04,
61 1.270_458_252_452_368_382_58,
62 0.241_780_725_177_450_611_77,
63 0.022_723_844_989_269_184_583_3,
64 7.745_450_142_783_414_076_4e-4,
65 ];
66 const D: [f64; 8] = [
67 1.0,
68 2.053_191_626_637_758_821_87,
69 1.676_384_830_183_803_849_4,
70 0.689_767_334_985_100_004_55,
71 0.148_103_976_427_480_074_59,
72 0.015_198_666_563_616_457_196_6,
73 5.475_938_084_995_344_946e-4,
74 1.050_750_071_644_416_843_24e-9,
75 ];
76 const E: [f64; 8] = [
77 6.657_904_643_501_103_777_2,
78 5.463_784_911_164_114_369_9,
79 1.784_826_539_917_291_335_8,
80 0.296_560_571_828_504_891_23,
81 0.026_532_189_526_576_123_093,
82 0.001_242_660_947_388_078_438_6,
83 2.711_555_568_743_487_578_15e-5,
84 2.010_334_399_292_288_132_65e-7,
85 ];
86 const F: [f64; 8] = [
87 1.0,
88 0.599_832_206_555_887_937_69,
89 0.136_929_880_922_735_805_31,
90 0.014_875_361_290_850_614_852_5,
91 7.868_691_311_456_132_591e-4,
92 1.846_318_317_510_054_681_8e-5,
93 1.421_511_758_316_445_888_7e-7,
94 2.044_263_103_389_939_785_64e-15,
95 ];
96 let q = p - 0.5;
97 if q.abs() <= 0.425 {
98 let r = 0.180_625 - q * q;
99 return q * poly(&A, r) / poly(&B, r);
100 }
101 let r = if q < 0.0 { p } else { 1.0 - p };
102 let r = (-r.ln()).sqrt();
103 let v = if r <= 5.0 {
104 let r = r - 1.6;
105 poly(&C, r) / poly(&D, r)
106 } else {
107 let r = r - 5.0;
108 poly(&E, r) / poly(&F, r)
109 };
110 if q < 0.0 {
111 -v
112 } else {
113 v
114 }
115}
116
117pub fn dnorm(x: f64) -> f64 {
119 (-0.5 * x * x).exp() / (2.0 * PI).sqrt()
120}
121
122pub fn ln_gamma(x: f64) -> f64 {
124 const G: f64 = 7.0;
125 const COEF: [f64; 9] = [
126 0.999_999_999_999_809_93,
127 676.520_368_121_885_1,
128 -1_259.139_216_722_402_8,
129 771.323_428_777_653_13,
130 -176.615_029_162_140_59,
131 12.507_343_278_686_905,
132 -0.138_571_095_265_720_12,
133 9.984_369_578_019_571_6e-6,
134 1.505_632_735_149_311_6e-7,
135 ];
136 if x < 0.5 {
137 return (PI / (PI * x).sin()).abs().ln() - ln_gamma(1.0 - x);
139 }
140 let x = x - 1.0;
141 let mut a = COEF[0];
142 let t = x + G + 0.5;
143 for (i, &c) in COEF.iter().enumerate().skip(1) {
144 a += c / (x + i as f64);
145 }
146 0.5 * (2.0 * PI).ln() + (x + 0.5) * t.ln() - t + a.ln()
147}
148
149fn beta_cf(a: f64, b: f64, x: f64) -> f64 {
151 const TINY: f64 = 1e-300;
152 let (qab, qap, qam) = (a + b, a + 1.0, a - 1.0);
153 let mut c = 1.0;
154 let mut d = 1.0 - qab * x / qap;
155 if d.abs() < TINY {
156 d = TINY;
157 }
158 d = 1.0 / d;
159 let mut h = d;
160 for m in 1..=500 {
161 let m = m as f64;
162 let m2 = 2.0 * m;
163 let aa = m * (b - m) * x / ((qam + m2) * (a + m2));
164 d = 1.0 + aa * d;
165 if d.abs() < TINY {
166 d = TINY;
167 }
168 c = 1.0 + aa / c;
169 if c.abs() < TINY {
170 c = TINY;
171 }
172 d = 1.0 / d;
173 h *= d * c;
174 let aa = -(a + m) * (qab + m) * x / ((a + m2) * (qap + m2));
175 d = 1.0 + aa * d;
176 if d.abs() < TINY {
177 d = TINY;
178 }
179 c = 1.0 + aa / c;
180 if c.abs() < TINY {
181 c = TINY;
182 }
183 d = 1.0 / d;
184 let del = d * c;
185 h *= del;
186 if (del - 1.0).abs() < 1e-15 {
187 break;
188 }
189 }
190 h
191}
192
193pub fn beta_inc(a: f64, b: f64, x: f64) -> f64 {
195 if x <= 0.0 {
196 return 0.0;
197 }
198 if x >= 1.0 {
199 return 1.0;
200 }
201 let ln_front = ln_gamma(a + b) - ln_gamma(a) - ln_gamma(b) + a * x.ln() + b * (1.0 - x).ln();
202 if x < (a + 1.0) / (a + b + 2.0) {
203 ln_front.exp() * beta_cf(a, b, x) / a
204 } else {
205 1.0 - ln_front.exp() * beta_cf(b, a, 1.0 - x) / b
206 }
207}
208
209pub fn pt(t: f64, df: f64) -> f64 {
211 if t.is_nan() || df.is_nan() || df <= 0.0 {
212 return f64::NAN;
213 }
214 if t.is_infinite() {
215 return if t > 0.0 { 1.0 } else { 0.0 };
216 }
217 let x = df / (df + t * t);
218 let tail = 0.5 * beta_inc(df / 2.0, 0.5, x);
219 if t > 0.0 {
220 1.0 - tail
221 } else {
222 tail
223 }
224}
225
226pub fn dt(x: f64, df: f64) -> f64 {
228 if df <= 0.0 || x.is_nan() {
229 return f64::NAN;
230 }
231 (ln_gamma((df + 1.0) / 2.0)
232 - ln_gamma(df / 2.0)
233 - 0.5 * (df * PI).ln()
234 - (df + 1.0) / 2.0 * (1.0 + x * x / df).ln())
235 .exp()
236}
237
238pub fn qt(p: f64, df: f64) -> f64 {
241 if p.is_nan() || df.is_nan() || df <= 0.0 || !(0.0..=1.0).contains(&p) {
242 return f64::NAN;
243 }
244 if p == 0.0 {
245 return f64::NEG_INFINITY;
246 }
247 if p == 1.0 {
248 return f64::INFINITY;
249 }
250 if p == 0.5 {
251 return 0.0;
252 }
253 if (df - 1.0).abs() < 1e-12 {
254 return (PI * (p - 0.5)).tan();
255 }
256 if (df - 2.0).abs() < 1e-12 {
257 return (2.0 * p - 1.0) / (2.0 * p * (1.0 - p)).sqrt();
258 }
259 if p > 0.5 {
260 return -qt_lower(1.0 - p, df);
261 }
262 qt_lower(p, df)
263}
264
265fn qt_lower(p: f64, df: f64) -> f64 {
268 let mut hi = 0.0;
270 let mut lo = -1.0;
271 let mut guard = 0;
272 while pt(lo, df) > p && guard < 2000 {
273 hi = lo;
274 lo *= 2.0;
275 guard += 1;
276 }
277 let z = qnorm(p);
279 let g1 = (z.powi(3) + z) / 4.0;
280 let mut t = (z + g1 / df).clamp(lo, hi);
281 for _ in 0..200 {
282 let f = pt(t, df) - p;
283 if f.abs() <= 1e-15 * p.max(1e-300) {
284 break;
285 }
286 if f > 0.0 {
287 hi = t;
288 } else {
289 lo = t;
290 }
291 let d = dt(t, df);
292 let mut next = t - f / d;
293 if !(next.is_finite() && next > lo && next < hi) {
294 next = 0.5 * (lo + hi);
295 }
296 if (next - t).abs() <= 1e-14 * t.abs().max(1.0) {
297 t = next;
298 break;
299 }
300 t = next;
301 }
302 t
303}
304
305#[cfg(test)]
306mod tests {
307 use super::*;
308
309 fn close(a: f64, b: f64, tol: f64) -> bool {
310 (a - b).abs() <= tol * b.abs().max(1.0)
311 }
312
313 #[test]
314 fn qnorm_matches_r() {
315 let cases = [
317 (1e-10, -6.361_340_902_404_056_6),
318 (0.001, -3.090_232_306_167_813_5),
319 (0.025, -1.959_963_984_540_054),
320 (0.25, -0.674_489_750_196_081_74),
321 (0.6, 0.253_347_103_135_799_78),
322 (0.975, 1.959_963_984_540_054),
323 (0.999_999, 4.753_424_308_817_089),
324 ];
325 for (p, want) in cases {
326 assert!(close(qnorm(p), want, 1e-13), "qnorm({p}) = {}", qnorm(p));
327 }
328 assert_eq!(qnorm(0.5), 0.0);
329 assert_eq!(qnorm(0.0), f64::NEG_INFINITY);
330 assert!(qnorm(1.5).is_nan());
331 }
332
333 #[test]
334 fn t_distribution_matches_r() {
335 let q3 = [
337 (0.01, -4.540_702_858_568_133),
338 (0.1, -1.637_744_353_696_208_9),
339 (0.3, -0.584_389_727_439_818_65),
340 (0.975, 3.182_446_305_283_707_8),
341 ];
342 for (p, want) in q3 {
343 assert!(
344 close(qt(p, 3.0), want, 1e-10),
345 "qt({p}, 3) = {}",
346 qt(p, 3.0)
347 );
348 }
349 assert!(close(qt(0.001, 7.5), -4.630_319_094_034_099_7, 1e-9));
350 assert!(close(pt(-2.5, 4.0), 0.033_383_272_405_994_063, 1e-11));
351 assert!(close(dt(1.3, 5.0), 0.158_476_735_728_982_41, 1e-12));
352 assert!(close(qt(0.9, 1.0), 3.077_683_537_175_254_4, 1e-12));
354 assert!(close(qt(0.9, 2.0), 1.885_618_083_164_126_9, 1e-12));
355 let t = qt(1e-9, 4.0);
357 assert!(close(pt(t, 4.0), 1e-9, 1e-8), "{t}");
358 }
359
360 #[test]
361 fn ln_gamma_matches_known_values() {
362 assert!(close(ln_gamma(0.5), 0.5 * PI.ln(), 1e-14));
363 assert!(close(ln_gamma(10.0), (362_880.0f64).ln(), 1e-14));
364 assert!(close(ln_gamma(1.0), 0.0, 1e-14));
365 }
366}