Skip to main content

tacet_core/preflight/
autocorr.rs

1//! Autocorrelation check for periodic interference detection.
2//!
3//! This check computes the autocorrelation function (ACF) on the timing
4//! sequences. High autocorrelation at low lags (especially lag-1 and lag-2)
5//! can indicate periodic interference from system processes.
6//!
7//! **Severity**: Informational
8//!
9//! High autocorrelation reduces effective sample size but the block bootstrap
10//! accounts for this. The Bayesian model's assumptions are still valid; you
11//! just needed more samples to reach the same confidence level.
12
13extern crate alloc;
14
15use alloc::string::String;
16use alloc::vec::Vec;
17
18use crate::result::{PreflightCategory, PreflightSeverity, PreflightWarningInfo};
19
20/// Warning from the autocorrelation check.
21#[derive(Debug, Clone)]
22#[cfg_attr(feature = "std", derive(serde::Serialize, serde::Deserialize))]
23pub enum AutocorrWarning {
24    /// High autocorrelation detected at specific lag.
25    ///
26    /// **Severity**: Informational
27    ///
28    /// High autocorrelation reduces effective sample size but the block
29    /// bootstrap accounts for this. Results are still valid.
30    PeriodicInterference {
31        /// The lag with high autocorrelation.
32        lag: usize,
33        /// The autocorrelation coefficient.
34        acf_value: f64,
35        /// Threshold that was exceeded.
36        threshold: f64,
37    },
38
39    /// Insufficient samples for autocorrelation analysis.
40    ///
41    /// **Severity**: Informational
42    InsufficientSamples {
43        /// Number of samples available.
44        available: usize,
45        /// Minimum required for the check.
46        required: usize,
47    },
48}
49
50impl AutocorrWarning {
51    /// Check if this warning undermines result confidence.
52    ///
53    /// Autocorrelation warnings are always informational - they affect
54    /// sampling efficiency but don't invalidate results.
55    pub fn is_result_undermining(&self) -> bool {
56        false
57    }
58
59    /// Get the severity of this warning.
60    pub fn severity(&self) -> PreflightSeverity {
61        // All autocorrelation warnings are informational
62        PreflightSeverity::Informational
63    }
64
65    /// Get a human-readable description of the warning.
66    pub fn description(&self) -> String {
67        match self {
68            AutocorrWarning::PeriodicInterference {
69                lag,
70                acf_value,
71                threshold,
72            } => {
73                alloc::format!(
74                    "High autocorrelation at lag {}: ACF={:.2} (threshold: {:.2}). \
75                     This reduces effective sample size but the block bootstrap \
76                     accounts for this.",
77                    lag,
78                    acf_value,
79                    threshold
80                )
81            }
82            AutocorrWarning::InsufficientSamples {
83                available,
84                required,
85            } => {
86                alloc::format!(
87                    "Insufficient samples for autocorrelation check: {} available, {} required.",
88                    available,
89                    required
90                )
91            }
92        }
93    }
94
95    /// Get guidance for addressing this warning.
96    pub fn guidance(&self) -> Option<String> {
97        match self {
98            AutocorrWarning::PeriodicInterference { .. } => Some(
99                "Consider checking for background processes or periodic system tasks \
100                 to improve sample efficiency."
101                    .into(),
102            ),
103            AutocorrWarning::InsufficientSamples { .. } => None,
104        }
105    }
106
107    /// Convert to a PreflightWarningInfo.
108    pub fn to_warning_info(&self) -> PreflightWarningInfo {
109        match self.guidance() {
110            Some(guidance) => PreflightWarningInfo::with_guidance(
111                PreflightCategory::Autocorrelation,
112                self.severity(),
113                self.description(),
114                guidance,
115            ),
116            None => PreflightWarningInfo::new(
117                PreflightCategory::Autocorrelation,
118                self.severity(),
119                self.description(),
120            ),
121        }
122    }
123}
124
125/// Threshold for autocorrelation to trigger warning.
126const ACF_THRESHOLD: f64 = 0.3;
127
128/// Maximum lag to check.
129const MAX_LAG: usize = 2;
130
131/// Minimum samples required for autocorrelation check.
132const MIN_SAMPLES_FOR_ACF: usize = 100;
133
134/// Perform autocorrelation check on per-class timing sequences.
135///
136/// Computes ACF for lag-1 and lag-2 for both classes and returns a warning if
137/// any exceeds the threshold (0.3).
138///
139/// # Arguments
140///
141/// * `fixed` - Timing samples from fixed class
142/// * `random` - Timing samples from random class
143///
144/// # Returns
145///
146/// `Some(AutocorrWarning)` if high autocorrelation detected, `None` otherwise.
147pub fn autocorrelation_check(fixed: &[f64], random: &[f64]) -> Option<AutocorrWarning> {
148    if fixed.len() < MIN_SAMPLES_FOR_ACF || random.len() < MIN_SAMPLES_FOR_ACF {
149        let n = fixed.len().min(random.len());
150        return Some(AutocorrWarning::InsufficientSamples {
151            available: n,
152            required: MIN_SAMPLES_FOR_ACF,
153        });
154    }
155
156    // Check autocorrelation at lag-1 and lag-2 for both classes
157    for lag in 1..=MAX_LAG {
158        let acf_f = compute_acf(fixed, lag);
159        let acf_r = compute_acf(random, lag);
160        let max_acf = if acf_f.abs() > acf_r.abs() {
161            acf_f
162        } else {
163            acf_r
164        };
165
166        if max_acf.abs() > ACF_THRESHOLD {
167            return Some(AutocorrWarning::PeriodicInterference {
168                lag,
169                acf_value: max_acf,
170                threshold: ACF_THRESHOLD,
171            });
172        }
173    }
174
175    None
176}
177
178/// Compute autocorrelation at a specific lag.
179///
180/// Uses the standard formula:
181/// ACF(k) = Cov(X_t, X_{t+k}) / Var(X)
182///
183/// # Arguments
184///
185/// * `data` - Time series data
186/// * `lag` - Lag to compute ACF for
187///
188/// # Returns
189///
190/// Autocorrelation coefficient at the specified lag.
191pub fn compute_acf(data: &[f64], lag: usize) -> f64 {
192    if data.len() <= lag {
193        return 0.0;
194    }
195
196    let n = data.len();
197
198    // Compute mean
199    let mean: f64 = data.iter().sum::<f64>() / n as f64;
200
201    // Compute variance
202    let variance: f64 = data.iter().map(|x| (x - mean) * (x - mean)).sum::<f64>() / n as f64;
203
204    if variance < 1e-10 {
205        return 0.0;
206    }
207
208    // Compute autocovariance at lag
209    let autocovariance: f64 = data
210        .iter()
211        .take(n - lag)
212        .zip(data.iter().skip(lag))
213        .map(|(x_t, x_t_k)| (x_t - mean) * (x_t_k - mean))
214        .sum::<f64>()
215        / (n - lag) as f64;
216
217    // ACF = autocovariance / variance
218    autocovariance / variance
219}
220
221/// Compute full ACF up to a maximum lag.
222///
223/// Useful for diagnostic purposes.
224///
225/// # Arguments
226///
227/// * `data` - Time series data
228/// * `max_lag` - Maximum lag to compute
229///
230/// # Returns
231///
232/// Vector of ACF values from lag-0 to max_lag.
233#[allow(dead_code)]
234pub fn compute_full_acf(data: &[f64], max_lag: usize) -> Vec<f64> {
235    (0..=max_lag).map(|lag| compute_acf(data, lag)).collect()
236}
237
238#[cfg(test)]
239mod tests {
240    use super::*;
241
242    #[test]
243    fn test_insufficient_samples() {
244        let data = alloc::vec![1.0; 50];
245        let result = autocorrelation_check(&data, &data);
246        assert!(matches!(
247            result,
248            Some(AutocorrWarning::InsufficientSamples { .. })
249        ));
250    }
251
252    #[test]
253    fn test_periodic_signal_detected() {
254        // Create a strongly periodic signal
255        let data: Vec<f64> = (0..1000)
256            .map(|i| if i % 2 == 0 { 100.0 } else { 200.0 })
257            .collect();
258
259        let result = autocorrelation_check(&data, &data);
260        assert!(
261            matches!(result, Some(AutocorrWarning::PeriodicInterference { .. })),
262            "Periodic signal should trigger warning"
263        );
264    }
265
266    #[test]
267    fn test_acf_at_lag_0() {
268        let data = alloc::vec![1.0, 2.0, 3.0, 4.0, 5.0];
269        let acf0 = compute_acf(&data, 0);
270        assert!(
271            (acf0 - 1.0).abs() < 1e-10,
272            "ACF at lag 0 should be 1.0, got {}",
273            acf0
274        );
275    }
276
277    #[test]
278    fn test_severity() {
279        let periodic = AutocorrWarning::PeriodicInterference {
280            lag: 1,
281            acf_value: 0.45,
282            threshold: 0.3,
283        };
284        assert_eq!(periodic.severity(), PreflightSeverity::Informational);
285        assert!(!periodic.is_result_undermining());
286    }
287}