arrow_graph/algorithms/
centrality.rs

1use arrow::record_batch::RecordBatch;
2use arrow::array::{StringArray, Float64Array, UInt32Array};
3use arrow::datatypes::{DataType, Field, Schema};
4use std::sync::Arc;
5use std::collections::HashMap;
6use crate::algorithms::{GraphAlgorithm, AlgorithmParams};
7use crate::graph::ArrowGraph;
8use crate::error::{GraphError, Result};
9
10pub struct PageRank;
11
12impl PageRank {
13    /// PageRank algorithm with power iteration and early termination
14    fn compute_pagerank(
15        &self,
16        graph: &ArrowGraph,
17        damping_factor: f64,
18        max_iterations: usize,
19        tolerance: f64,
20    ) -> Result<HashMap<String, f64>> {
21        let node_count = graph.node_count();
22        if node_count == 0 {
23            return Ok(HashMap::new());
24        }
25        
26        // Initialize PageRank scores
27        let initial_score = 1.0 / node_count as f64;
28        let mut current_scores: HashMap<String, f64> = HashMap::new();
29        let mut next_scores: HashMap<String, f64> = HashMap::new();
30        
31        // Initialize all nodes with equal probability
32        for node_id in graph.node_ids() {
33            current_scores.insert(node_id.clone(), initial_score);
34            next_scores.insert(node_id.clone(), 0.0);
35        }
36        
37        // Calculate out-degrees for each node
38        let mut out_degrees: HashMap<String, usize> = HashMap::new();
39        for node_id in graph.node_ids() {
40            let degree = graph.neighbors(node_id).map(|n| n.len()).unwrap_or(0);
41            out_degrees.insert(node_id.clone(), degree);
42        }
43        
44        // Power iteration
45        for iteration in 0..max_iterations {
46            // Reset next scores
47            for score in next_scores.values_mut() {
48                *score = (1.0 - damping_factor) / node_count as f64;
49            }
50            
51            // Distribute PageRank scores
52            for node_id in graph.node_ids() {
53                let current_score = current_scores.get(node_id).unwrap_or(&0.0);
54                let out_degree = out_degrees.get(node_id).unwrap_or(&0);
55                
56                if *out_degree > 0 {
57                    let contribution = current_score * damping_factor / *out_degree as f64;
58                    
59                    if let Some(neighbors) = graph.neighbors(node_id) {
60                        for neighbor in neighbors {
61                            if let Some(neighbor_score) = next_scores.get_mut(neighbor) {
62                                *neighbor_score += contribution;
63                            }
64                        }
65                    }
66                } else {
67                    // Handle dangling nodes - distribute equally to all nodes
68                    let dangling_contribution = current_score * damping_factor / node_count as f64;
69                    for score in next_scores.values_mut() {
70                        *score += dangling_contribution;
71                    }
72                }
73            }
74            
75            // Check for convergence
76            let mut diff = 0.0;
77            for node_id in graph.node_ids() {
78                let old_score = current_scores.get(node_id).unwrap_or(&0.0);
79                let new_score = next_scores.get(node_id).unwrap_or(&0.0);
80                diff += (new_score - old_score).abs();
81            }
82            
83            // Early termination if converged
84            if diff < tolerance {
85                log::debug!("PageRank converged after {} iterations", iteration + 1);
86                break;
87            }
88            
89            // Swap scores for next iteration
90            std::mem::swap(&mut current_scores, &mut next_scores);
91        }
92        
93        Ok(current_scores)
94    }
95}
96
97impl GraphAlgorithm for PageRank {
98    fn execute(&self, graph: &ArrowGraph, params: &AlgorithmParams) -> Result<RecordBatch> {
99        let damping_factor: f64 = params.get("damping_factor").unwrap_or(0.85);
100        let max_iterations: usize = params.get("max_iterations").unwrap_or(100);
101        let tolerance: f64 = params.get("tolerance").unwrap_or(1e-6);
102        
103        // Validate parameters
104        if !(0.0..=1.0).contains(&damping_factor) {
105            return Err(GraphError::invalid_parameter(
106                "damping_factor must be between 0.0 and 1.0"
107            ));
108        }
109        
110        if max_iterations == 0 {
111            return Err(GraphError::invalid_parameter(
112                "max_iterations must be greater than 0"
113            ));
114        }
115        
116        if tolerance <= 0.0 {
117            return Err(GraphError::invalid_parameter(
118                "tolerance must be greater than 0.0"
119            ));
120        }
121        
122        let scores = self.compute_pagerank(graph, damping_factor, max_iterations, tolerance)?;
123        
124        // Convert to Arrow RecordBatch
125        let schema = Arc::new(Schema::new(vec![
126            Field::new("node_id", DataType::Utf8, false),
127            Field::new("pagerank_score", DataType::Float64, false),
128        ]));
129        
130        let mut node_ids = Vec::new();
131        let mut pagerank_scores = Vec::new();
132        
133        // Sort by PageRank score (descending) for consistent output
134        let mut sorted_scores: Vec<(&String, &f64)> = scores.iter().collect();
135        sorted_scores.sort_by(|a, b| b.1.partial_cmp(a.1).unwrap_or(std::cmp::Ordering::Equal));
136        
137        for (node_id, score) in sorted_scores {
138            node_ids.push(node_id.clone());
139            pagerank_scores.push(*score);
140        }
141        
142        RecordBatch::try_new(
143            schema,
144            vec![
145                Arc::new(StringArray::from(node_ids)),
146                Arc::new(Float64Array::from(pagerank_scores)),
147            ],
148        ).map_err(GraphError::from)
149    }
150    
151    fn name(&self) -> &'static str {
152        "pagerank"
153    }
154    
155    fn description(&self) -> &'static str {
156        "Calculate PageRank scores using power iteration with early termination"
157    }
158}
159
160pub struct BetweennessCentrality;
161
162impl BetweennessCentrality {
163    /// Calculate betweenness centrality using Brandes' algorithm
164    fn compute_betweenness_centrality(&self, graph: &ArrowGraph) -> Result<HashMap<String, f64>> {
165        let mut centrality: HashMap<String, f64> = HashMap::new();
166        
167        // Initialize centrality scores
168        for node_id in graph.node_ids() {
169            centrality.insert(node_id.clone(), 0.0);
170        }
171        
172        // For each node as source
173        for source in graph.node_ids() {
174            let mut stack = Vec::new();
175            let mut paths: HashMap<String, Vec<String>> = HashMap::new();
176            let mut num_paths: HashMap<String, f64> = HashMap::new();
177            let mut distances: HashMap<String, i32> = HashMap::new();
178            let mut delta: HashMap<String, f64> = HashMap::new();
179            
180            // Initialize
181            for node_id in graph.node_ids() {
182                paths.insert(node_id.clone(), Vec::new());
183                num_paths.insert(node_id.clone(), 0.0);
184                distances.insert(node_id.clone(), -1);
185                delta.insert(node_id.clone(), 0.0);
186            }
187            
188            num_paths.insert(source.clone(), 1.0);
189            distances.insert(source.clone(), 0);
190            
191            // BFS
192            let mut queue = std::collections::VecDeque::new();
193            queue.push_back(source.clone());
194            
195            while let Some(current) = queue.pop_front() {
196                stack.push(current.clone());
197                
198                if let Some(neighbors) = graph.neighbors(&current) {
199                    for neighbor in neighbors {
200                        let current_dist = *distances.get(&current).unwrap_or(&-1);
201                        let neighbor_dist = *distances.get(neighbor).unwrap_or(&-1);
202                        
203                        // First time we reach this neighbor
204                        if neighbor_dist < 0 {
205                            queue.push_back(neighbor.clone());
206                            distances.insert(neighbor.clone(), current_dist + 1);
207                        }
208                        
209                        // Shortest path to neighbor via current
210                        if neighbor_dist == current_dist + 1 {
211                            let current_paths = *num_paths.get(&current).unwrap_or(&0.0);
212                            let neighbor_paths = num_paths.get_mut(neighbor).unwrap();
213                            *neighbor_paths += current_paths;
214                            
215                            paths.get_mut(neighbor).unwrap().push(current.clone());
216                        }
217                    }
218                }
219            }
220            
221            // Accumulation
222            while let Some(w) = stack.pop() {
223                if let Some(predecessors) = paths.get(&w) {
224                    for predecessor in predecessors {
225                        let w_delta = *delta.get(&w).unwrap_or(&0.0);
226                        let w_paths = *num_paths.get(&w).unwrap_or(&0.0);
227                        let pred_paths = *num_paths.get(predecessor).unwrap_or(&0.0);
228                        
229                        if pred_paths > 0.0 {
230                            let contribution = (pred_paths / w_paths) * (1.0 + w_delta);
231                            *delta.get_mut(predecessor).unwrap() += contribution;
232                        }
233                    }
234                }
235                
236                if w != *source {
237                    let w_delta = *delta.get(&w).unwrap_or(&0.0);
238                    *centrality.get_mut(&w).unwrap() += w_delta;
239                }
240            }
241        }
242        
243        // Normalize for undirected graphs
244        let node_count = graph.node_count() as f64;
245        if node_count > 2.0 {
246            let normalization = 2.0 / ((node_count - 1.0) * (node_count - 2.0));
247            for score in centrality.values_mut() {
248                *score *= normalization;
249            }
250        }
251        
252        Ok(centrality)
253    }
254}
255
256impl GraphAlgorithm for BetweennessCentrality {
257    fn execute(&self, _graph: &ArrowGraph, _params: &AlgorithmParams) -> Result<RecordBatch> {
258        todo!("Implement betweenness centrality - complex algorithm, implementing in future version")
259    }
260    
261    fn name(&self) -> &'static str {
262        "betweenness_centrality"
263    }
264    
265    fn description(&self) -> &'static str {
266        "Calculate betweenness centrality using Brandes' algorithm"
267    }
268}