Skip to main content

arrow_graph/streaming/
temporal.rs

1use crate::error::Result;
2use crate::streaming::incremental::{IncrementalGraphProcessor, UpdateResult};
3use arrow::array::Array;
4use std::collections::{HashMap, VecDeque};
5
6/// Time-based sliding window for graph evolution analysis
7/// Maintains snapshots of graph state over time and provides temporal analytics
8#[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/// A snapshot of graph state at a specific point in time
18#[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/// Configuration for temporal analysis
32#[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/// Temporal analytics results
41#[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/// Information about a metric's trend over time
50#[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/// Direction of trend
61#[derive(Debug, Clone, PartialEq)]
62pub enum TrendDirection {
63    Increasing,
64    Decreasing,
65    Stable,
66    Volatile,
67}
68
69/// Detected change point in graph evolution
70#[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/// Detected evolution pattern
81#[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/// Types of evolution patterns
91#[derive(Debug, Clone, PartialEq)]
92pub enum PatternType {
93    Growth,          // Steady growth in graph size
94    Decay,           // Steady decline in activity
95    Burst,           // Sudden spike in activity
96    Oscillation,     // Periodic changes
97    PhaseTransition, // Fundamental change in structure
98    Stabilization,   // Convergence to steady state
99}
100
101impl Default for TemporalConfig {
102    fn default() -> Self {
103        Self {
104            window_duration: std::time::Duration::from_secs(3600), // 1 hour
105            snapshot_interval: std::time::Duration::from_secs(60), // 1 minute
106            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    /// Default sliding window processor
127    pub fn default() -> Self {
128        Self::new(TemporalConfig::default())
129    }
130
131    /// Initialize with current graph state
132    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    /// Update window with new graph state
139    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        // Create new snapshot
146        let snapshot = self.create_snapshot(processor)?;
147        self.snapshots.push_back(snapshot);
148
149        // Remove old snapshots outside window
150        self.cleanup_old_snapshots(current_time);
151
152        // Limit total snapshots
153        while self.snapshots.len() > self.max_snapshots {
154            self.snapshots.pop_front();
155        }
156
157        Ok(())
158    }
159
160    /// Create a snapshot of current graph state
161    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        // Analyze components
179        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        // Estimate clustering coefficient
184        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(), // TODO: Implement custom metrics
201        })
202    }
203
204    /// Analyze connected components
205    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        // Get all nodes
214        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        // Build adjacency list
227        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        // Find components using DFS
248        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(&current) {
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    /// Estimate clustering coefficient
277    fn estimate_clustering_coefficient(&self, _processor: &IncrementalGraphProcessor) -> Result<f64> {
278        // Simplified implementation - in practice would calculate actual clustering
279        Ok(0.3) // Placeholder value
280    }
281
282    /// Remove snapshots outside the time window
283    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    /// Perform temporal analysis on the sliding window
296    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    /// Analyze trends for each metric
320    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    /// Get metric value from snapshot
340    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    /// Calculate trend information for a metric
353    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        // Calculate linear regression slope
358        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        // Calculate correlation coefficient
376        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        // Calculate volatility (standard deviation)
386        let volatility = y_std;
387
388        // Determine trend direction
389        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        // Recent change (last vs first value)
402        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    /// Calculate volatility metrics
425    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    /// Detect change points in time series
450    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; // Need enough data for change point detection
462            }
463
464            // Simple change point detection using moving averages
465            let window_size = values.len() / 4; // Quarter of the data
466            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                // Threshold for significant change
482                if relative_change > 0.2 {
483                    let confidence = (relative_change - 0.2) / 0.8; // Scale to 0-1
484                    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    /// Detect evolution patterns
500    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        // Detect growth patterns
508        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        // Detect burst patterns
523        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    /// Check if values show a growth pattern
541    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 // 70% increasing
551    }
552
553    /// Detect burst pattern (sudden spike followed by normalization)
554    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                // Found potential start of burst
567                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    /// Calculate pattern strength
579    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    /// Calculate burst strength
595    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 // Normalize to 0-1
605        } else {
606            0.0
607        }
608    }
609
610    /// Get current window statistics
611    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    /// Get recent snapshots
627    pub fn recent_snapshots(&self, count: usize) -> Vec<&GraphSnapshot> {
628        self.snapshots.iter().rev().take(count).collect()
629    }
630
631    /// Get metric evolution over time
632    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/// Statistics about the sliding window
640#[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        // Add more edges
709        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); // Should still be 2 (not incrementally updated)
726    }
727
728    #[test]
729    fn test_trend_analysis() {
730        let mut window = SlidingWindowProcessor::default();
731
732        // Create mock snapshots with increasing node counts
733        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        // Create snapshots with a sudden change
762        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        // Create growth pattern
792        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, // Different timestamps
827                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}