use crate::portable_math::PortableFloat;
pub fn gaussian_two_sided_pfa(k: f64) -> f64 {
if !k.is_finite() || k <= 0.0 {
return if k.is_finite() { 1.0 } else { 0.0 };
}
gamma_pq(0.5, 0.5 * k * k).1
}
pub fn ln_gamma(x: f64) -> f64 {
const G: [f64; 9] = [
0.999_999_999_999_809_9,
676.520_368_121_885_1,
-1_259.139_216_722_402_8,
771.323_428_777_653_1,
-176.615_029_162_140_6,
12.507_343_278_686_905,
-0.138_571_095_265_720_12,
9.984_369_578_019_572e-6,
1.505_632_735_149_311_6e-7,
];
if x < 0.5 {
let pi = std::f64::consts::PI;
return (pi / (pi * x).psin()).pln() - ln_gamma(1.0 - x);
}
let x = x - 1.0;
let mut a = G[0];
let t = x + 7.5;
for (i, &c) in G.iter().enumerate().skip(1) {
a += c / (x + i as f64);
}
0.5 * (2.0 * std::f64::consts::PI).pln() + (x + 0.5) * t.pln() - t + a.pln()
}
pub fn gamma_pq(s: f64, x: f64) -> (f64, f64) {
if x <= 0.0 {
return (0.0, 1.0);
}
let lead = (-x + s * x.pln() - ln_gamma(s)).pexp();
if x < s + 1.0 {
let mut ap = s;
let mut sum = 1.0 / s;
let mut del = sum;
for _ in 0..1_000_000 {
ap += 1.0;
del *= x / ap;
sum += del;
if del.abs() < sum.abs() * 1e-16 {
break;
}
}
let p = sum * lead;
(p, 1.0 - p)
} else {
let tiny = 1e-300;
let mut b = x + 1.0 - s;
let mut c = 1.0 / tiny;
let mut d = 1.0 / b;
let mut h = d;
for i in 1..1_000_000 {
let an = -(i as f64) * (i as f64 - s);
b += 2.0;
d = an * d + b;
if d.abs() < tiny {
d = tiny;
}
c = b + an / c;
if c.abs() < tiny {
c = tiny;
}
d = 1.0 / d;
let del = d * c;
h *= del;
if (del - 1.0).abs() < 1e-16 {
break;
}
}
let q = lead * h;
(1.0 - q, q)
}
}
pub fn power_block_pfa(n: usize, threshold_db: f64) -> f64 {
let n = n as f64;
let hi = 10f64.ppowf(threshold_db / 10.0);
let lo = 10f64.ppowf(-threshold_db / 10.0);
gamma_pq(n, n * hi).1 + gamma_pq(n, n * lo).0
}
pub fn complex_kurtosis_sd(n: usize) -> f64 {
2.0 / (n as f64).sqrt()
}
pub fn pulse_pfa(t: f64) -> f64 {
(-t).pexp()
}
pub fn binomial_upper_tail(n: u64, p: f64, c: u64) -> f64 {
if c == 0 {
return 1.0;
}
if c > n || p <= 0.0 {
return 0.0;
}
if p >= 1.0 {
return 1.0;
}
let (nf, cf) = (n as f64, c as f64);
let ln_pmf = ln_gamma(nf + 1.0) - ln_gamma(cf + 1.0) - ln_gamma(nf - cf + 1.0)
+ cf * p.pln()
+ (nf - cf) * (-p).pln_1p();
let ratio = p / (1.0 - p);
let mut term = ln_pmf.pexp();
let mut sum = term;
let mut k = c;
while k < n {
term *= (nf - k as f64) / (k as f64 + 1.0) * ratio;
k += 1;
sum += term;
if (k as f64) > nf * p && term < sum * 1e-17 {
break;
}
}
sum.min(1.0)
}
pub fn cusum_arl(k: f64, h: f64, mu: f64) -> f64 {
let b = h + 1.166;
let delta = mu - k;
if delta.abs() < 1e-12 {
return b * b;
}
((-2.0 * delta * b).pexp() + 2.0 * delta * b - 1.0) / (2.0 * delta * delta)
}
pub fn cusum_arl0(k: f64, h: f64) -> f64 {
cusum_arl(k, h, 0.0)
}
pub fn pli_mean(gamma: f64) -> f64 {
if gamma < 1e-9 {
return gamma / 2.0;
}
1.0 - (1.0 - (-gamma).pexp()) / gamma
}
pub fn welch_equivalent_segments(nfft: usize, step: usize, segments: usize) -> f64 {
let w: Vec<f64> = (0..nfft)
.map(|n| 0.5 - 0.5 * (std::f64::consts::TAU * n as f64 / nfft as f64).pcos())
.collect();
let s2: f64 = w.iter().map(|v| v * v).sum();
let k = segments.max(1) as f64;
let mut acc = 0.0;
for j in 1..segments {
let lag = j * step.max(1);
if lag >= nfft {
break;
}
let c: f64 = (0..nfft - lag).map(|n| w[n] * w[n + lag]).sum::<f64>() / s2;
acc += (1.0 - j as f64 / k) * c * c;
}
k / (1.0 + 2.0 * acc)
}
pub fn welch_max_excess_pfa(k_eff: f64, bins: usize, threshold_db: f64) -> f64 {
let q = gamma_pq(k_eff, k_eff * 10f64.ppowf(threshold_db / 10.0)).1;
1.0 - (1.0 - q).ppowi(bins as i32)
}
pub fn welch_excess_threshold_db(k_eff: f64, bins: usize, pfa: f64) -> f64 {
let (mut lo, mut hi) = (0.0f64, 60.0f64);
for _ in 0..100 {
let mid = 0.5 * (lo + hi);
if welch_max_excess_pfa(k_eff, bins, mid) > pfa {
lo = mid;
} else {
hi = mid;
}
}
hi
}
pub fn sqm_delta_sd(d: f64, cn0_t: f64) -> f64 {
(d / cn0_t).sqrt()
}
pub fn sqm_ratio_sd(d: f64, cn0_t: f64) -> f64 {
((1.0 - d / 2.0) * (d / 2.0) / (2.0 * cn0_t)).sqrt()
}
pub fn sqm_pair_sd(x: f64, cn0_t: f64) -> f64 {
sqm_delta_sd(2.0 * x, cn0_t)
}
#[cfg(test)]
mod tests {
#[test]
fn gaussian_two_sided_tail_matches_known_values() {
assert!((gaussian_two_sided_pfa(1.959_964) - 0.05).abs() < 1e-6);
assert!((gaussian_two_sided_pfa(2.575_829) - 0.01).abs() < 1e-6);
assert_eq!(gaussian_two_sided_pfa(0.0), 1.0);
assert!((gaussian_two_sided_pfa(6.0) - 1.973_175e-9).abs() < 1e-14);
}
use super::*;
#[test]
fn gamma_matches_known_values() {
assert!((gamma_pq(1.0, 2.0).0 - (1.0 - (-2.0f64).pexp())).abs() < 1e-14);
assert!((gamma_pq(1.0, 0.3).1 - (-0.3f64).pexp()).abs() < 1e-14);
assert!((ln_gamma(5.0) - 24f64.pln()).abs() < 1e-13);
assert!((ln_gamma(0.5) - std::f64::consts::PI.sqrt().pln()).abs() < 1e-13);
let (p, q) = gamma_pq(20_000.0, 20_000.0);
assert!(
(p - 0.5).abs() < 0.01 && (p + q - 1.0).abs() < 1e-12,
"{p} {q}"
);
let q37 = (-7.0f64).pexp() * (1.0 + 7.0 + 24.5);
assert!((gamma_pq(3.0, 7.0).1 - q37).abs() < 1e-14);
}
#[test]
fn binomial_tail_matches_direct_sums_and_limits() {
assert!((binomial_upper_tail(10, 0.5, 8) - 56.0 / 1024.0).abs() < 1e-14);
assert_eq!(binomial_upper_tail(10, 0.5, 0), 1.0);
assert_eq!(binomial_upper_tail(10, 0.5, 11), 0.0);
let (n, p): (u64, f64) = (4096, 6.1e-6);
let want = -(n as f64 * (-p).pln_1p()).pexp_m1();
assert!((binomial_upper_tail(n, p, 1) / want - 1.0).abs() < 1e-9);
assert!(binomial_upper_tail(n, p, 1) > 0.02);
assert!(binomial_upper_tail(n, p, 3) < 1e-5);
}
#[test]
fn closed_forms_have_their_limits() {
assert!((pli_mean(1e6) - 1.0).abs() < 2e-6);
assert!(pli_mean(1e-12).abs() < 1e-12);
let a = cusum_arl0(0.5, 5.0);
assert!((a - 938.0).abs() < 5.0, "{a}");
assert!((cusum_arl(0.5, 5.0, 1.0) - 10.3).abs() < 0.1);
assert!((power_block_pfa(1, 0.0) - 1.0).abs() < 1e-12);
}
}