Skip to main content

ipfrs_tensorlogic/hypothesis_test_engine/
functions.rs

1//! Auto-generated module
2//!
3//! 🤖 Generated with [SplitRS](https://github.com/cool-japan/splitrs)
4
5use super::types::SampleData;
6
7/// Compute descriptive statistics for a slice.
8pub 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}
30/// Standard normal CDF Φ(z).
31///
32/// Uses the Abramowitz & Stegun rational approximation (formula 26.2.17),
33/// accurate to |ε| ≤ 7.5 × 10⁻⁸.
34pub 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/// Two-tailed p-value from a z-score: p = 2 · (1 − Φ(|z|)).
48#[inline]
49pub(super) fn z_two_tailed(z: f64) -> f64 {
50    2.0 * (1.0 - normal_cdf(z.abs()))
51}
52/// Regularised incomplete gamma P(a, x) via series expansion.
53///
54/// Used internally by chi2_p_value.
55fn 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}
78/// Regularised incomplete gamma Q(a, x) = 1 − P(a, x) via continued fraction.
79fn 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}
111/// Natural log of the gamma function, Lanczos approximation (g=7).
112fn 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}
137/// Upper-tail p-value for a chi-square statistic: P(χ² > chi2 | df).
138pub 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}
150/// Approximate t-distribution CDF via normal for large df;
151/// for smaller df uses a beta-function-based approximation.
152pub 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}
170/// Regularised incomplete beta I_x(a, b) via continued fraction (Lentz).
171fn 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}
190/// Continued fraction for the incomplete beta (modified Lentz algorithm).
191fn 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/// Two-tailed p-value from a t-score: p = 2 · (1 − P(T ≤ |t|)).
236#[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/// xorshift64 PRNG step.
242#[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}
251/// Draw a standard-normal variate via Box-Muller using xorshift64.
252pub 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}
257/// Approximate inverse normal CDF Φ⁻¹(p) via rational approximation
258/// (Beasley-Springer-Moro, 1-6 digits accuracy).
259pub(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}
306/// Critical value t_{α/2, df} (two-tailed) via binary search on t_cdf_approx.
307pub(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}