1#[allow(clippy::excessive_precision)] pub fn ln_gamma(x: f64) -> f64 {
13 const G: [f64; 9] = [
14 0.99999999999980993,
15 676.5203681218851,
16 -1259.1392167224028,
17 771.32342877765313,
18 -176.61502916214059,
19 12.507343278686905,
20 -0.13857109526572012,
21 9.9843695780195716e-6,
22 1.5056327351493116e-7,
23 ];
24 if x < 0.5 {
25 return (std::f64::consts::PI / (std::f64::consts::PI * x).sin()).ln() - ln_gamma(1.0 - x);
27 }
28 let x = x - 1.0;
29 let mut a = G[0];
30 let t = x + 7.5;
31 for (i, &g) in G.iter().enumerate().skip(1) {
32 a += g / (x + i as f64);
33 }
34 0.5 * (2.0 * std::f64::consts::PI).ln() + (x + 0.5) * t.ln() - t + a.ln()
35}
36
37pub fn gamma_q(a: f64, x: f64) -> f64 {
39 if x < 0.0 || a <= 0.0 {
40 return f64::NAN;
41 }
42 if x == 0.0 {
43 return 1.0;
44 }
45 if x < a + 1.0 {
46 let mut sum = 1.0 / a;
48 let mut term = sum;
49 let mut ap = a;
50 for _ in 0..500 {
51 ap += 1.0;
52 term *= x / ap;
53 sum += term;
54 if term.abs() < sum.abs() * 1e-16 {
55 break;
56 }
57 }
58 1.0 - sum * (-x + a * x.ln() - ln_gamma(a)).exp()
59 } else {
60 let mut b = x + 1.0 - a;
62 let mut c = 1.0 / 1e-300;
63 let mut d = 1.0 / b;
64 let mut h = d;
65 for i in 1..500 {
66 let an = -(i as f64) * (i as f64 - a);
67 b += 2.0;
68 d = an * d + b;
69 if d.abs() < 1e-300 {
70 d = 1e-300;
71 }
72 c = b + an / c;
73 if c.abs() < 1e-300 {
74 c = 1e-300;
75 }
76 d = 1.0 / d;
77 let del = d * c;
78 h *= del;
79 if (del - 1.0).abs() < 1e-16 {
80 break;
81 }
82 }
83 (-x + a * x.ln() - ln_gamma(a)).exp() * h
84 }
85}
86
87pub fn chi2_sf(q: f64, df: f64) -> f64 {
89 gamma_q(df / 2.0, q / 2.0)
90}
91
92pub fn satterthwaite_sf(q: f64, weights: &[f64]) -> f64 {
95 let s1: f64 = weights.iter().sum();
96 let s2: f64 = weights.iter().map(|l| l * l).sum();
97 if s1 <= 0.0 || s2 <= 0.0 {
98 return if q > 0.0 { 0.0 } else { 1.0 };
99 }
100 chi2_sf(q * s1 / s2, s1 * s1 / s2)
101}
102
103pub fn imhof_sf(q: f64, weights: &[f64]) -> f64 {
107 let lam: Vec<f64> = weights.iter().copied().filter(|l| *l > 0.0).collect();
108 if lam.is_empty() {
109 return if q > 0.0 { 0.0 } else { 1.0 };
110 }
111 let sum_l: f64 = lam.iter().sum();
112 let integrand = |u: f64| -> f64 {
113 if u == 0.0 {
114 return 0.5 * (sum_l - q);
116 }
117 let mut theta = -0.5 * q * u;
118 let mut ln_rho = 0.0;
119 for &l in &lam {
120 theta += 0.5 * (l * u).atan();
121 ln_rho += 0.25 * (1.0 + l * l * u * u).ln();
122 }
123 theta.sin() / (u * (ln_rho).exp())
124 };
125 let rate = 0.5 * (q + sum_l) + 1.0;
127 let h = (std::f64::consts::PI / rate / 20.0).min(0.05);
128 let mut total = 0.0;
129 let mut u = 0.0;
130 let mut small_blocks = 0;
131 for _ in 0..200_000 {
132 let block = h / 3.0 * (integrand(u) + 4.0 * integrand(u + h) + integrand(u + 2.0 * h));
134 total += block;
135 u += 2.0 * h;
136 if block.abs() < 1e-13 {
137 small_blocks += 1;
138 if small_blocks > 50 {
139 break;
140 }
141 } else {
142 small_blocks = 0;
143 }
144 }
145 (0.5 + total / std::f64::consts::PI).clamp(0.0, 1.0)
146}
147
148const KPSS_TABLE: [(f64, f64); 12] = [
155 (0.0005, 1.3136),
156 (0.001, 1.1739),
157 (0.0025, 1.0087),
158 (0.005, 0.8769),
159 (0.01, 0.7521),
160 (0.025, 0.5837),
161 (0.05, 0.4635),
162 (0.10, 0.3477),
163 (0.15, 0.2847),
164 (0.20, 0.2421),
165 (0.30, 0.1842),
166 (0.50, 0.1189),
167];
168
169const DF_TAU_C_TABLE: [(f64, f64); 12] = [
174 (0.0005, -4.2971),
175 (0.001, -4.1176),
176 (0.0025, -3.8743),
177 (0.005, -3.6730),
178 (0.01, -3.4508),
179 (0.025, -3.1363),
180 (0.05, -2.8713),
181 (0.10, -2.5726),
182 (0.15, -2.3749),
183 (0.20, -2.2203),
184 (0.30, -1.9725),
185 (0.50, -1.5691),
186];
187
188fn interp_table(table: &[(f64, f64)], alpha: f64) -> f64 {
189 let a = alpha.clamp(table[0].0, table[table.len() - 1].0);
190 for w in table.windows(2) {
191 let ((a0, q0), (a1, q1)) = (w[0], w[1]);
192 if a <= a1 {
193 let t = (a.ln() - a0.ln()) / (a1.ln() - a0.ln());
195 return q0 + t * (q1 - q0);
196 }
197 }
198 table[table.len() - 1].1
199}
200
201pub fn kpss_critical(alpha: f64) -> f64 {
204 interp_table(&KPSS_TABLE, alpha)
205}
206
207pub fn df_tau_c_critical(alpha: f64) -> f64 {
210 interp_table(&DF_TAU_C_TABLE, alpha)
211}
212
213#[cfg(test)]
214mod tests {
215 use super::*;
216
217 #[test]
218 fn chi2_sf_matches_r_pchisq() {
219 let cases = [
221 (3.84, 1.0, 0.050043521248705),
222 (5.99, 2.0, 0.0500366270865863),
223 (2.5, 1.7, 0.231036876696032),
224 (10.0, 4.3, 0.0494046995154264),
225 (0.5, 0.5, 0.256322055268539),
226 ];
227 for (q, df, want) in cases {
228 let got = chi2_sf(q, df);
229 assert!((got - want).abs() < 1e-12, "chi2_sf({q},{df}) = {got}");
230 }
231 }
232
233 #[test]
234 fn imhof_matches_exact_chi2_and_r_compquadform() {
235 let got = imhof_sf(3.84, &[1.0]);
237 assert!((got - chi2_sf(3.84, 1.0)).abs() < 1e-6, "{got}");
238 let got = imhof_sf(5.99, &[1.0, 1.0]);
240 assert!((got - chi2_sf(5.99, 2.0)).abs() < 1e-6, "{got}");
241 let cases: [(f64, &[f64], f64); 4] = [
243 (5.0, &[2.0, 1.0], 0.186424873796),
244 (1.2, &[0.9, 0.5, 0.1], 0.449453666487),
245 (10.0, &[3.0, 2.0, 1.0, 0.5], 0.193055225569),
246 (0.3, &[0.2, 0.05], 0.279316877793),
247 ];
248 for (q, l, want) in cases {
249 let got = imhof_sf(q, l);
250 assert!((got - want).abs() < 5e-4, "imhof({q}) = {got} want {want}");
251 }
252 }
253
254 #[test]
255 fn satterthwaite_is_exact_for_equal_weights() {
256 let got = satterthwaite_sf(5.99, &[1.0, 1.0]);
258 assert!((got - chi2_sf(5.99, 2.0)).abs() < 1e-12);
259 let got = satterthwaite_sf(3.0, &[0.5, 0.5, 0.5]);
260 assert!((got - chi2_sf(6.0, 3.0)).abs() < 1e-12);
261 let a = satterthwaite_sf(5.0, &[2.0, 1.0]);
263 let b = imhof_sf(5.0, &[2.0, 1.0]);
264 assert!((a - b).abs() < 0.02, "satt {a} vs imhof {b}");
265 }
266}