use crate::{
math::{gf2_rank, igamc},
result::TestResult,
};
pub fn binary_rank_32x32(words: &[u32]) -> TestResult {
let bits_per = 32 * 32;
let n_matrices = 40_000;
if words.len() * 32 < bits_per * n_matrices {
return TestResult::insufficient("diehard::binary_rank_32x32", "not enough words");
}
rank_test(words, 32, 32, n_matrices, "diehard::binary_rank_32x32")
}
pub fn binary_rank_31x31(words: &[u32]) -> TestResult {
let n_matrices = 40_000;
rank_test(words, 31, 31, n_matrices, "diehard::binary_rank_31x31")
}
pub fn binary_rank_6x8(words: &[u32]) -> TestResult {
let rows = 6usize;
let cols = 8usize;
let n_matrices = 100_000;
if words.len() < rows * n_matrices {
return TestResult::insufficient("diehard::binary_rank_6x8", "not enough words");
}
let p_full: f64 = 217_613_271_859_200.0 / 281_474_976_710_656.0; let p_five: f64 = 61_203_732_710_400.0 / 281_474_976_710_656.0; let p_less: f64 = 1.0 - p_full - p_five;
let mut f = [0usize; 3]; let mut word_iter = words.iter().copied();
for _ in 0..n_matrices {
let mut matrix = [0u8; 6];
for slot in matrix.iter_mut().take(rows) {
*slot = (word_iter.next().unwrap_or(0) & 0xFF) as u8;
}
let rank = gf2_rank_6x8(&matrix, rows, cols);
match rank {
6 => f[2] += 1,
5 => f[1] += 1,
_ => f[0] += 1,
}
}
let m = n_matrices as f64;
let chi_sq = (f[0] as f64 - m * p_less).powi(2) / (m * p_less)
+ (f[1] as f64 - m * p_five).powi(2) / (m * p_five)
+ (f[2] as f64 - m * p_full).powi(2) / (m * p_full);
let p_value = igamc(1.0, chi_sq / 2.0);
TestResult::with_note(
"diehard::binary_rank_6x8",
p_value,
format!("N={n_matrices}, χ²={chi_sq:.4}"),
)
}
fn rank_test(
words: &[u32],
rows: usize,
cols: usize,
n_matrices: usize,
name: &'static str,
) -> TestResult {
if words.len() < rows * n_matrices {
return TestResult::insufficient(name, "not enough words");
}
let (p3, p2, p1, p0) = theoretical_probs(rows, cols);
let mut f = [0usize; 4];
let mask = if cols < 32 {
(1u32 << cols) - 1
} else {
u32::MAX
};
let mut matrix = Vec::with_capacity(rows);
for m_idx in 0..n_matrices {
let slice = &words[m_idx * rows..(m_idx + 1) * rows];
matrix.clear();
matrix.extend(slice.iter().map(|&w| w & mask));
let rank = gf2_rank(&matrix, rows, cols);
let full = rows.min(cols);
if rank == full {
f[3] += 1;
} else if rank == full - 1 {
f[2] += 1;
} else if rank == full - 2 {
f[1] += 1;
} else {
f[0] += 1;
}
}
let m = n_matrices as f64;
let probs = [p0, p1, p2, p3];
let chi_sq: f64 = f
.iter()
.zip(probs.iter())
.filter(|(_, &p)| p * m >= 5.0)
.map(|(&cnt, &p)| (cnt as f64 - m * p).powi(2) / (m * p))
.sum();
let df = f
.iter()
.zip(probs.iter())
.filter(|(_, &p)| p * m >= 5.0)
.count()
.saturating_sub(1);
let p_value = igamc(df as f64 / 2.0, chi_sq / 2.0);
TestResult::with_note(
name,
p_value,
format!("{rows}×{cols}, N={n_matrices}, χ²={chi_sq:.4}"),
)
}
fn theoretical_probs(rows: usize, cols: usize) -> (f64, f64, f64, f64) {
match (rows, cols) {
(32, 32) => (0.2887880952, 0.5775761902, 0.1283502644, 0.0052854502),
(31, 31) => {
let full = gf2_rank_probability(31, 31, 31);
let full_minus_1 = gf2_rank_probability(31, 31, 30);
let full_minus_2 = gf2_rank_probability(31, 31, 29);
let tail = (1.0 - full - full_minus_1 - full_minus_2).max(0.0);
(full, full_minus_1, full_minus_2, tail)
}
_ => {
let full = rows.min(cols);
let p_full = gf2_rank_probability(rows, cols, full);
let p_full_minus_1 = if full >= 1 {
gf2_rank_probability(rows, cols, full - 1)
} else {
0.0
};
let p_full_minus_2 = if full >= 2 {
gf2_rank_probability(rows, cols, full - 2)
} else {
0.0
};
let p_tail = (1.0 - p_full - p_full_minus_1 - p_full_minus_2).max(0.0);
(p_full, p_full_minus_1, p_full_minus_2, p_tail)
}
}
}
fn gf2_rank_probability(rows: usize, cols: usize, rank: usize) -> f64 {
if rank > rows.min(cols) {
return 0.0;
}
if rank == 0 {
return 2f64.powi(-((rows * cols) as i32));
}
let mut log_prob = -((rows * cols) as f64) * std::f64::consts::LN_2;
for i in 0..rank {
let ip = i as i32;
let rows_term = 2f64.powi(rows as i32) - 2f64.powi(ip);
let cols_term = 2f64.powi(cols as i32) - 2f64.powi(ip);
let rank_term = 2f64.powi(rank as i32) - 2f64.powi(ip);
log_prob += rows_term.ln() + cols_term.ln() - rank_term.ln();
}
log_prob.exp()
}
fn gf2_rank_6x8(matrix: &[u8; 6], rows: usize, cols: usize) -> usize {
let as_u32: [u32; 6] = std::array::from_fn(|i| matrix[i] as u32);
gf2_rank(&as_u32, rows, cols)
}
#[cfg(test)]
mod tests {
use super::{gf2_rank_probability, theoretical_probs};
#[test]
fn exact_31x31_probabilities_sum_to_one() {
let (p_full, p_m1, p_m2, p_tail) = theoretical_probs(31, 31);
assert!(((p_full + p_m1 + p_m2 + p_tail) - 1.0).abs() < 1e-12);
assert!(p_full > 0.28 && p_full < 0.29);
assert!(p_tail > 0.0);
}
#[test]
fn generic_rank_probability_matches_32x32_reference_close() {
let p = gf2_rank_probability(32, 32, 32);
assert!((p - 0.2887880952).abs() < 1e-9);
}
}