arrow_graph/streaming/
algorithms.rs

1use crate::error::Result;
2use crate::streaming::incremental::{IncrementalGraphProcessor, UpdateResult};
3use arrow::array::Array;
4use std::collections::HashMap;
5
6/// Streaming algorithms that can update incrementally as the graph changes
7/// These algorithms maintain state and update efficiently rather than recomputing from scratch
8pub trait StreamingAlgorithm<T> {
9    /// Initialize the algorithm with the current graph state
10    fn initialize(&mut self, processor: &IncrementalGraphProcessor) -> Result<()>;
11    
12    /// Update the algorithm state based on graph changes
13    fn update(&mut self, processor: &IncrementalGraphProcessor, changes: &UpdateResult) -> Result<()>;
14    
15    /// Get the current result/state of the algorithm
16    fn get_result(&self) -> &T;
17    
18    /// Force a full recomputation (fallback when incremental update is not sufficient)
19    fn recompute(&mut self, processor: &IncrementalGraphProcessor) -> Result<()>;
20    
21    /// Check if the algorithm needs full recomputation
22    fn needs_recomputation(&self, changes: &UpdateResult) -> bool;
23}
24
25/// Streaming PageRank algorithm that updates incrementally
26#[derive(Debug, Clone)]
27pub struct StreamingPageRank {
28    scores: HashMap<String, f64>,
29    damping_factor: f64,
30    max_iterations: usize,
31    tolerance: f64,
32    iteration_count: usize,
33    converged: bool,
34}
35
36impl StreamingPageRank {
37    pub fn new(damping_factor: f64, max_iterations: usize, tolerance: f64) -> Self {
38        Self {
39            scores: HashMap::new(),
40            damping_factor,
41            max_iterations,
42            tolerance,
43            iteration_count: 0,
44            converged: false,
45        }
46    }
47
48    /// Default parameters for streaming PageRank
49    pub fn default() -> Self {
50        Self::new(0.85, 50, 1e-6)
51    }
52
53    /// Perform one iteration of PageRank updates
54    fn iterate(&mut self, adjacency: &HashMap<String, Vec<(String, f64)>>, nodes: &[String]) -> Result<bool> {
55        let node_count = nodes.len() as f64;
56        let base_score = (1.0 - self.damping_factor) / node_count;
57        
58        let mut new_scores = HashMap::new();
59        
60        // Initialize all nodes with base score
61        for node in nodes {
62            new_scores.insert(node.clone(), base_score);
63        }
64        
65        // Add contributions from incoming links
66        for (source, targets) in adjacency {
67            let source_score = self.scores.get(source).copied().unwrap_or(1.0 / node_count);
68            let out_degree = targets.len() as f64;
69            
70            if out_degree > 0.0 {
71                let contribution_per_link = self.damping_factor * source_score / out_degree;
72                
73                for (target, _weight) in targets {
74                    *new_scores.entry(target.clone()).or_insert(base_score) += contribution_per_link;
75                }
76            }
77        }
78        
79        // Check for convergence
80        let mut max_change: f64 = 0.0;
81        for (node, new_score) in &new_scores {
82            let old_score = self.scores.get(node).copied().unwrap_or(1.0 / node_count);
83            let change = (new_score - old_score).abs();
84            max_change = max_change.max(change);
85        }
86        
87        self.scores = new_scores;
88        self.iteration_count += 1;
89        
90        let converged = max_change < self.tolerance;
91        self.converged = converged;
92        
93        Ok(converged)
94    }
95
96    /// Get top-k nodes by PageRank score
97    pub fn top_nodes(&self, k: usize) -> Vec<(String, f64)> {
98        let mut node_scores: Vec<(String, f64)> = self.scores.iter()
99            .map(|(node, score)| (node.clone(), *score))
100            .collect();
101            
102        node_scores.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
103        node_scores.truncate(k);
104        node_scores
105    }
106
107    /// Get score for a specific node
108    pub fn node_score(&self, node_id: &str) -> Option<f64> {
109        self.scores.get(node_id).copied()
110    }
111}
112
113impl StreamingAlgorithm<HashMap<String, f64>> for StreamingPageRank {
114    fn initialize(&mut self, processor: &IncrementalGraphProcessor) -> Result<()> {
115        // Build adjacency list from current graph
116        let graph = processor.graph();
117        let nodes_batch = &graph.nodes;
118        let edges_batch = &graph.edges;
119        
120        // Extract nodes
121        let mut nodes = Vec::new();
122        if nodes_batch.num_rows() > 0 {
123            let node_ids = nodes_batch.column(0)
124                .as_any()
125                .downcast_ref::<arrow::array::StringArray>()
126                .ok_or_else(|| crate::error::GraphError::graph_construction("Expected string array for node IDs"))?;
127                
128            for i in 0..node_ids.len() {
129                nodes.push(node_ids.value(i).to_string());
130            }
131        }
132        
133        // Build adjacency map
134        let mut adjacency: HashMap<String, Vec<(String, f64)>> = HashMap::new();
135        if edges_batch.num_rows() > 0 {
136            let source_ids = edges_batch.column(0)
137                .as_any()
138                .downcast_ref::<arrow::array::StringArray>()
139                .ok_or_else(|| crate::error::GraphError::graph_construction("Expected string array for source IDs"))?;
140            let target_ids = edges_batch.column(1)
141                .as_any()
142                .downcast_ref::<arrow::array::StringArray>()
143                .ok_or_else(|| crate::error::GraphError::graph_construction("Expected string array for target IDs"))?;
144            let weights = edges_batch.column(2)
145                .as_any()
146                .downcast_ref::<arrow::array::Float64Array>()
147                .ok_or_else(|| crate::error::GraphError::graph_construction("Expected float64 array for weights"))?;
148                
149            for i in 0..source_ids.len() {
150                let source = source_ids.value(i).to_string();
151                let target = target_ids.value(i).to_string();
152                let weight = weights.value(i);
153                
154                adjacency.entry(source).or_insert_with(Vec::new).push((target, weight));
155            }
156        }
157        
158        // Initialize scores
159        let node_count = nodes.len() as f64;
160        if node_count > 0.0 {
161            let initial_score = 1.0 / node_count;
162            for node in &nodes {
163                self.scores.insert(node.clone(), initial_score);
164            }
165            
166            // Run initial PageRank computation
167            for _ in 0..self.max_iterations {
168                if self.iterate(&adjacency, &nodes)? {
169                    break;
170                }
171            }
172        }
173        
174        Ok(())
175    }
176    
177    fn update(&mut self, processor: &IncrementalGraphProcessor, changes: &UpdateResult) -> Result<()> {
178        // For significant changes, we recompute
179        if self.needs_recomputation(changes) {
180            return self.recompute(processor);
181        }
182        
183        // For minor changes, we can do incremental updates
184        // This is a simplified approach - full incremental PageRank is complex
185        let graph = processor.graph();
186        let nodes_batch = &graph.nodes;
187        let edges_batch = &graph.edges;
188        
189        // Update node scores for new nodes
190        if changes.vertices_added > 0 {
191            let node_count = processor.graph().node_count() as f64;
192            let initial_score = 1.0 / node_count;
193            
194            // Normalize existing scores
195            for score in self.scores.values_mut() {
196                *score *= (node_count - changes.vertices_added as f64) / node_count;
197            }
198            
199            // Add new nodes
200            if nodes_batch.num_rows() > 0 {
201                let node_ids = nodes_batch.column(0)
202                    .as_any()
203                    .downcast_ref::<arrow::array::StringArray>()
204                    .ok_or_else(|| crate::error::GraphError::graph_construction("Expected string array for node IDs"))?;
205                    
206                for i in 0..node_ids.len() {
207                    let node = node_ids.value(i).to_string();
208                    self.scores.entry(node).or_insert(initial_score);
209                }
210            }
211        }
212        
213        // Remove deleted nodes
214        if changes.vertices_removed > 0 {
215            // Note: This is simplified - we'd need to track which specific nodes were removed
216            // For now, we just clean up any orphaned scores
217            let mut valid_nodes = std::collections::HashSet::new();
218            if nodes_batch.num_rows() > 0 {
219                let node_ids = nodes_batch.column(0)
220                    .as_any()
221                    .downcast_ref::<arrow::array::StringArray>()
222                    .ok_or_else(|| crate::error::GraphError::graph_construction("Expected string array for node IDs"))?;
223                    
224                for i in 0..node_ids.len() {
225                    valid_nodes.insert(node_ids.value(i).to_string());
226                }
227            }
228            
229            self.scores.retain(|node, _| valid_nodes.contains(node));
230        }
231        
232        // For edge changes, do a few iterations to re-stabilize
233        if changes.edges_added > 0 || changes.edges_removed > 0 {
234            // Build current adjacency map
235            let mut adjacency: HashMap<String, Vec<(String, f64)>> = HashMap::new();
236            if edges_batch.num_rows() > 0 {
237                let source_ids = edges_batch.column(0)
238                    .as_any()
239                    .downcast_ref::<arrow::array::StringArray>()
240                    .ok_or_else(|| crate::error::GraphError::graph_construction("Expected string array for source IDs"))?;
241                let target_ids = edges_batch.column(1)
242                    .as_any()
243                    .downcast_ref::<arrow::array::StringArray>()
244                    .ok_or_else(|| crate::error::GraphError::graph_construction("Expected string array for target IDs"))?;
245                let weights = edges_batch.column(2)
246                    .as_any()
247                    .downcast_ref::<arrow::array::Float64Array>()
248                    .ok_or_else(|| crate::error::GraphError::graph_construction("Expected float64 array for weights"))?;
249                    
250                for i in 0..source_ids.len() {
251                    let source = source_ids.value(i).to_string();
252                    let target = target_ids.value(i).to_string();
253                    let weight = weights.value(i);
254                    
255                    adjacency.entry(source).or_insert_with(Vec::new).push((target, weight));
256                }
257            }
258            
259            let nodes: Vec<String> = self.scores.keys().cloned().collect();
260            
261            // Do a few iterations to re-stabilize
262            let update_iterations = std::cmp::min(10, self.max_iterations);
263            for _ in 0..update_iterations {
264                if self.iterate(&adjacency, &nodes)? {
265                    break;
266                }
267            }
268        }
269        
270        Ok(())
271    }
272    
273    fn get_result(&self) -> &HashMap<String, f64> {
274        &self.scores
275    }
276    
277    fn recompute(&mut self, processor: &IncrementalGraphProcessor) -> Result<()> {
278        self.scores.clear();
279        self.iteration_count = 0;
280        self.converged = false;
281        self.initialize(processor)
282    }
283    
284    fn needs_recomputation(&self, changes: &UpdateResult) -> bool {
285        // Recompute for large changes or if we haven't converged
286        let total_changes = changes.vertices_added + changes.vertices_removed + 
287                           changes.edges_added + changes.edges_removed;
288        
289        total_changes > 10 || !self.converged
290    }
291}
292
293/// Streaming Connected Components algorithm that updates incrementally
294#[derive(Debug, Clone)]
295pub struct StreamingConnectedComponents {
296    components: HashMap<String, String>, // node -> component_id
297    component_sizes: HashMap<String, usize>, // component_id -> size
298}
299
300impl StreamingConnectedComponents {
301    pub fn new() -> Self {
302        Self {
303            components: HashMap::new(),
304            component_sizes: HashMap::new(),
305        }
306    }
307
308    /// Get the component ID for a node
309    pub fn component_of(&self, node_id: &str) -> Option<&String> {
310        self.components.get(node_id)
311    }
312
313    /// Get the size of the component containing a node
314    pub fn component_size(&self, node_id: &str) -> Option<usize> {
315        self.components.get(node_id)
316            .and_then(|comp_id| self.component_sizes.get(comp_id))
317            .copied()
318    }
319
320    /// Get all components and their sizes
321    pub fn all_components(&self) -> Vec<(String, usize)> {
322        self.component_sizes.iter()
323            .map(|(id, size)| (id.clone(), *size))
324            .collect()
325    }
326
327    /// Get number of components
328    pub fn component_count(&self) -> usize {
329        self.component_sizes.len()
330    }
331
332    /// Union-Find helper: find root with path compression
333    fn find_root(&self, mut node: String, temp_parents: &mut HashMap<String, String>) -> String {
334        let mut path = Vec::new();
335        
336        // Find root
337        while let Some(parent) = temp_parents.get(&node).or_else(|| self.components.get(&node)) {
338            if parent == &node {
339                break; // Found root
340            }
341            path.push(node.clone());
342            node = parent.clone();
343        }
344        
345        // Path compression
346        for path_node in path {
347            temp_parents.insert(path_node, node.clone());
348        }
349        
350        node
351    }
352
353    /// Union two components
354    fn union_components(&mut self, node1: &str, node2: &str) {
355        let comp1 = self.components.get(node1).cloned().unwrap_or_else(|| node1.to_string());
356        let comp2 = self.components.get(node2).cloned().unwrap_or_else(|| node2.to_string());
357        
358        if comp1 == comp2 {
359            return; // Already in same component
360        }
361        
362        // Merge smaller component into larger one
363        let size1 = self.component_sizes.get(&comp1).copied().unwrap_or(1);
364        let size2 = self.component_sizes.get(&comp2).copied().unwrap_or(1);
365        
366        let (smaller, larger, new_size) = if size1 <= size2 {
367            (comp1, comp2, size1 + size2)
368        } else {
369            (comp2, comp1, size1 + size2)
370        };
371        
372        // Update all nodes in smaller component
373        let nodes_to_update: Vec<String> = self.components.iter()
374            .filter(|(_, comp)| *comp == &smaller)
375            .map(|(node, _)| node.clone())
376            .collect();
377            
378        for node in nodes_to_update {
379            self.components.insert(node, larger.clone());
380        }
381        
382        // Update component sizes
383        self.component_sizes.insert(larger, new_size);
384        self.component_sizes.remove(&smaller);
385    }
386}
387
388impl Default for StreamingConnectedComponents {
389    fn default() -> Self {
390        Self::new()
391    }
392}
393
394impl StreamingAlgorithm<HashMap<String, String>> for StreamingConnectedComponents {
395    fn initialize(&mut self, processor: &IncrementalGraphProcessor) -> Result<()> {
396        let graph = processor.graph();
397        let nodes_batch = &graph.nodes;
398        let edges_batch = &graph.edges;
399        
400        self.components.clear();
401        self.component_sizes.clear();
402        
403        // Initialize each node as its own component
404        if nodes_batch.num_rows() > 0 {
405            let node_ids = nodes_batch.column(0)
406                .as_any()
407                .downcast_ref::<arrow::array::StringArray>()
408                .ok_or_else(|| crate::error::GraphError::graph_construction("Expected string array for node IDs"))?;
409                
410            for i in 0..node_ids.len() {
411                let node = node_ids.value(i).to_string();
412                self.components.insert(node.clone(), node.clone());
413                self.component_sizes.insert(node, 1);
414            }
415        }
416        
417        // Process edges to union components
418        if edges_batch.num_rows() > 0 {
419            let source_ids = edges_batch.column(0)
420                .as_any()
421                .downcast_ref::<arrow::array::StringArray>()
422                .ok_or_else(|| crate::error::GraphError::graph_construction("Expected string array for source IDs"))?;
423            let target_ids = edges_batch.column(1)
424                .as_any()
425                .downcast_ref::<arrow::array::StringArray>()
426                .ok_or_else(|| crate::error::GraphError::graph_construction("Expected string array for target IDs"))?;
427                
428            for i in 0..source_ids.len() {
429                let source = source_ids.value(i);
430                let target = target_ids.value(i);
431                self.union_components(source, target);
432            }
433        }
434        
435        Ok(())
436    }
437    
438    fn update(&mut self, processor: &IncrementalGraphProcessor, changes: &UpdateResult) -> Result<()> {
439        // For large changes, recompute
440        if self.needs_recomputation(changes) {
441            return self.recompute(processor);
442        }
443        
444        let graph = processor.graph();
445        let nodes_batch = &graph.nodes;
446        let edges_batch = &graph.edges;
447        
448        // Handle new vertices
449        if changes.vertices_added > 0 {
450            if nodes_batch.num_rows() > 0 {
451                let node_ids = nodes_batch.column(0)
452                    .as_any()
453                    .downcast_ref::<arrow::array::StringArray>()
454                    .ok_or_else(|| crate::error::GraphError::graph_construction("Expected string array for node IDs"))?;
455                    
456                for i in 0..node_ids.len() {
457                    let node = node_ids.value(i).to_string();
458                    if !self.components.contains_key(&node) {
459                        self.components.insert(node.clone(), node.clone());
460                        self.component_sizes.insert(node, 1);
461                    }
462                }
463            }
464        }
465        
466        // Handle removed vertices (simplified)
467        if changes.vertices_removed > 0 {
468            // For simplicity, we'll do a full recomputation for vertex removals
469            // A full implementation would track specific removed vertices
470            return self.recompute(processor);
471        }
472        
473        // Handle new edges - union components
474        if changes.edges_added > 0 {
475            if edges_batch.num_rows() > 0 {
476                let source_ids = edges_batch.column(0)
477                    .as_any()
478                    .downcast_ref::<arrow::array::StringArray>()
479                    .ok_or_else(|| crate::error::GraphError::graph_construction("Expected string array for source IDs"))?;
480                let target_ids = edges_batch.column(1)
481                    .as_any()
482                    .downcast_ref::<arrow::array::StringArray>()
483                    .ok_or_else(|| crate::error::GraphError::graph_construction("Expected string array for target IDs"))?;
484                    
485                for i in 0..source_ids.len() {
486                    let source = source_ids.value(i);
487                    let target = target_ids.value(i);
488                    
489                    // Ensure both nodes exist in components
490                    if !self.components.contains_key(source) {
491                        self.components.insert(source.to_string(), source.to_string());
492                        self.component_sizes.insert(source.to_string(), 1);
493                    }
494                    if !self.components.contains_key(target) {
495                        self.components.insert(target.to_string(), target.to_string());
496                        self.component_sizes.insert(target.to_string(), 1);
497                    }
498                    
499                    self.union_components(source, target);
500                }
501            }
502        }
503        
504        // Handle edge removals (complex - may split components)
505        if changes.edges_removed > 0 {
506            // For simplicity, recompute when edges are removed
507            // A full implementation would check if removal splits components
508            return self.recompute(processor);
509        }
510        
511        Ok(())
512    }
513    
514    fn get_result(&self) -> &HashMap<String, String> {
515        &self.components
516    }
517    
518    fn recompute(&mut self, processor: &IncrementalGraphProcessor) -> Result<()> {
519        self.initialize(processor)
520    }
521    
522    fn needs_recomputation(&self, changes: &UpdateResult) -> bool {
523        // Recompute for vertex removals or edge removals (may split components)
524        // or for very large changes
525        let total_changes = changes.vertices_added + changes.vertices_removed + 
526                           changes.edges_added + changes.edges_removed;
527        
528        changes.vertices_removed > 0 || changes.edges_removed > 0 || total_changes > 20
529    }
530}
531
532#[cfg(test)]
533mod tests {
534    use super::*;
535    use crate::graph::ArrowGraph;
536    use arrow::array::{StringArray, Float64Array};
537    use arrow::record_batch::RecordBatch;
538    use arrow::datatypes::{Schema, Field, DataType};
539    use std::sync::Arc;
540
541    fn create_test_graph() -> Result<ArrowGraph> {
542        // Create nodes
543        let nodes_schema = Arc::new(Schema::new(vec![
544            Field::new("id", DataType::Utf8, false),
545        ]));
546        let node_ids = StringArray::from(vec!["A", "B", "C", "D"]);
547        let nodes_batch = RecordBatch::try_new(
548            nodes_schema,
549            vec![Arc::new(node_ids)],
550        )?;
551
552        // Create edges: A->B, B->C, D isolated
553        let edges_schema = Arc::new(Schema::new(vec![
554            Field::new("source", DataType::Utf8, false),
555            Field::new("target", DataType::Utf8, false),
556            Field::new("weight", DataType::Float64, false),
557        ]));
558        let sources = StringArray::from(vec!["A", "B"]);
559        let targets = StringArray::from(vec!["B", "C"]);
560        let weights = Float64Array::from(vec![1.0, 1.0]);
561        let edges_batch = RecordBatch::try_new(
562            edges_schema,
563            vec![Arc::new(sources), Arc::new(targets), Arc::new(weights)],
564        )?;
565
566        ArrowGraph::new(nodes_batch, edges_batch)
567    }
568
569    #[test]
570    fn test_streaming_pagerank_initialization() {
571        let graph = create_test_graph().unwrap();
572        let processor = IncrementalGraphProcessor::new(graph).unwrap();
573        
574        let mut pagerank = StreamingPageRank::default();
575        pagerank.initialize(&processor).unwrap();
576        
577        let scores = pagerank.get_result();
578        assert_eq!(scores.len(), 4); // A, B, C, D
579        
580        // All nodes should have some score
581        for node in ["A", "B", "C", "D"] {
582            assert!(scores.contains_key(node));
583            assert!(scores[node] > 0.0);
584        }
585        
586        // B and C should have higher scores due to incoming links
587        assert!(scores["B"] > scores["A"]);
588        assert!(scores["C"] > scores["D"]);
589    }
590
591    #[test]
592    fn test_streaming_pagerank_update() {
593        let graph = create_test_graph().unwrap();
594        let mut processor = IncrementalGraphProcessor::new(graph).unwrap();
595        processor.set_batch_size(1); // Force immediate flush
596        
597        let mut pagerank = StreamingPageRank::default();
598        pagerank.initialize(&processor).unwrap();
599        
600        let initial_scores = pagerank.get_result().clone();
601        
602        // Add new edge A->D
603        processor.add_edge("A".to_string(), "D".to_string(), 1.0).unwrap();
604        
605        // Create fake update result
606        let update_result = crate::streaming::incremental::UpdateResult {
607            vertices_added: 0,
608            vertices_removed: 0,
609            edges_added: 1,
610            edges_removed: 0,
611            affected_components: vec![],
612            recomputation_needed: false,
613        };
614        
615        pagerank.update(&processor, &update_result).unwrap();
616        
617        let updated_scores = pagerank.get_result();
618        
619        // D's score should have increased
620        assert!(updated_scores["D"] > initial_scores["D"]);
621    }
622
623    #[test]
624    fn test_streaming_connected_components_initialization() {
625        let graph = create_test_graph().unwrap();
626        let processor = IncrementalGraphProcessor::new(graph).unwrap();
627        
628        let mut components = StreamingConnectedComponents::new();
629        components.initialize(&processor).unwrap();
630        
631        let result = components.get_result();
632        assert_eq!(result.len(), 4); // A, B, C, D
633        
634        // A, B, C should be in same component
635        let comp_a = &result["A"];
636        let comp_b = &result["B"];
637        let comp_c = &result["C"];
638        assert_eq!(comp_a, comp_b);
639        assert_eq!(comp_b, comp_c);
640        
641        // D should be in its own component
642        let comp_d = &result["D"];
643        assert_ne!(comp_a, comp_d);
644        
645        // Should have 2 components total
646        assert_eq!(components.component_count(), 2);
647    }
648
649    #[test]
650    fn test_streaming_connected_components_update() {
651        let graph = create_test_graph().unwrap();
652        let mut processor = IncrementalGraphProcessor::new(graph).unwrap();
653        processor.set_batch_size(1); // Force immediate flush
654        
655        let mut components = StreamingConnectedComponents::new();
656        components.initialize(&processor).unwrap();
657        
658        assert_eq!(components.component_count(), 2); // {A,B,C} and {D}
659        
660        // Add edge to connect D to the main component
661        processor.add_edge("C".to_string(), "D".to_string(), 1.0).unwrap();
662        
663        let update_result = crate::streaming::incremental::UpdateResult {
664            vertices_added: 0,
665            vertices_removed: 0,
666            edges_added: 1,
667            edges_removed: 0,
668            affected_components: vec![],
669            recomputation_needed: false,
670        };
671        
672        components.update(&processor, &update_result).unwrap();
673        
674        // Now should have only 1 component
675        assert_eq!(components.component_count(), 1);
676        
677        let result = components.get_result();
678        let comp_a = &result["A"];
679        let comp_d = &result["D"];
680        assert_eq!(comp_a, comp_d); // All nodes in same component
681    }
682
683    #[test]
684    fn test_streaming_algorithm_recomputation() {
685        let graph = create_test_graph().unwrap();
686        let mut processor = IncrementalGraphProcessor::new(graph).unwrap();
687        
688        let mut pagerank = StreamingPageRank::default();
689        pagerank.initialize(&processor).unwrap();
690        
691        // Simulate large changes that require recomputation
692        let large_changes = crate::streaming::incremental::UpdateResult {
693            vertices_added: 15,
694            vertices_removed: 5,
695            edges_added: 20,
696            edges_removed: 10,
697            affected_components: vec![],
698            recomputation_needed: true,
699        };
700        
701        assert!(pagerank.needs_recomputation(&large_changes));
702        
703        // Should trigger recomputation
704        pagerank.update(&processor, &large_changes).unwrap();
705        
706        // Algorithm should still work after recomputation
707        let scores = pagerank.get_result();
708        assert_eq!(scores.len(), 4);
709    }
710
711    #[test]
712    fn test_pagerank_top_nodes() {
713        let graph = create_test_graph().unwrap();
714        let processor = IncrementalGraphProcessor::new(graph).unwrap();
715        
716        let mut pagerank = StreamingPageRank::default();
717        pagerank.initialize(&processor).unwrap();
718        
719        let top_2 = pagerank.top_nodes(2);
720        assert_eq!(top_2.len(), 2);
721        
722        // Should be sorted by score descending
723        assert!(top_2[0].1 >= top_2[1].1);
724        
725        // Check specific node score
726        assert!(pagerank.node_score("A").is_some());
727        assert!(pagerank.node_score("nonexistent").is_none());
728    }
729
730    #[test]
731    fn test_connected_components_queries() {
732        let graph = create_test_graph().unwrap();
733        let processor = IncrementalGraphProcessor::new(graph).unwrap();
734        
735        let mut components = StreamingConnectedComponents::new();
736        components.initialize(&processor).unwrap();
737        
738        // Test component queries
739        assert!(components.component_of("A").is_some());
740        assert!(components.component_of("nonexistent").is_none());
741        
742        assert!(components.component_size("A").is_some());
743        assert_eq!(components.component_size("A"), Some(3)); // A, B, C
744        assert_eq!(components.component_size("D"), Some(1)); // D alone
745        
746        let all_components = components.all_components();
747        assert_eq!(all_components.len(), 2);
748        
749        // Check total sizes
750        let total_size: usize = all_components.iter().map(|(_, size)| size).sum();
751        assert_eq!(total_size, 4); // All 4 nodes
752    }
753}