trueno_graph/algorithms/
pagerank.rs1use crate::storage::CsrGraph;
7use anyhow::Result;
8
9const DAMPING_FACTOR: f32 = 0.85;
11
12#[allow(clippy::cast_precision_loss)] #[allow(clippy::cast_possible_truncation)] pub fn pagerank(graph: &CsrGraph, max_iterations: usize, tolerance: f32) -> Result<Vec<f32>> {
60 let n = graph.num_nodes();
61
62 if n == 0 {
63 return Ok(Vec::new());
64 }
65
66 let teleport = (1.0 - DAMPING_FACTOR) / n as f32;
67
68 let mut ranks = vec![1.0 / n as f32; n];
70 let mut new_ranks = vec![0.0; n];
71
72 let (row_offsets, col_indices, _edge_weights) = graph.csr_components();
74
75 let mut out_degrees = vec![0_u32; n];
77 for node in 0..n {
78 let start = row_offsets[node] as usize;
79 let end = row_offsets[node + 1] as usize;
80 out_degrees[node] = (end - start) as u32;
81 }
82
83 #[allow(unused_variables)] for iteration in 0..max_iterations {
86 new_ranks.fill(teleport);
88
89 for node in 0..n {
91 let start = row_offsets[node] as usize;
92 let end = row_offsets[node + 1] as usize;
93
94 if out_degrees[node] > 0 {
95 let rank_contribution = DAMPING_FACTOR * ranks[node] / out_degrees[node] as f32;
96
97 for &target in &col_indices[start..end] {
98 new_ranks[target as usize] += rank_contribution;
99 }
100 } else {
101 let dangling_contribution = DAMPING_FACTOR * ranks[node] / n as f32;
103 for r in &mut new_ranks {
104 *r += dangling_contribution;
105 }
106 }
107 }
108
109 let mut diff = 0.0;
111 for i in 0..n {
112 diff += (new_ranks[i] - ranks[i]).abs();
113 }
114
115 std::mem::swap(&mut ranks, &mut new_ranks);
117
118 if diff < tolerance {
119 #[cfg(test)]
120 eprintln!("PageRank converged after {} iterations (diff={:.2e})", iteration + 1, diff);
121 break;
122 }
123 }
124
125 Ok(ranks)
126}
127
128#[cfg(test)]
129mod tests {
130 use super::*;
131 use crate::NodeId;
132
133 #[test]
134 fn test_pagerank_simple_chain() {
135 let edges = vec![(NodeId(0), NodeId(1), 1.0), (NodeId(1), NodeId(2), 1.0)];
137 let graph = CsrGraph::from_edge_list(&edges).unwrap();
138
139 let scores = pagerank(&graph, 20, 1e-6).unwrap();
140
141 assert_eq!(scores.len(), 3);
143
144 let sum: f32 = scores.iter().sum();
146 assert!((sum - 1.0).abs() < 1e-5, "Sum = {sum}");
147
148 assert!(scores[2] > scores[1], "Node 2 should have higher score than 1");
150 assert!(scores[1] > scores[0], "Node 1 should have higher score than 0");
151 }
152
153 #[test]
154 fn test_pagerank_cycle() {
155 let edges = vec![
157 (NodeId(0), NodeId(1), 1.0),
158 (NodeId(1), NodeId(2), 1.0),
159 (NodeId(2), NodeId(0), 1.0),
160 ];
161 let graph = CsrGraph::from_edge_list(&edges).unwrap();
162
163 let scores = pagerank(&graph, 50, 1e-6).unwrap();
164
165 assert_eq!(scores.len(), 3);
167
168 let sum: f32 = scores.iter().sum();
169 assert!((sum - 1.0).abs() < 1e-5);
170
171 for score in &scores {
173 assert!((*score - 1.0 / 3.0).abs() < 0.01, "Score = {score}");
174 }
175 }
176
177 #[test]
178 fn test_pagerank_star() {
179 let edges = vec![
181 (NodeId(1), NodeId(0), 1.0),
182 (NodeId(2), NodeId(0), 1.0),
183 (NodeId(3), NodeId(0), 1.0),
184 ];
185 let graph = CsrGraph::from_edge_list(&edges).unwrap();
186
187 let scores = pagerank(&graph, 20, 1e-6).unwrap();
188
189 assert!(scores[0] > scores[1]);
191 assert!(scores[0] > scores[2]);
192 assert!(scores[0] > scores[3]);
193
194 assert!((scores[1] - scores[2]).abs() < 0.01);
196 assert!((scores[2] - scores[3]).abs() < 0.01);
197 }
198
199 #[test]
200 fn test_pagerank_empty_graph() {
201 let graph = CsrGraph::new();
202 let scores = pagerank(&graph, 20, 1e-6).unwrap();
203 assert_eq!(scores.len(), 0);
204 }
205
206 #[test]
207 fn test_pagerank_single_node() {
208 let mut graph = CsrGraph::new();
209 graph.add_edge(NodeId(0), NodeId(0), 1.0).unwrap(); let scores = pagerank(&graph, 20, 1e-6).unwrap();
212 assert_eq!(scores.len(), 1);
213 assert!((scores[0] - 1.0).abs() < 1e-5); }
215
216 #[test]
217 fn test_pagerank_convergence() {
218 let mut edges = Vec::new();
220 for i in 0..10 {
221 edges.push((NodeId(i), NodeId((i + 1) % 10), 1.0));
222 }
223 let graph = CsrGraph::from_edge_list(&edges).unwrap();
224
225 let scores = pagerank(&graph, 100, 1e-6).unwrap();
226
227 for score in &scores {
229 assert!((*score - 0.1).abs() < 0.01, "Score = {score}");
230 }
231 }
232}