pub fn erfc(x: f64) -> f64 {
if x < 0.0 {
return 2.0 - erfc(-x);
}
let t = 1.0 / (1.0 + 0.3275911 * x);
let poly = t
* (0.254_829_592
+ t * (-0.284_496_736
+ t * (1.421_413_741 + t * (-1.453_152_027 + t * 1.061_405_429))));
poly * (-x * x).exp()
}
pub fn igamc(a: f64, x: f64) -> f64 {
if x < 0.0 || a <= 0.0 {
return 1.0;
}
if x < a + 1.0 {
1.0 - igam_series(a, x)
} else {
igam_cf(a, x)
}
}
fn igam_series(a: f64, x: f64) -> f64 {
let ln_gamma_a = ln_gamma(a);
let mut sum = 1.0 / a;
let mut term = 1.0 / a;
let mut n = 1.0_f64;
loop {
term *= x / (a + n);
sum += term;
if term.abs() < 1e-14 * sum.abs() {
break;
}
n += 1.0;
if n > 200.0 {
break;
}
}
sum * (-x + a * x.ln() - ln_gamma_a).exp()
}
fn igam_cf(a: f64, x: f64) -> f64 {
let ln_gamma_a = ln_gamma(a);
let fpmin = f64::MIN_POSITIVE / f64::EPSILON;
let mut b = x + 1.0 - a;
let mut c = 1.0 / fpmin;
let mut d = 1.0 / b;
let mut h = d;
let mut i = 1_u32;
loop {
let ai = -(i as f64) * ((i as f64) - a);
b += 2.0;
d = ai * d + b;
if d.abs() < fpmin {
d = fpmin;
}
c = b + ai / c;
if c.abs() < fpmin {
c = fpmin;
}
d = 1.0 / d;
h *= d * c;
if (d * c - 1.0).abs() < 1e-14 {
break;
}
i += 1;
if i > 200 {
break;
}
}
h * (-x + a * x.ln() - ln_gamma_a).exp()
}
fn ln_gamma(z: f64) -> f64 {
let c: [f64; 9] = [
0.999_999_999_999_809_3,
676.520_368_121_885_1,
-1_259.139_216_722_402_8,
771.323_428_777_653_1,
-176.615_029_162_140_6,
12.507_343_278_686_905,
-0.138_571_095_265_720_12,
9.984_369_578_019_572e-6,
1.505_632_735_149_311_6e-7,
];
let x = z - 1.0;
let mut t = x + 7.5;
let mut ser = c[0];
for (i, ci) in c.iter().enumerate().skip(1) {
ser += ci / (x + i as f64);
}
t = (x + 0.5) * t.ln() - t;
t + (2.0 * std::f64::consts::PI).sqrt().ln() + ser.ln()
}
#[derive(Debug, Clone)]
pub struct TestResult {
pub passed: bool,
pub statistic: f64,
pub p_value: f64,
pub description: String,
}
pub fn frequency_test(samples: &[u32]) -> Result<TestResult, &'static str> {
if samples.is_empty() {
return Err("frequency_test: samples must not be empty");
}
let n_bits = samples.len() as f64 * 32.0;
let ones: u64 = samples.iter().map(|&s| s.count_ones() as u64).sum();
let zeros = samples.len() as u64 * 32 - ones;
let s_n = (ones as f64 - zeros as f64) / n_bits.sqrt();
let p_value = erfc(s_n.abs() / std::f64::consts::SQRT_2);
let passed = p_value >= 0.01;
Ok(TestResult {
passed,
statistic: s_n,
p_value,
description: format!(
"NIST Frequency (Monobit): n_bits={n_bits:.0}, ones={ones}, \
S_n={s_n:.6}, p={p_value:.6} → {}",
if passed { "PASS" } else { "FAIL" }
),
})
}
pub fn block_frequency_test(
samples: &[u32],
block_size: usize,
) -> Result<TestResult, &'static str> {
if samples.is_empty() {
return Err("block_frequency_test: samples must not be empty");
}
if block_size == 0 {
return Err("block_frequency_test: block_size must be > 0");
}
let bits: Vec<u8> = samples
.iter()
.flat_map(|&s| (0..32).map(move |i| ((s >> i) & 1) as u8))
.collect();
let n_bits = bits.len();
let n_blocks = n_bits / block_size;
if n_blocks == 0 {
return Err("block_frequency_test: not enough bits for even one block");
}
let chi_sq: f64 = bits
.chunks_exact(block_size)
.take(n_blocks)
.map(|block| {
let ones: f64 = block.iter().map(|&b| b as f64).sum();
let pi = ones / block_size as f64;
(pi - 0.5) * (pi - 0.5)
})
.sum::<f64>()
* 4.0
* block_size as f64;
let p_value = igamc(n_blocks as f64 / 2.0, chi_sq / 2.0);
let passed = p_value >= 0.01;
Ok(TestResult {
passed,
statistic: chi_sq,
p_value,
description: format!(
"NIST Block Frequency: M={block_size}, N={n_blocks}, \
χ²={chi_sq:.4}, p={p_value:.6} → {}",
if passed { "PASS" } else { "FAIL" }
),
})
}
pub fn runs_test(samples: &[u32]) -> Result<TestResult, &'static str> {
if samples.is_empty() {
return Err("runs_test: samples must not be empty");
}
let bits: Vec<u8> = samples
.iter()
.flat_map(|&s| (0..32).map(move |i| ((s >> i) & 1) as u8))
.collect();
let n = bits.len() as f64;
let ones: f64 = bits.iter().map(|&b| b as f64).sum();
let pi = ones / n;
let tau = 2.0 / n.sqrt();
if (pi - 0.5).abs() >= tau {
return Ok(TestResult {
passed: false,
statistic: 0.0,
p_value: 0.0,
description: format!(
"NIST Runs: prerequisite FAILED |π-0.5|={:.6} ≥ τ={tau:.6} → FAIL",
(pi - 0.5).abs()
),
});
}
let v_n: f64 = 1.0 + bits.windows(2).filter(|w| w[0] != w[1]).count() as f64;
let numer = (v_n - 2.0 * n * pi * (1.0 - pi)).abs();
let denom = 2.0 * (2.0 * n).sqrt() * pi * (1.0 - pi);
let p_value = erfc(numer / denom);
let passed = p_value >= 0.01;
Ok(TestResult {
passed,
statistic: v_n,
p_value,
description: format!(
"NIST Runs: n={n:.0}, π={pi:.6}, V_n={v_n:.0}, p={p_value:.6} → {}",
if passed { "PASS" } else { "FAIL" }
),
})
}
pub fn longest_run_test(samples: &[u32]) -> Result<TestResult, &'static str> {
if samples.is_empty() {
return Err("longest_run_test: samples must not be empty");
}
let bits: Vec<u8> = samples
.iter()
.flat_map(|&s| (0..32).map(move |i| ((s >> i) & 1) as u8))
.collect();
let n_bits = bits.len();
struct Params {
block_size: usize,
upper_bounds: &'static [usize],
probs: &'static [f64],
}
let params = if n_bits >= 6272 {
Params {
block_size: 128,
upper_bounds: &[4, 5, 6, 7, 8], probs: &[0.1174, 0.2430, 0.2490, 0.1752, 0.1027, 0.1127],
}
} else if n_bits >= 128 {
Params {
block_size: 8,
upper_bounds: &[1, 2, 3], probs: &[0.2148, 0.3672, 0.2305, 0.1875],
}
} else {
return Err("longest_run_test: need at least 128 bits");
};
let n_blocks = n_bits / params.block_size;
if n_blocks == 0 {
return Err("longest_run_test: not enough bits for one block");
}
let n_buckets = params.probs.len(); let mut freq = vec![0u64; n_buckets];
for block in bits.chunks_exact(params.block_size).take(n_blocks) {
let mut max_run = 0usize;
let mut cur_run = 0usize;
for &bit in block {
if bit == 1 {
cur_run += 1;
if cur_run > max_run {
max_run = cur_run;
}
} else {
cur_run = 0;
}
}
let bucket = params
.upper_bounds
.iter()
.position(|&ub| max_run <= ub)
.unwrap_or(n_buckets - 1);
freq[bucket] += 1;
}
let n_blocks_f = n_blocks as f64;
let chi_sq: f64 = freq
.iter()
.zip(params.probs.iter())
.map(|(&observed, &prob)| {
if prob > 0.0 {
let expected = prob * n_blocks_f;
let diff = observed as f64 - expected;
diff * diff / expected
} else {
0.0
}
})
.sum();
let df = (n_buckets - 1) as f64;
let p_value = igamc(df / 2.0, chi_sq / 2.0);
let passed = p_value >= 0.01;
Ok(TestResult {
passed,
statistic: chi_sq,
p_value,
description: format!(
"NIST Longest Run: M={}, N={n_blocks}, \
χ²={chi_sq:.4}, p={p_value:.6} → {}",
params.block_size,
if passed { "PASS" } else { "FAIL" }
),
})
}
pub fn uniform_chi_squared(samples: &[u32], n_buckets: usize) -> Result<TestResult, &'static str> {
if samples.is_empty() {
return Err("uniform_chi_squared: samples must not be empty");
}
if n_buckets < 2 {
return Err("uniform_chi_squared: n_buckets must be >= 2");
}
let mut counts = vec![0u64; n_buckets];
let scale = 1.0 / 4_294_967_296.0_f64;
for &s in samples {
let u = s as f64 * scale;
let bucket = ((u * n_buckets as f64) as usize).min(n_buckets - 1);
counts[bucket] += 1;
}
let expected = samples.len() as f64 / n_buckets as f64;
let chi_sq: f64 = counts
.iter()
.map(|&c| {
let d = c as f64 - expected;
d * d / expected
})
.sum();
let df = (n_buckets - 1) as f64;
let p_value = igamc(df / 2.0, chi_sq / 2.0);
let passed = p_value >= 0.01;
Ok(TestResult {
passed,
statistic: chi_sq,
p_value,
description: format!(
"Uniform χ²: n={}, k={n_buckets}, χ²={chi_sq:.4}, \
p={p_value:.6} → {}",
samples.len(),
if passed { "PASS" } else { "FAIL" }
),
})
}
fn normal_cdf(x: f64) -> f64 {
0.5 * erfc(-x / std::f64::consts::SQRT_2)
}
fn box_muller(u1: f64, u2: f64) -> f64 {
let u1_safe = u1.max(5.96e-8_f64); let radius = (-2.0 * u1_safe.ln()).sqrt();
let angle = 2.0 * std::f64::consts::PI * u2;
radius * angle.cos()
}
pub fn normal_ks_test(samples: &[u32]) -> Result<TestResult, &'static str> {
if samples.len() < 2 {
return Err("normal_ks_test: need at least 2 samples (one u1/u2 pair)");
}
let scale = 1.0 / 4_294_967_296.0_f64;
let n_pairs = samples.len() / 2;
let mut normals: Vec<f64> = (0..n_pairs)
.map(|i| {
let u1 = samples[2 * i] as f64 * scale;
let u2 = samples[2 * i + 1] as f64 * scale;
box_muller(u1.max(scale), u2) })
.collect();
normals.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let n = normals.len() as f64;
let ks_stat = normals
.iter()
.enumerate()
.map(|(i, &x)| {
let theoretical = normal_cdf(x);
let empirical_hi = (i + 1) as f64 / n;
let empirical_lo = i as f64 / n;
(empirical_hi - theoretical)
.abs()
.max((theoretical - empirical_lo).abs())
})
.fold(0.0_f64, f64::max);
let critical = 1.628 / n.sqrt();
let passed = ks_stat < critical;
let lambda = (n.sqrt() + 0.12 + 0.11 / n.sqrt()) * ks_stat;
let p_value = {
let mut pv = 0.0_f64;
for k in 1..=50_i32 {
let sign = if k % 2 == 0 { -1.0 } else { 1.0 };
pv += sign * (-2.0 * (k as f64).powi(2) * lambda * lambda).exp();
}
(2.0 * pv).clamp(0.0, 1.0)
};
Ok(TestResult {
passed,
statistic: ks_stat,
p_value,
description: format!(
"Normal KS: n={n_pairs}, D={ks_stat:.6}, critical={critical:.6}, \
p≈{p_value:.6} → {}",
if passed { "PASS" } else { "FAIL" }
),
})
}
pub fn independent_streams_correlation(seed: u64, n: usize) -> Result<TestResult, &'static str> {
use crate::engines::philox::philox_generate_u32s;
if n == 0 {
return Err("independent_streams_correlation: n must be > 0");
}
let stream_a = philox_generate_u32s(seed, n, 0);
let stream_b = philox_generate_u32s(seed, n, n as u64);
let scale = 1.0 / 4_294_967_296.0_f64;
let fa: Vec<f64> = stream_a.iter().map(|&v| v as f64 * scale).collect();
let fb: Vec<f64> = stream_b.iter().map(|&v| v as f64 * scale).collect();
let mean_a = fa.iter().sum::<f64>() / n as f64;
let mean_b = fb.iter().sum::<f64>() / n as f64;
let cov: f64 = fa
.iter()
.zip(fb.iter())
.map(|(&a, &b)| (a - mean_a) * (b - mean_b))
.sum::<f64>()
/ n as f64;
let var_a: f64 = fa.iter().map(|&a| (a - mean_a).powi(2)).sum::<f64>() / n as f64;
let var_b: f64 = fb.iter().map(|&b| (b - mean_b).powi(2)).sum::<f64>() / n as f64;
let corr = if var_a > 0.0 && var_b > 0.0 {
cov / (var_a.sqrt() * var_b.sqrt())
} else {
0.0
};
let passed = corr.abs() < 0.01;
Ok(TestResult {
passed,
statistic: corr,
p_value: 1.0 - corr.abs(), description: format!(
"Independent Streams: n={n}, r={corr:.8} → {}",
if passed { "PASS" } else { "FAIL" }
),
})
}
fn lcg_uniform(state: &mut u64) -> f64 {
const A: u64 = 16_807;
const M: u64 = 2_147_483_647; *state = (A * *state) % M;
*state as f64 / M as f64
}
pub fn box_muller_lcg_accuracy(n: usize) -> Result<TestResult, &'static str> {
if n < 2 {
return Err("box_muller_lcg_accuracy: need at least 2 samples");
}
let mut state: u64 = 123_456_789;
let mut normals: Vec<f64> = Vec::with_capacity(n);
while normals.len() < n {
let u1 = lcg_uniform(&mut state);
let u2 = lcg_uniform(&mut state);
let radius = (-2.0 * u1.ln()).sqrt();
let angle = 2.0 * std::f64::consts::PI * u2;
normals.push(radius * angle.cos());
if normals.len() < n {
normals.push(radius * angle.sin());
}
}
normals.truncate(n);
let n_f = n as f64;
let mean = normals.iter().sum::<f64>() / n_f;
let variance = normals.iter().map(|&x| (x - mean).powi(2)).sum::<f64>() / n_f;
let std_dev = variance.sqrt();
let mean_ok = mean.abs() < 0.1;
let std_ok = (std_dev - 1.0).abs() < 0.1;
let passed = mean_ok && std_ok;
Ok(TestResult {
passed,
statistic: mean,
p_value: if passed { 1.0 } else { 0.0 },
description: format!(
"Box-Muller LCG: n={n}, mean={mean:.6}, std_dev={std_dev:.6} → {}",
if passed { "PASS" } else { "FAIL" }
),
})
}
pub fn philox_counter_offset_independence(
seed: u64,
n: usize,
offset: u64,
) -> Result<TestResult, &'static str> {
use crate::engines::philox::philox_generate_u32s;
if n == 0 {
return Err("philox_counter_offset_independence: n must be > 0");
}
if offset == 0 {
return Err("philox_counter_offset_independence: offset must be > 0");
}
let seq_a = philox_generate_u32s(seed, n, 0);
let seq_b = philox_generate_u32s(seed, n, offset);
let matches: usize = seq_a
.iter()
.zip(seq_b.iter())
.filter(|(a, b)| a == b)
.count();
let fraction_matching = matches as f64 / n as f64;
let passed = fraction_matching < 0.001;
Ok(TestResult {
passed,
statistic: fraction_matching,
p_value: if passed { 1.0 - fraction_matching } else { 0.0 },
description: format!(
"Philox counter offset independence: n={n}, offset={offset}, \
matches={matches} ({:.4}%) → {}",
fraction_matching * 100.0,
if passed { "PASS" } else { "FAIL" }
),
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::engines::philox::philox_generate_u32s;
const SEED: u64 = 42;
const N_1M: usize = 1_000_000;
const N_10K: usize = 10_000;
const N_100K: usize = 100_000;
#[test]
fn erfc_known_values() {
let v = erfc(0.0);
assert!((v - 1.0).abs() < 1e-6, "erfc(0) = {v}");
let v = erfc(10.0);
assert!(v < 1e-20, "erfc(10) = {v}");
let v = erfc(-10.0);
assert!((v - 2.0).abs() < 1e-20, "erfc(-10) = {v}");
let v = erfc(1.0);
assert!((v - 0.157_299_207_0).abs() < 1e-6, "erfc(1) = {v}");
}
#[test]
fn igamc_known_values() {
let v = igamc(1.0, 0.0);
assert!((v - 1.0).abs() < 1e-8, "igamc(1,0) = {v}");
let v = igamc(0.5, 0.0);
assert!((v - 1.0).abs() < 1e-8, "igamc(0.5,0) = {v}");
let v = igamc(1.0, 1.0);
assert!(
(v - (-1.0_f64).exp()).abs() < 1e-6,
"igamc(1,1) = {v}, expected {}",
(-1.0_f64).exp()
);
}
#[test]
fn test_philox_nist_frequency() {
let samples = philox_generate_u32s(SEED, N_1M, 0);
let result = frequency_test(&samples).expect("frequency_test should not error");
assert!(
result.passed,
"NIST Frequency test FAILED: {}",
result.description
);
assert!(result.p_value >= 0.01, "p_value={} < 0.01", result.p_value);
}
#[test]
fn test_philox_nist_block_frequency() {
let samples = philox_generate_u32s(SEED, N_1M, 0);
let result =
block_frequency_test(&samples, 128).expect("block_frequency_test should not error");
assert!(
result.passed,
"NIST Block Frequency test FAILED: {}",
result.description
);
assert!(result.p_value >= 0.01, "p_value={} < 0.01", result.p_value);
}
#[test]
fn test_philox_nist_runs() {
let samples = philox_generate_u32s(SEED, N_1M, 0);
let result = runs_test(&samples).expect("runs_test should not error");
assert!(
result.passed,
"NIST Runs test FAILED: {}",
result.description
);
assert!(result.p_value >= 0.01, "p_value={} < 0.01", result.p_value);
}
#[test]
fn test_philox_nist_longest_run() {
let samples = philox_generate_u32s(SEED, N_1M, 0);
let result = longest_run_test(&samples).expect("longest_run_test should not error");
assert!(
result.passed,
"NIST Longest Run test FAILED: {}",
result.description
);
assert!(result.p_value >= 0.01, "p_value={} < 0.01", result.p_value);
}
#[test]
fn test_uniform_chi_squared_goodness_of_fit() {
let samples = philox_generate_u32s(SEED, N_100K, 0);
let result =
uniform_chi_squared(&samples, 100).expect("uniform_chi_squared should not error");
assert!(
result.passed,
"Uniform χ² test FAILED: {}",
result.description
);
assert!(
result.statistic < 150.0,
"χ²={} suspiciously large for 99 df",
result.statistic
);
}
#[test]
fn test_normal_kolmogorov_smirnov() {
let samples = philox_generate_u32s(SEED, N_10K, 0);
let result = normal_ks_test(&samples).expect("normal_ks_test should not error");
assert!(
result.passed,
"Normal KS test FAILED: {}",
result.description
);
}
#[test]
fn test_philox_independent_streams() {
let result = independent_streams_correlation(SEED, N_100K)
.expect("correlation test should not error");
assert!(
result.passed,
"Independent streams correlation FAILED: {}",
result.description
);
assert!(
result.statistic.abs() < 0.01,
"|r|={} ≥ 0.01",
result.statistic
);
}
#[test]
fn frequency_test_rejects_empty() {
assert!(frequency_test(&[]).is_err());
}
#[test]
fn block_frequency_test_rejects_empty() {
assert!(block_frequency_test(&[], 128).is_err());
}
#[test]
fn block_frequency_test_rejects_zero_block_size() {
assert!(block_frequency_test(&[1, 2, 3], 0).is_err());
}
#[test]
fn runs_test_rejects_empty() {
assert!(runs_test(&[]).is_err());
}
#[test]
fn longest_run_test_rejects_empty() {
assert!(longest_run_test(&[]).is_err());
}
#[test]
fn uniform_chi_squared_rejects_empty() {
assert!(uniform_chi_squared(&[], 100).is_err());
}
#[test]
fn normal_ks_test_rejects_empty() {
assert!(normal_ks_test(&[]).is_err());
}
#[test]
fn test_box_muller_lcg_mean_accuracy() {
let result =
box_muller_lcg_accuracy(1000).expect("box_muller_lcg_accuracy should not error");
assert!(
result.statistic.abs() < 0.1,
"Box-Muller LCG mean={:.6} not within ±0.1 of 0.0",
result.statistic
);
}
#[test]
fn test_box_muller_lcg_std_accuracy() {
let mut state: u64 = 123_456_789;
let n = 1000usize;
let mut normals: Vec<f64> = Vec::with_capacity(n);
while normals.len() < n {
let u1 = lcg_uniform(&mut state);
let u2 = lcg_uniform(&mut state);
let radius = (-2.0 * u1.ln()).sqrt();
let angle = 2.0 * std::f64::consts::PI * u2;
normals.push(radius * angle.cos());
if normals.len() < n {
normals.push(radius * angle.sin());
}
}
normals.truncate(n);
let mean = normals.iter().sum::<f64>() / n as f64;
let variance = normals.iter().map(|&x| (x - mean).powi(2)).sum::<f64>() / n as f64;
let std_dev = variance.sqrt();
assert!(
(std_dev - 1.0).abs() < 0.1,
"Box-Muller LCG std_dev={std_dev:.6} not within 10% of 1.0"
);
}
#[test]
fn test_box_muller_lcg_combined_accuracy() {
let result =
box_muller_lcg_accuracy(1000).expect("box_muller_lcg_accuracy should not error");
assert!(
result.passed,
"Box-Muller LCG combined accuracy FAILED: {}",
result.description
);
}
#[test]
fn test_box_muller_lcg_rejects_small_n() {
assert!(box_muller_lcg_accuracy(0).is_err());
assert!(box_muller_lcg_accuracy(1).is_err());
}
#[test]
fn test_philox_counter_offset_by_1000() {
let result = philox_counter_offset_independence(SEED, 1000, 1000)
.expect("philox_counter_offset_independence should not error");
assert!(
result.passed,
"Philox offset independence FAILED: {}",
result.description
);
assert!(
result.statistic < 0.001,
"too many matching values: {:.4}%",
result.statistic * 100.0
);
}
#[test]
fn test_philox_counter_offset_non_overlapping() {
let n = 500usize;
let result = philox_counter_offset_independence(SEED, n, n as u64)
.expect("philox_counter_offset_independence should not error");
assert!(
result.passed,
"Philox non-overlapping window independence FAILED: {}",
result.description
);
}
#[test]
fn test_philox_different_seeds_differ() {
let seq_a = philox_generate_u32s(1, 1000, 0);
let seq_b = philox_generate_u32s(2, 1000, 0);
let matches: usize = seq_a
.iter()
.zip(seq_b.iter())
.filter(|(a, b)| a == b)
.count();
let fraction = matches as f64 / 1000.0;
assert!(
fraction < 0.001,
"Different seeds produced {:.4}% matching values — PRNG may be broken",
fraction * 100.0
);
}
#[test]
fn test_philox_counter_offset_rejects_zero_offset() {
assert!(philox_counter_offset_independence(SEED, 100, 0).is_err());
}
#[test]
fn test_philox_counter_offset_rejects_zero_n() {
assert!(philox_counter_offset_independence(SEED, 0, 10).is_err());
}
}