ipfrs_tensorlogic/hypothesis_test_engine/
functions.rs1use super::types::SampleData;
6
7pub fn sample_stats(data: &[f64]) -> SampleData {
9 let n = data.len();
10 let mean = if n == 0 {
11 0.0
12 } else {
13 data.iter().copied().sum::<f64>() / n as f64
14 };
15 let variance = if n < 2 {
16 0.0
17 } else {
18 data.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / (n - 1) as f64
19 };
20 let std_dev = variance.sqrt();
21 SampleData {
22 values: data.to_vec(),
23 label: String::new(),
24 n,
25 mean,
26 variance,
27 std_dev,
28 }
29}
30pub fn normal_cdf(z: f64) -> f64 {
35 let t = 1.0 / (1.0 + 0.2316419 * z.abs());
36 let poly = t
37 * (0.319_381_530
38 + t * (-0.356_563_782
39 + t * (1.781_477_937 + t * (-1.821_255_978 + t * 1.330_274_429))));
40 let phi = ((-z * z / 2.0).exp()) / (2.0 * std::f64::consts::PI).sqrt() * poly;
41 if z >= 0.0 {
42 1.0 - phi
43 } else {
44 phi
45 }
46}
47#[inline]
49pub(super) fn z_two_tailed(z: f64) -> f64 {
50 2.0 * (1.0 - normal_cdf(z.abs()))
51}
52fn regularised_gamma_p(a: f64, x: f64) -> f64 {
56 if x < 0.0 {
57 return 0.0;
58 }
59 if x == 0.0 {
60 return 0.0;
61 }
62 let ln_gamma_a = ln_gamma(a);
63 let max_iter = 200;
64 let mut term = 1.0 / a;
65 let mut sum = term;
66 let mut ap = a;
67 for _ in 0..max_iter {
68 ap += 1.0;
69 term *= x / ap;
70 sum += term;
71 if term.abs() < sum.abs() * 1e-10 {
72 break;
73 }
74 }
75 let val = (-x + a * x.ln() - ln_gamma_a).exp() * sum;
76 val.clamp(0.0, 1.0)
77}
78fn regularised_gamma_q(a: f64, x: f64) -> f64 {
80 if x < 0.0 {
81 return 1.0;
82 }
83 let ln_gamma_a = ln_gamma(a);
84 let fpmin = 1e-300_f64;
85 let mut b = x + 1.0 - a;
86 let mut c = 1.0 / fpmin;
87 let mut d = 1.0 / b;
88 let mut h = d;
89 let max_iter = 200;
90 for i in 1..=max_iter {
91 let an = -(i as f64) * (i as f64 - a);
92 b += 2.0;
93 d = an * d + b;
94 if d.abs() < fpmin {
95 d = fpmin;
96 }
97 c = b + an / c;
98 if c.abs() < fpmin {
99 c = fpmin;
100 }
101 d = 1.0 / d;
102 let del = d * c;
103 h *= del;
104 if (del - 1.0).abs() < 1e-10 {
105 break;
106 }
107 }
108 let val = (-x + a * x.ln() - ln_gamma_a).exp() * h;
109 val.clamp(0.0, 1.0)
110}
111fn ln_gamma(z: f64) -> f64 {
113 const G: f64 = 7.0;
114 const C: [f64; 9] = [
115 0.999_999_999_999_809_3,
116 676.520_368_121_885_1,
117 -1_259.139_216_722_403,
118 771.323_428_777_653_1,
119 -176.615_029_162_140_6,
120 12.507_343_278_686_905,
121 -0.138_571_095_265_720_12,
122 9.984_369_578_019_572e-6,
123 1.505_632_735_149_311_6e-7,
124 ];
125 if z < 0.5 {
126 std::f64::consts::PI.ln() - (std::f64::consts::PI * z).sin().ln() - ln_gamma(1.0 - z)
127 } else {
128 let x = z - 1.0;
129 let mut a = C[0];
130 for (i, &ci) in C[1..].iter().enumerate() {
131 a += ci / (x + i as f64 + 1.0);
132 }
133 let t = x + G + 0.5;
134 (2.0 * std::f64::consts::PI).sqrt().ln() + (x + 0.5) * t.ln() - t + a.ln()
135 }
136}
137pub fn chi2_p_value(chi2: f64, df: u32) -> f64 {
139 if chi2 <= 0.0 {
140 return 1.0;
141 }
142 let a = df as f64 / 2.0;
143 let x = chi2 / 2.0;
144 if x < a + 1.0 {
145 1.0 - regularised_gamma_p(a, x)
146 } else {
147 regularised_gamma_q(a, x)
148 }
149}
150pub fn t_cdf_approx(t: f64, df: u32) -> f64 {
153 if df == 0 {
154 return 0.5;
155 }
156 if df >= 200 {
157 return normal_cdf(t);
158 }
159 let df_f = df as f64;
160 let t2 = t * t;
161 let x = df_f / (df_f + t2);
162 let ibeta = regularised_incomplete_beta(x, df_f / 2.0, 0.5);
163 let p = 1.0 - 0.5 * ibeta;
164 if t >= 0.0 {
165 p
166 } else {
167 1.0 - p
168 }
169}
170fn regularised_incomplete_beta(x: f64, a: f64, b: f64) -> f64 {
172 if x <= 0.0 {
173 return 0.0;
174 }
175 if x >= 1.0 {
176 return 1.0;
177 }
178 let symmetry = x > (a + 1.0) / (a + b + 2.0);
179 let (xx, aa, bb) = if symmetry { (1.0 - x, b, a) } else { (x, a, b) };
180 let ln_beta = ln_gamma(aa) + ln_gamma(bb) - ln_gamma(aa + bb);
181 let front = (aa * xx.ln() + bb * (1.0 - xx).ln() - ln_beta).exp() / aa;
182 let cf = beta_cf(xx, aa, bb);
183 let result = front * cf;
184 if symmetry {
185 1.0 - result
186 } else {
187 result
188 }
189}
190fn beta_cf(x: f64, a: f64, b: f64) -> f64 {
192 let fpmin = 1e-300_f64;
193 let max_iter = 200;
194 let qab = a + b;
195 let qap = a + 1.0;
196 let qam = a - 1.0;
197 let mut c = 1.0_f64;
198 let mut d = 1.0 - qab * x / qap;
199 if d.abs() < fpmin {
200 d = fpmin;
201 }
202 d = 1.0 / d;
203 let mut h = d;
204 for m in 1..=max_iter {
205 let m_f = m as f64;
206 let aa = m_f * (b - m_f) * x / ((qam + 2.0 * m_f) * (a + 2.0 * m_f));
207 d = 1.0 + aa * d;
208 if d.abs() < fpmin {
209 d = fpmin;
210 }
211 c = 1.0 + aa / c;
212 if c.abs() < fpmin {
213 c = fpmin;
214 }
215 d = 1.0 / d;
216 h *= d * c;
217 let aa2 = -(a + m_f) * (qab + m_f) * x / ((a + 2.0 * m_f) * (qap + 2.0 * m_f));
218 d = 1.0 + aa2 * d;
219 if d.abs() < fpmin {
220 d = fpmin;
221 }
222 c = 1.0 + aa2 / c;
223 if c.abs() < fpmin {
224 c = fpmin;
225 }
226 d = 1.0 / d;
227 let del = d * c;
228 h *= del;
229 if (del - 1.0).abs() < 1e-10 {
230 break;
231 }
232 }
233 h
234}
235#[inline]
237pub(super) fn t_two_tailed(t: f64, df: u32) -> f64 {
238 let p = t_cdf_approx(t.abs(), df);
239 2.0 * (1.0 - p)
240}
241#[inline]
243pub fn xorshift64(state: &mut u64) -> u64 {
244 let mut x = *state;
245 x ^= x << 13;
246 x ^= x >> 7;
247 x ^= x << 17;
248 *state = x;
249 x
250}
251pub fn xorshift_normal(state: &mut u64) -> f64 {
253 let u1 = (xorshift64(state) >> 11) as f64 / (1u64 << 53) as f64 + 1e-10;
254 let u2 = (xorshift64(state) >> 11) as f64 / (1u64 << 53) as f64;
255 (-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos()
256}
257pub(super) fn inverse_normal_cdf(p: f64) -> f64 {
260 const A: [f64; 6] = [
261 -3.969_683_028_665_376e+01,
262 2.209_460_984_245_205e+02,
263 -2.759_285_104_469_687e+02,
264 1.383_577_518_672_69e2,
265 -3.066_479_806_614_716e+01,
266 2.506_628_277_459_239e+00,
267 ];
268 const B: [f64; 5] = [
269 -5.447_609_879_822_406e+01,
270 1.615_858_368_580_409e+02,
271 -1.556_989_798_598_866e+02,
272 6.680_131_188_771_972e+01,
273 -1.328_068_155_288_572e+01,
274 ];
275 const C: [f64; 6] = [
276 -7.784_894_002_430_293e-03,
277 -3.223_964_580_411_365e-01,
278 -2.400_758_277_161_838e+00,
279 -2.549_732_539_343_734e+00,
280 4.374_664_141_464_968e+00,
281 2.938_163_982_698_783e+00,
282 ];
283 const D: [f64; 4] = [
284 7.784_695_709_041_462e-03,
285 3.224_671_290_700_398e-01,
286 2.445_134_137_142_996e+00,
287 3.754_408_661_907_416e+00,
288 ];
289 let p_low = 0.02425;
290 let p_high = 1.0 - p_low;
291 if p < p_low {
292 let q = (-2.0 * p.ln()).sqrt();
293 (((((C[0] * q + C[1]) * q + C[2]) * q + C[3]) * q + C[4]) * q + C[5])
294 / ((((D[0] * q + D[1]) * q + D[2]) * q + D[3]) * q + 1.0)
295 } else if p <= p_high {
296 let q = p - 0.5;
297 let r = q * q;
298 (((((A[0] * r + A[1]) * r + A[2]) * r + A[3]) * r + A[4]) * r + A[5]) * q
299 / (((((B[0] * r + B[1]) * r + B[2]) * r + B[3]) * r + B[4]) * r + 1.0)
300 } else {
301 let q = (-2.0 * (1.0 - p).ln()).sqrt();
302 -(((((C[0] * q + C[1]) * q + C[2]) * q + C[3]) * q + C[4]) * q + C[5])
303 / ((((D[0] * q + D[1]) * q + D[2]) * q + D[3]) * q + 1.0)
304 }
305}
306pub(super) fn t_critical(alpha: f64, df: u32) -> f64 {
308 let target_p = 1.0 - alpha / 2.0;
309 let mut lo = 0.0_f64;
310 let mut hi = 20.0_f64;
311 for _ in 0..60 {
312 let mid = (lo + hi) / 2.0;
313 if t_cdf_approx(mid, df) < target_p {
314 lo = mid;
315 } else {
316 hi = mid;
317 }
318 }
319 (lo + hi) / 2.0
320}