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