#![allow(clippy::excessive_precision)]
use crate::matrix::ExprMatrix;
const SIGMA_FACTOR: f64 = 4.0;
const PRECOMPUTE_RESOLUTION: usize = 10_000;
const MAX_PRECOMPUTE: f64 = 10.0;
pub fn std_normal_cdf(x: f64) -> f64 {
if x < 0.0 {
return 1.0 - std_normal_cdf(-x);
}
const A: [f64; 5] = [
2.2352520354606839287,
161.02823106855587881,
1067.6894854603709582,
18154.981253343561249,
0.065682337918207449113,
];
const B: [f64; 4] = [
47.20258190468824187,
976.09855173777669322,
10260.932208618978205,
45507.789335026729956,
];
const C: [f64; 9] = [
0.39894151208813466764,
8.8831497943883759412,
93.506656132177855979,
597.27027639480026226,
2494.5375852903726711,
6848.1904505362823326,
11602.651437647350124,
9842.7148383839780218,
1.0765576773720192317e-8,
];
const D: [f64; 8] = [
22.266688044328115691,
235.38790178262499861,
1519.377599407554805,
6485.558298266760755,
18615.571640885098091,
34900.952721145977266,
38912.003286093271411,
19685.429676859990727,
];
const P: [f64; 6] = [
0.21589853405795699,
0.1274011611602473639,
0.022235277870649807,
0.001421619193227893466,
2.9112874951168792e-5,
0.02307344176494017303,
];
const Q: [f64; 5] = [
1.28426009614491121,
0.468238212480865118,
0.0659881378689285515,
0.00378239633202758244,
7.29751555083966205e-5,
];
const M_1_SQRT_2PI: f64 = 0.398942280401432677939946059934;
let y = x; if y <= 0.67448975 {
let xsq = if y > f64::EPSILON * 0.5 { x * x } else { 0.0 };
let mut xnum = A[4] * xsq;
let mut xden = xsq;
for i in 0..3 {
xnum = (xnum + A[i]) * xsq;
xden = (xden + B[i]) * xsq;
}
let temp = x * (xnum + A[3]) / (xden + B[3]);
0.5 + temp
} else if y <= 32.0_f64.sqrt() {
let mut xnum = C[8] * y;
let mut xden = y;
for i in 0..7 {
xnum = (xnum + C[i]) * y;
xden = (xden + D[i]) * y;
}
let temp = (xnum + C[7]) / (xden + D[7]);
let xsq = (y * 16.0).trunc() / 16.0;
let del = (y - xsq) * (y + xsq);
let small = (-xsq * xsq * 0.5).exp() * (-del * 0.5).exp() * temp;
1.0 - small
} else {
let xsq = 1.0 / (x * x);
let mut xnum = P[5] * xsq;
let mut xden = xsq;
for i in 0..4 {
xnum = (xnum + P[i]) * xsq;
xden = (xden + Q[i]) * xsq;
}
let mut temp = xsq * (xnum + P[4]) / (xden + Q[4]);
temp = (M_1_SQRT_2PI - temp) / y;
let xsq = (y * 16.0).trunc() / 16.0;
let del = (y - xsq) * (y + xsq);
let small = (-xsq * xsq * 0.5).exp() * (-del * 0.5).exp() * temp;
1.0 - small
}
}
fn build_table() -> Vec<f64> {
(0..=PRECOMPUTE_RESOLUTION)
.map(|i| std_normal_cdf(MAX_PRECOMPUTE * i as f64 / PRECOMPUTE_RESOLUTION as f64))
.collect()
}
#[inline]
fn precomputed_cdf(table: &[f64], x: f64, sigma: f64) -> f64 {
let v = x / sigma;
if v < -MAX_PRECOMPUTE {
return 0.0;
}
if v > MAX_PRECOMPUTE {
return 1.0;
}
let idx = ((v.abs() / MAX_PRECOMPUTE) * PRECOMPUTE_RESOLUTION as f64) as usize;
let base = table[idx];
if v < 0.0 {
1.0 - base
} else {
base
}
}
fn bandwidth(row: &[f64]) -> f64 {
let n = row.len();
let nf = n as f64;
let mut mean = row.iter().sum::<f64>() / nf;
let mut corr = 0.0f64;
for &v in row {
corr += v - mean;
}
mean += corr / nf;
let mut ss = 0.0f64;
for &v in row {
let d = v - mean;
ss += d * d;
}
let sd = (ss / (nf - 1.0)).sqrt();
let bw = sd / SIGMA_FACTOR;
if bw == 0.0 || bw.is_nan() {
0.001
} else {
bw
}
}
pub(crate) fn gaussian_left_tail(expr: &ExprMatrix) -> Vec<f64> {
let p = expr.nrow();
let n = expr.ncol();
let table = build_table();
let nf = n as f64;
let mut out = vec![0.0f64; p * n];
crate::par::fill_chunks_mut(&mut out, n, |g, row_out| {
let row = expr.row(g);
let bw = bandwidth(row);
for (slot, &yj) in row_out.iter_mut().zip(row) {
let mut lt = 0.0f64;
for &xl in row {
lt += precomputed_cdf(&table, yj - xl, bw);
}
*slot = lt / nf;
}
});
out
}
fn ln_gamma(x: f64) -> f64 {
const G: f64 = 7.0;
const C: [f64; 9] = [
0.99999999999980993,
676.5203681218851,
-1259.1392167224028,
771.32342877765313,
-176.61502916214059,
12.507343278686905,
-0.13857109526572012,
9.9843695780195716e-6,
1.5056327351493116e-7,
];
if x < 0.5 {
(std::f64::consts::PI / (std::f64::consts::PI * x).sin()).ln() - ln_gamma(1.0 - x)
} else {
let x = x - 1.0;
let mut a = C[0];
let t = x + G + 0.5;
for (i, &c) in C.iter().enumerate().skip(1) {
a += c / (x + i as f64);
}
0.5 * (2.0 * std::f64::consts::PI).ln() + (x + 0.5) * t.ln() - t + a.ln()
}
}
fn gamma_p_series(a: f64, x: f64) -> f64 {
let mut ap = a;
let mut del = 1.0 / a;
let mut sum = del;
for _ in 0..300 {
ap += 1.0;
del *= x / ap;
sum += del;
if del.abs() < sum.abs() * 1e-16 {
break;
}
}
sum * (-x + a * x.ln() - ln_gamma(a)).exp()
}
fn gamma_q_cf(a: f64, x: f64) -> f64 {
const FPMIN: f64 = 1e-300;
let mut b = x + 1.0 - a;
let mut c = 1.0 / FPMIN;
let mut d = 1.0 / b;
let mut h = d;
for i in 1..300 {
let an = -(i as f64) * (i as f64 - a);
b += 2.0;
d = an * d + b;
if d.abs() < FPMIN {
d = FPMIN;
}
c = b + an / c;
if c.abs() < FPMIN {
c = FPMIN;
}
d = 1.0 / d;
let del = d * c;
h *= del;
if (del - 1.0).abs() < 1e-16 {
break;
}
}
(-x + a * x.ln() - ln_gamma(a)).exp() * h
}
fn gamma_q(a: f64, x: f64) -> f64 {
if x <= 0.0 {
return 1.0;
}
if x < a + 1.0 {
1.0 - gamma_p_series(a, x)
} else {
gamma_q_cf(a, x)
}
}
fn ppois(k: f64, lambda: f64) -> f64 {
let kk = k.floor();
if kk < 0.0 {
return 0.0;
}
gamma_q(kk + 1.0, lambda)
}
pub(crate) fn poisson_left_tail(expr: &ExprMatrix) -> Vec<f64> {
let p = expr.nrow();
let n = expr.ncol();
let nf = n as f64;
let mut out = vec![0.0f64; p * n];
crate::par::fill_chunks_mut(&mut out, n, |g, row_out| {
let row = expr.row(g);
for (slot, &yj) in row_out.iter_mut().zip(row) {
let mut lt = 0.0f64;
for &xl in row {
lt += ppois(yj, xl + 0.5);
}
*slot = lt / nf;
}
});
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn pnorm_matches_known_values() {
let cases = [
(0.0, 0.5),
(0.5, 0.69146246127401301),
(1.0, 0.84134474606854293),
(1.6166, 0.94701767404030579),
(2.0, 0.97724986805182079),
(3.0, 0.9986501019683699),
(5.0, 0.99999971334842808),
(8.0, 0.99999999999999933),
];
for (z, want) in cases {
let got = std_normal_cdf(z);
assert!(
(got - want).abs() <= 1e-15,
"pnorm({z}) = {got:.17e}, want {want:.17e}"
);
}
assert!((std_normal_cdf(-1.0) - (1.0 - 0.84134474606854293)).abs() <= 1e-15);
}
#[test]
fn ppois_matches_known_values() {
let cases = [
(0.0, 0.5, 0.60653065971263342),
(3.0, 2.5, 0.75757613313306593),
(5.0, 5.5, 0.52891868652586216),
(10.0, 4.5, 0.99333132791281809),
(0.0, 10.5, 2.7536449349747158e-5),
(20.0, 15.5, 0.89436693722434268),
];
for (k, lambda, want) in cases {
let got = ppois(k, lambda);
assert!(
(got - want).abs() <= 1e-12,
"ppois({k}, {lambda}) = {got:.17e}, want {want:.17e}"
);
}
}
#[test]
fn left_tail_sums_to_half_n() {
let expr = ExprMatrix::new(
vec!["G1".into(), "G2".into()],
vec!["S1".into(), "S2".into(), "S3".into(), "S4".into()],
vec![1.0, 2.0, 5.0, 9.0, 3.0, 3.1, 2.9, 4.0],
);
let lt = gaussian_left_tail(&expr);
for g in 0..2 {
let s: f64 = lt[g * 4..(g + 1) * 4].iter().sum();
assert!((s - 2.0).abs() < 1e-12, "row {g} sums to {s}, want 2.0");
}
}
}