use crate::{math::ks_test, result::TestResult, rng::Rng};
const Q_CORRECTION: [f64; 6] = [0.0, 0.0, 0.4135, 0.5312, 0.6202, 1.3789];
pub fn minimum_distance_nd(rng: &mut impl Rng, d: usize, quick: bool) -> TestResult {
if !(2..=5).contains(&d) {
return TestResult::insufficient(
"dieharder::minimum_distance_nd",
"d must be 2..=5 (Fischler Q table only covers these dimensions)",
);
}
let n_points = if quick { 500 } else { 8_000 };
let repeats = if quick { 20 } else { 100 };
let mut coords = vec![0.0f64; n_points * d];
let mut p_values = Vec::with_capacity(repeats);
for _ in 0..repeats {
for v in coords.iter_mut() {
*v = rng.next_f64();
}
let mindist = min_dist_nd(&coords, n_points, d);
let dvolume = ball_volume(mindist, d);
let n = n_points as f64;
let earg = -n * (n - 1.0) * dvolume / 2.0;
let qarg = 1.0 + ((2.0 + Q_CORRECTION[d]) / 6.0) * n.powi(3) * dvolume.powi(2);
let p = 1.0 - earg.exp() * qarg;
p_values.push(p.clamp(1e-15, 1.0 - 1e-15));
}
let p_value = ks_test(&mut p_values);
TestResult::with_note(
"dieharder::minimum_distance_nd",
p_value,
format!("d={d}, n={n_points}, repeats={repeats}"),
)
}
fn ball_volume(r: f64, d: usize) -> f64 {
use std::f64::consts::PI;
if d.is_multiple_of(2) {
let half_d = d / 2;
let factorial: f64 = (1..=half_d).map(|k| k as f64).product();
PI.powf(half_d as f64) * r.powi(d as i32) / factorial
} else {
let half_d_minus1 = (d - 1) / 2;
let double_factorial: f64 = (1..=d).step_by(2).map(|k| k as f64).product();
2.0 * (2.0 * PI).powf(half_d_minus1 as f64) * r.powi(d as i32) / double_factorial
}
}
fn min_dist_nd(coords: &[f64], n: usize, d: usize) -> f64 {
let mut min_dist = f64::MAX;
for i in 0..n {
for j in i + 1..n {
let pi = &coords[i * d..(i + 1) * d];
let pj = &coords[j * d..(j + 1) * d];
let dist_sq: f64 = pi.iter().zip(pj).map(|(a, b)| (a - b).powi(2)).sum();
let dist = dist_sq.sqrt();
if dist < min_dist {
min_dist = dist;
}
}
}
min_dist
}