extern crate alloc;
use alloc::vec;
use alloc::vec::Vec;
use rand::Rng;
use rand::SeedableRng;
use rand_xoshiro::Xoshiro256PlusPlus;
use crate::math;
use crate::types::{Class, Matrix9, TimingSample, Vector9};
use super::block_length::{optimal_block_length, paired_optimal_block_length};
use super::bootstrap::{
block_bootstrap_resample_into, block_bootstrap_resample_joint_into, counter_rng_seed,
};
use super::quantile::{compute_deciles_inplace, compute_midquantile_deciles};
#[cfg(feature = "parallel")]
use rayon::prelude::*;
#[cfg(feature = "parallel")]
fn ensure_rayon_configured() {
use std::sync::Once;
static INIT: Once = Once::new();
INIT.call_once(|| {
let _ = rayon::ThreadPoolBuilder::new()
.stack_size(8 * 1024 * 1024)
.build_global();
});
}
#[derive(Debug, Clone)]
pub struct CovarianceEstimate {
pub matrix: Matrix9,
pub n_bootstrap: usize,
pub block_size: usize,
pub min_eigenvalue: f64,
pub jitter_added: f64,
pub q_thresh: f64,
}
impl CovarianceEstimate {
pub fn is_stable(&self) -> bool {
nalgebra::Cholesky::new(self.matrix).is_some()
}
}
#[derive(Debug, Clone)]
pub struct WelfordCovariance9 {
n: usize,
mean: Vector9,
m2: Matrix9,
}
impl WelfordCovariance9 {
pub fn new() -> Self {
Self {
n: 0,
mean: Vector9::zeros(),
m2: Matrix9::zeros(),
}
}
pub fn update(&mut self, x: &Vector9) {
self.n += 1;
let n = self.n as f64;
let delta = x - self.mean;
self.mean += delta / n;
let delta2 = x - self.mean;
self.m2 += delta * delta2.transpose();
}
pub fn finalize(&self) -> Matrix9 {
if self.n < 2 {
return Matrix9::from_diagonal(&Vector9::repeat(1e6));
}
self.m2 / (self.n - 1) as f64
}
#[allow(dead_code)]
pub fn merge(&mut self, other: &Self) {
if other.n == 0 {
return;
}
if self.n == 0 {
*self = other.clone();
return;
}
let n_a = self.n as f64;
let n_b = other.n as f64;
let n_ab = n_a + n_b;
let delta = other.mean - self.mean;
self.mean = (self.mean * n_a + other.mean * n_b) / n_ab;
let correction = delta * delta.transpose() * (n_a * n_b / n_ab);
self.m2 = self.m2 + other.m2 + correction;
self.n += other.n;
}
#[allow(dead_code)]
pub fn count(&self) -> usize {
self.n
}
}
impl Default for WelfordCovariance9 {
fn default() -> Self {
Self::new()
}
}
pub fn bootstrap_covariance_matrix(
data: &[f64],
n_bootstrap: usize,
seed: u64,
) -> CovarianceEstimate {
let n = data.len();
let block_size = if n >= 10 {
math::ceil(optimal_block_length(data).circular) as usize
} else {
math::ceil(1.3 * math::cbrt(n as f64)) as usize
}
.max(1);
#[cfg(feature = "parallel")]
let cov_accumulator: WelfordCovariance9 = {
ensure_rayon_configured();
(0..n_bootstrap)
.into_par_iter()
.fold_with(
(
Xoshiro256PlusPlus::seed_from_u64(seed),
vec![0.0; n],
WelfordCovariance9::new(),
),
|(_, mut buffer, mut acc), i| {
let mut rng =
Xoshiro256PlusPlus::seed_from_u64(counter_rng_seed(seed, i as u64));
block_bootstrap_resample_into(data, block_size, &mut rng, &mut buffer);
let quantiles = compute_deciles_inplace(&mut buffer);
acc.update(&quantiles);
(rng, buffer, acc)
},
)
.map(|(_, _, acc)| acc)
.reduce(WelfordCovariance9::new, |mut a, b| {
a.merge(&b);
a
})
};
#[cfg(not(feature = "parallel"))]
let cov_accumulator: WelfordCovariance9 = {
let mut accumulator = WelfordCovariance9::new();
let mut buffer = vec![0.0; n];
for i in 0..n_bootstrap {
let mut rng = Xoshiro256PlusPlus::seed_from_u64(counter_rng_seed(seed, i as u64));
block_bootstrap_resample_into(data, block_size, &mut rng, &mut buffer);
let quantiles = compute_deciles_inplace(&mut buffer);
accumulator.update(&quantiles);
}
accumulator
};
let cov_matrix = cov_accumulator.finalize();
let (stabilized_matrix, jitter) = add_diagonal_jitter(cov_matrix);
let min_eigenvalue = estimate_min_eigenvalue(&stabilized_matrix);
CovarianceEstimate {
matrix: stabilized_matrix,
n_bootstrap,
block_size,
min_eigenvalue,
jitter_added: jitter,
q_thresh: 18.48,
}
}
fn resample_with_indices(data: &[f64], indices: &[usize], block_size: usize, buffer: &mut [f64]) {
let n = buffer.len();
let mut pos = 0;
for &start in indices {
for offset in 0..block_size {
if pos >= n {
break;
}
let idx = (start + offset) % data.len();
buffer[pos] = data[idx];
pos += 1;
}
}
}
pub fn bootstrap_difference_covariance(
interleaved: &[TimingSample],
n_bootstrap: usize,
seed: u64,
is_fragile: bool,
) -> CovarianceEstimate {
let n = interleaved.len();
let block_size =
super::block_length::class_conditional_optimal_block_length(interleaved, is_fragile);
#[cfg(feature = "parallel")]
let cov_accumulator: WelfordCovariance9 = {
ensure_rayon_configured();
let estimated_class_size = (n / 2) + 1;
(0..n_bootstrap)
.into_par_iter()
.fold_with(
(
Xoshiro256PlusPlus::seed_from_u64(seed),
vec![
TimingSample {
time_ns: 0.0,
class: Class::Baseline
};
n
],
Vec::with_capacity(estimated_class_size), Vec::with_capacity(estimated_class_size), WelfordCovariance9::new(),
),
|(_, mut buffer, mut baseline_samples, mut sample_samples, mut acc), i| {
let mut rng =
Xoshiro256PlusPlus::seed_from_u64(counter_rng_seed(seed, i as u64));
block_bootstrap_resample_joint_into(
interleaved,
block_size,
&mut rng,
&mut buffer,
);
baseline_samples.clear();
sample_samples.clear();
for sample in &buffer {
match sample.class {
Class::Baseline => baseline_samples.push(sample.time_ns),
Class::Sample => sample_samples.push(sample.time_ns),
}
}
let q_baseline = compute_deciles_inplace(&mut baseline_samples);
let q_sample = compute_deciles_inplace(&mut sample_samples);
let delta = q_baseline - q_sample;
acc.update(&delta);
(rng, buffer, baseline_samples, sample_samples, acc)
},
)
.map(|(_, _, _, _, acc)| acc)
.reduce(WelfordCovariance9::new, |mut a, b| {
a.merge(&b);
a
})
};
#[cfg(not(feature = "parallel"))]
let cov_accumulator: WelfordCovariance9 = {
let mut accumulator = WelfordCovariance9::new();
let mut buffer = vec![
TimingSample {
time_ns: 0.0,
class: Class::Baseline
};
n
];
let estimated_class_size = (n / 2) + 1;
let mut baseline_samples: Vec<f64> = Vec::with_capacity(estimated_class_size);
let mut sample_samples: Vec<f64> = Vec::with_capacity(estimated_class_size);
for i in 0..n_bootstrap {
let mut rng = Xoshiro256PlusPlus::seed_from_u64(counter_rng_seed(seed, i as u64));
block_bootstrap_resample_joint_into(interleaved, block_size, &mut rng, &mut buffer);
baseline_samples.clear();
sample_samples.clear();
for sample in &buffer {
match sample.class {
Class::Baseline => baseline_samples.push(sample.time_ns),
Class::Sample => sample_samples.push(sample.time_ns),
}
}
let q_baseline = compute_deciles_inplace(&mut baseline_samples);
let q_sample = compute_deciles_inplace(&mut sample_samples);
let delta = q_baseline - q_sample;
accumulator.update(&delta);
}
accumulator
};
let cov_matrix = cov_accumulator.finalize();
let (stabilized_matrix, jitter) = add_diagonal_jitter(cov_matrix);
let min_eigenvalue = estimate_min_eigenvalue(&stabilized_matrix);
let q_thresh = match stabilized_matrix.try_inverse() {
Some(sigma_inv) => {
compute_bootstrap_q_thresh(interleaved, n_bootstrap, block_size, &sigma_inv, seed)
}
None => 18.48, };
CovarianceEstimate {
matrix: stabilized_matrix,
n_bootstrap,
block_size,
min_eigenvalue,
jitter_added: jitter,
q_thresh,
}
}
pub fn bootstrap_difference_covariance_discrete(
baseline: &[f64],
sample: &[f64],
n_bootstrap: usize,
seed: u64,
) -> CovarianceEstimate {
let n = baseline.len().min(sample.len());
let baseline = &baseline[..n];
let sample = &sample[..n];
let m = if n < 2000 {
let half = math::floor(0.5 * n as f64) as usize;
half.max(200).min(n)
} else {
let m = math::floor(math::pow(n as f64, 2.0 / 3.0)) as usize;
m.max(400).min(n)
};
let base_block = if n >= 10 {
paired_optimal_block_length(baseline, sample)
} else {
math::ceil(1.3 * math::cbrt(n as f64)).max(1.0) as usize
};
let inflated_block = math::ceil((base_block as f64) * 1.5) as usize;
let mut block_size = inflated_block.max(10); let max_block = (m / 5).max(1);
block_size = block_size.min(max_block).max(1);
#[cfg(feature = "parallel")]
let cov_accumulator: WelfordCovariance9 = {
ensure_rayon_configured();
(0..n_bootstrap)
.into_par_iter()
.fold_with(
(
Xoshiro256PlusPlus::seed_from_u64(seed),
vec![0.0; m],
vec![0.0; m],
Vec::<usize>::with_capacity(m.div_ceil(block_size)),
WelfordCovariance9::new(),
),
|(_, mut baseline_buf, mut sample_buf, mut indices, mut acc), i| {
let mut rng =
Xoshiro256PlusPlus::seed_from_u64(counter_rng_seed(seed, i as u64));
indices.clear();
let n_blocks = m.div_ceil(block_size);
let max_start = n.saturating_sub(block_size);
for _ in 0..n_blocks {
indices.push(rng.random_range(0..=max_start));
}
resample_with_indices(baseline, &indices, block_size, &mut baseline_buf);
resample_with_indices(sample, &indices, block_size, &mut sample_buf);
let q_baseline = compute_midquantile_deciles(&baseline_buf);
let q_sample = compute_midquantile_deciles(&sample_buf);
let delta = q_baseline - q_sample;
acc.update(&delta);
(rng, baseline_buf, sample_buf, indices, acc)
},
)
.map(|(_, _, _, _, acc)| acc)
.reduce(WelfordCovariance9::new, |mut a, b| {
a.merge(&b);
a
})
};
#[cfg(not(feature = "parallel"))]
let cov_accumulator: WelfordCovariance9 = {
let mut accumulator = WelfordCovariance9::new();
let mut baseline_buf = vec![0.0; m];
let mut sample_buf = vec![0.0; m];
let mut indices = Vec::with_capacity(m.div_ceil(block_size));
for i in 0..n_bootstrap {
let mut rng = Xoshiro256PlusPlus::seed_from_u64(counter_rng_seed(seed, i as u64));
indices.clear();
let n_blocks = m.div_ceil(block_size);
let max_start = n.saturating_sub(block_size);
for _ in 0..n_blocks {
indices.push(rng.random_range(0..=max_start));
}
resample_with_indices(baseline, &indices, block_size, &mut baseline_buf);
resample_with_indices(sample, &indices, block_size, &mut sample_buf);
let q_baseline = compute_midquantile_deciles(&baseline_buf);
let q_sample = compute_midquantile_deciles(&sample_buf);
let delta = q_baseline - q_sample;
accumulator.update(&delta);
}
accumulator
};
let mut cov_matrix = cov_accumulator.finalize();
if n > 0 {
cov_matrix *= (m as f64) / (n as f64);
}
let (stabilized_matrix, jitter) = add_diagonal_jitter(cov_matrix);
let min_eigenvalue = estimate_min_eigenvalue(&stabilized_matrix);
let q_thresh = 18.48;
CovarianceEstimate {
matrix: stabilized_matrix,
n_bootstrap,
block_size,
min_eigenvalue,
jitter_added: jitter,
q_thresh,
}
}
#[cfg(test)]
fn compute_sample_covariance(vectors: &[Vector9]) -> Matrix9 {
let n = vectors.len();
if n < 2 {
return Matrix9::from_diagonal(&Vector9::repeat(1e6));
}
let mut mean = Vector9::zeros();
for v in vectors {
mean += v;
}
mean /= n as f64;
let mut cov = Matrix9::zeros();
for v in vectors {
let centered = v - mean;
cov += centered * centered.transpose();
}
cov /= (n - 1) as f64;
cov
}
fn add_diagonal_jitter(mut matrix: Matrix9) -> (Matrix9, f64) {
let trace = matrix.trace();
let sigma_bar_sq = trace / 9.0;
let floor = 0.01 * sigma_bar_sq;
let epsilon = 1e-10 + sigma_bar_sq * 1e-8;
for i in 0..9 {
matrix[(i, i)] = matrix[(i, i)].max(floor) + epsilon;
}
(matrix, epsilon)
}
fn compute_q_statistic(delta: &Vector9, sigma_inv: &Matrix9) -> f64 {
let q = delta.transpose() * sigma_inv * delta;
q[(0, 0)].max(0.0)
}
fn compute_bootstrap_q_thresh(
interleaved: &[TimingSample],
n_bootstrap: usize,
block_size: usize,
sigma_inv: &Matrix9,
seed: u64,
) -> f64 {
const FALLBACK_Q_THRESH: f64 = 18.48;
if interleaved.is_empty() || n_bootstrap == 0 {
return FALLBACK_Q_THRESH;
}
let n = interleaved.len();
#[cfg(feature = "parallel")]
let q_values: Vec<f64> = {
ensure_rayon_configured();
let estimated_class_size = (n / 2) + 1;
(0..n_bootstrap)
.into_par_iter()
.fold_with(
(
vec![
TimingSample {
time_ns: 0.0,
class: Class::Baseline,
};
n
],
Vec::with_capacity(estimated_class_size),
Vec::with_capacity(estimated_class_size),
Vec::with_capacity(n_bootstrap / rayon::current_num_threads().max(1) + 1),
),
|(mut buffer, mut baseline_samples, mut sample_samples, mut results), i| {
let mut rng = Xoshiro256PlusPlus::seed_from_u64(counter_rng_seed(
seed.wrapping_add(1),
i as u64,
));
block_bootstrap_resample_joint_into(
interleaved,
block_size,
&mut rng,
&mut buffer,
);
baseline_samples.clear();
sample_samples.clear();
for sample in &buffer {
match sample.class {
Class::Baseline => baseline_samples.push(sample.time_ns),
Class::Sample => sample_samples.push(sample.time_ns),
}
}
let q_baseline = compute_deciles_inplace(&mut baseline_samples);
let q_sample = compute_deciles_inplace(&mut sample_samples);
let delta_star = q_baseline - q_sample;
results.push(compute_q_statistic(&delta_star, sigma_inv));
(buffer, baseline_samples, sample_samples, results)
},
)
.flat_map(|(_, _, _, results)| results)
.collect()
};
#[cfg(not(feature = "parallel"))]
let q_values: Vec<f64> = {
let mut values = Vec::with_capacity(n_bootstrap);
let mut buffer = vec![
TimingSample {
time_ns: 0.0,
class: Class::Baseline,
};
n
];
let estimated_class_size = (n / 2) + 1;
let mut baseline_samples: Vec<f64> = Vec::with_capacity(estimated_class_size);
let mut sample_samples: Vec<f64> = Vec::with_capacity(estimated_class_size);
for i in 0..n_bootstrap {
let mut rng =
Xoshiro256PlusPlus::seed_from_u64(counter_rng_seed(seed.wrapping_add(1), i as u64));
block_bootstrap_resample_joint_into(interleaved, block_size, &mut rng, &mut buffer);
baseline_samples.clear();
sample_samples.clear();
for sample in &buffer {
match sample.class {
Class::Baseline => baseline_samples.push(sample.time_ns),
Class::Sample => sample_samples.push(sample.time_ns),
}
}
let q_baseline = compute_deciles_inplace(&mut baseline_samples);
let q_sample = compute_deciles_inplace(&mut sample_samples);
let delta_star = q_baseline - q_sample;
values.push(compute_q_statistic(&delta_star, sigma_inv));
}
values
};
let mut finite_q: Vec<f64> = q_values.into_iter().filter(|q| q.is_finite()).collect();
if finite_q.is_empty() {
return FALLBACK_Q_THRESH;
}
finite_q.sort_by(|a, b| a.partial_cmp(b).unwrap_or(core::cmp::Ordering::Equal));
let p99_idx = ((finite_q.len() as f64) * 0.99) as usize;
finite_q
.get(p99_idx.min(finite_q.len().saturating_sub(1)))
.copied()
.unwrap_or(FALLBACK_Q_THRESH)
}
pub fn apply_variance_floor(mut matrix: Matrix9, timer_resolution_ns: f64) -> Matrix9 {
let floor = math::sq(timer_resolution_ns) / 12.0;
for i in 0..9 {
matrix[(i, i)] += floor;
}
matrix
}
pub fn scale_covariance_for_inference(
matrix: Matrix9,
n_calibration: usize,
n_inference: usize,
) -> Matrix9 {
let scale = n_calibration as f64 / n_inference as f64;
matrix * scale
}
pub fn compute_covariance_rate(covariance: &Matrix9, n_calibration: usize) -> Matrix9 {
let scale = n_calibration as f64;
covariance * scale
}
pub fn scale_covariance_rate(rate: &Matrix9, n: usize) -> Matrix9 {
assert!(n > 0, "Cannot scale covariance rate for 0 samples");
let scale = 1.0 / (n as f64);
rate * scale
}
fn estimate_min_eigenvalue(matrix: &Matrix9) -> f64 {
let mut min_diag = f64::MAX;
let mut max_off_diag_sum: f64 = 0.0;
for i in 0..9 {
min_diag = min_diag.min(matrix[(i, i)]);
let mut row_sum = 0.0;
for j in 0..9 {
if i != j {
row_sum += matrix[(i, j)].abs();
}
}
max_off_diag_sum = max_off_diag_sum.max(row_sum);
}
min_diag - max_off_diag_sum
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_covariance_estimate_basic() {
let data: Vec<f64> = (0..1000).map(|x| (x as f64) + 100.0).collect();
let estimate = bootstrap_covariance_matrix(&data, 100, 42);
assert_eq!(estimate.n_bootstrap, 100);
assert!(estimate.block_size > 0);
assert!(estimate.jitter_added > 0.0);
}
#[test]
fn test_covariance_symmetry() {
let data: Vec<f64> = (0..500).map(|x| (x as f64) * 0.1).collect();
let estimate = bootstrap_covariance_matrix(&data, 50, 123);
for i in 0..9 {
for j in 0..9 {
let diff = (estimate.matrix[(i, j)] - estimate.matrix[(j, i)]).abs();
assert!(diff < 1e-12, "Matrix not symmetric at ({}, {})", i, j);
}
}
}
#[test]
fn test_sample_covariance_identity() {
let vectors: Vec<Vector9> = (0..100).map(|_| Vector9::from_element(1.0)).collect();
let cov = compute_sample_covariance(&vectors);
for i in 0..9 {
for j in 0..9 {
assert!(cov[(i, j)].abs() < 1e-12);
}
}
}
#[test]
fn test_welford_numerical_equivalence() {
let vectors: Vec<Vector9> = (0..100)
.map(|i| {
Vector9::from_fn(|j, _| {
(i * 7 + j * 13) as f64 % 17.0
})
})
.collect();
let batch_cov = compute_sample_covariance(&vectors);
let mut welford = WelfordCovariance9::new();
for v in &vectors {
welford.update(v);
}
let welford_cov = welford.finalize();
for i in 0..9 {
for j in 0..9 {
let diff = (batch_cov[(i, j)] - welford_cov[(i, j)]).abs();
assert!(
diff < 1e-9,
"Mismatch at ({}, {}): batch={}, welford={}, diff={}",
i,
j,
batch_cov[(i, j)],
welford_cov[(i, j)],
diff
);
}
}
}
#[test]
fn test_welford_edge_cases() {
let empty = WelfordCovariance9::new();
let cov0 = empty.finalize();
let expected_conservative = Matrix9::from_diagonal(&Vector9::repeat(1e6));
assert_eq!(
cov0, expected_conservative,
"n=0 should return conservative diagonal"
);
let mut one = WelfordCovariance9::new();
one.update(&Vector9::from_element(42.0));
let cov1 = one.finalize();
assert_eq!(
cov1, expected_conservative,
"n=1 should return conservative diagonal"
);
let mut two = WelfordCovariance9::new();
two.update(&Vector9::from_element(1.0));
two.update(&Vector9::from_element(2.0));
let cov2 = two.finalize();
assert!(
cov2 != expected_conservative,
"n=2 should not return conservative fallback"
);
for i in 0..9 {
for j in 0..9 {
assert!(
(cov2[(i, j)] - cov2[(j, i)]).abs() < 1e-12,
"n=2 result not symmetric"
);
}
}
}
#[test]
fn test_welford_merge_correctness() {
let vectors_a: Vec<Vector9> = (0..50).map(|i| Vector9::from_element(i as f64)).collect();
let vectors_b: Vec<Vector9> = (50..100).map(|i| Vector9::from_element(i as f64)).collect();
let mut acc_a = WelfordCovariance9::new();
for v in &vectors_a {
acc_a.update(v);
}
let mut acc_b = WelfordCovariance9::new();
for v in &vectors_b {
acc_b.update(v);
}
let mut merged = acc_a.clone();
merged.merge(&acc_b);
let merged_cov = merged.finalize();
let mut combined = WelfordCovariance9::new();
for v in vectors_a.iter().chain(vectors_b.iter()) {
combined.update(v);
}
let combined_cov = combined.finalize();
for i in 0..9 {
for j in 0..9 {
let diff = (merged_cov[(i, j)] - combined_cov[(i, j)]).abs();
assert!(
diff < 1e-9,
"Merge mismatch at ({}, {}): merged={}, combined={}, diff={}",
i,
j,
merged_cov[(i, j)],
combined_cov[(i, j)],
diff
);
}
}
}
#[test]
fn test_welford_symmetry() {
let mut welford = WelfordCovariance9::new();
for i in 0..100 {
welford.update(&Vector9::from_fn(|j, _| ((i * 7 + j * 11) % 23) as f64));
}
let cov = welford.finalize();
for i in 0..9 {
for j in 0..9 {
let diff = (cov[(i, j)] - cov[(j, i)]).abs();
assert!(
diff < 1e-12,
"Welford result not symmetric at ({}, {}): diff={}",
i,
j,
diff
);
}
}
}
#[test]
fn test_covariance_rate_roundtrip() {
let original = Matrix9::from_fn(|i, j| {
if i == j {
10.0 + i as f64
} else {
(i as f64 - j as f64).abs() * 0.5
}
});
let n_cal = 5000;
let rate = compute_covariance_rate(&original, n_cal);
let recovered = scale_covariance_rate(&rate, n_cal);
for i in 0..9 {
for j in 0..9 {
let diff = (original[(i, j)] - recovered[(i, j)]).abs();
assert!(
diff < 1e-10,
"Roundtrip failed at ({}, {}): original={}, recovered={}, diff={}",
i,
j,
original[(i, j)],
recovered[(i, j)],
diff
);
}
}
}
#[test]
fn test_covariance_rate_scaling() {
let sigma_cal = Matrix9::from_fn(|i, j| {
if i == j {
100.0 } else if (i as i32 - j as i32).abs() == 1 {
50.0 } else {
10.0 }
});
let n_cal = 1000;
let rate = compute_covariance_rate(&sigma_cal, n_cal);
let sigma_2n = scale_covariance_rate(&rate, 2 * n_cal);
for i in 0..9 {
for j in 0..9 {
let expected = sigma_cal[(i, j)] / 2.0;
let actual = sigma_2n[(i, j)];
let diff = (expected - actual).abs();
assert!(
diff < 1e-10,
"2n scaling failed at ({}, {}): expected={}, actual={}",
i,
j,
expected,
actual
);
}
}
let sigma_10n = scale_covariance_rate(&rate, 10 * n_cal);
for i in 0..9 {
for j in 0..9 {
let expected = sigma_cal[(i, j)] / 10.0;
let actual = sigma_10n[(i, j)];
let diff = (expected - actual).abs();
assert!(
diff < 1e-10,
"10n scaling failed at ({}, {}): expected={}, actual={}",
i,
j,
expected,
actual
);
}
}
}
#[test]
#[should_panic(expected = "Cannot scale covariance rate for 0 samples")]
fn test_scale_covariance_rate_zero_panics() {
let rate = Matrix9::identity();
let _ = scale_covariance_rate(&rate, 0);
}
#[test]
fn test_covariance_rate_preserves_symmetry() {
let symmetric = Matrix9::from_fn(|i, j| {
if i == j {
50.0
} else {
25.0 / (1.0 + (i as i32 - j as i32).abs() as f64)
}
});
for i in 0..9 {
for j in 0..9 {
assert!(
(symmetric[(i, j)] - symmetric[(j, i)]).abs() < 1e-12,
"Input not symmetric"
);
}
}
let rate = compute_covariance_rate(&symmetric, 5000);
let scaled = scale_covariance_rate(&rate, 10000);
for i in 0..9 {
for j in 0..9 {
let diff = (scaled[(i, j)] - scaled[(j, i)]).abs();
assert!(
diff < 1e-12,
"Rate operations broke symmetry at ({}, {}): diff={}",
i,
j,
diff
);
}
}
}
#[test]
fn test_q_statistic_mahalanobis() {
let delta = Vector9::from_fn(|i, _| (i as f64) * 2.0);
let sigma_inv = Matrix9::identity();
let q = compute_q_statistic(&delta, &sigma_inv);
let expected: f64 = (0..9).map(|i| ((i as f64) * 2.0).powi(2)).sum();
assert!(
(q - expected).abs() < 1e-10,
"Q should equal sum of squares with identity cov, got {} expected {}",
q,
expected
);
}
#[test]
fn test_q_statistic_zero_delta() {
let delta = Vector9::zeros();
let sigma_inv = Matrix9::identity();
let q = compute_q_statistic(&delta, &sigma_inv);
assert!(q < 1e-10, "Q should be ~0 for zero delta, got {}", q);
}
#[test]
fn test_q_statistic_is_non_negative() {
for seed in 0..10u64 {
let delta = Vector9::from_fn(|i, _| ((seed * 7 + i as u64 * 13) % 100) as f64 - 50.0);
let sigma_inv = Matrix9::identity();
let q = compute_q_statistic(&delta, &sigma_inv);
assert!(q >= 0.0, "Q should be non-negative, got {}", q);
}
}
#[test]
fn test_bootstrap_q_thresh_computed() {
use crate::types::Class;
let n = 1000;
let mut samples = Vec::with_capacity(2 * n);
for i in 0..n {
samples.push(TimingSample {
time_ns: 100.0 + (i as f64) * 0.01,
class: Class::Baseline,
});
samples.push(TimingSample {
time_ns: 101.0 + (i as f64) * 0.01,
class: Class::Sample,
});
}
let estimate = bootstrap_difference_covariance(&samples, 100, 42, false);
assert!(
estimate.q_thresh > 0.0 && estimate.q_thresh.is_finite(),
"q_thresh should be positive and finite, got {}",
estimate.q_thresh
);
println!("Computed q_thresh: {}", estimate.q_thresh);
}
#[test]
fn test_single_class_q_thresh_fallback() {
let data: Vec<f64> = (0..500).map(|x| (x as f64) * 0.1).collect();
let estimate = bootstrap_covariance_matrix(&data, 50, 42);
assert!(
(estimate.q_thresh - 18.48).abs() < 1e-6,
"Single-class q_thresh should use fallback 18.48, got {}",
estimate.q_thresh
);
}
}