arrow_graph/streaming/
detection.rs

1use crate::error::Result;
2use crate::streaming::incremental::{IncrementalGraphProcessor, UpdateResult};
3use arrow::array::Array;
4use std::collections::{HashMap, HashSet, VecDeque};
5
6/// Graph change detection algorithms for anomaly detection and community drift
7/// These algorithms monitor graph evolution and detect significant structural changes
8#[derive(Debug, Clone)]
9pub struct GraphChangeDetector {
10    // Community tracking
11    historical_communities: VecDeque<HashMap<String, String>>, // Rolling window of community assignments
12    community_stability_threshold: f64, // Threshold for detecting community drift
13    max_history_length: usize,
14    
15    // Anomaly detection
16    baseline_metrics: GraphMetrics,
17    anomaly_thresholds: AnomalyThresholds,
18    anomaly_history: VecDeque<AnomalyEvent>,
19    
20    // Change tracking
21    significant_changes: Vec<GraphChange>,
22    change_sensitivity: f64,
23}
24
25/// Key graph metrics for anomaly detection
26#[derive(Debug, Clone)]
27pub struct GraphMetrics {
28    pub node_count: usize,
29    pub edge_count: usize,
30    pub density: f64,
31    pub average_degree: f64,
32    pub clustering_coefficient: f64,
33    pub largest_component_size: usize,
34    pub component_count: usize,
35}
36
37/// Thresholds for detecting anomalous changes
38#[derive(Debug, Clone)]
39pub struct AnomalyThresholds {
40    pub density_change: f64,        // Threshold for density change
41    pub degree_change: f64,         // Threshold for average degree change
42    pub clustering_change: f64,     // Threshold for clustering coefficient change
43    pub component_change: f64,      // Threshold for component structure change
44    pub edge_burst_rate: f64,       // Threshold for edge addition rate
45    pub node_burst_rate: f64,       // Threshold for node addition rate
46}
47
48/// Detected anomaly event
49#[derive(Debug, Clone)]
50pub struct AnomalyEvent {
51    pub timestamp: u64,
52    pub anomaly_type: AnomalyType,
53    pub severity: f64,  // 0.0 to 1.0
54    pub description: String,
55    pub affected_nodes: Vec<String>,
56}
57
58/// Types of anomalies that can be detected
59#[derive(Debug, Clone, PartialEq)]
60pub enum AnomalyType {
61    DensitySpike,       // Sudden increase in edge density
62    DensityDrop,        // Sudden decrease in edge density
63    DegreeBurst,        // Unusual degree distribution changes
64    CommunityShift,     // Significant community structure changes
65    ComponentMerge,     // Connected components merging
66    ComponentSplit,     // Connected components splitting
67    EdgeBurst,          // Rapid edge additions
68    NodeBurst,          // Rapid node additions
69    IsolatedNodes,      // Nodes becoming isolated
70    HubFormation,       // New high-degree nodes appearing
71}
72
73/// Detected significant graph change
74#[derive(Debug, Clone)]
75pub struct GraphChange {
76    pub change_type: ChangeType,
77    pub magnitude: f64,
78    pub affected_nodes: Vec<String>,
79    pub timestamp: u64,
80}
81
82/// Types of significant structural changes
83#[derive(Debug, Clone, PartialEq)]
84pub enum ChangeType {
85    CommunityMerge,
86    CommunitySplit,
87    CommunityDrift,
88    TopologyChange,
89    ScaleChange,
90}
91
92impl Default for AnomalyThresholds {
93    fn default() -> Self {
94        Self {
95            density_change: 0.1,      // 10% change in density
96            degree_change: 0.15,      // 15% change in average degree
97            clustering_change: 0.2,   // 20% change in clustering
98            component_change: 0.05,   // 5% change in component structure
99            edge_burst_rate: 10.0,    // 10+ edges per update
100            node_burst_rate: 5.0,     // 5+ nodes per update
101        }
102    }
103}
104
105impl GraphChangeDetector {
106    pub fn new(max_history: usize, change_sensitivity: f64) -> Self {
107        Self {
108            historical_communities: VecDeque::new(),
109            community_stability_threshold: 0.8, // 80% stability required
110            max_history_length: max_history,
111            baseline_metrics: GraphMetrics {
112                node_count: 0,
113                edge_count: 0,
114                density: 0.0,
115                average_degree: 0.0,
116                clustering_coefficient: 0.0,
117                largest_component_size: 0,
118                component_count: 0,
119            },
120            anomaly_thresholds: AnomalyThresholds::default(),
121            anomaly_history: VecDeque::new(),
122            significant_changes: Vec::new(),
123            change_sensitivity,
124        }
125    }
126
127    /// Default change detector
128    pub fn default() -> Self {
129        Self::new(10, 0.1) // Keep 10 snapshots, 10% change sensitivity
130    }
131
132    /// Initialize the detector with current graph state
133    pub fn initialize(&mut self, processor: &IncrementalGraphProcessor) -> Result<()> {
134        self.baseline_metrics = self.compute_graph_metrics(processor)?;
135        
136        // Initialize community detection
137        let communities = self.detect_communities(processor)?;
138        self.historical_communities.push_back(communities);
139        
140        Ok(())
141    }
142
143    /// Update detector with new graph changes
144    pub fn update(&mut self, processor: &IncrementalGraphProcessor, changes: &UpdateResult) -> Result<Vec<AnomalyEvent>> {
145        let mut detected_anomalies = Vec::new();
146        let current_time = std::time::SystemTime::now()
147            .duration_since(std::time::UNIX_EPOCH)
148            .unwrap()
149            .as_secs();
150
151        // Compute current metrics
152        let current_metrics = self.compute_graph_metrics(processor)?;
153
154        // Detect anomalies based on metric changes
155        detected_anomalies.extend(self.detect_metric_anomalies(&current_metrics, current_time)?);
156
157        // Detect burst anomalies
158        detected_anomalies.extend(self.detect_burst_anomalies(changes, current_time)?);
159
160        // Detect community changes
161        let current_communities = self.detect_communities(processor)?;
162        detected_anomalies.extend(self.detect_community_anomalies(&current_communities, current_time)?);
163
164        // Update history
165        self.update_history(current_communities, &detected_anomalies);
166        self.baseline_metrics = current_metrics;
167
168        // Store significant changes
169        if !detected_anomalies.is_empty() {
170            let change = GraphChange {
171                change_type: self.classify_change(&detected_anomalies),
172                magnitude: self.calculate_change_magnitude(&detected_anomalies),
173                affected_nodes: self.extract_affected_nodes(&detected_anomalies),
174                timestamp: current_time,
175            };
176            self.significant_changes.push(change);
177        }
178
179        Ok(detected_anomalies)
180    }
181
182    /// Compute key graph metrics
183    fn compute_graph_metrics(&self, processor: &IncrementalGraphProcessor) -> Result<GraphMetrics> {
184        let graph = processor.graph();
185        let node_count = graph.node_count();
186        let edge_count = graph.edge_count();
187        
188        let density = if node_count > 1 {
189            edge_count as f64 / (node_count * (node_count - 1)) as f64
190        } else {
191            0.0
192        };
193
194        let average_degree = if node_count > 0 {
195            (2 * edge_count) as f64 / node_count as f64
196        } else {
197            0.0
198        };
199
200        // Simplified clustering coefficient (would need more complex calculation)
201        let clustering_coefficient = self.estimate_clustering_coefficient(processor)?;
202
203        // Component analysis
204        let components = self.analyze_components(processor)?;
205        let largest_component_size = components.iter().map(|c| c.len()).max().unwrap_or(0);
206        let component_count = components.len();
207
208        Ok(GraphMetrics {
209            node_count,
210            edge_count,
211            density,
212            average_degree,
213            clustering_coefficient,
214            largest_component_size,
215            component_count,
216        })
217    }
218
219    /// Estimate clustering coefficient (simplified)
220    fn estimate_clustering_coefficient(&self, _processor: &IncrementalGraphProcessor) -> Result<f64> {
221        // Simplified implementation - in practice would calculate actual clustering
222        Ok(0.3) // Placeholder value
223    }
224
225    /// Analyze connected components
226    fn analyze_components(&self, processor: &IncrementalGraphProcessor) -> Result<Vec<Vec<String>>> {
227        let graph = processor.graph();
228        let edges_batch = &graph.edges;
229        let nodes_batch = &graph.nodes;
230
231        let mut components = Vec::new();
232        let mut visited = HashSet::new();
233        
234        // Get all nodes
235        let mut nodes = Vec::new();
236        if nodes_batch.num_rows() > 0 {
237            let node_ids = nodes_batch.column(0)
238                .as_any()
239                .downcast_ref::<arrow::array::StringArray>()
240                .ok_or_else(|| crate::error::GraphError::graph_construction("Expected string array for node IDs"))?;
241                
242            for i in 0..node_ids.len() {
243                nodes.push(node_ids.value(i).to_string());
244            }
245        }
246
247        // Build adjacency list
248        let mut adjacency: HashMap<String, Vec<String>> = HashMap::new();
249        if edges_batch.num_rows() > 0 {
250            let source_ids = edges_batch.column(0)
251                .as_any()
252                .downcast_ref::<arrow::array::StringArray>()
253                .ok_or_else(|| crate::error::GraphError::graph_construction("Expected string array for source IDs"))?;
254            let target_ids = edges_batch.column(1)
255                .as_any()
256                .downcast_ref::<arrow::array::StringArray>()
257                .ok_or_else(|| crate::error::GraphError::graph_construction("Expected string array for target IDs"))?;
258                
259            for i in 0..source_ids.len() {
260                let source = source_ids.value(i).to_string();
261                let target = target_ids.value(i).to_string();
262                
263                adjacency.entry(source.clone()).or_insert_with(Vec::new).push(target.clone());
264                adjacency.entry(target).or_insert_with(Vec::new).push(source);
265            }
266        }
267
268        // Find components using DFS
269        for node in nodes {
270            if !visited.contains(&node) {
271                let mut component = Vec::new();
272                let mut stack = vec![node.clone()];
273                
274                while let Some(current) = stack.pop() {
275                    if visited.insert(current.clone()) {
276                        component.push(current.clone());
277                        
278                        if let Some(neighbors) = adjacency.get(&current) {
279                            for neighbor in neighbors {
280                                if !visited.contains(neighbor) {
281                                    stack.push(neighbor.clone());
282                                }
283                            }
284                        }
285                    }
286                }
287                
288                if !component.is_empty() {
289                    components.push(component);
290                }
291            }
292        }
293
294        Ok(components)
295    }
296
297    /// Detect communities using simplified algorithm
298    fn detect_communities(&self, processor: &IncrementalGraphProcessor) -> Result<HashMap<String, String>> {
299        // Simplified community detection - in practice would use more sophisticated algorithms
300        let components = self.analyze_components(processor)?;
301        let mut communities = HashMap::new();
302        
303        for (i, component) in components.iter().enumerate() {
304            let community_id = format!("community_{}", i);
305            for node in component {
306                communities.insert(node.clone(), community_id.clone());
307            }
308        }
309        
310        Ok(communities)
311    }
312
313    /// Detect anomalies based on metric changes
314    fn detect_metric_anomalies(&self, current: &GraphMetrics, timestamp: u64) -> Result<Vec<AnomalyEvent>> {
315        let mut anomalies = Vec::new();
316        let baseline = &self.baseline_metrics;
317
318        // Density anomalies
319        if baseline.density > 0.0 {
320            let density_change = (current.density - baseline.density).abs() / baseline.density;
321            if density_change > self.anomaly_thresholds.density_change {
322                let anomaly_type = if current.density > baseline.density {
323                    AnomalyType::DensitySpike
324                } else {
325                    AnomalyType::DensityDrop
326                };
327                
328                anomalies.push(AnomalyEvent {
329                    timestamp,
330                    anomaly_type,
331                    severity: (density_change / self.anomaly_thresholds.density_change).min(1.0),
332                    description: format!("Density changed by {:.2}%", density_change * 100.0),
333                    affected_nodes: Vec::new(),
334                });
335            }
336        }
337
338        // Degree anomalies
339        if baseline.average_degree > 0.0 {
340            let degree_change = (current.average_degree - baseline.average_degree).abs() / baseline.average_degree;
341            if degree_change > self.anomaly_thresholds.degree_change {
342                anomalies.push(AnomalyEvent {
343                    timestamp,
344                    anomaly_type: AnomalyType::DegreeBurst,
345                    severity: (degree_change / self.anomaly_thresholds.degree_change).min(1.0),
346                    description: format!("Average degree changed by {:.2}%", degree_change * 100.0),
347                    affected_nodes: Vec::new(),
348                });
349            }
350        }
351
352        // Component anomalies
353        if baseline.component_count > 0 {
354            let component_change = (current.component_count as f64 - baseline.component_count as f64).abs() / baseline.component_count as f64;
355            if component_change > self.anomaly_thresholds.component_change {
356                let anomaly_type = if current.component_count < baseline.component_count {
357                    AnomalyType::ComponentMerge
358                } else {
359                    AnomalyType::ComponentSplit
360                };
361                
362                anomalies.push(AnomalyEvent {
363                    timestamp,
364                    anomaly_type,
365                    severity: (component_change / self.anomaly_thresholds.component_change).min(1.0),
366                    description: format!("Component count changed from {} to {}", baseline.component_count, current.component_count),
367                    affected_nodes: Vec::new(),
368                });
369            }
370        }
371
372        Ok(anomalies)
373    }
374
375    /// Detect burst anomalies
376    fn detect_burst_anomalies(&self, changes: &UpdateResult, timestamp: u64) -> Result<Vec<AnomalyEvent>> {
377        let mut anomalies = Vec::new();
378
379        // Edge burst detection
380        if changes.edges_added as f64 > self.anomaly_thresholds.edge_burst_rate {
381            anomalies.push(AnomalyEvent {
382                timestamp,
383                anomaly_type: AnomalyType::EdgeBurst,
384                severity: (changes.edges_added as f64 / self.anomaly_thresholds.edge_burst_rate).min(1.0),
385                description: format!("Burst of {} edges added", changes.edges_added),
386                affected_nodes: Vec::new(),
387            });
388        }
389
390        // Node burst detection
391        if changes.vertices_added as f64 > self.anomaly_thresholds.node_burst_rate {
392            anomalies.push(AnomalyEvent {
393                timestamp,
394                anomaly_type: AnomalyType::NodeBurst,
395                severity: (changes.vertices_added as f64 / self.anomaly_thresholds.node_burst_rate).min(1.0),
396                description: format!("Burst of {} nodes added", changes.vertices_added),
397                affected_nodes: Vec::new(),
398            });
399        }
400
401        Ok(anomalies)
402    }
403
404    /// Detect community-based anomalies
405    fn detect_community_anomalies(&self, current_communities: &HashMap<String, String>, timestamp: u64) -> Result<Vec<AnomalyEvent>> {
406        let mut anomalies = Vec::new();
407
408        if let Some(previous_communities) = self.historical_communities.back() {
409            let stability = self.calculate_community_stability(previous_communities, current_communities);
410            
411            if stability < self.community_stability_threshold {
412                anomalies.push(AnomalyEvent {
413                    timestamp,
414                    anomaly_type: AnomalyType::CommunityShift,
415                    severity: 1.0 - stability,
416                    description: format!("Community stability: {:.2}%", stability * 100.0),
417                    affected_nodes: self.find_community_changes(previous_communities, current_communities),
418                });
419            }
420        }
421
422        Ok(anomalies)
423    }
424
425    /// Calculate community stability between two snapshots
426    fn calculate_community_stability(&self, previous: &HashMap<String, String>, current: &HashMap<String, String>) -> f64 {
427        let mut stable_assignments = 0;
428        let mut total_nodes = 0;
429
430        for (node, prev_community) in previous {
431            if let Some(curr_community) = current.get(node) {
432                if prev_community == curr_community {
433                    stable_assignments += 1;
434                }
435                total_nodes += 1;
436            }
437        }
438
439        if total_nodes > 0 {
440            stable_assignments as f64 / total_nodes as f64
441        } else {
442            1.0
443        }
444    }
445
446    /// Find nodes that changed communities
447    fn find_community_changes(&self, previous: &HashMap<String, String>, current: &HashMap<String, String>) -> Vec<String> {
448        let mut changed_nodes = Vec::new();
449
450        for (node, prev_community) in previous {
451            if let Some(curr_community) = current.get(node) {
452                if prev_community != curr_community {
453                    changed_nodes.push(node.clone());
454                }
455            }
456        }
457
458        changed_nodes
459    }
460
461    /// Update history with new data
462    fn update_history(&mut self, communities: HashMap<String, String>, anomalies: &[AnomalyEvent]) {
463        // Update community history
464        self.historical_communities.push_back(communities);
465        if self.historical_communities.len() > self.max_history_length {
466            self.historical_communities.pop_front();
467        }
468
469        // Update anomaly history
470        for anomaly in anomalies {
471            self.anomaly_history.push_back(anomaly.clone());
472        }
473        if self.anomaly_history.len() > self.max_history_length * 5 {
474            self.anomaly_history.pop_front();
475        }
476    }
477
478    /// Classify the type of change based on detected anomalies
479    fn classify_change(&self, anomalies: &[AnomalyEvent]) -> ChangeType {
480        if anomalies.iter().any(|a| a.anomaly_type == AnomalyType::CommunityShift) {
481            ChangeType::CommunityDrift
482        } else if anomalies.iter().any(|a| matches!(a.anomaly_type, AnomalyType::ComponentMerge | AnomalyType::ComponentSplit)) {
483            ChangeType::TopologyChange
484        } else if anomalies.iter().any(|a| matches!(a.anomaly_type, AnomalyType::EdgeBurst | AnomalyType::NodeBurst)) {
485            ChangeType::ScaleChange
486        } else {
487            ChangeType::TopologyChange
488        }
489    }
490
491    /// Calculate the magnitude of change
492    fn calculate_change_magnitude(&self, anomalies: &[AnomalyEvent]) -> f64 {
493        if anomalies.is_empty() {
494            0.0
495        } else {
496            anomalies.iter().map(|a| a.severity).sum::<f64>() / anomalies.len() as f64
497        }
498    }
499
500    /// Extract all affected nodes from anomalies
501    fn extract_affected_nodes(&self, anomalies: &[AnomalyEvent]) -> Vec<String> {
502        let mut nodes = HashSet::new();
503        for anomaly in anomalies {
504            for node in &anomaly.affected_nodes {
505                nodes.insert(node.clone());
506            }
507        }
508        nodes.into_iter().collect()
509    }
510
511    /// Get recent anomalies
512    pub fn recent_anomalies(&self, count: usize) -> Vec<&AnomalyEvent> {
513        self.anomaly_history.iter().rev().take(count).collect()
514    }
515
516    /// Get significant changes
517    pub fn significant_changes(&self) -> &[GraphChange] {
518        &self.significant_changes
519    }
520
521    /// Check if the graph is in an anomalous state
522    pub fn is_anomalous_state(&self) -> bool {
523        // Check if recent anomalies exceed threshold
524        let recent_severity: f64 = self.anomaly_history.iter()
525            .rev()
526            .take(3) // Last 3 anomalies
527            .map(|a| a.severity)
528            .sum();
529        
530        recent_severity > 2.0 // Threshold for anomalous state
531    }
532
533    /// Get current community stability
534    pub fn current_community_stability(&self) -> Option<f64> {
535        if self.historical_communities.len() >= 2 {
536            let current = self.historical_communities.back()?;
537            let previous = self.historical_communities.get(self.historical_communities.len() - 2)?;
538            Some(self.calculate_community_stability(previous, current))
539        } else {
540            None
541        }
542    }
543}
544
545#[cfg(test)]
546mod tests {
547    use super::*;
548    use crate::graph::ArrowGraph;
549    use crate::streaming::incremental::IncrementalGraphProcessor;
550    use arrow::array::{StringArray, Float64Array};
551    use arrow::record_batch::RecordBatch;
552    use arrow::datatypes::{Schema, Field, DataType};
553    use std::sync::Arc;
554
555    fn create_test_graph() -> Result<ArrowGraph> {
556        let nodes_schema = Arc::new(Schema::new(vec![
557            Field::new("id", DataType::Utf8, false),
558        ]));
559        let node_ids = StringArray::from(vec!["A", "B", "C", "D"]);
560        let nodes_batch = RecordBatch::try_new(
561            nodes_schema,
562            vec![Arc::new(node_ids)],
563        )?;
564
565        let edges_schema = Arc::new(Schema::new(vec![
566            Field::new("source", DataType::Utf8, false),
567            Field::new("target", DataType::Utf8, false),
568            Field::new("weight", DataType::Float64, false),
569        ]));
570        let sources = StringArray::from(vec!["A", "B"]);
571        let targets = StringArray::from(vec!["B", "C"]);
572        let weights = Float64Array::from(vec![1.0, 1.0]);
573        let edges_batch = RecordBatch::try_new(
574            edges_schema,
575            vec![Arc::new(sources), Arc::new(targets), Arc::new(weights)],
576        )?;
577
578        ArrowGraph::new(nodes_batch, edges_batch)
579    }
580
581    #[test]
582    fn test_change_detector_initialization() {
583        let graph = create_test_graph().unwrap();
584        let processor = IncrementalGraphProcessor::new(graph).unwrap();
585        
586        let mut detector = GraphChangeDetector::default();
587        detector.initialize(&processor).unwrap();
588        
589        assert_eq!(detector.baseline_metrics.node_count, 4);
590        assert_eq!(detector.baseline_metrics.edge_count, 2);
591        assert!(detector.baseline_metrics.density > 0.0);
592        assert_eq!(detector.historical_communities.len(), 1);
593    }
594
595    #[test]
596    fn test_metric_computation() {
597        let graph = create_test_graph().unwrap();
598        let processor = IncrementalGraphProcessor::new(graph).unwrap();
599        
600        let detector = GraphChangeDetector::default();
601        let metrics = detector.compute_graph_metrics(&processor).unwrap();
602        
603        assert_eq!(metrics.node_count, 4);
604        assert_eq!(metrics.edge_count, 2);
605        assert!(metrics.density > 0.0);
606        assert!(metrics.average_degree > 0.0);
607    }
608
609    #[test]
610    fn test_component_analysis() {
611        let graph = create_test_graph().unwrap();
612        let processor = IncrementalGraphProcessor::new(graph).unwrap();
613        
614        let detector = GraphChangeDetector::default();
615        let components = detector.analyze_components(&processor).unwrap();
616        
617        assert_eq!(components.len(), 2); // {A,B,C} and {D}
618        
619        // Find the larger component
620        let larger_component = components.iter().max_by_key(|c| c.len()).unwrap();
621        assert_eq!(larger_component.len(), 3);
622    }
623
624    #[test]
625    fn test_burst_anomaly_detection() {
626        let graph = create_test_graph().unwrap();
627        let processor = IncrementalGraphProcessor::new(graph).unwrap();
628        
629        let detector = GraphChangeDetector::default();
630        
631        // Simulate edge burst
632        let edge_burst_changes = UpdateResult {
633            vertices_added: 0,
634            vertices_removed: 0,
635            edges_added: 15, // Above threshold of 10
636            edges_removed: 0,
637            affected_components: vec![],
638            recomputation_needed: false,
639        };
640        
641        let anomalies = detector.detect_burst_anomalies(&edge_burst_changes, 12345).unwrap();
642        assert_eq!(anomalies.len(), 1);
643        assert_eq!(anomalies[0].anomaly_type, AnomalyType::EdgeBurst);
644        assert!(anomalies[0].severity > 0.0);
645    }
646
647    #[test]
648    fn test_community_stability() {
649        let detector = GraphChangeDetector::default();
650        
651        let mut community1 = HashMap::new();
652        community1.insert("A".to_string(), "comm1".to_string());
653        community1.insert("B".to_string(), "comm1".to_string());
654        community1.insert("C".to_string(), "comm2".to_string());
655        
656        let mut community2 = HashMap::new();
657        community2.insert("A".to_string(), "comm1".to_string());
658        community2.insert("B".to_string(), "comm2".to_string()); // Changed
659        community2.insert("C".to_string(), "comm2".to_string());
660        
661        let stability = detector.calculate_community_stability(&community1, &community2);
662        assert!((stability - 0.666667).abs() < 0.001); // 2/3 stability
663    }
664
665    #[test]
666    fn test_full_anomaly_detection() {
667        let graph = create_test_graph().unwrap();
668        let mut processor = IncrementalGraphProcessor::new(graph).unwrap();
669        processor.set_batch_size(1);
670        
671        let mut detector = GraphChangeDetector::default();
672        detector.initialize(&processor).unwrap();
673        
674        // Add many edges to trigger anomaly
675        for i in 0..12 {
676            processor.add_edge(format!("node_{}", i), format!("node_{}", i + 1), 1.0).unwrap();
677        }
678        
679        let changes = UpdateResult {
680            vertices_added: 24, // Added nodes
681            vertices_removed: 0,
682            edges_added: 12,
683            edges_removed: 0,
684            affected_components: vec![],
685            recomputation_needed: false,
686        };
687        
688        let anomalies = detector.update(&processor, &changes).unwrap();
689        assert!(!anomalies.is_empty());
690        
691        // Should detect burst anomalies
692        assert!(anomalies.iter().any(|a| matches!(a.anomaly_type, AnomalyType::EdgeBurst | AnomalyType::NodeBurst)));
693    }
694
695    #[test]
696    fn test_anomalous_state_detection() {
697        let mut detector = GraphChangeDetector::default();
698        
699        // Add high-severity anomalies
700        for i in 0..3 {
701            detector.anomaly_history.push_back(AnomalyEvent {
702                timestamp: i,
703                anomaly_type: AnomalyType::DensitySpike,
704                severity: 0.8,
705                description: "Test anomaly".to_string(),
706                affected_nodes: vec![],
707            });
708        }
709        
710        assert!(detector.is_anomalous_state());
711        
712        // Clear history
713        detector.anomaly_history.clear();
714        assert!(!detector.is_anomalous_state());
715    }
716
717    #[test]
718    fn test_change_classification() {
719        let detector = GraphChangeDetector::default();
720        
721        let community_anomaly = AnomalyEvent {
722            timestamp: 0,
723            anomaly_type: AnomalyType::CommunityShift,
724            severity: 0.5,
725            description: "Test".to_string(),
726            affected_nodes: vec![],
727        };
728        
729        let change_type = detector.classify_change(&[community_anomaly]);
730        assert_eq!(change_type, ChangeType::CommunityDrift);
731        
732        let burst_anomaly = AnomalyEvent {
733            timestamp: 0,
734            anomaly_type: AnomalyType::EdgeBurst,
735            severity: 0.7,
736            description: "Test".to_string(),
737            affected_nodes: vec![],
738        };
739        
740        let change_type = detector.classify_change(&[burst_anomaly]);
741        assert_eq!(change_type, ChangeType::ScaleChange);
742    }
743}