Skip to main content

34_tiny_pagerank/
34_tiny_pagerank.rs

1//! # Example: Tiny PageRank
2//!
3//! Run: cargo run --example 34_tiny_pagerank
4//!
5//! ## Problem
6//! Rank the nodes of a tiny directed graph by importance, where a node is
7//! important if important nodes link to it — the core idea behind PageRank.
8//!
9//! ## Math idea
10//! Build a column-stochastic link matrix `M` (`M[i, j] = 1/outdeg(j)` when `j -> i`).
11//! With damping `d` and `N` nodes, power-iterate
12//! ```text
13//! r_next[i] = (1 - d)/N + d * (M · r)[i]
14//! ```
15//! until `r` settles. The `(1 - d)/N` term is the uniform "teleport" that keeps the
16//! walk from getting stuck.
17//!
18//! ## Tensor representation
19//! `M` is an N×N `Tensor`; the rank vector `r` is a length-N `Tensor`. The matrix
20//! product `M · r` is one `Tensor::matmul` (`[m, n] × [n] -> [m]`); the damping and
21//! teleport are ordinary Rust over `as_slice`.
22//!
23//! ## What this demonstrates
24//! - matrix × vector multiplication via `Tensor::matmul`;
25//! - power iteration (repeated multiply-and-renormalize);
26//! - composing `Tensor` math with plain Rust arithmetic.
27//!
28//! ## Expected output
29//! ```text
30//! PageRank after 50 iterations (d = 0.85):
31//!   node 0: 0.3725
32//!   node 1: 0.1958
33//!   node 2: 0.3941
34//!   node 3: 0.0375
35//! highest-ranked node: 2
36//! Tiny PageRank: OK
37//! ```
38
39use matten::Tensor;
40
41/// One PageRank power-iteration step.
42fn pagerank_step(m: &Tensor, r: &Tensor, damping: f64, n: usize) -> Tensor {
43    let mr = m.matmul(r); // [n, n] × [n] -> [n]
44    let base = (1.0 - damping) / n as f64;
45    let next: Vec<f64> = mr.as_slice().iter().map(|&x| base + damping * x).collect();
46    Tensor::from_vec(next)
47}
48
49fn main() {
50    // Directed graph on 4 nodes:
51    //   0 -> 1, 0 -> 2, 1 -> 2, 2 -> 0, 3 -> 2
52    // Column-stochastic link matrix M (M[i, j] = share of j's rank flowing to i):
53    //          from: 0    1   2   3
54    //   to 0 [ 0     0    1   0  ]
55    //   to 1 [ 0.5   0    0   0  ]
56    //   to 2 [ 0.5   1    0   1  ]
57    //   to 3 [ 0     0    0   0  ]
58    let m = Tensor::new(
59        vec![
60            0.0, 0.0, 1.0, 0.0, //
61            0.5, 0.0, 0.0, 0.0, //
62            0.5, 1.0, 0.0, 1.0, //
63            0.0, 0.0, 0.0, 0.0, //
64        ],
65        &[4, 4],
66    );
67
68    let n = 4;
69    let damping = 0.85;
70    let mut r = Tensor::from_vec(vec![0.25, 0.25, 0.25, 0.25]);
71    for _ in 0..50 {
72        r = pagerank_step(&m, &r, damping, n);
73    }
74
75    let ranks = r.as_slice();
76    println!("PageRank after 50 iterations (d = {damping}):");
77    for (i, &rank) in ranks.iter().enumerate() {
78        println!("  node {i}: {rank:.4}");
79    }
80
81    // The best-connected node should win; the link-less node 3 only gets teleport.
82    let top = ranks
83        .iter()
84        .enumerate()
85        .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
86        .map(|(i, _)| i)
87        .unwrap();
88    println!("highest-ranked node: {top}");
89
90    let total: f64 = ranks.iter().sum();
91    assert!((total - 1.0).abs() < 1e-9, "ranks must sum to 1");
92    assert_eq!(top, 2, "node 2 receives the most links");
93    println!("Tiny PageRank: OK");
94}