use crate::seed::splitmix64;
use core::fmt;
#[derive(Debug, Clone, Copy, Default)]
struct CorrAccum {
n: usize,
sum_x: f64,
sum_y: f64,
sum_xy: f64,
}
impl CorrAccum {
fn observe(&mut self, x: bool, y: bool) {
let xf = if x { 1.0 } else { 0.0 };
let yf = if y { 1.0 } else { 0.0 };
self.n += 1;
self.sum_x += xf;
self.sum_y += yf;
self.sum_xy += xf * yf;
}
fn correlation(self) -> f64 {
if self.n == 0 {
return 0.0;
}
let n = self.n as f64;
let ex = self.sum_x / n;
let ey = self.sum_y / n;
let cov = (self.sum_xy / n) - ex * ey;
let var_x = ex * (1.0 - ex);
let var_y = ey * (1.0 - ey);
let denom = (var_x * var_y).sqrt();
if denom <= f64::EPSILON {
0.0
} else {
cov / denom
}
}
}
#[derive(Debug, Clone)]
pub struct AvalancheReport {
pub input_bits: usize,
pub output_bits: usize,
pub samples: usize,
pub exact: bool,
pub dependence: Vec<Vec<f64>>,
pub max_sac_bias: f64,
pub mean_sac_bias: f64,
pub rms_sac_bias: f64,
pub max_bic_abs_corr: f64,
pub mean_bic_abs_corr: f64,
}
impl fmt::Display for AvalancheReport {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"samples={}{} input_bits={} output_bits={} sac[max={:.4}, mean={:.4}, rms={:.4}] bic[max|rho|={:.4}, mean|rho|={:.4}]",
self.samples,
if self.exact { " exact" } else { "" },
self.input_bits,
self.output_bits,
self.max_sac_bias,
self.mean_sac_bias,
self.rms_sac_bias,
self.max_bic_abs_corr,
self.mean_bic_abs_corr,
)
}
}
fn pair_count(n: usize) -> usize {
n.saturating_mul(n.saturating_sub(1)) / 2
}
fn pair_index(output_bits: usize, a: usize, b: usize) -> usize {
debug_assert!(a < b && b < output_bits);
a * (2 * output_bits - a - 1) / 2 + (b - a - 1)
}
pub fn evaluate_u64<F>(
input_bits: usize,
output_bits: usize,
samples: usize,
mut f: F,
) -> AvalancheReport
where
F: FnMut(u64) -> u64,
{
assert!(
(1..=64).contains(&input_bits),
"input_bits must be in 1..=64"
);
assert!(
(1..=64).contains(&output_bits),
"output_bits must be in 1..=64"
);
assert!(samples > 0, "samples must be positive");
let input_mask = if input_bits == 64 {
u64::MAX
} else {
(1u64 << input_bits) - 1
};
let output_mask = if output_bits == 64 {
u64::MAX
} else {
(1u64 << output_bits) - 1
};
let full_domain = if input_bits == 64 {
None
} else {
Some(1usize << input_bits)
};
let exact = full_domain.is_some_and(|domain| samples >= domain);
let base_inputs: Vec<u64> = if exact {
(0..full_domain.unwrap()).map(|x| x as u64).collect()
} else {
let mut state = 0x243f_6a88_85a3_08d3u64;
(0..samples)
.map(|_| splitmix64(&mut state) & input_mask)
.collect()
};
let used_samples = base_inputs.len();
let mut ones = vec![vec![0usize; input_bits]; output_bits];
let mut bic = vec![vec![CorrAccum::default(); pair_count(output_bits)]; input_bits];
for &x in &base_inputs {
let y = f(x) & output_mask;
for j in 0..input_bits {
let xj = x ^ (1u64 << j);
let avalanche = (y ^ (f(xj & input_mask) & output_mask)) & output_mask;
for (i, col) in ones.iter_mut().enumerate().take(output_bits) {
if ((avalanche >> i) & 1) != 0 {
col[j] += 1;
}
}
for a in 0..output_bits {
let bit_a = ((avalanche >> a) & 1) != 0;
for b in (a + 1)..output_bits {
let bit_b = ((avalanche >> b) & 1) != 0;
let idx = pair_index(output_bits, a, b);
bic[j][idx].observe(bit_a, bit_b);
}
}
}
}
let total_cells = (input_bits * output_bits) as f64;
let mut dependence = vec![vec![0.0; input_bits]; output_bits];
let mut max_sac_bias = 0.0f64;
let mut sum_abs_bias = 0.0;
let mut sum_sq_bias = 0.0;
for i in 0..output_bits {
for j in 0..input_bits {
let p = ones[i][j] as f64 / used_samples as f64;
let bias = (p - 0.5).abs();
dependence[i][j] = p;
max_sac_bias = max_sac_bias.max(bias);
sum_abs_bias += bias;
sum_sq_bias += bias * bias;
}
}
let mut max_bic_abs_corr = 0.0f64;
let mut sum_abs_corr = 0.0;
let mut corr_terms = 0usize;
for accs in &bic {
for &acc in accs {
let rho = acc.correlation().abs();
max_bic_abs_corr = max_bic_abs_corr.max(rho);
sum_abs_corr += rho;
corr_terms += 1;
}
}
AvalancheReport {
input_bits,
output_bits,
samples: used_samples,
exact,
dependence,
max_sac_bias,
mean_sac_bias: sum_abs_bias / total_cells,
rms_sac_bias: (sum_sq_bias / total_cells).sqrt(),
max_bic_abs_corr,
mean_bic_abs_corr: if corr_terms == 0 {
0.0
} else {
sum_abs_corr / corr_terms as f64
},
}
}
#[cfg(test)]
mod tests {
use super::evaluate_u64;
#[test]
fn identity_mapping_is_perfectly_non_avalanche() {
let report = evaluate_u64(4, 4, 16, |x| x);
assert!(report.exact);
assert_eq!(16, report.samples);
for i in 0..4 {
for j in 0..4 {
let expected = if i == j { 1.0 } else { 0.0 };
assert!((report.dependence[i][j] - expected).abs() < 1e-12);
}
}
assert!((report.max_sac_bias - 0.5).abs() < 1e-12);
}
#[test]
fn parity_broadcast_is_maximally_correlated() {
let report = evaluate_u64(4, 4, 16, |x| {
let g = (x & 1) & ((x >> 1) & 1);
g | (g << 1)
});
for i in 0..2 {
for j in 0..4 {
let expected = if j <= 1 { 0.5 } else { 0.0 };
assert!((report.dependence[i][j] - expected).abs() < 1e-12);
}
}
assert!((report.max_bic_abs_corr - 1.0).abs() < 1e-12);
}
}