1use crate::error::Result;
2use crate::streaming::incremental::{IncrementalGraphProcessor, UpdateResult};
3use arrow::array::Array;
4use std::collections::{HashMap, VecDeque};
5
6#[derive(Debug, Clone)]
9pub struct SlidingWindowProcessor {
10 window_size: std::time::Duration,
11 max_snapshots: usize,
12 snapshots: VecDeque<GraphSnapshot>,
13 #[allow(dead_code)]
14 current_metrics: HashMap<String, f64>,
15}
16
17#[derive(Debug, Clone)]
19pub struct GraphSnapshot {
20 pub timestamp: u64,
21 pub node_count: usize,
22 pub edge_count: usize,
23 pub density: f64,
24 pub avg_degree: f64,
25 pub largest_component_size: usize,
26 pub component_count: usize,
27 pub clustering_coefficient: f64,
28 pub custom_metrics: HashMap<String, f64>,
29}
30
31#[derive(Debug, Clone)]
33pub struct TemporalConfig {
34 pub window_duration: std::time::Duration,
35 pub snapshot_interval: std::time::Duration,
36 pub max_snapshots: usize,
37 pub track_custom_metrics: Vec<String>,
38}
39
40#[derive(Debug, Clone)]
42pub struct TemporalAnalytics {
43 pub trend_analysis: HashMap<String, TrendInfo>,
44 pub volatility_metrics: HashMap<String, f64>,
45 pub change_points: Vec<ChangePoint>,
46 pub evolution_patterns: Vec<EvolutionPattern>,
47}
48
49#[derive(Debug, Clone)]
51pub struct TrendInfo {
52 pub metric_name: String,
53 pub direction: TrendDirection,
54 pub slope: f64,
55 pub correlation: f64,
56 pub volatility: f64,
57 pub recent_change: f64,
58}
59
60#[derive(Debug, Clone, PartialEq)]
62pub enum TrendDirection {
63 Increasing,
64 Decreasing,
65 Stable,
66 Volatile,
67}
68
69#[derive(Debug, Clone)]
71pub struct ChangePoint {
72 pub timestamp: u64,
73 pub metric_name: String,
74 pub before_value: f64,
75 pub after_value: f64,
76 pub change_magnitude: f64,
77 pub confidence: f64,
78}
79
80#[derive(Debug, Clone)]
82pub struct EvolutionPattern {
83 pub pattern_type: PatternType,
84 pub start_time: u64,
85 pub end_time: u64,
86 pub affected_metrics: Vec<String>,
87 pub strength: f64,
88}
89
90#[derive(Debug, Clone, PartialEq)]
92pub enum PatternType {
93 Growth, Decay, Burst, Oscillation, PhaseTransition, Stabilization, }
100
101impl Default for TemporalConfig {
102 fn default() -> Self {
103 Self {
104 window_duration: std::time::Duration::from_secs(3600), snapshot_interval: std::time::Duration::from_secs(60), max_snapshots: 1000,
107 track_custom_metrics: vec![
108 "pagerank_entropy".to_string(),
109 "edge_density_variance".to_string(),
110 "community_modularity".to_string(),
111 ],
112 }
113 }
114}
115
116impl SlidingWindowProcessor {
117 pub fn new(config: TemporalConfig) -> Self {
118 Self {
119 window_size: config.window_duration,
120 max_snapshots: config.max_snapshots,
121 snapshots: VecDeque::new(),
122 current_metrics: HashMap::new(),
123 }
124 }
125
126 pub fn default() -> Self {
128 Self::new(TemporalConfig::default())
129 }
130
131 pub fn initialize(&mut self, processor: &IncrementalGraphProcessor) -> Result<()> {
133 let snapshot = self.create_snapshot(processor)?;
134 self.snapshots.push_back(snapshot);
135 Ok(())
136 }
137
138 pub fn update(&mut self, processor: &IncrementalGraphProcessor, _changes: &UpdateResult) -> Result<()> {
140 let current_time = std::time::SystemTime::now()
141 .duration_since(std::time::UNIX_EPOCH)
142 .unwrap()
143 .as_secs();
144
145 let snapshot = self.create_snapshot(processor)?;
147 self.snapshots.push_back(snapshot);
148
149 self.cleanup_old_snapshots(current_time);
151
152 while self.snapshots.len() > self.max_snapshots {
154 self.snapshots.pop_front();
155 }
156
157 Ok(())
158 }
159
160 fn create_snapshot(&self, processor: &IncrementalGraphProcessor) -> Result<GraphSnapshot> {
162 let graph = processor.graph();
163 let node_count = graph.node_count();
164 let edge_count = graph.edge_count();
165
166 let density = if node_count > 1 {
167 edge_count as f64 / (node_count * (node_count - 1)) as f64
168 } else {
169 0.0
170 };
171
172 let avg_degree = if node_count > 0 {
173 (2 * edge_count) as f64 / node_count as f64
174 } else {
175 0.0
176 };
177
178 let components = self.analyze_components(processor)?;
180 let largest_component_size = components.iter().map(|c| c.len()).max().unwrap_or(0);
181 let component_count = components.len();
182
183 let clustering_coefficient = self.estimate_clustering_coefficient(processor)?;
185
186 let timestamp = std::time::SystemTime::now()
187 .duration_since(std::time::UNIX_EPOCH)
188 .unwrap()
189 .as_secs();
190
191 Ok(GraphSnapshot {
192 timestamp,
193 node_count,
194 edge_count,
195 density,
196 avg_degree,
197 largest_component_size,
198 component_count,
199 clustering_coefficient,
200 custom_metrics: HashMap::new(), })
202 }
203
204 fn analyze_components(&self, processor: &IncrementalGraphProcessor) -> Result<Vec<Vec<String>>> {
206 let graph = processor.graph();
207 let edges_batch = &graph.edges;
208 let nodes_batch = &graph.nodes;
209
210 let mut components = Vec::new();
211 let mut visited = std::collections::HashSet::new();
212
213 let mut nodes = Vec::new();
215 if nodes_batch.num_rows() > 0 {
216 let node_ids = nodes_batch.column(0)
217 .as_any()
218 .downcast_ref::<arrow::array::StringArray>()
219 .ok_or_else(|| crate::error::GraphError::graph_construction("Expected string array for node IDs"))?;
220
221 for i in 0..node_ids.len() {
222 nodes.push(node_ids.value(i).to_string());
223 }
224 }
225
226 let mut adjacency: HashMap<String, Vec<String>> = HashMap::new();
228 if edges_batch.num_rows() > 0 {
229 let source_ids = edges_batch.column(0)
230 .as_any()
231 .downcast_ref::<arrow::array::StringArray>()
232 .ok_or_else(|| crate::error::GraphError::graph_construction("Expected string array for source IDs"))?;
233 let target_ids = edges_batch.column(1)
234 .as_any()
235 .downcast_ref::<arrow::array::StringArray>()
236 .ok_or_else(|| crate::error::GraphError::graph_construction("Expected string array for target IDs"))?;
237
238 for i in 0..source_ids.len() {
239 let source = source_ids.value(i).to_string();
240 let target = target_ids.value(i).to_string();
241
242 adjacency.entry(source.clone()).or_insert_with(Vec::new).push(target.clone());
243 adjacency.entry(target).or_insert_with(Vec::new).push(source);
244 }
245 }
246
247 for node in nodes {
249 if !visited.contains(&node) {
250 let mut component = Vec::new();
251 let mut stack = vec![node.clone()];
252
253 while let Some(current) = stack.pop() {
254 if visited.insert(current.clone()) {
255 component.push(current.clone());
256
257 if let Some(neighbors) = adjacency.get(¤t) {
258 for neighbor in neighbors {
259 if !visited.contains(neighbor) {
260 stack.push(neighbor.clone());
261 }
262 }
263 }
264 }
265 }
266
267 if !component.is_empty() {
268 components.push(component);
269 }
270 }
271 }
272
273 Ok(components)
274 }
275
276 fn estimate_clustering_coefficient(&self, _processor: &IncrementalGraphProcessor) -> Result<f64> {
278 Ok(0.3) }
281
282 fn cleanup_old_snapshots(&mut self, current_time: u64) {
284 let window_start = current_time.saturating_sub(self.window_size.as_secs());
285
286 while let Some(front) = self.snapshots.front() {
287 if front.timestamp < window_start {
288 self.snapshots.pop_front();
289 } else {
290 break;
291 }
292 }
293 }
294
295 pub fn analyze_temporal_patterns(&self) -> Result<TemporalAnalytics> {
297 if self.snapshots.len() < 2 {
298 return Ok(TemporalAnalytics {
299 trend_analysis: HashMap::new(),
300 volatility_metrics: HashMap::new(),
301 change_points: Vec::new(),
302 evolution_patterns: Vec::new(),
303 });
304 }
305
306 let trend_analysis = self.analyze_trends()?;
307 let volatility_metrics = self.calculate_volatility()?;
308 let change_points = self.detect_change_points()?;
309 let evolution_patterns = self.detect_evolution_patterns()?;
310
311 Ok(TemporalAnalytics {
312 trend_analysis,
313 volatility_metrics,
314 change_points,
315 evolution_patterns,
316 })
317 }
318
319 fn analyze_trends(&self) -> Result<HashMap<String, TrendInfo>> {
321 let mut trends = HashMap::new();
322
323 let metrics = ["node_count", "edge_count", "density", "avg_degree", "component_count"];
324
325 for metric in &metrics {
326 let values: Vec<f64> = self.snapshots.iter()
327 .map(|s| self.get_metric_value(s, metric))
328 .collect();
329
330 if values.len() >= 2 {
331 let trend_info = self.calculate_trend_info(metric, &values);
332 trends.insert(metric.to_string(), trend_info);
333 }
334 }
335
336 Ok(trends)
337 }
338
339 fn get_metric_value(&self, snapshot: &GraphSnapshot, metric: &str) -> f64 {
341 match metric {
342 "node_count" => snapshot.node_count as f64,
343 "edge_count" => snapshot.edge_count as f64,
344 "density" => snapshot.density,
345 "avg_degree" => snapshot.avg_degree,
346 "component_count" => snapshot.component_count as f64,
347 "clustering_coefficient" => snapshot.clustering_coefficient,
348 _ => snapshot.custom_metrics.get(metric).copied().unwrap_or(0.0),
349 }
350 }
351
352 fn calculate_trend_info(&self, metric_name: &str, values: &[f64]) -> TrendInfo {
354 let n = values.len() as f64;
355 let x_values: Vec<f64> = (0..values.len()).map(|i| i as f64).collect();
356
357 let x_mean = x_values.iter().sum::<f64>() / n;
359 let y_mean = values.iter().sum::<f64>() / n;
360
361 let numerator: f64 = x_values.iter().zip(values.iter())
362 .map(|(x, y)| (x - x_mean) * (y - y_mean))
363 .sum();
364
365 let denominator: f64 = x_values.iter()
366 .map(|x| (x - x_mean).powi(2))
367 .sum();
368
369 let slope = if denominator.abs() > 1e-10 {
370 numerator / denominator
371 } else {
372 0.0
373 };
374
375 let x_std = (x_values.iter().map(|x| (x - x_mean).powi(2)).sum::<f64>() / n).sqrt();
377 let y_std = (values.iter().map(|y| (y - y_mean).powi(2)).sum::<f64>() / n).sqrt();
378
379 let correlation = if x_std > 1e-10 && y_std > 1e-10 {
380 numerator / (n * x_std * y_std)
381 } else {
382 0.0
383 };
384
385 let volatility = y_std;
387
388 let direction = if slope.abs() < 0.01 {
390 if volatility > y_mean * 0.1 {
391 TrendDirection::Volatile
392 } else {
393 TrendDirection::Stable
394 }
395 } else if slope > 0.0 {
396 TrendDirection::Increasing
397 } else {
398 TrendDirection::Decreasing
399 };
400
401 let recent_change = if values.len() > 1 {
403 let first = values[0];
404 let last = values[values.len() - 1];
405 if first.abs() > 1e-10 {
406 (last - first) / first
407 } else {
408 0.0
409 }
410 } else {
411 0.0
412 };
413
414 TrendInfo {
415 metric_name: metric_name.to_string(),
416 direction,
417 slope,
418 correlation,
419 volatility,
420 recent_change,
421 }
422 }
423
424 fn calculate_volatility(&self) -> Result<HashMap<String, f64>> {
426 let mut volatility = HashMap::new();
427
428 let metrics = ["node_count", "edge_count", "density", "avg_degree"];
429
430 for metric in &metrics {
431 let values: Vec<f64> = self.snapshots.iter()
432 .map(|s| self.get_metric_value(s, metric))
433 .collect();
434
435 if values.len() > 1 {
436 let mean = values.iter().sum::<f64>() / values.len() as f64;
437 let variance = values.iter()
438 .map(|v| (v - mean).powi(2))
439 .sum::<f64>() / values.len() as f64;
440 let vol = variance.sqrt();
441
442 volatility.insert(metric.to_string(), vol);
443 }
444 }
445
446 Ok(volatility)
447 }
448
449 fn detect_change_points(&self) -> Result<Vec<ChangePoint>> {
451 let mut change_points = Vec::new();
452
453 let metrics = ["node_count", "edge_count", "density", "avg_degree"];
454
455 for metric in &metrics {
456 let values: Vec<f64> = self.snapshots.iter()
457 .map(|s| self.get_metric_value(s, metric))
458 .collect();
459
460 if values.len() < 10 {
461 continue; }
463
464 let window_size = values.len() / 4; if window_size < 2 {
467 continue;
468 }
469
470 for i in window_size..(values.len() - window_size) {
471 let before_mean = values[(i - window_size)..i].iter().sum::<f64>() / window_size as f64;
472 let after_mean = values[i..(i + window_size)].iter().sum::<f64>() / window_size as f64;
473
474 let change_magnitude = (after_mean - before_mean).abs();
475 let relative_change = if before_mean.abs() > 1e-10 {
476 change_magnitude / before_mean.abs()
477 } else {
478 0.0
479 };
480
481 if relative_change > 0.2 {
483 let confidence = (relative_change - 0.2) / 0.8; change_points.push(ChangePoint {
485 timestamp: self.snapshots[i].timestamp,
486 metric_name: metric.to_string(),
487 before_value: before_mean,
488 after_value: after_mean,
489 change_magnitude,
490 confidence: confidence.min(1.0),
491 });
492 }
493 }
494 }
495
496 Ok(change_points)
497 }
498
499 fn detect_evolution_patterns(&self) -> Result<Vec<EvolutionPattern>> {
501 let mut patterns = Vec::new();
502
503 if self.snapshots.len() < 5 {
504 return Ok(patterns);
505 }
506
507 let node_values: Vec<f64> = self.snapshots.iter()
509 .map(|s| s.node_count as f64)
510 .collect();
511
512 if self.is_growth_pattern(&node_values) {
513 patterns.push(EvolutionPattern {
514 pattern_type: PatternType::Growth,
515 start_time: self.snapshots[0].timestamp,
516 end_time: self.snapshots[self.snapshots.len() - 1].timestamp,
517 affected_metrics: vec!["node_count".to_string(), "edge_count".to_string()],
518 strength: self.calculate_pattern_strength(&node_values),
519 });
520 }
521
522 let edge_values: Vec<f64> = self.snapshots.iter()
524 .map(|s| s.edge_count as f64)
525 .collect();
526
527 if let Some((start_idx, end_idx)) = self.detect_burst_pattern(&edge_values) {
528 patterns.push(EvolutionPattern {
529 pattern_type: PatternType::Burst,
530 start_time: self.snapshots[start_idx].timestamp,
531 end_time: self.snapshots[end_idx].timestamp,
532 affected_metrics: vec!["edge_count".to_string(), "density".to_string()],
533 strength: self.calculate_burst_strength(&edge_values, start_idx, end_idx),
534 });
535 }
536
537 Ok(patterns)
538 }
539
540 fn is_growth_pattern(&self, values: &[f64]) -> bool {
542 if values.len() < 3 {
543 return false;
544 }
545
546 let increasing_count = values.windows(2)
547 .filter(|w| w[1] > w[0])
548 .count();
549
550 increasing_count as f64 / (values.len() - 1) as f64 > 0.7 }
552
553 fn detect_burst_pattern(&self, values: &[f64]) -> Option<(usize, usize)> {
555 if values.len() < 5 {
556 return None;
557 }
558
559 let mean = values.iter().sum::<f64>() / values.len() as f64;
560 let std = (values.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / values.len() as f64).sqrt();
561
562 let threshold = mean + 2.0 * std;
563
564 for i in 1..(values.len() - 1) {
565 if values[i] > threshold && values[i - 1] < threshold {
566 for j in (i + 1)..values.len() {
568 if values[j] < threshold {
569 return Some((i, j));
570 }
571 }
572 }
573 }
574
575 None
576 }
577
578 fn calculate_pattern_strength(&self, values: &[f64]) -> f64 {
580 if values.len() < 2 {
581 return 0.0;
582 }
583
584 let first = values[0];
585 let last = values[values.len() - 1];
586
587 if first.abs() > 1e-10 {
588 ((last - first) / first).abs().min(1.0)
589 } else {
590 0.0
591 }
592 }
593
594 fn calculate_burst_strength(&self, values: &[f64], start_idx: usize, end_idx: usize) -> f64 {
596 if start_idx >= values.len() || end_idx >= values.len() || start_idx >= end_idx {
597 return 0.0;
598 }
599
600 let baseline = values[0..start_idx].iter().sum::<f64>() / start_idx.max(1) as f64;
601 let peak = values[start_idx..=end_idx].iter().fold(0.0f64, |a, b| a.max(*b));
602
603 if baseline > 1e-10 {
604 ((peak - baseline) / baseline).min(2.0) / 2.0 } else {
606 0.0
607 }
608 }
609
610 pub fn window_stats(&self) -> WindowStats {
612 let duration = if let (Some(first), Some(last)) = (self.snapshots.front(), self.snapshots.back()) {
613 last.timestamp - first.timestamp
614 } else {
615 0
616 };
617
618 WindowStats {
619 snapshot_count: self.snapshots.len(),
620 window_duration_seconds: duration,
621 oldest_timestamp: self.snapshots.front().map(|s| s.timestamp).unwrap_or(0),
622 newest_timestamp: self.snapshots.back().map(|s| s.timestamp).unwrap_or(0),
623 }
624 }
625
626 pub fn recent_snapshots(&self, count: usize) -> Vec<&GraphSnapshot> {
628 self.snapshots.iter().rev().take(count).collect()
629 }
630
631 pub fn metric_evolution(&self, metric_name: &str) -> Vec<(u64, f64)> {
633 self.snapshots.iter()
634 .map(|s| (s.timestamp, self.get_metric_value(s, metric_name)))
635 .collect()
636 }
637}
638
639#[derive(Debug, Clone)]
641pub struct WindowStats {
642 pub snapshot_count: usize,
643 pub window_duration_seconds: u64,
644 pub oldest_timestamp: u64,
645 pub newest_timestamp: u64,
646}
647
648#[cfg(test)]
649mod tests {
650 use super::*;
651 use crate::graph::ArrowGraph;
652 use crate::streaming::incremental::IncrementalGraphProcessor;
653 use arrow::array::{StringArray, Float64Array};
654 use arrow::record_batch::RecordBatch;
655 use arrow::datatypes::{Schema, Field, DataType};
656 use std::sync::Arc;
657
658 fn create_test_graph() -> Result<ArrowGraph> {
659 let nodes_schema = Arc::new(Schema::new(vec![
660 Field::new("id", DataType::Utf8, false),
661 ]));
662 let node_ids = StringArray::from(vec!["A", "B", "C"]);
663 let nodes_batch = RecordBatch::try_new(
664 nodes_schema,
665 vec![Arc::new(node_ids)],
666 )?;
667
668 let edges_schema = Arc::new(Schema::new(vec![
669 Field::new("source", DataType::Utf8, false),
670 Field::new("target", DataType::Utf8, false),
671 Field::new("weight", DataType::Float64, false),
672 ]));
673 let sources = StringArray::from(vec!["A", "B"]);
674 let targets = StringArray::from(vec!["B", "C"]);
675 let weights = Float64Array::from(vec![1.0, 1.0]);
676 let edges_batch = RecordBatch::try_new(
677 edges_schema,
678 vec![Arc::new(sources), Arc::new(targets), Arc::new(weights)],
679 )?;
680
681 ArrowGraph::new(nodes_batch, edges_batch)
682 }
683
684 #[test]
685 fn test_sliding_window_initialization() {
686 let graph = create_test_graph().unwrap();
687 let processor = IncrementalGraphProcessor::new(graph).unwrap();
688
689 let mut window = SlidingWindowProcessor::default();
690 window.initialize(&processor).unwrap();
691
692 assert_eq!(window.snapshots.len(), 1);
693
694 let snapshot = &window.snapshots[0];
695 assert_eq!(snapshot.node_count, 3);
696 assert_eq!(snapshot.edge_count, 2);
697 assert!(snapshot.density > 0.0);
698 }
699
700 #[test]
701 fn test_sliding_window_update() {
702 let graph = create_test_graph().unwrap();
703 let mut processor = IncrementalGraphProcessor::new(graph).unwrap();
704
705 let mut window = SlidingWindowProcessor::default();
706 window.initialize(&processor).unwrap();
707
708 processor.add_edge("A".to_string(), "C".to_string(), 1.0).unwrap();
710
711 let update_result = UpdateResult {
712 vertices_added: 0,
713 vertices_removed: 0,
714 edges_added: 1,
715 edges_removed: 0,
716 affected_components: vec![],
717 recomputation_needed: false,
718 };
719
720 window.update(&processor, &update_result).unwrap();
721
722 assert_eq!(window.snapshots.len(), 2);
723
724 let latest = &window.snapshots[1];
725 assert_eq!(latest.edge_count, 2); }
727
728 #[test]
729 fn test_trend_analysis() {
730 let mut window = SlidingWindowProcessor::default();
731
732 for i in 0..5 {
734 let snapshot = GraphSnapshot {
735 timestamp: i,
736 node_count: (i + 1) as usize,
737 edge_count: i as usize,
738 density: (i as f64) * 0.1,
739 avg_degree: (i as f64) * 0.5,
740 largest_component_size: (i + 1) as usize,
741 component_count: 1,
742 clustering_coefficient: 0.3,
743 custom_metrics: HashMap::new(),
744 };
745 window.snapshots.push_back(snapshot);
746 }
747
748 let analytics = window.analyze_temporal_patterns().unwrap();
749
750 assert!(!analytics.trend_analysis.is_empty());
751
752 let node_trend = analytics.trend_analysis.get("node_count").unwrap();
753 assert_eq!(node_trend.direction, TrendDirection::Increasing);
754 assert!(node_trend.slope > 0.0);
755 }
756
757 #[test]
758 fn test_change_point_detection() {
759 let mut window = SlidingWindowProcessor::default();
760
761 let values = vec![1.0, 1.0, 1.0, 1.0, 1.0, 5.0, 5.0, 5.0, 5.0, 5.0];
763 for (i, &value) in values.iter().enumerate() {
764 let snapshot = GraphSnapshot {
765 timestamp: i as u64,
766 node_count: value as usize,
767 edge_count: 0,
768 density: 0.0,
769 avg_degree: 0.0,
770 largest_component_size: 1,
771 component_count: 1,
772 clustering_coefficient: 0.0,
773 custom_metrics: HashMap::new(),
774 };
775 window.snapshots.push_back(snapshot);
776 }
777
778 let analytics = window.analyze_temporal_patterns().unwrap();
779
780 assert!(!analytics.change_points.is_empty());
781
782 let change_point = &analytics.change_points[0];
783 assert_eq!(change_point.metric_name, "node_count");
784 assert!(change_point.change_magnitude > 0.0);
785 }
786
787 #[test]
788 fn test_evolution_pattern_detection() {
789 let mut window = SlidingWindowProcessor::default();
790
791 for i in 0..10 {
793 let snapshot = GraphSnapshot {
794 timestamp: i,
795 node_count: (i + 1) as usize,
796 edge_count: (i * 2) as usize,
797 density: 0.1,
798 avg_degree: 1.0,
799 largest_component_size: (i + 1) as usize,
800 component_count: 1,
801 clustering_coefficient: 0.3,
802 custom_metrics: HashMap::new(),
803 };
804 window.snapshots.push_back(snapshot);
805 }
806
807 let analytics = window.analyze_temporal_patterns().unwrap();
808
809 assert!(!analytics.evolution_patterns.is_empty());
810
811 let growth_pattern = analytics.evolution_patterns.iter()
812 .find(|p| p.pattern_type == PatternType::Growth);
813 assert!(growth_pattern.is_some());
814
815 let pattern = growth_pattern.unwrap();
816 assert!(pattern.strength > 0.0);
817 assert!(pattern.affected_metrics.contains(&"node_count".to_string()));
818 }
819
820 #[test]
821 fn test_metric_evolution() {
822 let mut window = SlidingWindowProcessor::default();
823
824 for i in 0..5 {
825 let snapshot = GraphSnapshot {
826 timestamp: i * 1000, node_count: ((i + 1) * 2) as usize,
828 edge_count: i as usize,
829 density: 0.1,
830 avg_degree: 1.0,
831 largest_component_size: 1,
832 component_count: 1,
833 clustering_coefficient: 0.3,
834 custom_metrics: HashMap::new(),
835 };
836 window.snapshots.push_back(snapshot);
837 }
838
839 let evolution = window.metric_evolution("node_count");
840 assert_eq!(evolution.len(), 5);
841
842 assert_eq!(evolution[0], (0, 2.0));
843 assert_eq!(evolution[1], (1000, 4.0));
844 assert_eq!(evolution[4], (4000, 10.0));
845 }
846
847 #[test]
848 fn test_window_stats() {
849 let mut window = SlidingWindowProcessor::default();
850
851 for i in 0..3 {
852 let snapshot = GraphSnapshot {
853 timestamp: i * 1000,
854 node_count: 1,
855 edge_count: 0,
856 density: 0.0,
857 avg_degree: 0.0,
858 largest_component_size: 1,
859 component_count: 1,
860 clustering_coefficient: 0.0,
861 custom_metrics: HashMap::new(),
862 };
863 window.snapshots.push_back(snapshot);
864 }
865
866 let stats = window.window_stats();
867 assert_eq!(stats.snapshot_count, 3);
868 assert_eq!(stats.window_duration_seconds, 2000);
869 assert_eq!(stats.oldest_timestamp, 0);
870 assert_eq!(stats.newest_timestamp, 2000);
871 }
872}