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 change_sensitivity: f64,
23}
24
25#[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#[derive(Debug, Clone)]
39pub struct AnomalyThresholds {
40 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, }
47
48#[derive(Debug, Clone)]
50pub struct AnomalyEvent {
51 pub timestamp: u64,
52 pub anomaly_type: AnomalyType,
53 pub severity: f64, pub description: String,
55 pub affected_nodes: Vec<String>,
56}
57
58#[derive(Debug, Clone, PartialEq)]
60pub enum AnomalyType {
61 DensitySpike, DensityDrop, DegreeBurst, CommunityShift, ComponentMerge, ComponentSplit, EdgeBurst, NodeBurst, IsolatedNodes, HubFormation, }
72
73#[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#[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, degree_change: 0.15, clustering_change: 0.2, component_change: 0.05, edge_burst_rate: 10.0, node_burst_rate: 5.0, }
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, 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 pub fn default() -> Self {
129 Self::new(10, 0.1) }
131
132 pub fn initialize(&mut self, processor: &IncrementalGraphProcessor) -> Result<()> {
134 self.baseline_metrics = self.compute_graph_metrics(processor)?;
135
136 let communities = self.detect_communities(processor)?;
138 self.historical_communities.push_back(communities);
139
140 Ok(())
141 }
142
143 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 let current_metrics = self.compute_graph_metrics(processor)?;
153
154 detected_anomalies.extend(self.detect_metric_anomalies(¤t_metrics, current_time)?);
156
157 detected_anomalies.extend(self.detect_burst_anomalies(changes, current_time)?);
159
160 let current_communities = self.detect_communities(processor)?;
162 detected_anomalies.extend(self.detect_community_anomalies(¤t_communities, current_time)?);
163
164 self.update_history(current_communities, &detected_anomalies);
166 self.baseline_metrics = current_metrics;
167
168 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 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 let clustering_coefficient = self.estimate_clustering_coefficient(processor)?;
202
203 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 fn estimate_clustering_coefficient(&self, _processor: &IncrementalGraphProcessor) -> Result<f64> {
221 Ok(0.3) }
224
225 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 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 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 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(¤t) {
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 fn detect_communities(&self, processor: &IncrementalGraphProcessor) -> Result<HashMap<String, String>> {
299 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 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 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 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 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 fn detect_burst_anomalies(&self, changes: &UpdateResult, timestamp: u64) -> Result<Vec<AnomalyEvent>> {
377 let mut anomalies = Vec::new();
378
379 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 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 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 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 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 fn update_history(&mut self, communities: HashMap<String, String>, anomalies: &[AnomalyEvent]) {
463 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 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 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 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 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 pub fn recent_anomalies(&self, count: usize) -> Vec<&AnomalyEvent> {
513 self.anomaly_history.iter().rev().take(count).collect()
514 }
515
516 pub fn significant_changes(&self) -> &[GraphChange] {
518 &self.significant_changes
519 }
520
521 pub fn is_anomalous_state(&self) -> bool {
523 let recent_severity: f64 = self.anomaly_history.iter()
525 .rev()
526 .take(3) .map(|a| a.severity)
528 .sum();
529
530 recent_severity > 2.0 }
532
533 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); 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 let edge_burst_changes = UpdateResult {
633 vertices_added: 0,
634 vertices_removed: 0,
635 edges_added: 15, 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()); 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); }
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 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, 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 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 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 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}