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