1#![allow(clippy::excessive_precision)]
31
32use crate::matrix::ExprMatrix;
33
34const SIGMA_FACTOR: f64 = 4.0;
36const PRECOMPUTE_RESOLUTION: usize = 10_000;
38const MAX_PRECOMPUTE: f64 = 10.0;
40
41pub fn std_normal_cdf(x: f64) -> f64 {
48 if x < 0.0 {
49 return 1.0 - std_normal_cdf(-x);
50 }
51
52 const A: [f64; 5] = [
54 2.2352520354606839287,
55 161.02823106855587881,
56 1067.6894854603709582,
57 18154.981253343561249,
58 0.065682337918207449113,
59 ];
60 const B: [f64; 4] = [
61 47.20258190468824187,
62 976.09855173777669322,
63 10260.932208618978205,
64 45507.789335026729956,
65 ];
66 const C: [f64; 9] = [
67 0.39894151208813466764,
68 8.8831497943883759412,
69 93.506656132177855979,
70 597.27027639480026226,
71 2494.5375852903726711,
72 6848.1904505362823326,
73 11602.651437647350124,
74 9842.7148383839780218,
75 1.0765576773720192317e-8,
76 ];
77 const D: [f64; 8] = [
78 22.266688044328115691,
79 235.38790178262499861,
80 1519.377599407554805,
81 6485.558298266760755,
82 18615.571640885098091,
83 34900.952721145977266,
84 38912.003286093271411,
85 19685.429676859990727,
86 ];
87 const P: [f64; 6] = [
88 0.21589853405795699,
89 0.1274011611602473639,
90 0.022235277870649807,
91 0.001421619193227893466,
92 2.9112874951168792e-5,
93 0.02307344176494017303,
94 ];
95 const Q: [f64; 5] = [
96 1.28426009614491121,
97 0.468238212480865118,
98 0.0659881378689285515,
99 0.00378239633202758244,
100 7.29751555083966205e-5,
101 ];
102 const M_1_SQRT_2PI: f64 = 0.398942280401432677939946059934;
103
104 let y = x; if y <= 0.67448975 {
106 let xsq = if y > f64::EPSILON * 0.5 { x * x } else { 0.0 };
108 let mut xnum = A[4] * xsq;
109 let mut xden = xsq;
110 for i in 0..3 {
111 xnum = (xnum + A[i]) * xsq;
112 xden = (xden + B[i]) * xsq;
113 }
114 let temp = x * (xnum + A[3]) / (xden + B[3]);
115 0.5 + temp
116 } else if y <= 32.0_f64.sqrt() {
117 let mut xnum = C[8] * y;
119 let mut xden = y;
120 for i in 0..7 {
121 xnum = (xnum + C[i]) * y;
122 xden = (xden + D[i]) * y;
123 }
124 let temp = (xnum + C[7]) / (xden + D[7]);
125 let xsq = (y * 16.0).trunc() / 16.0;
126 let del = (y - xsq) * (y + xsq);
127 let small = (-xsq * xsq * 0.5).exp() * (-del * 0.5).exp() * temp;
128 1.0 - small
129 } else {
130 let xsq = 1.0 / (x * x);
132 let mut xnum = P[5] * xsq;
133 let mut xden = xsq;
134 for i in 0..4 {
135 xnum = (xnum + P[i]) * xsq;
136 xden = (xden + Q[i]) * xsq;
137 }
138 let mut temp = xsq * (xnum + P[4]) / (xden + Q[4]);
139 temp = (M_1_SQRT_2PI - temp) / y;
140 let xsq = (y * 16.0).trunc() / 16.0;
141 let del = (y - xsq) * (y + xsq);
142 let small = (-xsq * xsq * 0.5).exp() * (-del * 0.5).exp() * temp;
143 1.0 - small
144 }
145}
146
147fn build_table() -> Vec<f64> {
150 (0..=PRECOMPUTE_RESOLUTION)
151 .map(|i| std_normal_cdf(MAX_PRECOMPUTE * i as f64 / PRECOMPUTE_RESOLUTION as f64))
152 .collect()
153}
154
155#[inline]
159fn precomputed_cdf(table: &[f64], x: f64, sigma: f64) -> f64 {
160 let v = x / sigma;
161 if v < -MAX_PRECOMPUTE {
162 return 0.0;
163 }
164 if v > MAX_PRECOMPUTE {
165 return 1.0;
166 }
167 let idx = ((v.abs() / MAX_PRECOMPUTE) * PRECOMPUTE_RESOLUTION as f64) as usize;
169 let base = table[idx];
170 if v < 0.0 {
171 1.0 - base
172 } else {
173 base
174 }
175}
176
177fn bandwidth(row: &[f64]) -> f64 {
183 let n = row.len();
184 let nf = n as f64;
185 let mut mean = row.iter().sum::<f64>() / nf;
186 let mut corr = 0.0f64;
187 for &v in row {
188 corr += v - mean;
189 }
190 mean += corr / nf;
191 let mut ss = 0.0f64;
192 for &v in row {
193 let d = v - mean;
194 ss += d * d;
195 }
196 let sd = (ss / (nf - 1.0)).sqrt();
197 let bw = sd / SIGMA_FACTOR;
198 if bw == 0.0 || bw.is_nan() {
199 0.001
200 } else {
201 bw
202 }
203}
204
205pub(crate) fn gaussian_left_tail(expr: &ExprMatrix) -> Vec<f64> {
213 let p = expr.nrow();
214 let n = expr.ncol();
215 let table = build_table();
216 let nf = n as f64;
217 let mut out = vec![0.0f64; p * n];
218 crate::par::fill_chunks_mut(&mut out, n, |g, row_out| {
222 let row = expr.row(g);
223 let bw = bandwidth(row);
224 for (slot, &yj) in row_out.iter_mut().zip(row) {
225 let mut lt = 0.0f64;
226 for &xl in row {
227 lt += precomputed_cdf(&table, yj - xl, bw);
228 }
229 *slot = lt / nf;
230 }
231 });
232 out
233}
234
235fn ln_gamma(x: f64) -> f64 {
240 const G: f64 = 7.0;
241 const C: [f64; 9] = [
242 0.99999999999980993,
243 676.5203681218851,
244 -1259.1392167224028,
245 771.32342877765313,
246 -176.61502916214059,
247 12.507343278686905,
248 -0.13857109526572012,
249 9.9843695780195716e-6,
250 1.5056327351493116e-7,
251 ];
252 if x < 0.5 {
253 (std::f64::consts::PI / (std::f64::consts::PI * x).sin()).ln() - ln_gamma(1.0 - x)
255 } else {
256 let x = x - 1.0;
257 let mut a = C[0];
258 let t = x + G + 0.5;
259 for (i, &c) in C.iter().enumerate().skip(1) {
260 a += c / (x + i as f64);
261 }
262 0.5 * (2.0 * std::f64::consts::PI).ln() + (x + 0.5) * t.ln() - t + a.ln()
263 }
264}
265
266fn gamma_p_series(a: f64, x: f64) -> f64 {
269 let mut ap = a;
270 let mut del = 1.0 / a;
271 let mut sum = del;
272 for _ in 0..300 {
273 ap += 1.0;
274 del *= x / ap;
275 sum += del;
276 if del.abs() < sum.abs() * 1e-16 {
277 break;
278 }
279 }
280 sum * (-x + a * x.ln() - ln_gamma(a)).exp()
281}
282
283fn gamma_q_cf(a: f64, x: f64) -> f64 {
286 const FPMIN: f64 = 1e-300;
287 let mut b = x + 1.0 - a;
288 let mut c = 1.0 / FPMIN;
289 let mut d = 1.0 / b;
290 let mut h = d;
291 for i in 1..300 {
292 let an = -(i as f64) * (i as f64 - a);
293 b += 2.0;
294 d = an * d + b;
295 if d.abs() < FPMIN {
296 d = FPMIN;
297 }
298 c = b + an / c;
299 if c.abs() < FPMIN {
300 c = FPMIN;
301 }
302 d = 1.0 / d;
303 let del = d * c;
304 h *= del;
305 if (del - 1.0).abs() < 1e-16 {
306 break;
307 }
308 }
309 (-x + a * x.ln() - ln_gamma(a)).exp() * h
310}
311
312fn gamma_q(a: f64, x: f64) -> f64 {
314 if x <= 0.0 {
315 return 1.0;
316 }
317 if x < a + 1.0 {
318 1.0 - gamma_p_series(a, x)
319 } else {
320 gamma_q_cf(a, x)
321 }
322}
323
324fn ppois(k: f64, lambda: f64) -> f64 {
327 let kk = k.floor();
328 if kk < 0.0 {
329 return 0.0;
330 }
331 gamma_q(kk + 1.0, lambda)
332}
333
334pub(crate) fn poisson_left_tail(expr: &ExprMatrix) -> Vec<f64> {
340 let p = expr.nrow();
341 let n = expr.ncol();
342 let nf = n as f64;
343 let mut out = vec![0.0f64; p * n];
344 crate::par::fill_chunks_mut(&mut out, n, |g, row_out| {
347 let row = expr.row(g);
348 for (slot, &yj) in row_out.iter_mut().zip(row) {
349 let mut lt = 0.0f64;
350 for &xl in row {
351 lt += ppois(yj, xl + 0.5);
352 }
353 *slot = lt / nf;
354 }
355 });
356 out
357}
358
359#[cfg(test)]
360mod tests {
361 use super::*;
362
363 #[test]
364 fn pnorm_matches_known_values() {
365 let cases = [
367 (0.0, 0.5),
368 (0.5, 0.69146246127401301),
369 (1.0, 0.84134474606854293),
370 (1.6166, 0.94701767404030579),
371 (2.0, 0.97724986805182079),
372 (3.0, 0.9986501019683699),
373 (5.0, 0.99999971334842808),
374 (8.0, 0.99999999999999933),
375 ];
376 for (z, want) in cases {
377 let got = std_normal_cdf(z);
378 assert!(
379 (got - want).abs() <= 1e-15,
380 "pnorm({z}) = {got:.17e}, want {want:.17e}"
381 );
382 }
383 assert!((std_normal_cdf(-1.0) - (1.0 - 0.84134474606854293)).abs() <= 1e-15);
385 }
386
387 #[test]
388 fn ppois_matches_known_values() {
389 let cases = [
391 (0.0, 0.5, 0.60653065971263342),
392 (3.0, 2.5, 0.75757613313306593),
393 (5.0, 5.5, 0.52891868652586216),
394 (10.0, 4.5, 0.99333132791281809),
395 (0.0, 10.5, 2.7536449349747158e-5),
396 (20.0, 15.5, 0.89436693722434268),
397 ];
398 for (k, lambda, want) in cases {
399 let got = ppois(k, lambda);
400 assert!(
401 (got - want).abs() <= 1e-12,
402 "ppois({k}, {lambda}) = {got:.17e}, want {want:.17e}"
403 );
404 }
405 }
406
407 #[test]
408 fn left_tail_sums_to_half_n() {
409 let expr = ExprMatrix::new(
412 vec!["G1".into(), "G2".into()],
413 vec!["S1".into(), "S2".into(), "S3".into(), "S4".into()],
414 vec![1.0, 2.0, 5.0, 9.0, 3.0, 3.1, 2.9, 4.0],
415 );
416 let lt = gaussian_left_tail(&expr);
417 for g in 0..2 {
418 let s: f64 = lt[g * 4..(g + 1) * 4].iter().sum();
419 assert!((s - 2.0).abs() < 1e-12, "row {g} sums to {s}, want 2.0");
420 }
421 }
422}