Skip to main content

ruvector_dag/healing/
anomaly.rs

1//! Anomaly Detection using Z-score analysis
2
3use std::collections::VecDeque;
4
5#[derive(Debug, Clone)]
6pub struct AnomalyConfig {
7    pub z_threshold: f64,   // Z-score threshold (default: 3.0)
8    pub window_size: usize, // Rolling window size (default: 100)
9    pub min_samples: usize, // Minimum samples before detection (default: 10)
10}
11
12impl Default for AnomalyConfig {
13    fn default() -> Self {
14        Self {
15            z_threshold: 3.0,
16            window_size: 100,
17            min_samples: 10,
18        }
19    }
20}
21
22#[derive(Debug, Clone)]
23pub enum AnomalyType {
24    LatencySpike,
25    PatternDrift,
26    MemoryPressure,
27    CacheEviction,
28    LearningStall,
29}
30
31#[derive(Debug, Clone)]
32pub struct Anomaly {
33    pub anomaly_type: AnomalyType,
34    pub z_score: f64,
35    pub value: f64,
36    pub expected: f64,
37    pub timestamp: std::time::Instant,
38    pub component: String,
39}
40
41pub struct AnomalyDetector {
42    config: AnomalyConfig,
43    observations: VecDeque<f64>,
44    sum: f64,
45    sum_sq: f64,
46}
47
48impl AnomalyDetector {
49    pub fn new(config: AnomalyConfig) -> Self {
50        Self {
51            config,
52            observations: VecDeque::with_capacity(100),
53            sum: 0.0,
54            sum_sq: 0.0,
55        }
56    }
57
58    pub fn observe(&mut self, value: f64) {
59        // Add to window
60        if self.observations.len() >= self.config.window_size {
61            if let Some(old) = self.observations.pop_front() {
62                self.sum -= old;
63                self.sum_sq -= old * old;
64            }
65        }
66
67        self.observations.push_back(value);
68        self.sum += value;
69        self.sum_sq += value * value;
70    }
71
72    pub fn is_anomaly(&self, value: f64) -> Option<f64> {
73        if self.observations.len() < self.config.min_samples {
74            return None;
75        }
76
77        let n = self.observations.len() as f64;
78        let mean = self.sum / n;
79        let variance = (self.sum_sq / n) - (mean * mean);
80        let std_dev = variance.sqrt();
81
82        if std_dev < 1e-10 {
83            return None;
84        }
85
86        let z_score = (value - mean) / std_dev;
87
88        if z_score.abs() > self.config.z_threshold {
89            Some(z_score)
90        } else {
91            None
92        }
93    }
94
95    pub fn detect(&self) -> Vec<Anomaly> {
96        // Check recent observations for anomalies
97        let mut anomalies = Vec::new();
98
99        if let Some(&last) = self.observations.back() {
100            if let Some(z_score) = self.is_anomaly(last) {
101                let n = self.observations.len() as f64;
102                let mean = self.sum / n;
103
104                anomalies.push(Anomaly {
105                    anomaly_type: AnomalyType::LatencySpike,
106                    z_score,
107                    value: last,
108                    expected: mean,
109                    timestamp: std::time::Instant::now(),
110                    component: "unknown".to_string(),
111                });
112            }
113        }
114
115        anomalies
116    }
117
118    pub fn mean(&self) -> f64 {
119        if self.observations.is_empty() {
120            0.0
121        } else {
122            self.sum / self.observations.len() as f64
123        }
124    }
125
126    pub fn std_dev(&self) -> f64 {
127        if self.observations.len() < 2 {
128            return 0.0;
129        }
130        let n = self.observations.len() as f64;
131        let mean = self.sum / n;
132        let variance = (self.sum_sq / n) - (mean * mean);
133        variance.sqrt()
134    }
135}
136
137#[cfg(test)]
138mod tests {
139    use super::*;
140
141    #[test]
142    fn test_anomaly_detection() {
143        let mut detector = AnomalyDetector::new(AnomalyConfig::default());
144
145        // Add normal observations
146        for i in 0..20 {
147            detector.observe(10.0 + (i as f64) * 0.1);
148        }
149
150        // Add anomaly
151        detector.observe(50.0);
152
153        let anomalies = detector.detect();
154        assert!(!anomalies.is_empty());
155    }
156
157    #[test]
158    fn test_rolling_window() {
159        let config = AnomalyConfig {
160            z_threshold: 3.0,
161            window_size: 10,
162            min_samples: 5,
163        };
164        let mut detector = AnomalyDetector::new(config);
165
166        for i in 0..20 {
167            detector.observe(i as f64);
168        }
169
170        assert_eq!(detector.observations.len(), 10);
171    }
172}