arrow_graph/algorithms/
centrality.rs1use 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 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 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 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 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 for iteration in 0..max_iterations {
46 for score in next_scores.values_mut() {
48 *score = (1.0 - damping_factor) / node_count as f64;
49 }
50
51 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 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 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 if diff < tolerance {
85 log::debug!("PageRank converged after {} iterations", iteration + 1);
86 break;
87 }
88
89 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 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 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 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 fn compute_betweenness_centrality(&self, graph: &ArrowGraph) -> Result<HashMap<String, f64>> {
165 let mut centrality: HashMap<String, f64> = HashMap::new();
166
167 for node_id in graph.node_ids() {
169 centrality.insert(node_id.clone(), 0.0);
170 }
171
172 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 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 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(¤t) {
199 for neighbor in neighbors {
200 let current_dist = *distances.get(¤t).unwrap_or(&-1);
201 let neighbor_dist = *distances.get(neighbor).unwrap_or(&-1);
202
203 if neighbor_dist < 0 {
205 queue.push_back(neighbor.clone());
206 distances.insert(neighbor.clone(), current_dist + 1);
207 }
208
209 if neighbor_dist == current_dist + 1 {
211 let current_paths = *num_paths.get(¤t).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 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 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}