use crate::Snapshot;
pub const DAMPING: f32 = 0.85;
pub const EPSILON: f64 = 1e-6;
pub const ROUNDS: u32 = 100;
#[derive(Debug, Clone, Default)]
pub struct Rank {
of: Vec<f32>,
rounds: u32,
delta: f64,
settled: bool,
}
impl Rank {
#[must_use]
pub fn of(&self, node: u32) -> f32 {
self.of[node as usize]
}
#[must_use]
pub fn scores(&self) -> &[f32] {
&self.of
}
#[must_use]
pub fn rounds(&self) -> u32 {
self.rounds
}
#[must_use]
pub fn delta(&self) -> f64 {
self.delta
}
#[must_use]
pub fn converged(&self) -> bool {
self.settled
}
#[must_use]
pub fn top(&self, k: usize) -> Vec<(u32, f32)> {
let mut all: Vec<(u32, f32)> = self
.of
.iter()
.enumerate()
.map(|(at, score)| (at as u32, *score))
.collect();
let k = k.min(all.len());
if k < all.len() {
all.select_nth_unstable_by(k, |a, b| better(*a, *b));
all.truncate(k);
}
all.sort_unstable_by(|a, b| better(*a, *b));
all
}
}
fn better(a: (u32, f32), b: (u32, f32)) -> std::cmp::Ordering {
b.1.total_cmp(&a.1).then(a.0.cmp(&b.0))
}
#[must_use]
pub fn pagerank(g: &Snapshot) -> Rank {
pagerank_with(g, DAMPING, EPSILON, ROUNDS)
}
#[must_use]
pub fn pagerank_with(g: &Snapshot, damping: f32, epsilon: f64, rounds: u32) -> Rank {
let n = g.nodes() as usize;
if n == 0 {
return Rank {
settled: true,
..Rank::default()
};
}
let start = 1.0 / n as f32;
let mut score = vec![start; n];
let mut share = vec![0f32; n];
let mut delta = f64::INFINITY;
let mut round = 0;
while round < rounds && delta >= epsilon {
let mut stuck = 0f64;
for node in 0..n {
let out = g.out_degree(node as u32);
if out == 0 {
stuck += f64::from(score[node]);
share[node] = 0.0;
} else {
share[node] = score[node] / out as f32;
}
}
let base = ((1.0 - f64::from(damping)) + f64::from(damping) * stuck) / n as f64;
delta = 0.0;
for (node, score) in score.iter_mut().enumerate() {
let mut sum = 0f64;
for from in g.into_(node as u32) {
sum += f64::from(share[*from as usize]);
}
let next = base + f64::from(damping) * sum;
delta += (next - f64::from(*score)).abs();
*score = next as f32;
}
round += 1;
}
Rank {
of: score,
rounds: round,
delta,
settled: delta < epsilon,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::graph::NO_PROPS;
use crate::{Graph, Snapshot};
use yo_common::Rng;
fn reference(g: &Snapshot, rounds: u32) -> Vec<f64> {
let n = g.nodes() as usize;
let d = f64::from(DAMPING);
let mut score = vec![1.0 / n as f64; n];
for _ in 0..rounds {
let mut next = vec![0f64; n];
let mut stuck = 0f64;
for (node, score) in score.iter().enumerate() {
let out = g.out_degree(node as u32);
if out == 0 {
stuck += score;
} else {
let share = score / f64::from(out);
for to in g.out(node as u32) {
next[*to as usize] += share;
}
}
}
let base = ((1.0 - d) + d * stuck) / n as f64;
for got in &mut next {
*got = base + d * *got;
}
score = next;
}
score
}
fn linked(edges: &[(u64, u64)]) -> Graph {
let mut g = Graph::new();
for (from, to) in edges {
g.link(*from, *to, 1, NO_PROPS).expect("an edge");
}
g
}
#[test]
fn a_ring_gives_everybody_the_same_score() {
let edges: Vec<(u64, u64)> = (0..10u64).map(|i| (i, (i + 1) % 10)).collect();
let s = Snapshot::of(&linked(&edges));
let r = pagerank(&s);
assert!(r.converged(), "a ring settles");
for node in 0..s.nodes() {
assert!((r.of(node) - 0.1).abs() < 1e-5, "{}", r.of(node));
}
}
#[test]
fn the_node_everybody_points_at_wins() {
let edges: Vec<(u64, u64)> = (0..20u64).map(|i| (i, 100)).collect();
let s = Snapshot::of(&linked(&edges));
let r = pagerank(&s);
let hub = s.dense(100).expect("the hub");
let top = r.top(3);
assert_eq!(top[0].0, hub);
assert!(top[0].1 > 10.0 * top[1].1, "{top:?}");
}
#[test]
fn the_scores_add_up_to_one() {
let s = Snapshot::of(&linked(&[(0, 1), (1, 2), (3, 1), (4, 5)]));
let r = pagerank(&s);
let total: f64 = r.scores().iter().map(|s| f64::from(*s)).sum();
assert!((total - 1.0).abs() < 1e-4, "{total}");
}
#[test]
fn no_damping_is_the_uniform_vector() {
let s = Snapshot::of(&linked(&[(0, 1), (1, 2), (2, 0), (3, 0)]));
let r = pagerank_with(&s, 0.0, EPSILON, ROUNDS);
for node in 0..s.nodes() {
assert!((r.of(node) - 0.25).abs() < 1e-6, "{}", r.of(node));
}
}
#[test]
fn an_empty_graph_has_no_scores() {
let r = pagerank(&Snapshot::default());
assert!(r.scores().is_empty());
assert_eq!(r.rounds(), 0);
assert!(r.top(5).is_empty());
}
#[test]
fn one_node_holds_everything() {
let mut g = Graph::new();
g.add_node(7).expect("a node");
let r = pagerank(&Snapshot::of(&g));
assert!((r.of(0) - 1.0).abs() < 1e-6, "{}", r.of(0));
}
#[test]
fn a_self_loop_keeps_what_it_is_given() {
let s = Snapshot::of(&linked(&[(0, 0), (1, 0), (2, 0)]));
let r = pagerank(&s);
let sink = s.dense(0).expect("the sink");
assert!(r.of(sink) > 0.7, "{}", r.of(sink));
}
#[test]
fn two_runs_agree_to_the_bit() {
let mut rng = Rng::new(0x51ee);
let (wanted, nodes) = if cfg!(miri) { (60, 30) } else { (2000, 300) };
let edges: Vec<(u64, u64)> = (0..wanted)
.map(|_| (rng.next_u64() % nodes, rng.next_u64() % nodes))
.collect();
let s = Snapshot::of(&linked(&edges));
assert_eq!(pagerank(&s).scores(), pagerank(&s).scores());
}
#[test]
fn it_says_when_it_ran_out_of_rounds() {
let s = Snapshot::of(&linked(&[(0, 1), (1, 2), (2, 0)]));
let r = pagerank_with(&s, DAMPING, 1e-30, 5);
assert_eq!(r.rounds(), 5);
assert!(!r.converged());
assert!(r.delta() > 0.0);
}
#[test]
fn it_agrees_with_the_slow_one() {
let mut rng = Rng::new(0xbead);
let (cases, spread) = if cfg!(miri) { (3, 8) } else { (40, 60) };
for case in 0..cases {
let nodes = 2 + rng.next_u64() % spread;
let edges: Vec<(u64, u64)> = (0..nodes * 3)
.map(|_| (rng.next_u64() % nodes, rng.next_u64() % nodes))
.collect();
let s = Snapshot::of(&linked(&edges));
let mine = pagerank(&s);
let theirs = reference(&s, mine.rounds());
for node in 0..s.nodes() {
let (a, b) = (f64::from(mine.of(node)), theirs[node as usize]);
assert!((a - b).abs() < 1e-5, "case {case} node {node}: {a} {b}");
}
}
}
}