extern crate alloc;
use alloc::string::String;
use alloc::vec::Vec;
use crate::result::{PreflightCategory, PreflightSeverity, PreflightWarningInfo};
#[derive(Debug, Clone)]
#[cfg_attr(feature = "std", derive(serde::Serialize, serde::Deserialize))]
pub enum AutocorrWarning {
PeriodicInterference {
lag: usize,
acf_value: f64,
threshold: f64,
},
InsufficientSamples {
available: usize,
required: usize,
},
}
impl AutocorrWarning {
pub fn is_result_undermining(&self) -> bool {
false
}
pub fn severity(&self) -> PreflightSeverity {
PreflightSeverity::Informational
}
pub fn description(&self) -> String {
match self {
AutocorrWarning::PeriodicInterference {
lag,
acf_value,
threshold,
} => {
alloc::format!(
"High autocorrelation at lag {}: ACF={:.2} (threshold: {:.2}). \
This reduces effective sample size but the block bootstrap \
accounts for this.",
lag,
acf_value,
threshold
)
}
AutocorrWarning::InsufficientSamples {
available,
required,
} => {
alloc::format!(
"Insufficient samples for autocorrelation check: {} available, {} required.",
available,
required
)
}
}
}
pub fn guidance(&self) -> Option<String> {
match self {
AutocorrWarning::PeriodicInterference { .. } => Some(
"Consider checking for background processes or periodic system tasks \
to improve sample efficiency."
.into(),
),
AutocorrWarning::InsufficientSamples { .. } => None,
}
}
pub fn to_warning_info(&self) -> PreflightWarningInfo {
match self.guidance() {
Some(guidance) => PreflightWarningInfo::with_guidance(
PreflightCategory::Autocorrelation,
self.severity(),
self.description(),
guidance,
),
None => PreflightWarningInfo::new(
PreflightCategory::Autocorrelation,
self.severity(),
self.description(),
),
}
}
}
const ACF_THRESHOLD: f64 = 0.3;
const MAX_LAG: usize = 2;
const MIN_SAMPLES_FOR_ACF: usize = 100;
pub fn autocorrelation_check(fixed: &[f64], random: &[f64]) -> Option<AutocorrWarning> {
if fixed.len() < MIN_SAMPLES_FOR_ACF || random.len() < MIN_SAMPLES_FOR_ACF {
let n = fixed.len().min(random.len());
return Some(AutocorrWarning::InsufficientSamples {
available: n,
required: MIN_SAMPLES_FOR_ACF,
});
}
for lag in 1..=MAX_LAG {
let acf_f = compute_acf(fixed, lag);
let acf_r = compute_acf(random, lag);
let max_acf = if acf_f.abs() > acf_r.abs() {
acf_f
} else {
acf_r
};
if max_acf.abs() > ACF_THRESHOLD {
return Some(AutocorrWarning::PeriodicInterference {
lag,
acf_value: max_acf,
threshold: ACF_THRESHOLD,
});
}
}
None
}
pub fn compute_acf(data: &[f64], lag: usize) -> f64 {
if data.len() <= lag {
return 0.0;
}
let n = data.len();
let mean: f64 = data.iter().sum::<f64>() / n as f64;
let variance: f64 = data.iter().map(|x| (x - mean) * (x - mean)).sum::<f64>() / n as f64;
if variance < 1e-10 {
return 0.0;
}
let autocovariance: f64 = data
.iter()
.take(n - lag)
.zip(data.iter().skip(lag))
.map(|(x_t, x_t_k)| (x_t - mean) * (x_t_k - mean))
.sum::<f64>()
/ (n - lag) as f64;
autocovariance / variance
}
#[allow(dead_code)]
pub fn compute_full_acf(data: &[f64], max_lag: usize) -> Vec<f64> {
(0..=max_lag).map(|lag| compute_acf(data, lag)).collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_insufficient_samples() {
let data = alloc::vec![1.0; 50];
let result = autocorrelation_check(&data, &data);
assert!(matches!(
result,
Some(AutocorrWarning::InsufficientSamples { .. })
));
}
#[test]
fn test_periodic_signal_detected() {
let data: Vec<f64> = (0..1000)
.map(|i| if i % 2 == 0 { 100.0 } else { 200.0 })
.collect();
let result = autocorrelation_check(&data, &data);
assert!(
matches!(result, Some(AutocorrWarning::PeriodicInterference { .. })),
"Periodic signal should trigger warning"
);
}
#[test]
fn test_acf_at_lag_0() {
let data = alloc::vec![1.0, 2.0, 3.0, 4.0, 5.0];
let acf0 = compute_acf(&data, 0);
assert!(
(acf0 - 1.0).abs() < 1e-10,
"ACF at lag 0 should be 1.0, got {}",
acf0
);
}
#[test]
fn test_severity() {
let periodic = AutocorrWarning::PeriodicInterference {
lag: 1,
acf_value: 0.45,
threshold: 0.3,
};
assert_eq!(periodic.severity(), PreflightSeverity::Informational);
assert!(!periodic.is_result_undermining());
}
}