extern crate alloc;
use alloc::vec;
use alloc::vec::Vec;
use crate::math;
use crate::types::{Class, TimingSample};
#[derive(Debug, Clone, Copy)]
pub struct OptimalBlockLength {
pub stationary: f64,
pub circular: f64,
}
pub fn optimal_block_length(x: &[f64]) -> OptimalBlockLength {
let n = x.len();
assert!(
n >= 10,
"Need at least 10 observations for block length estimation"
);
let mean = x.iter().sum::<f64>() / n as f64;
let centered: Vec<f64> = x.iter().map(|&xi| xi - mean).collect();
let max_block_length = math::ceil((3.0 * math::sqrt(n as f64)).min(n as f64 / 3.0));
let consecutive_insignificant_needed = 5.max(math::log10(n as f64) as usize);
let max_lag = math::ceil(math::sqrt(n as f64)) as usize + consecutive_insignificant_needed;
let insignificance_threshold = 2.0 * math::sqrt(math::log10(n as f64) / n as f64);
let mut autocovariances = vec![0.0; max_lag + 1];
let mut abs_autocorrelations = vec![0.0; max_lag + 1];
let mut first_insignificant_run_start: Option<usize> = None;
for lag in 0..=max_lag {
if lag + 1 >= n {
break;
}
let leading_segment = ¢ered[lag + 1..]; let trailing_segment = ¢ered[..n - lag - 1];
let variance_leading: f64 = leading_segment.iter().map(|e| e * e).sum();
let variance_trailing: f64 = trailing_segment.iter().map(|e| e * e).sum();
let cross_product: f64 = centered[lag..]
.iter()
.zip(centered[..n - lag].iter())
.map(|(&a, &b)| a * b)
.sum();
autocovariances[lag] = cross_product / n as f64;
let denominator = math::sqrt(variance_leading * variance_trailing);
abs_autocorrelations[lag] = if denominator > 0.0 {
cross_product.abs() / denominator
} else {
0.0
};
if lag >= consecutive_insignificant_needed && first_insignificant_run_start.is_none() {
let recent_autocorrelations =
&abs_autocorrelations[lag - consecutive_insignificant_needed..lag];
let all_insignificant = recent_autocorrelations
.iter()
.all(|&r| r < insignificance_threshold);
if all_insignificant {
first_insignificant_run_start = Some(lag - consecutive_insignificant_needed);
}
}
}
let truncation_lag = match first_insignificant_run_start {
Some(start) => (2 * start.max(1)).min(max_lag),
None => max_lag,
};
let mut g = 0.0; let mut long_run_variance = autocovariances[0];
for (lag, &acv) in autocovariances[1..=truncation_lag].iter().enumerate() {
let lag = lag + 1;
let kernel_arg = lag as f64 / truncation_lag as f64;
let kernel_weight = if kernel_arg <= 0.5 {
1.0
} else {
2.0 * (1.0 - kernel_arg)
};
g += 2.0 * kernel_weight * lag as f64 * acv;
long_run_variance += 2.0 * kernel_weight * acv;
}
let variance_squared = math::sq(long_run_variance);
let d_stationary = 2.0 * variance_squared;
let d_circular = (4.0 / 3.0) * variance_squared;
let n_cuberoot = math::cbrt(n as f64);
let block_stationary = if d_stationary > 0.0 {
let ratio = (2.0 * math::sq(g)) / d_stationary;
math::cbrt(ratio) * n_cuberoot
} else {
1.0
};
let block_circular = if d_circular > 0.0 {
let ratio = (2.0 * math::sq(g)) / d_circular;
math::cbrt(ratio) * n_cuberoot
} else {
1.0
};
OptimalBlockLength {
stationary: block_stationary.min(max_block_length),
circular: block_circular.min(max_block_length),
}
}
pub fn paired_optimal_block_length(baseline: &[f64], sample: &[f64]) -> usize {
let opt_baseline = optimal_block_length(baseline);
let opt_sample = optimal_block_length(sample);
let max_circular = opt_baseline.circular.max(opt_sample.circular);
math::ceil(max_circular).max(1.0) as usize
}
const BLOCK_LENGTH_FLOOR: usize = 10;
pub fn class_conditional_optimal_block_length(stream: &[TimingSample], is_fragile: bool) -> usize {
let n = stream.len();
if n < 20 {
return BLOCK_LENGTH_FLOOR;
}
let (max_abs_acf, max_acv, truncation_lag) = compute_class_conditional_acf(stream);
if truncation_lag == 0 || max_acv.is_empty() {
return BLOCK_LENGTH_FLOOR;
}
let block_length = politis_white_from_acf(&max_abs_acf, &max_acv, truncation_lag, n);
let mut result = (math::ceil(block_length) as usize).max(BLOCK_LENGTH_FLOOR);
if is_fragile {
result = math::ceil((result as f64) * 1.5) as usize;
}
let max_block = ((3.0 * math::sqrt(n as f64)).min(n as f64 / 3.0)) as usize;
result.min(max_block).max(BLOCK_LENGTH_FLOOR)
}
fn compute_class_conditional_acf(stream: &[TimingSample]) -> (Vec<f64>, Vec<f64>, usize) {
let n = stream.len();
let (sum_f, count_f, sum_r, count_r) =
stream
.iter()
.fold((0.0, 0usize, 0.0, 0usize), |(sf, cf, sr, cr), s| {
match s.class {
Class::Baseline => (sf + s.time_ns, cf + 1, sr, cr),
Class::Sample => (sf, cf, sr + s.time_ns, cr + 1),
}
});
if count_f < 5 || count_r < 5 {
return (vec![], vec![], 0);
}
let mean_f = sum_f / count_f as f64;
let mean_r = sum_r / count_r as f64;
let var_f: f64 = stream
.iter()
.filter(|s| s.class == Class::Baseline)
.map(|s| math::sq(s.time_ns - mean_f))
.sum::<f64>()
/ count_f as f64;
let var_r: f64 = stream
.iter()
.filter(|s| s.class == Class::Sample)
.map(|s| math::sq(s.time_ns - mean_r))
.sum::<f64>()
/ count_r as f64;
if var_f < 1e-12 || var_r < 1e-12 {
return (vec![], vec![], 0);
}
let consecutive_insignificant_needed = 5.max(math::log10(n as f64) as usize);
let max_lag =
(math::ceil(math::sqrt(n as f64)) as usize + consecutive_insignificant_needed).min(n / 2);
let insignificance_threshold = 2.0 * math::sqrt(math::log10(n as f64) / n as f64);
let mut max_abs_acf = vec![0.0; max_lag + 1];
let mut max_acv = vec![0.0; max_lag + 1];
let mut first_insignificant_run_start: Option<usize> = None;
max_abs_acf[0] = 1.0;
max_acv[0] = var_f.max(var_r);
for lag in 1..=max_lag {
let (acf_f, acv_f) = compute_single_lag_acf(stream, lag, Class::Baseline, mean_f, var_f);
let (acf_r, acv_r) = compute_single_lag_acf(stream, lag, Class::Sample, mean_r, var_r);
let abs_acf_f = acf_f.abs();
let abs_acf_r = acf_r.abs();
if abs_acf_f >= abs_acf_r {
max_abs_acf[lag] = abs_acf_f;
max_acv[lag] = acv_f;
} else {
max_abs_acf[lag] = abs_acf_r;
max_acv[lag] = acv_r;
}
if lag >= consecutive_insignificant_needed && first_insignificant_run_start.is_none() {
let recent = &max_abs_acf[lag - consecutive_insignificant_needed..lag];
if recent.iter().all(|&r| r < insignificance_threshold) {
first_insignificant_run_start = Some(lag - consecutive_insignificant_needed);
}
}
}
let truncation_lag = match first_insignificant_run_start {
Some(start) => (2 * start.max(1)).min(max_lag),
None => max_lag,
};
(max_abs_acf, max_acv, truncation_lag)
}
fn compute_single_lag_acf(
stream: &[TimingSample],
lag: usize,
class: Class,
mean: f64,
var: f64,
) -> (f64, f64) {
let n = stream.len();
if lag >= n {
return (0.0, 0.0);
}
let mut cross_sum = 0.0;
let mut pair_count = 0usize;
for t in 0..(n - lag) {
if stream[t].class == class && stream[t + lag].class == class {
let x_t = stream[t].time_ns - mean;
let x_t_lag = stream[t + lag].time_ns - mean;
cross_sum += x_t * x_t_lag;
pair_count += 1;
}
}
if pair_count < 3 {
return (0.0, 0.0);
}
let autocovariance = cross_sum / pair_count as f64;
let autocorrelation = if var > 1e-12 {
autocovariance / var
} else {
0.0
};
(autocorrelation, autocovariance)
}
fn politis_white_from_acf(
_abs_acf: &[f64],
autocovariances: &[f64],
truncation_lag: usize,
n: usize,
) -> f64 {
if truncation_lag == 0 || autocovariances.is_empty() {
return BLOCK_LENGTH_FLOOR as f64;
}
let mut g = 0.0;
let mut long_run_variance = autocovariances[0];
for (lag, &acv) in autocovariances
.iter()
.enumerate()
.skip(1)
.take(truncation_lag.min(autocovariances.len() - 1))
{
let kernel_arg = lag as f64 / truncation_lag as f64;
let kernel_weight = if kernel_arg <= 0.5 {
1.0
} else {
2.0 * (1.0 - kernel_arg)
};
g += 2.0 * kernel_weight * lag as f64 * acv;
long_run_variance += 2.0 * kernel_weight * acv;
}
let variance_squared = math::sq(long_run_variance);
let d_circular = (4.0 / 3.0) * variance_squared;
if d_circular > 1e-12 {
let ratio = (2.0 * math::sq(g)) / d_circular;
let n_cuberoot = math::cbrt(n as f64);
math::cbrt(ratio) * n_cuberoot
} else {
BLOCK_LENGTH_FLOOR as f64
}
}
#[cfg(test)]
mod tests {
use super::*;
use rand::Rng;
use rand::SeedableRng;
use rand_xoshiro::Xoshiro256PlusPlus;
fn generate_ar1(n: usize, phi: f64, seed: u64) -> Vec<f64> {
let mut rng = Xoshiro256PlusPlus::seed_from_u64(seed);
let mut x = vec![0.0; n];
x[0] = rng.random::<f64>() - 0.5;
for i in 1..n {
let innovation = rng.random::<f64>() - 0.5;
x[i] = phi * x[i - 1] + innovation;
}
x
}
#[test]
fn test_iid_data_small_block() {
let mut rng = Xoshiro256PlusPlus::seed_from_u64(42);
let x: Vec<f64> = (0..500).map(|_| rng.random::<f64>()).collect();
let opt = optimal_block_length(&x);
assert!(
opt.stationary < 10.0,
"IID stationary block {} should be small",
opt.stationary
);
assert!(
opt.circular < 10.0,
"IID circular block {} should be small",
opt.circular
);
}
#[test]
fn test_ar1_moderate_dependence() {
let x = generate_ar1(500, 0.5, 123);
let opt = optimal_block_length(&x);
assert!(
opt.stationary > 2.0 && opt.stationary < 40.0,
"AR(1) φ=0.5 stationary block {} outside expected range",
opt.stationary
);
}
#[test]
fn test_ar1_strong_dependence() {
let x = generate_ar1(500, 0.9, 456);
let opt = optimal_block_length(&x);
assert!(
opt.stationary > 5.0,
"AR(1) φ=0.9 stationary block {} should be substantial",
opt.stationary
);
}
#[test]
fn test_stationary_vs_circular() {
let x = generate_ar1(500, 0.6, 789);
let opt = optimal_block_length(&x);
let expected_ratio = (2.0_f64 / (4.0 / 3.0)).powf(1.0 / 3.0);
let actual_ratio = opt.circular / opt.stationary;
assert!(
(actual_ratio - expected_ratio).abs() < 0.01,
"Circular/stationary ratio {} should be ~{}",
actual_ratio,
expected_ratio
);
}
#[test]
fn test_paired_optimal_takes_max() {
let x = generate_ar1(500, 0.9, 111); let y = generate_ar1(500, 0.3, 222);
let paired = paired_optimal_block_length(&x, &y);
let opt_x = optimal_block_length(&x);
let opt_y = optimal_block_length(&y);
let expected = math::ceil(opt_x.circular.max(opt_y.circular)) as usize;
assert_eq!(
paired, expected,
"Paired block length {} should equal max of individual circular estimates {}",
paired, expected
);
}
#[test]
fn test_minimum_sample_size() {
let mut rng = Xoshiro256PlusPlus::seed_from_u64(999);
let x: Vec<f64> = (0..10).map(|_| rng.random::<f64>()).collect();
let opt = optimal_block_length(&x);
assert!(opt.stationary >= 1.0, "Block length should be at least 1");
assert!(
opt.circular <= 10.0,
"Block length should not exceed sample size"
);
}
#[test]
#[should_panic(expected = "Need at least 10 observations")]
fn test_insufficient_samples_panics() {
let x = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let _ = optimal_block_length(&x);
}
#[test]
fn test_constant_series() {
let x = vec![42.0; 100];
let opt = optimal_block_length(&x);
assert_eq!(opt.stationary, 1.0, "Constant series should give block = 1");
assert_eq!(opt.circular, 1.0, "Constant series should give block = 1");
}
#[test]
fn test_deterministic_results() {
let x = generate_ar1(500, 0.5, 42);
let opt1 = optimal_block_length(&x);
let opt2 = optimal_block_length(&x);
assert_eq!(opt1.stationary, opt2.stationary, "Should be deterministic");
assert_eq!(opt1.circular, opt2.circular, "Should be deterministic");
}
}