use crate::special::ln_choose;
use crate::tests_stat::Alternative;
#[must_use]
pub fn one_sample_p(stat: f64, size: usize, alternative: Alternative) -> f64 {
match alternative {
Alternative::TwoSided => (1.0 - kolmogorov_cdf(stat, size)).clamp(0.0, 1.0),
Alternative::Less | Alternative::Greater => smirnov_sf(stat, size).clamp(0.0, 1.0),
}
}
#[must_use]
fn kolmogorov_cdf(stat: f64, size: usize) -> f64 {
if stat <= 0.0 {
return 0.0;
}
if stat >= 1.0 {
return 1.0;
}
let size_f = f64_of(size);
let nd = size_f * stat;
let k = ceil_usize(nd, size); let gap = f64_of(k) - nd; let dim = 2 * k - 1;
let mut mat = vec![0.0_f64; dim * dim];
for row in 0..dim {
for col in 0..dim {
if row + 1 >= col {
set(&mut mat, dim, row, col, 1.0);
}
}
}
for idx in 0..dim {
let first = get(&mat, dim, idx, 0);
set(&mut mat, dim, idx, 0, first - gap.powi(i32_of(idx + 1)));
let last = get(&mat, dim, dim - 1, idx);
set(
&mut mat,
dim,
dim - 1,
idx,
last - gap.powi(i32_of(dim - idx)),
);
}
let corner = get(&mat, dim, dim - 1, 0);
let two_gap = 2.0f64.mul_add(gap, -1.0);
let extra = if two_gap > 0.0 {
two_gap.powi(i32_of(dim))
} else {
0.0
};
set(&mut mat, dim, dim - 1, 0, corner + extra);
for row in 0..dim {
for col in 0..dim {
if row + 1 > col {
let depth = row - col + 1;
let mut val = get(&mat, dim, row, col);
for fact in 1..=depth {
val /= f64_of(fact);
}
set(&mut mat, dim, row, col, val);
}
}
}
let (power, scale_exp) = matrix_power_scaled(&mat, dim, size);
let mut acc = get(&power, dim, k - 1, k - 1);
acc *= 10.0_f64.powi(8 * scale_exp);
for step in 1..=size {
acc = acc * f64_of(step) / size_f;
}
acc.clamp(0.0, 1.0)
}
#[must_use]
fn smirnov_sf(stat: f64, size: usize) -> f64 {
if stat <= 0.0 {
return 1.0;
}
if stat >= 1.0 {
return 0.0;
}
let size_f = f64_of(size);
let jmax = floor_usize(size_f * (1.0 - stat), size);
let mut sum = 0.0_f64;
for j in 0..=jmax {
let jf = f64_of(j);
let comb = ln_choose(size, j).exp();
let base_lo = 1.0 - stat - jf / size_f;
let base_hi = stat + jf / size_f;
let term = comb * base_lo.powf(size_f - jf) * base_hi.powf(jf - 1.0);
sum += term;
}
(stat * sum).clamp(0.0, 1.0)
}
#[must_use]
pub fn two_sample_p(stat: f64, n1: usize, n2: usize) -> f64 {
let prod = f64_of(n1) * f64_of(n2);
let dnum = (stat * prod).round();
let cols = n2 + 1;
let mut dp = vec![0.0_f64; (n1 + 1) * cols];
if let Some(start) = dp.first_mut() {
*start = 1.0;
}
for row in 0..=n1 {
for col in 0..=n2 {
if row == 0 && col == 0 {
continue;
}
let band = (i64_of(row) * i64_of(n2) - i64_of(col) * i64_of(n1)).abs();
let value = if f64_of_i64(band) < dnum {
let from_left = if row > 0 {
dp.get((row - 1) * cols + col).copied().unwrap_or(0.0)
} else {
0.0
};
let from_down = if col > 0 {
dp.get(row * cols + (col - 1)).copied().unwrap_or(0.0)
} else {
0.0
};
from_left + from_down
} else {
0.0
};
if let Some(slot) = dp.get_mut(row * cols + col) {
*slot = value;
}
}
}
let inside = dp.get(n1 * cols + n2).copied().unwrap_or(0.0);
let total = ln_choose(n1 + n2, n1).exp();
(1.0 - inside / total).clamp(0.0, 1.0)
}
fn get(mat: &[f64], dim: usize, row: usize, col: usize) -> f64 {
mat.get(row * dim + col).copied().unwrap_or(0.0)
}
fn set(mat: &mut [f64], dim: usize, row: usize, col: usize, val: f64) {
if let Some(slot) = mat.get_mut(row * dim + col) {
*slot = val;
}
}
fn matrix_power_scaled(mat: &[f64], dim: usize, exponent: usize) -> (Vec<f64>, i32) {
let mut result = vec![0.0_f64; dim * dim];
for diag in 0..dim {
set(&mut result, dim, diag, diag, 1.0);
}
let mut base = mat.to_vec();
let mut exp = exponent;
let mut scale = 0_i32;
while exp > 0 {
if exp & 1 == 1 {
result = mat_mul(&result, &base, dim);
scale += rescale(&mut result, dim);
}
exp >>= 1;
if exp > 0 {
base = mat_mul(&base, &base, dim);
scale += rescale(&mut base, dim);
}
}
(result, scale)
}
fn mat_mul(lhs: &[f64], rhs: &[f64], dim: usize) -> Vec<f64> {
let mut out = vec![0.0_f64; dim * dim];
for row in 0..dim {
for mid in 0..dim {
let left = get(lhs, dim, row, mid);
if left == 0.0 {
continue;
}
for col in 0..dim {
let val = left.mul_add(get(rhs, dim, mid, col), get(&out, dim, row, col));
set(&mut out, dim, row, col, val);
}
}
}
out
}
fn rescale(mat: &mut [f64], dim: usize) -> i32 {
let center = (dim - 1) / 2;
if get(mat, dim, center, center) > 1e140 {
for entry in mat.iter_mut() {
*entry *= 1e-8;
}
1
} else {
0
}
}
fn f64_of(n: usize) -> f64 {
u32::try_from(n).map_or(f64::INFINITY, f64::from)
}
fn f64_of_i64(n: i64) -> f64 {
i32::try_from(n).map_or(f64::INFINITY, f64::from)
}
fn ceil_usize(x: f64, bound: usize) -> usize {
(0..=bound).find(|&k| f64_of(k) >= x).unwrap_or(bound)
}
fn floor_usize(x: f64, bound: usize) -> usize {
(0..=bound).rev().find(|&j| f64_of(j) <= x).unwrap_or(0)
}
fn i64_of(n: usize) -> i64 {
i64::try_from(n).unwrap_or(i64::MAX)
}
fn i32_of(n: usize) -> i32 {
i32::try_from(n).unwrap_or(i32::MAX)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn two_sample_disjoint_is_significant() {
let p = two_sample_p(1.0, 3, 4);
assert!((0.0..=1.0).contains(&p), "p out of range: {p}");
}
#[test]
fn smirnov_is_monotone() {
let n = 8;
let a = smirnov_sf(0.1, n);
let b = smirnov_sf(0.3, n);
assert!(a >= b, "not monotone: {a} < {b}");
assert!((0.0..=1.0).contains(&a), "a out of range: {a}");
}
#[test]
fn kolmogorov_cdf_is_monotone() {
let n = 8;
let lo = kolmogorov_cdf(0.1, n);
let hi = kolmogorov_cdf(0.4, n);
assert!(hi >= lo, "not monotone: {hi} < {lo}");
assert!((0.0..=1.0).contains(&hi), "hi out of range: {hi}");
}
}