oxicuda_graph/centrality/
pagerank.rs1use crate::error::{GraphError, GraphResult};
25
26#[derive(Debug, Clone)]
28pub struct PageRankConfig {
29 pub damping: f64,
32 pub max_iter: usize,
34 pub tol: f64,
36}
37
38impl Default for PageRankConfig {
39 fn default() -> Self {
40 Self {
41 damping: 0.85,
42 max_iter: 100,
43 tol: 1e-9,
44 }
45 }
46}
47
48pub fn pagerank(adj: &[Vec<usize>], n_nodes: usize, cfg: &PageRankConfig) -> GraphResult<Vec<f64>> {
59 if n_nodes == 0 {
60 return Err(GraphError::EmptyGraph);
61 }
62 if !(0.0..=1.0).contains(&cfg.damping) || !cfg.damping.is_finite() {
63 return Err(GraphError::InvalidPlan(format!(
64 "damping must be in [0, 1], got {}",
65 cfg.damping
66 )));
67 }
68 for (j, heads) in adj.iter().enumerate() {
71 for &i in heads {
72 if i >= n_nodes {
73 return Err(GraphError::InvalidPlan(format!(
74 "adjacency edge {j} -> {i} references node >= n_nodes={n_nodes}"
75 )));
76 }
77 }
78 }
79
80 let n_f = n_nodes as f64;
81 let teleport = (1.0 - cfg.damping) / n_f;
82
83 let mut outdeg = vec![0usize; n_nodes];
85 for (j, heads) in adj.iter().enumerate().take(n_nodes) {
86 outdeg[j] = heads.len();
87 }
88
89 let mut r = vec![1.0 / n_f; n_nodes];
90 let mut next = vec![0.0; n_nodes];
91
92 for _ in 0..cfg.max_iter {
93 let mut dangling_mass = 0.0;
95 for i in 0..n_nodes {
96 if outdeg[i] == 0 {
97 dangling_mass += r[i];
98 }
99 }
100 let base = teleport + cfg.damping * dangling_mass / n_f;
101 for slot in next.iter_mut() {
102 *slot = base;
103 }
104 for (j, heads) in adj.iter().enumerate().take(n_nodes) {
106 if heads.is_empty() {
107 continue;
108 }
109 let share = cfg.damping * r[j] / heads.len() as f64;
110 for &i in heads {
111 next[i] += share;
112 }
113 }
114
115 let mut delta = 0.0;
117 for i in 0..n_nodes {
118 delta += (next[i] - r[i]).abs();
119 }
120 std::mem::swap(&mut r, &mut next);
121 if delta < cfg.tol {
122 break;
123 }
124 }
125
126 let sum: f64 = r.iter().sum();
128 if sum > 0.0 {
129 for v in &mut r {
130 *v /= sum;
131 }
132 }
133 Ok(r)
134}
135
136#[cfg(test)]
137mod tests {
138 use super::*;
139
140 fn cfg() -> PageRankConfig {
141 PageRankConfig::default()
142 }
143
144 #[test]
146 fn sums_to_1() {
147 let adj = vec![vec![1, 2], vec![2], vec![0]];
148 let r = pagerank(&adj, 3, &cfg()).expect("value should be present");
149 let s: f64 = r.iter().sum();
150 assert!((s - 1.0).abs() < 1e-9, "sum = {s}");
151 }
152
153 #[test]
155 fn all_positive() {
156 let adj = vec![vec![1], vec![2], vec![0]];
157 let r = pagerank(&adj, 3, &cfg()).expect("value should be present");
158 for &v in &r {
159 assert!(v > 0.0, "non-positive score {v}");
160 }
161 }
162
163 #[test]
165 fn n_nodes_0_error() {
166 let adj: Vec<Vec<usize>> = vec![];
167 let err = pagerank(&adj, 0, &cfg());
168 assert!(matches!(err, Err(GraphError::EmptyGraph)), "got {err:?}");
169 }
170
171 #[test]
173 fn hub_higher_rank() {
174 let adj = vec![vec![1], vec![0], vec![0], vec![0]];
176 let r = pagerank(&adj, 4, &cfg()).expect("value should be present");
177 assert!(
178 r[0] > r[1] && r[0] > r[2] && r[0] > r[3],
179 "hub {} should exceed {:?}",
180 r[0],
181 &r[1..]
182 );
183 }
184
185 #[test]
187 fn symmetric_graph_uniform() {
188 let adj = vec![vec![1, 3], vec![0, 2], vec![1, 3], vec![2, 0]];
190 let r = pagerank(&adj, 4, &cfg()).expect("value should be present");
191 for &v in &r {
192 assert!((v - 0.25).abs() < 1e-6, "score {v} not ~0.25");
193 }
194 }
195
196 #[test]
198 fn dangling_node_handled() {
199 let adj = vec![vec![1], vec![2], vec![]];
201 let r = pagerank(&adj, 3, &cfg()).expect("value should be present");
202 let s: f64 = r.iter().sum();
203 assert!((s - 1.0).abs() < 1e-9, "sum = {s}");
204 for &v in &r {
205 assert!(v > 0.0, "score {v} should be positive");
206 }
207 }
208
209 #[test]
211 fn damping_0_uniform() {
212 let adj = vec![vec![1], vec![0], vec![0], vec![1]];
213 let c = PageRankConfig {
214 damping: 0.0,
215 ..cfg()
216 };
217 let r = pagerank(&adj, 4, &c).expect("pagerank should succeed");
218 for &v in &r {
219 assert!((v - 0.25).abs() < 1e-9, "score {v} not uniform");
220 }
221 }
222
223 #[test]
226 fn converges() {
227 let adj = vec![vec![1, 2], vec![2], vec![0], vec![0, 1]];
228 let r1 = pagerank(&adj, 4, &cfg()).expect("value should be present");
229 let c2 = PageRankConfig {
230 max_iter: 1000,
231 tol: 1e-12,
232 ..cfg()
233 };
234 let r2 = pagerank(&adj, 4, &c2).expect("pagerank should succeed");
235 for i in 0..4 {
236 assert!(
237 (r1[i] - r2[i]).abs() < 1e-6,
238 "node {i}: {} vs {}",
239 r1[i],
240 r2[i]
241 );
242 }
243 }
244
245 #[test]
247 fn single_node() {
248 let adj = vec![vec![]];
249 let r = pagerank(&adj, 1, &cfg()).expect("value should be present");
250 assert_eq!(r.len(), 1);
251 assert!((r[0] - 1.0).abs() < 1e-12, "score {}", r[0]);
252 }
253
254 #[test]
256 fn out_of_range_edge_error() {
257 let adj = vec![vec![5]]; let err = pagerank(&adj, 2, &cfg());
259 assert!(
260 matches!(err, Err(GraphError::InvalidPlan(_))),
261 "got {err:?}"
262 );
263 }
264
265 #[test]
267 fn invalid_damping_error() {
268 let adj = vec![vec![1], vec![0]];
269 let c = PageRankConfig {
270 damping: 1.5,
271 ..cfg()
272 };
273 let err = pagerank(&adj, 2, &c);
274 assert!(
275 matches!(err, Err(GraphError::InvalidPlan(_))),
276 "got {err:?}"
277 );
278 }
279}