use crate::{math::igamc, result::TestResult};
const STATES: [i32; 8] = [-4, -3, -2, -1, 1, 2, 3, 4];
pub fn random_excursions(bits: &[u8]) -> TestResult {
let results = random_excursions_all(bits);
results
.into_iter()
.min_by(|a, b| a.p_value.partial_cmp(&b.p_value).unwrap_or(std::cmp::Ordering::Equal))
.unwrap_or_else(|| TestResult::insufficient("nist::random_excursions", "J < 500"))
}
pub fn random_excursions_all(bits: &[u8]) -> Vec<TestResult> {
let walk: Vec<i32> = {
let mut s = 0i32;
let mut w = vec![0i32];
for &b in bits {
s += if b == 1 { 1 } else { -1 };
w.push(s);
}
w.push(0);
w
};
let zero_positions: Vec<usize> = walk
.iter()
.enumerate()
.filter(|(_, &v)| v == 0)
.map(|(i, _)| i)
.collect();
let j = zero_positions.len() - 1;
if j < 500 {
return vec![TestResult::insufficient(
"nist::random_excursions",
&format!("J={j} < 500"),
)];
}
STATES
.iter()
.map(|&x| {
let mut nu = [0usize; 6]; for cycle_idx in 0..j {
let start = zero_positions[cycle_idx];
let end = zero_positions[cycle_idx + 1];
let visits = walk[start + 1..=end].iter().filter(|&&v| v == x).count();
nu[visits.min(5)] += 1;
}
let chi_sq = chi_sq_for_state(x, &nu, j);
let p_value = igamc(2.5, chi_sq / 2.0); TestResult::with_note(
"nist::random_excursions",
p_value,
format!("x={x}, J={j}, χ²={chi_sq:.4}"),
)
})
.collect()
}
fn pi_k(x: i32, k: usize) -> f64 {
let ax = x.unsigned_abs() as f64;
match k {
0 => 1.0 - 1.0 / (2.0 * ax),
1..=4 => (1.0 / (4.0 * ax * ax)) * (1.0 - 1.0 / (2.0 * ax)).powi(k as i32 - 1),
_ => {
let q = 1.0 - 1.0 / (2.0 * ax);
q.powi(4) * (1.0 / (2.0 * ax))
}
}
}
fn chi_sq_for_state(x: i32, nu: &[usize; 6], j: usize) -> f64 {
(0..6)
.map(|k| {
let expected = j as f64 * pi_k(x, k);
(nu[k] as f64 - expected).powi(2) / expected
})
.sum()
}