ruvector_dag/healing/
anomaly.rs1use std::collections::VecDeque;
4
5#[derive(Debug, Clone)]
6pub struct AnomalyConfig {
7 pub z_threshold: f64, pub window_size: usize, pub min_samples: usize, }
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 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 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 for i in 0..20 {
147 detector.observe(10.0 + (i as f64) * 0.1);
148 }
149
150 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}