aimds_analysis/
behavioral.rs1use midstreamer_attractor::{AttractorAnalyzer, AttractorInfo};
9use crate::errors::{AnalysisError, AnalysisResult};
10use std::sync::Arc;
11use std::sync::RwLock;
12
13#[derive(Debug, Clone)]
15pub struct BehaviorProfile {
16 pub baseline_attractors: Vec<AttractorInfo>,
18 pub dimensions: usize,
20 pub threshold: f64,
22}
23
24impl Default for BehaviorProfile {
25 fn default() -> Self {
26 Self {
27 baseline_attractors: Vec::new(),
28 dimensions: 10,
29 threshold: 0.75,
30 }
31 }
32}
33
34#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
36pub struct AnomalyScore {
37 pub score: f64,
39 pub is_anomalous: bool,
41 pub confidence: f64,
43}
44
45impl AnomalyScore {
46 pub fn normal() -> Self {
48 Self {
49 score: 0.0,
50 is_anomalous: false,
51 confidence: 1.0,
52 }
53 }
54
55 pub fn anomalous(score: f64, confidence: f64) -> Self {
57 Self {
58 score,
59 is_anomalous: true,
60 confidence,
61 }
62 }
63}
64
65pub struct BehavioralAnalyzer {
67 #[allow(dead_code)]
68 analyzer: Arc<AttractorAnalyzer>,
69 profile: Arc<RwLock<BehaviorProfile>>,
70}
71
72impl BehavioralAnalyzer {
73 pub fn new(dimensions: usize) -> AnalysisResult<Self> {
75 let analyzer = AttractorAnalyzer::new(dimensions, 1000);
76
77 let profile = BehaviorProfile {
78 dimensions,
79 threshold: 0.75,
80 ..Default::default()
81 };
82
83 Ok(Self {
84 analyzer: Arc::new(analyzer),
85 profile: Arc::new(RwLock::new(profile)),
86 })
87 }
88
89 pub async fn analyze_behavior(&self, sequence: &[f64]) -> AnalysisResult<AnomalyScore> {
98 if sequence.is_empty() {
99 return Err(AnalysisError::InvalidInput("Empty sequence".to_string()));
100 }
101
102 let (dimensions, baseline_attractors, baseline_len, threshold) = {
104 let profile = self.profile.read().unwrap();
105 (profile.dimensions, profile.baseline_attractors.clone(), profile.baseline_attractors.len(), profile.threshold)
106 };
107
108 let expected_len = dimensions;
110 if !sequence.len().is_multiple_of(expected_len) {
111 return Err(AnalysisError::InvalidInput(
112 format!("Sequence length {} not divisible by dimensions {}",
113 sequence.len(), expected_len)
114 ));
115 }
116
117 let attractor_result = tokio::task::spawn_blocking({
119 let seq = sequence.to_vec();
120 move || {
121 let mut temp_analyzer = AttractorAnalyzer::new(dimensions, 1000);
123
124 for (i, chunk) in seq.chunks(dimensions).enumerate() {
126 let point = midstreamer_attractor::PhasePoint::new(
127 chunk.to_vec(),
128 i as u64,
129 );
130 temp_analyzer.add_point(point)?;
131 }
132
133 temp_analyzer.analyze()
135 }
136 })
137 .await
138 .map_err(|e| AnalysisError::Internal(e.to_string()))?
139 .map_err(|e| AnalysisError::TemporalAttractor(e.to_string()))?;
140
141 if baseline_attractors.is_empty() {
143 return Ok(AnomalyScore::normal());
144 }
145
146 let current_lyapunov = attractor_result.lyapunov_exponents.first().copied().unwrap_or(0.0);
148 let baseline_lyapunov: f64 = baseline_attractors.iter()
149 .filter_map(|a| a.lyapunov_exponents.first().copied())
150 .sum::<f64>() / baseline_len as f64;
151
152 let deviation = (current_lyapunov - baseline_lyapunov).abs();
154 let normalized_deviation = if baseline_lyapunov.abs() > 1e-10 {
155 (deviation / baseline_lyapunov.abs()).min(1.0)
156 } else {
157 0.0
158 };
159
160 let is_anomalous = normalized_deviation > threshold;
162 let confidence: f64 = if is_anomalous {
163 ((normalized_deviation - threshold) / (1.0 - threshold)).clamp(0.0, 1.0)
164 } else {
165 (1.0 - (normalized_deviation / threshold)).clamp(0.0, 1.0)
166 };
167
168 Ok(AnomalyScore {
169 score: normalized_deviation,
170 is_anomalous,
171 confidence,
172 })
173 }
174
175 pub async fn train_baseline(&self, sequences: Vec<Vec<f64>>) -> AnalysisResult<()> {
177 if sequences.is_empty() {
178 return Err(AnalysisError::InvalidInput("No training sequences".to_string()));
179 }
180
181 let mut attractors = Vec::new();
182 let dimensions = self.profile.read().unwrap().dimensions;
183
184 for sequence in sequences {
185 let result = tokio::task::spawn_blocking({
186 let seq = sequence.clone();
187 let dims = dimensions;
188 move || {
189 let mut temp_analyzer = AttractorAnalyzer::new(dims, 1000);
190
191 for (i, chunk) in seq.chunks(dims).enumerate() {
193 let point = midstreamer_attractor::PhasePoint::new(
194 chunk.to_vec(),
195 i as u64,
196 );
197 temp_analyzer.add_point(point)?;
198 }
199
200 temp_analyzer.analyze()
202 }
203 })
204 .await
205 .map_err(|e| AnalysisError::Internal(e.to_string()))?
206 .map_err(|e| AnalysisError::TemporalAttractor(e.to_string()))?;
207
208 attractors.push(result);
209 }
210
211 let mut profile = self.profile.write().unwrap();
212 profile.baseline_attractors = attractors;
213
214 tracing::info!("Trained baseline with {} attractors", profile.baseline_attractors.len());
215
216 Ok(())
217 }
218
219 pub fn is_anomalous(&self, score: &AnomalyScore) -> bool {
221 score.is_anomalous
222 }
223
224 pub fn set_threshold(&self, threshold: f64) {
226 let mut profile = self.profile.write().unwrap();
227 profile.threshold = threshold.clamp(0.0, 1.0);
228 }
229
230 pub fn threshold(&self) -> f64 {
232 self.profile.read().unwrap().threshold
233 }
234
235 pub fn baseline_count(&self) -> usize {
237 self.profile.read().unwrap().baseline_attractors.len()
238 }
239}
240
241#[cfg(test)]
242mod tests {
243 use super::*;
244
245 #[tokio::test]
246 async fn test_analyzer_creation() {
247 let analyzer = BehavioralAnalyzer::new(10).unwrap();
248 assert_eq!(analyzer.threshold(), 0.75);
249 assert_eq!(analyzer.baseline_count(), 0);
250 }
251
252 #[tokio::test]
253 async fn test_empty_sequence() {
254 let analyzer = BehavioralAnalyzer::new(10).unwrap();
255 let result = analyzer.analyze_behavior(&[]).await;
256 assert!(result.is_err());
257 }
258
259 #[tokio::test]
260 async fn test_invalid_dimensions() {
261 let analyzer = BehavioralAnalyzer::new(10).unwrap();
262 let sequence = vec![1.0; 15]; let result = analyzer.analyze_behavior(&sequence).await;
264 assert!(result.is_err());
265 }
266
267 #[tokio::test]
268 async fn test_normal_behavior_without_baseline() {
269 let analyzer = BehavioralAnalyzer::new(10).unwrap();
270 let sequence = vec![0.5; 1000]; let score = analyzer.analyze_behavior(&sequence).await.unwrap();
272 assert!(!score.is_anomalous);
273 }
274
275 #[tokio::test]
276 async fn test_threshold_update() {
277 let analyzer = BehavioralAnalyzer::new(10).unwrap();
278 analyzer.set_threshold(0.9);
279 assert!((analyzer.threshold() - 0.9).abs() < 1e-6);
280 }
281
282 #[tokio::test]
283 async fn test_anomaly_score_helpers() {
284 let normal = AnomalyScore::normal();
285 assert!(!normal.is_anomalous);
286 assert_eq!(normal.score, 0.0);
287
288 let anomalous = AnomalyScore::anomalous(0.9, 0.95);
289 assert!(anomalous.is_anomalous);
290 assert_eq!(anomalous.score, 0.9);
291 }
292}