use super::common::{GraphView, NodeId};
use std::collections::HashMap;
use rayon::prelude::*;
pub struct PageRankConfig {
pub damping_factor: f64,
pub iterations: usize,
pub tolerance: f64,
pub dangling_redistribution: bool,
}
impl Default for PageRankConfig {
fn default() -> Self {
Self {
damping_factor: 0.85,
iterations: 20,
tolerance: 0.0001,
dangling_redistribution: true,
}
}
}
pub fn page_rank(
view: &GraphView,
config: PageRankConfig,
) -> HashMap<NodeId, f64> {
let n = view.node_count;
if n == 0 {
return HashMap::new();
}
let initial_score = 1.0 / n as f64;
let mut scores = vec![initial_score; n];
let mut next_scores = vec![0.0; n];
let d = config.damping_factor;
let base_score = (1.0 - d) / n as f64;
let use_parallel = n >= 1000;
for _ in 0..config.iterations {
let dangling_contrib = if config.dangling_redistribution {
let dangling_sum: f64 = if use_parallel {
(0..n).into_par_iter()
.filter(|&i| view.out_degree(i) == 0)
.map(|i| scores[i])
.sum()
} else {
(0..n).filter(|&i| view.out_degree(i) == 0)
.map(|i| scores[i])
.sum()
};
dangling_sum / n as f64
} else {
0.0
};
let total_diff = if use_parallel {
next_scores.par_iter_mut().enumerate().map(|(i, next_score)| {
let mut sum_incoming = 0.0;
for &source_idx in view.predecessors(i) {
let out_degree = view.out_degree(source_idx);
if out_degree > 0 {
sum_incoming += scores[source_idx] / out_degree as f64;
}
}
*next_score = base_score + d * (sum_incoming + dangling_contrib);
(*next_score - scores[i]).abs()
}).sum::<f64>()
} else {
let mut diff = 0.0;
for i in 0..n {
let mut sum_incoming = 0.0;
for &source_idx in view.predecessors(i) {
let out_degree = view.out_degree(source_idx);
if out_degree > 0 {
sum_incoming += scores[source_idx] / out_degree as f64;
}
}
next_scores[i] = base_score + d * (sum_incoming + dangling_contrib);
diff += (next_scores[i] - scores[i]).abs();
}
diff
};
scores.copy_from_slice(&next_scores);
if total_diff < config.tolerance {
break;
}
}
let mut result = HashMap::with_capacity(n);
for (idx, score) in scores.into_iter().enumerate() {
result.insert(view.index_to_node[idx], score);
}
result
}
#[cfg(test)]
mod tests {
use super::*;
use crate::common::GraphView;
fn build_triangle_graph() -> GraphView {
let node_count = 3;
let index_to_node = vec![1, 2, 3];
let mut node_to_index = HashMap::new();
for (i, &id) in index_to_node.iter().enumerate() {
node_to_index.insert(id, i);
}
let outgoing = vec![vec![1], vec![2], vec![0]]; let incoming = vec![vec![2], vec![0], vec![1]]; GraphView::from_adjacency_list(node_count, index_to_node, node_to_index, outgoing, incoming, None)
}
fn build_star_graph() -> GraphView {
let node_count = 4;
let index_to_node = vec![1, 2, 3, 4];
let mut node_to_index = HashMap::new();
for (i, &id) in index_to_node.iter().enumerate() {
node_to_index.insert(id, i);
}
let outgoing = vec![vec![1, 2, 3], vec![], vec![], vec![]];
let incoming = vec![vec![], vec![0], vec![0], vec![0]];
GraphView::from_adjacency_list(node_count, index_to_node, node_to_index, outgoing, incoming, None)
}
#[test]
fn test_pagerank_empty_graph() {
let view = GraphView::from_adjacency_list(
0, vec![], HashMap::new(), vec![], vec![], None,
);
let result = page_rank(&view, PageRankConfig::default());
assert!(result.is_empty());
}
#[test]
fn test_pagerank_single_node() {
let mut node_to_index = HashMap::new();
node_to_index.insert(1u64, 0);
let view = GraphView::from_adjacency_list(
1, vec![1], node_to_index, vec![vec![]], vec![vec![]], None,
);
let result = page_rank(&view, PageRankConfig {
damping_factor: 0.85,
iterations: 20,
tolerance: 0.0001,
dangling_redistribution: true,
});
assert_eq!(result.len(), 1);
let score = result[&1];
assert!((score - 1.0).abs() < 0.01, "Single node score should be ~1.0, got {}", score);
}
#[test]
fn test_pagerank_triangle_symmetric() {
let view = build_triangle_graph();
let result = page_rank(&view, PageRankConfig {
damping_factor: 0.85,
iterations: 100,
tolerance: 1e-10,
dangling_redistribution: true,
});
assert_eq!(result.len(), 3);
let scores: Vec<f64> = vec![result[&1], result[&2], result[&3]];
let avg = scores.iter().sum::<f64>() / 3.0;
for s in &scores {
assert!((s - avg).abs() < 0.001, "Triangle nodes should have equal rank, got {:?}", scores);
}
}
#[test]
fn test_pagerank_star_hub_has_lowest_rank() {
let view = build_star_graph();
let result = page_rank(&view, PageRankConfig {
damping_factor: 0.85,
iterations: 50,
tolerance: 1e-10,
dangling_redistribution: false,
});
assert_eq!(result.len(), 4);
let hub_score = result[&1];
let leaf_score = result[&2];
assert!(leaf_score > hub_score,
"Leaf ({}) should have higher rank than hub ({}) without dangling redistribution",
leaf_score, hub_score);
}
#[test]
fn test_pagerank_scores_sum_to_one() {
let view = build_triangle_graph();
let result = page_rank(&view, PageRankConfig {
damping_factor: 0.85,
iterations: 100,
tolerance: 1e-10,
dangling_redistribution: true,
});
let total: f64 = result.values().sum();
assert!((total - 1.0).abs() < 0.01,
"PageRank scores should sum to ~1.0 with dangling redistribution, got {}", total);
}
#[test]
fn test_pagerank_convergence() {
let view = build_triangle_graph();
let result_1 = page_rank(&view, PageRankConfig {
damping_factor: 0.85,
iterations: 1,
tolerance: 0.0,
dangling_redistribution: true,
});
let result_100 = page_rank(&view, PageRankConfig {
damping_factor: 0.85,
iterations: 100,
tolerance: 0.0,
dangling_redistribution: true,
});
let target = 1.0 / 3.0;
let diff_1 = (result_1[&1] - target).abs();
let diff_100 = (result_100[&1] - target).abs();
assert!(diff_100 <= diff_1,
"100 iterations should be closer to target than 1: diff_1={}, diff_100={}", diff_1, diff_100);
}
#[test]
fn test_pagerank_dangling_redistribution_flag() {
let view = build_star_graph(); let with_dangling = page_rank(&view, PageRankConfig {
damping_factor: 0.85,
iterations: 50,
tolerance: 1e-10,
dangling_redistribution: true,
});
let without_dangling = page_rank(&view, PageRankConfig {
damping_factor: 0.85,
iterations: 50,
tolerance: 1e-10,
dangling_redistribution: false,
});
let total_with: f64 = with_dangling.values().sum();
assert!((total_with - 1.0).abs() < 0.01,
"With dangling redistribution, sum should be ~1.0, got {}", total_with);
let total_without: f64 = without_dangling.values().sum();
assert!(total_without < 0.9,
"Without dangling redistribution, rank should leak, got sum={}", total_without);
}
#[test]
fn test_pagerank_damping_factor_effect() {
let view = build_triangle_graph();
let low_damping = page_rank(&view, PageRankConfig {
damping_factor: 0.5,
iterations: 100,
tolerance: 1e-10,
dangling_redistribution: true,
});
let high_damping = page_rank(&view, PageRankConfig {
damping_factor: 0.99,
iterations: 100,
tolerance: 1e-10,
dangling_redistribution: true,
});
let total_low: f64 = low_damping.values().sum();
let total_high: f64 = high_damping.values().sum();
assert!((total_low - 1.0).abs() < 0.01);
assert!((total_high - 1.0).abs() < 0.01);
}
}