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    current_metrics: HashMap<String, f64>,
14}
15
16/// A snapshot of graph state at a specific point in time
17#[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/// Configuration for temporal analysis
31#[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/// Temporal analytics results
40#[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/// Information about a metric's trend over time
49#[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/// Direction of trend
60#[derive(Debug, Clone, PartialEq)]
61pub enum TrendDirection {
62    Increasing,
63    Decreasing,
64    Stable,
65    Volatile,
66}
67
68/// Detected change point in graph evolution
69#[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/// Detected evolution pattern
80#[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/// Types of evolution patterns
90#[derive(Debug, Clone, PartialEq)]
91pub enum PatternType {
92    Growth,          // Steady growth in graph size
93    Decay,           // Steady decline in activity
94    Burst,           // Sudden spike in activity
95    Oscillation,     // Periodic changes
96    PhaseTransition, // Fundamental change in structure
97    Stabilization,   // Convergence to steady state
98}
99
100impl Default for TemporalConfig {
101    fn default() -> Self {
102        Self {
103            window_duration: std::time::Duration::from_secs(3600), // 1 hour
104            snapshot_interval: std::time::Duration::from_secs(60), // 1 minute
105            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    /// Default sliding window processor
126    pub fn default() -> Self {
127        Self::new(TemporalConfig::default())
128    }
129
130    /// Initialize with current graph state
131    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    /// Update window with new graph state
138    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        // Create new snapshot
145        let snapshot = self.create_snapshot(processor)?;
146        self.snapshots.push_back(snapshot);
147
148        // Remove old snapshots outside window
149        self.cleanup_old_snapshots(current_time);
150
151        // Limit total snapshots
152        while self.snapshots.len() > self.max_snapshots {
153            self.snapshots.pop_front();
154        }
155
156        Ok(())
157    }
158
159    /// Create a snapshot of current graph state
160    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        // Analyze components
178        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        // Estimate clustering coefficient
183        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(), // TODO: Implement custom metrics
200        })
201    }
202
203    /// Analyze connected components
204    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        // Get all nodes
213        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        // Build adjacency list
226        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        // Find components using DFS
247        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(&current) {
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    /// Estimate clustering coefficient
276    fn estimate_clustering_coefficient(&self, _processor: &IncrementalGraphProcessor) -> Result<f64> {
277        // Simplified implementation - in practice would calculate actual clustering
278        Ok(0.3) // Placeholder value
279    }
280
281    /// Remove snapshots outside the time window
282    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    /// Perform temporal analysis on the sliding window
295    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    /// Analyze trends for each metric
319    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    /// Get metric value from snapshot
339    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    /// Calculate trend information for a metric
352    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        // Calculate linear regression slope
357        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        // Calculate correlation coefficient
375        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        // Calculate volatility (standard deviation)
385        let volatility = y_std;
386
387        // Determine trend direction
388        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        // Recent change (last vs first value)
401        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    /// Calculate volatility metrics
424    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    /// Detect change points in time series
449    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; // Need enough data for change point detection
461            }
462
463            // Simple change point detection using moving averages
464            let window_size = values.len() / 4; // Quarter of the data
465            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                // Threshold for significant change
481                if relative_change > 0.2 {
482                    let confidence = (relative_change - 0.2) / 0.8; // Scale to 0-1
483                    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    /// Detect evolution patterns
499    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        // Detect growth patterns
507        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        // Detect burst patterns
522        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    /// Check if values show a growth pattern
540    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 // 70% increasing
550    }
551
552    /// Detect burst pattern (sudden spike followed by normalization)
553    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                // Found potential start of burst
566                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    /// Calculate pattern strength
578    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    /// Calculate burst strength
594    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 // Normalize to 0-1
604        } else {
605            0.0
606        }
607    }
608
609    /// Get current window statistics
610    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    /// Get recent snapshots
626    pub fn recent_snapshots(&self, count: usize) -> Vec<&GraphSnapshot> {
627        self.snapshots.iter().rev().take(count).collect()
628    }
629
630    /// Get metric evolution over time
631    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/// Statistics about the sliding window
639#[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        // Add more edges
708        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); // Should still be 2 (not incrementally updated)
725    }
726
727    #[test]
728    fn test_trend_analysis() {
729        let mut window = SlidingWindowProcessor::default();
730
731        // Create mock snapshots with increasing node counts
732        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        // Create snapshots with a sudden change
761        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        // Create growth pattern
791        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, // Different timestamps
826                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}