1use crate::core::{Graph, IgraphError, IgraphResult};
12
13const DEFAULT_DAMPING: f64 = 0.85;
14const DEFAULT_EPS: f64 = 1e-10;
15const DEFAULT_MAX_ITER: usize = 1000;
16
17pub fn personalized_pagerank(graph: &Graph, reset: &[f64], damping: f64) -> IgraphResult<Vec<f64>> {
66 let n = graph.vcount();
67 let n_us = n as usize;
68
69 if n == 0 {
70 if reset.is_empty() {
71 return Ok(Vec::new());
72 }
73 return Err(IgraphError::InvalidArgument(
74 "personalized_pagerank: reset vector non-empty but graph has no vertices".into(),
75 ));
76 }
77
78 if reset.len() != n_us {
79 return Err(IgraphError::InvalidArgument(format!(
80 "personalized_pagerank: reset length ({}) does not match vcount ({n})",
81 reset.len()
82 )));
83 }
84
85 if damping <= 0.0 || damping >= 1.0 {
86 return Err(IgraphError::InvalidArgument(format!(
87 "personalized_pagerank: damping ({damping}) must be in (0, 1)"
88 )));
89 }
90
91 let reset_sum: f64 = reset.iter().sum();
92 if reset_sum <= 0.0 {
93 return Err(IgraphError::InvalidArgument(
94 "personalized_pagerank: reset vector must sum to a positive value".into(),
95 ));
96 }
97 for (i, &val) in reset.iter().enumerate() {
98 if val < 0.0 {
99 return Err(IgraphError::InvalidArgument(format!(
100 "personalized_pagerank: negative reset value ({val}) at index {i}"
101 )));
102 }
103 }
104
105 if n == 1 {
106 return Ok(vec![1.0]);
107 }
108
109 let inv_sum = 1.0 / reset_sum;
111 let norm_reset: Vec<f64> = reset.iter().map(|&v| v * inv_sum).collect();
112
113 let directed = graph.is_directed();
114
115 let mut out_deg = vec![0u64; n_us];
117 for v in 0..n {
118 let nbrs = graph.neighbors(v)?;
119 out_deg[v as usize] = nbrs.len() as u64;
120 }
121
122 let m =
124 u32::try_from(graph.ecount()).map_err(|_| IgraphError::Internal("ecount overflows u32"))?;
125 let mut in_adj: Vec<Vec<u32>> = vec![Vec::new(); n_us];
126
127 if directed {
128 for e in 0..m {
129 let (u, v) = graph.edge(e)?;
130 in_adj[v as usize].push(u);
131 }
132 } else {
133 for e in 0..m {
134 let (u, v) = graph.edge(e)?;
135 if u == v {
136 in_adj[v as usize].push(u);
137 in_adj[v as usize].push(u);
138 } else {
139 in_adj[u as usize].push(v);
140 in_adj[v as usize].push(u);
141 }
142 }
143 }
144
145 let mut pr = norm_reset.clone();
147 let mut pr_new = vec![0.0_f64; n_us];
148
149 for _ in 0..DEFAULT_MAX_ITER {
150 let mut dangling_sum: f64 = 0.0;
152 for v in 0..n_us {
153 if out_deg[v] == 0 {
154 dangling_sum += pr[v];
155 }
156 }
157
158 for v in 0..n_us {
159 let mut incoming: f64 = 0.0;
160 for &u in &in_adj[v] {
161 #[allow(clippy::cast_precision_loss)]
162 let denom = out_deg[u as usize] as f64;
163 if denom > 0.0 {
164 incoming += pr[u as usize] / denom;
165 }
166 }
167 pr_new[v] = (1.0 - damping) * norm_reset[v]
170 + damping * (incoming + dangling_sum * norm_reset[v]);
171 }
172
173 let mut diff: f64 = 0.0;
175 for v in 0..n_us {
176 diff += (pr_new[v] - pr[v]).abs();
177 }
178 std::mem::swap(&mut pr, &mut pr_new);
179 if diff < DEFAULT_EPS {
180 break;
181 }
182 }
183
184 Ok(pr)
185}
186
187pub fn personalized_pagerank_default(graph: &Graph, reset: &[f64]) -> IgraphResult<Vec<f64>> {
207 personalized_pagerank(graph, reset, DEFAULT_DAMPING)
208}
209
210pub fn personalized_pagerank_vs(
242 graph: &Graph,
243 reset_vids: &[u32],
244 damping: f64,
245) -> IgraphResult<Vec<f64>> {
246 let n = graph.vcount();
247 let n_us = n as usize;
248
249 if reset_vids.is_empty() {
250 return Err(IgraphError::InvalidArgument(
251 "reset_vids must not be empty".into(),
252 ));
253 }
254
255 let mut reset = vec![0.0_f64; n_us];
256 #[allow(clippy::cast_precision_loss)]
257 let weight = 1.0 / reset_vids.len() as f64;
258 for &v in reset_vids {
259 if v >= n {
260 return Err(IgraphError::InvalidArgument(format!(
261 "reset vertex {v} out of range (vcount = {n})"
262 )));
263 }
264 reset[v as usize] += weight;
265 }
266
267 personalized_pagerank(graph, &reset, damping)
268}
269
270#[cfg(test)]
271mod tests {
272 use super::*;
273
274 fn close(actual: &[f64], expected: &[f64], tol: f64) {
275 assert_eq!(actual.len(), expected.len(), "length mismatch");
276 for (i, (a, e)) in actual.iter().zip(expected.iter()).enumerate() {
277 assert!((a - e).abs() < tol, "vertex {i}: actual={a} expected={e}");
278 }
279 }
280
281 #[test]
282 fn empty_graph() {
283 let g = Graph::with_vertices(0);
284 let pr = personalized_pagerank(&g, &[], 0.85).unwrap();
285 assert!(pr.is_empty());
286 }
287
288 #[test]
289 fn singleton() {
290 let g = Graph::with_vertices(1);
291 let pr = personalized_pagerank(&g, &[1.0], 0.85).unwrap();
292 assert_eq!(pr, vec![1.0]);
293 }
294
295 #[test]
296 fn uniform_reset_matches_standard_pagerank() {
297 let mut g = Graph::with_vertices(4);
298 g.add_edge(0, 1).unwrap();
299 g.add_edge(0, 2).unwrap();
300 g.add_edge(1, 2).unwrap();
301 g.add_edge(2, 3).unwrap();
302
303 let standard = crate::pagerank(&g).unwrap();
304 let uniform = vec![0.25; 4];
305 let personalized = personalized_pagerank(&g, &uniform, 0.85).unwrap();
306 close(&personalized, &standard, 1e-9);
307 }
308
309 #[test]
310 fn biased_reset_changes_ranking() {
311 let mut g = Graph::with_vertices(4);
313 g.add_edge(0, 1).unwrap();
314 g.add_edge(0, 2).unwrap();
315 g.add_edge(0, 3).unwrap();
316
317 let uniform = vec![0.25; 4];
319 let pr_u = personalized_pagerank(&g, &uniform, 0.85).unwrap();
320 assert!(pr_u[0] > pr_u[1]);
321
322 let biased = vec![0.0, 1.0, 0.0, 0.0];
326 let pr_b = personalized_pagerank(&g, &biased, 0.85).unwrap();
327 assert!(pr_b[1] > pr_b[2], "leaf 1 > leaf 2");
328 assert!(pr_b[1] > pr_b[3], "leaf 1 > leaf 3");
329 assert!(pr_b[0] > pr_b[1], "center > biased leaf in star");
331 }
332
333 #[test]
334 fn sums_to_one() {
335 let mut g = Graph::with_vertices(5);
336 g.add_edge(0, 1).unwrap();
337 g.add_edge(1, 2).unwrap();
338 g.add_edge(2, 3).unwrap();
339 g.add_edge(3, 4).unwrap();
340 g.add_edge(4, 0).unwrap();
341
342 let reset = vec![0.5, 0.2, 0.1, 0.1, 0.1];
343 let pr = personalized_pagerank(&g, &reset, 0.85).unwrap();
344 let total: f64 = pr.iter().sum();
345 assert!((total - 1.0).abs() < 1e-9, "sum={total}");
346 }
347
348 #[test]
349 fn directed_biased() {
350 let mut g = Graph::new(3, true).unwrap();
352 g.add_edge(0, 1).unwrap();
353 g.add_edge(1, 2).unwrap();
354
355 let reset = vec![1.0, 0.0, 0.0];
356 let pr = personalized_pagerank(&g, &reset, 0.85).unwrap();
357 assert!(pr[0] > pr[1], "pr[0]={} > pr[1]={}", pr[0], pr[1]);
364 assert!(pr[1] > pr[2], "pr[1]={} > pr[2]={}", pr[1], pr[2]);
365 let total: f64 = pr.iter().sum();
366 assert!((total - 1.0).abs() < 1e-9);
367 }
368
369 #[test]
370 fn dangling_vertices_redistribute_to_reset() {
371 let mut g = Graph::new(2, true).unwrap();
374 g.add_edge(0, 1).unwrap();
375 let reset = vec![0.3, 0.7];
376 let pr = personalized_pagerank(&g, &reset, 0.85).unwrap();
377 let total: f64 = pr.iter().sum();
378 assert!((total - 1.0).abs() < 1e-9);
379 }
380
381 #[test]
382 fn different_damping_factors() {
383 let mut g = Graph::with_vertices(3);
384 g.add_edge(0, 1).unwrap();
385 g.add_edge(1, 2).unwrap();
386 g.add_edge(2, 0).unwrap();
387 let reset = vec![1.0, 0.0, 0.0];
388
389 let pr_high = personalized_pagerank(&g, &reset, 0.99).unwrap();
391 let pr_low = personalized_pagerank(&g, &reset, 0.5).unwrap();
393 assert!(pr_low[0] > pr_high[0]);
395 }
396
397 #[test]
398 fn error_on_wrong_reset_length() {
399 let g = Graph::with_vertices(3);
400 assert!(personalized_pagerank(&g, &[1.0, 1.0], 0.85).is_err());
401 }
402
403 #[test]
404 fn error_on_negative_reset() {
405 let g = Graph::with_vertices(3);
406 assert!(personalized_pagerank(&g, &[1.0, -0.5, 0.5], 0.85).is_err());
407 }
408
409 #[test]
410 fn error_on_zero_sum_reset() {
411 let g = Graph::with_vertices(3);
412 assert!(personalized_pagerank(&g, &[0.0, 0.0, 0.0], 0.85).is_err());
413 }
414
415 #[test]
416 fn error_on_invalid_damping() {
417 let g = Graph::with_vertices(2);
418 assert!(personalized_pagerank(&g, &[0.5, 0.5], 0.0).is_err());
419 assert!(personalized_pagerank(&g, &[0.5, 0.5], 1.0).is_err());
420 assert!(personalized_pagerank(&g, &[0.5, 0.5], -0.1).is_err());
421 assert!(personalized_pagerank(&g, &[0.5, 0.5], 1.5).is_err());
422 }
423
424 #[test]
425 fn unnormalized_reset_works() {
426 let mut g = Graph::with_vertices(3);
427 g.add_edge(0, 1).unwrap();
428 g.add_edge(1, 2).unwrap();
429 g.add_edge(2, 0).unwrap();
430 let pr1 = personalized_pagerank(&g, &[10.0, 0.0, 0.0], 0.85).unwrap();
432 let pr2 = personalized_pagerank(&g, &[1.0, 0.0, 0.0], 0.85).unwrap();
433 close(&pr1, &pr2, 1e-9);
434 }
435
436 #[test]
437 fn default_wrapper() {
438 let mut g = Graph::with_vertices(3);
439 g.add_edge(0, 1).unwrap();
440 g.add_edge(1, 2).unwrap();
441 g.add_edge(2, 0).unwrap();
442 let reset = vec![1.0, 0.0, 0.0];
443 let pr1 = personalized_pagerank(&g, &reset, 0.85).unwrap();
444 let pr2 = personalized_pagerank_default(&g, &reset).unwrap();
445 close(&pr1, &pr2, 1e-15);
446 }
447
448 #[test]
449 fn isolated_vertices_with_biased_reset() {
450 let g = Graph::with_vertices(4);
453 let reset = vec![0.0, 0.0, 1.0, 0.0];
454 let pr = personalized_pagerank(&g, &reset, 0.85).unwrap();
455 assert!((pr[2] - 1.0).abs() < 1e-9);
457 assert!(pr[0].abs() < 1e-9);
458 assert!(pr[1].abs() < 1e-9);
459 assert!(pr[3].abs() < 1e-9);
460 }
461
462 #[test]
463 fn oracle_5_cycle_biased() {
464 let mut g = Graph::with_vertices(5);
466 g.add_edge(0, 1).unwrap();
467 g.add_edge(1, 2).unwrap();
468 g.add_edge(2, 3).unwrap();
469 g.add_edge(3, 4).unwrap();
470 g.add_edge(4, 0).unwrap();
471 let reset = vec![0.5, 0.2, 0.1, 0.1, 0.1];
472 let pr = personalized_pagerank(&g, &reset, 0.85).unwrap();
473 let expected = [
474 0.246_408_839_8,
475 0.210_246_107_5,
476 0.177_699_648_4,
477 0.172_576_594_7,
478 0.193_068_809_6,
479 ];
480 close(&pr, &expected, 1e-6);
481 }
482
483 #[test]
484 fn vs_matches_manual_reset() {
485 let mut g = Graph::with_vertices(4);
486 g.add_edge(0, 1).unwrap();
487 g.add_edge(1, 2).unwrap();
488 g.add_edge(2, 3).unwrap();
489 g.add_edge(3, 0).unwrap();
490
491 let pr_vs = personalized_pagerank_vs(&g, &[0, 1], 0.85).unwrap();
492 let reset = vec![0.5, 0.5, 0.0, 0.0];
493 let pr_manual = personalized_pagerank(&g, &reset, 0.85).unwrap();
494 close(&pr_vs, &pr_manual, 1e-12);
495 }
496
497 #[test]
498 fn vs_single_vertex() {
499 let mut g = Graph::with_vertices(3);
500 g.add_edge(0, 1).unwrap();
501 g.add_edge(1, 2).unwrap();
502 g.add_edge(2, 0).unwrap();
503
504 let pr = personalized_pagerank_vs(&g, &[2], 0.85).unwrap();
505 let sum: f64 = pr.iter().sum();
506 assert!((sum - 1.0).abs() < 1e-9);
507 assert!(pr[2] > pr[0]);
508 assert!(pr[2] > pr[1]);
509 }
510
511 #[test]
512 fn vs_empty_errors() {
513 let g = Graph::with_vertices(3);
514 assert!(personalized_pagerank_vs(&g, &[], 0.85).is_err());
515 }
516
517 #[test]
518 fn vs_out_of_range_errors() {
519 let g = Graph::with_vertices(3);
520 assert!(personalized_pagerank_vs(&g, &[5], 0.85).is_err());
521 }
522}