use crate::error::{GraphError, GraphResult};
#[derive(Debug, Clone)]
pub struct PageRankConfig {
pub damping: f64,
pub max_iter: usize,
pub tol: f64,
}
impl Default for PageRankConfig {
fn default() -> Self {
Self {
damping: 0.85,
max_iter: 100,
tol: 1e-9,
}
}
}
pub fn pagerank(adj: &[Vec<usize>], n_nodes: usize, cfg: &PageRankConfig) -> GraphResult<Vec<f64>> {
if n_nodes == 0 {
return Err(GraphError::EmptyGraph);
}
if !(0.0..=1.0).contains(&cfg.damping) || !cfg.damping.is_finite() {
return Err(GraphError::InvalidPlan(format!(
"damping must be in [0, 1], got {}",
cfg.damping
)));
}
for (j, heads) in adj.iter().enumerate() {
for &i in heads {
if i >= n_nodes {
return Err(GraphError::InvalidPlan(format!(
"adjacency edge {j} -> {i} references node >= n_nodes={n_nodes}"
)));
}
}
}
let n_f = n_nodes as f64;
let teleport = (1.0 - cfg.damping) / n_f;
let mut outdeg = vec![0usize; n_nodes];
for (j, heads) in adj.iter().enumerate().take(n_nodes) {
outdeg[j] = heads.len();
}
let mut r = vec![1.0 / n_f; n_nodes];
let mut next = vec![0.0; n_nodes];
for _ in 0..cfg.max_iter {
let mut dangling_mass = 0.0;
for i in 0..n_nodes {
if outdeg[i] == 0 {
dangling_mass += r[i];
}
}
let base = teleport + cfg.damping * dangling_mass / n_f;
for slot in next.iter_mut() {
*slot = base;
}
for (j, heads) in adj.iter().enumerate().take(n_nodes) {
if heads.is_empty() {
continue;
}
let share = cfg.damping * r[j] / heads.len() as f64;
for &i in heads {
next[i] += share;
}
}
let mut delta = 0.0;
for i in 0..n_nodes {
delta += (next[i] - r[i]).abs();
}
std::mem::swap(&mut r, &mut next);
if delta < cfg.tol {
break;
}
}
let sum: f64 = r.iter().sum();
if sum > 0.0 {
for v in &mut r {
*v /= sum;
}
}
Ok(r)
}
#[cfg(test)]
mod tests {
use super::*;
fn cfg() -> PageRankConfig {
PageRankConfig::default()
}
#[test]
fn sums_to_1() {
let adj = vec![vec![1, 2], vec![2], vec![0]];
let r = pagerank(&adj, 3, &cfg()).expect("value should be present");
let s: f64 = r.iter().sum();
assert!((s - 1.0).abs() < 1e-9, "sum = {s}");
}
#[test]
fn all_positive() {
let adj = vec![vec![1], vec![2], vec![0]];
let r = pagerank(&adj, 3, &cfg()).expect("value should be present");
for &v in &r {
assert!(v > 0.0, "non-positive score {v}");
}
}
#[test]
fn n_nodes_0_error() {
let adj: Vec<Vec<usize>> = vec![];
let err = pagerank(&adj, 0, &cfg());
assert!(matches!(err, Err(GraphError::EmptyGraph)), "got {err:?}");
}
#[test]
fn hub_higher_rank() {
let adj = vec![vec![1], vec![0], vec![0], vec![0]];
let r = pagerank(&adj, 4, &cfg()).expect("value should be present");
assert!(
r[0] > r[1] && r[0] > r[2] && r[0] > r[3],
"hub {} should exceed {:?}",
r[0],
&r[1..]
);
}
#[test]
fn symmetric_graph_uniform() {
let adj = vec![vec![1, 3], vec![0, 2], vec![1, 3], vec![2, 0]];
let r = pagerank(&adj, 4, &cfg()).expect("value should be present");
for &v in &r {
assert!((v - 0.25).abs() < 1e-6, "score {v} not ~0.25");
}
}
#[test]
fn dangling_node_handled() {
let adj = vec![vec![1], vec![2], vec![]];
let r = pagerank(&adj, 3, &cfg()).expect("value should be present");
let s: f64 = r.iter().sum();
assert!((s - 1.0).abs() < 1e-9, "sum = {s}");
for &v in &r {
assert!(v > 0.0, "score {v} should be positive");
}
}
#[test]
fn damping_0_uniform() {
let adj = vec![vec![1], vec![0], vec![0], vec![1]];
let c = PageRankConfig {
damping: 0.0,
..cfg()
};
let r = pagerank(&adj, 4, &c).expect("pagerank should succeed");
for &v in &r {
assert!((v - 0.25).abs() < 1e-9, "score {v} not uniform");
}
}
#[test]
fn converges() {
let adj = vec![vec![1, 2], vec![2], vec![0], vec![0, 1]];
let r1 = pagerank(&adj, 4, &cfg()).expect("value should be present");
let c2 = PageRankConfig {
max_iter: 1000,
tol: 1e-12,
..cfg()
};
let r2 = pagerank(&adj, 4, &c2).expect("pagerank should succeed");
for i in 0..4 {
assert!(
(r1[i] - r2[i]).abs() < 1e-6,
"node {i}: {} vs {}",
r1[i],
r2[i]
);
}
}
#[test]
fn single_node() {
let adj = vec![vec![]];
let r = pagerank(&adj, 1, &cfg()).expect("value should be present");
assert_eq!(r.len(), 1);
assert!((r[0] - 1.0).abs() < 1e-12, "score {}", r[0]);
}
#[test]
fn out_of_range_edge_error() {
let adj = vec![vec![5]]; let err = pagerank(&adj, 2, &cfg());
assert!(
matches!(err, Err(GraphError::InvalidPlan(_))),
"got {err:?}"
);
}
#[test]
fn invalid_damping_error() {
let adj = vec![vec![1], vec![0]];
let c = PageRankConfig {
damping: 1.5,
..cfg()
};
let err = pagerank(&adj, 2, &c);
assert!(
matches!(err, Err(GraphError::InvalidPlan(_))),
"got {err:?}"
);
}
}