Skip to main content

rust_igraph/algorithms/properties/
pagerank_weighted.rs

1//! Weighted `PageRank` centrality (ALGO-PR-011b).
2//!
3//! Counterpart of `igraph_pagerank(_, IGRAPH_PAGERANK_ALGO_POWER, _, _,
4//! vss_all(), directed, 0.85, &weights, NULL_options)` from
5//! `references/igraph/src/centrality/pagerank.c`. Same power-iteration
6//! framework as PR-011 but every edge `u → v` contributes its weight
7//! into the in-flow term:
8//!
9//! ```text
10//! PR_new[v] = (1 - d) / N
11//!           + d * Σ_{u → v} w(u, v) * PR[u] / out_strength(u)
12//!           + d * dangling_sum / N
13//! ```
14//!
15//! where `out_strength(u) = Σ_{e: u → x} w(e)` and a vertex is
16//! "dangling" iff its `out_strength == 0`. For undirected graphs each
17//! edge contributes to both endpoints' strengths and propagates rank
18//! in both directions.
19//!
20//! Phase-1 minimal slice: damping `0.85`, `eps = 1e-10`, `max_iter =
21//! 1000`. Weights must be non-negative + finite; violations return
22//! [`crate::IgraphError::InvalidArgument`].
23
24use crate::core::graph::EdgeId;
25use crate::core::{Graph, IgraphError, IgraphResult};
26
27const DEFAULT_DAMPING: f64 = 0.85;
28const DEFAULT_EPS: f64 = 1e-10;
29const DEFAULT_MAX_ITER: usize = 1000;
30
31/// Weighted `PageRank` scores via power iteration with damping `0.85`.
32///
33/// Returns a `Vec<f64>` summing approximately to 1. `weights[e]` is
34/// the weight of edge `e`; weights must be non-negative and finite,
35/// and `weights.len()` must equal `graph.ecount()`.
36///
37/// # Examples
38///
39/// ```
40/// use rust_igraph::{Graph, pagerank_weighted};
41///
42/// // Triangle with unit weights → matches unweighted PR-011 (uniform 1/3).
43/// let mut g = Graph::with_vertices(3);
44/// g.add_edge(0, 1).unwrap();
45/// g.add_edge(1, 2).unwrap();
46/// g.add_edge(2, 0).unwrap();
47/// let pr = pagerank_weighted(&g, &[1.0, 1.0, 1.0]).unwrap();
48/// for v in 0..3 {
49///     assert!((pr[v] - 1.0/3.0).abs() < 1e-9);
50/// }
51/// ```
52pub fn pagerank_weighted(graph: &Graph, weights: &[f64]) -> IgraphResult<Vec<f64>> {
53    let n = graph.vcount();
54    let n_us = n as usize;
55    if n == 0 {
56        return Ok(Vec::new());
57    }
58    if n == 1 {
59        return Ok(vec![1.0]);
60    }
61
62    let m = graph.ecount();
63    if weights.len() != m {
64        return Err(IgraphError::InvalidArgument(format!(
65            "weights vector size ({}) differs from edge count ({})",
66            weights.len(),
67            m
68        )));
69    }
70    for (e, &w) in weights.iter().enumerate() {
71        if w.is_nan() || w < 0.0 || !w.is_finite() {
72            return Err(IgraphError::InvalidArgument(format!(
73                "weight at edge {e} must be non-negative and finite (got {w})"
74            )));
75        }
76    }
77
78    let directed = graph.is_directed();
79    let n_f = f64::from(n);
80
81    // Out-strength per vertex + weighted in-adjacency `(u, w/strength)`.
82    let mut out_strength = vec![0.0_f64; n_us];
83    let m_u = u32::try_from(m).map_err(|_| IgraphError::Internal("ecount overflows u32"))?;
84    if directed {
85        for e in 0..m_u {
86            let (u, _v) = graph.edge(e as EdgeId)?;
87            out_strength[u as usize] += weights[e as usize];
88        }
89    } else {
90        for e in 0..m_u {
91            let (u, v) = graph.edge(e as EdgeId)?;
92            // Self-loop: contributes 2*w to its vertex's "out-strength"
93            // because both directions count.
94            if u == v {
95                out_strength[u as usize] += 2.0 * weights[e as usize];
96            } else {
97                out_strength[u as usize] += weights[e as usize];
98                out_strength[v as usize] += weights[e as usize];
99            }
100        }
101    }
102
103    // Build weighted in-adjacency: for each in-edge u→v contribute
104    // (u, weight(u→v)) so the power-iteration loop can compute
105    //     incoming[v] = Σ (w * PR[u] / out_strength[u]).
106    let mut in_adj: Vec<Vec<(u32, f64)>> = vec![Vec::new(); n_us];
107    if directed {
108        for e in 0..m_u {
109            let (u, v) = graph.edge(e as EdgeId)?;
110            in_adj[v as usize].push((u, weights[e as usize]));
111        }
112    } else {
113        for e in 0..m_u {
114            let (u, v) = graph.edge(e as EdgeId)?;
115            let edge_w = weights[e as usize];
116            if u == v {
117                in_adj[v as usize].push((u, edge_w));
118                in_adj[v as usize].push((u, edge_w));
119            } else {
120                in_adj[u as usize].push((v, edge_w));
121                in_adj[v as usize].push((u, edge_w));
122            }
123        }
124    }
125
126    let mut pr = vec![1.0 / n_f; n_us];
127    let mut pr_new = vec![0.0_f64; n_us];
128
129    for _ in 0..DEFAULT_MAX_ITER {
130        let mut dangling_sum: f64 = 0.0;
131        for v in 0..n_us {
132            if out_strength[v] == 0.0 {
133                dangling_sum += pr[v];
134            }
135        }
136
137        let teleport = (1.0 - DEFAULT_DAMPING) / n_f;
138        let dangling_share = DEFAULT_DAMPING * dangling_sum / n_f;
139
140        for v in 0..n_us {
141            let mut incoming: f64 = 0.0;
142            for &(u, w) in &in_adj[v] {
143                let s = out_strength[u as usize];
144                if s > 0.0 {
145                    incoming += w * pr[u as usize] / s;
146                }
147            }
148            pr_new[v] = teleport + dangling_share + DEFAULT_DAMPING * incoming;
149        }
150
151        let mut diff: f64 = 0.0;
152        for v in 0..n_us {
153            diff += (pr_new[v] - pr[v]).abs();
154        }
155        std::mem::swap(&mut pr, &mut pr_new);
156        if diff < DEFAULT_EPS {
157            break;
158        }
159    }
160
161    Ok(pr)
162}
163
164#[cfg(test)]
165mod tests {
166    use super::*;
167
168    fn close(actual: &[f64], expected: &[f64], tol: f64) {
169        assert_eq!(actual.len(), expected.len(), "length mismatch");
170        for (i, (a, e)) in actual.iter().zip(expected.iter()).enumerate() {
171            assert!((a - e).abs() < tol, "vertex {i}: actual={a} expected={e}");
172        }
173    }
174
175    #[test]
176    fn empty_graph_yields_empty() {
177        let g = Graph::with_vertices(0);
178        assert!(pagerank_weighted(&g, &[]).unwrap().is_empty());
179    }
180
181    #[test]
182    fn singleton_yields_one() {
183        let g = Graph::with_vertices(1);
184        assert_eq!(pagerank_weighted(&g, &[]).unwrap(), vec![1.0]);
185    }
186
187    #[test]
188    fn isolated_vertices_uniform() {
189        let g = Graph::with_vertices(4);
190        let pr = pagerank_weighted(&g, &[]).unwrap();
191        close(&pr, &[0.25, 0.25, 0.25, 0.25], 1e-9);
192    }
193
194    #[test]
195    fn unit_weights_match_unweighted_triangle() {
196        let mut g = Graph::with_vertices(3);
197        g.add_edge(0, 1).unwrap();
198        g.add_edge(1, 2).unwrap();
199        g.add_edge(2, 0).unwrap();
200        let pw = pagerank_weighted(&g, &[1.0; 3]).unwrap();
201        let pu = crate::pagerank(&g).unwrap();
202        for (a, b) in pw.iter().zip(pu.iter()) {
203            assert!((a - b).abs() < 1e-9, "{a} vs {b}");
204        }
205    }
206
207    #[test]
208    fn unit_weights_match_unweighted_directed_4cycle() {
209        let mut g = Graph::new(4, true).unwrap();
210        for i in 0..4u32 {
211            g.add_edge(i, (i + 1) % 4).unwrap();
212        }
213        let pw = pagerank_weighted(&g, &[1.0; 4]).unwrap();
214        let pu = crate::pagerank(&g).unwrap();
215        for (a, b) in pw.iter().zip(pu.iter()) {
216            assert!((a - b).abs() < 1e-9, "{a} vs {b}");
217        }
218    }
219
220    #[test]
221    fn pagerank_weighted_sums_to_one() {
222        // K4 minus an edge with non-uniform weights.
223        let mut g = Graph::with_vertices(4);
224        g.add_edge(0, 1).unwrap();
225        g.add_edge(0, 2).unwrap();
226        g.add_edge(1, 2).unwrap();
227        g.add_edge(1, 3).unwrap();
228        g.add_edge(2, 3).unwrap();
229        let pr = pagerank_weighted(&g, &[1.0, 2.0, 0.5, 1.5, 1.0]).unwrap();
230        let total: f64 = pr.iter().sum();
231        assert!((total - 1.0).abs() < 1e-9, "sum {total} != 1.0");
232    }
233
234    #[test]
235    fn heavy_edge_concentrates_pagerank() {
236        // Directed 0 → 1 with weight 100, plus 0 → 2 with weight 0.01.
237        // Almost all of 0's flow goes to 1, so 1 should have higher
238        // PageRank than 2.
239        let mut g = Graph::new(3, true).unwrap();
240        g.add_edge(0, 1).unwrap();
241        g.add_edge(0, 2).unwrap();
242        let pr = pagerank_weighted(&g, &[100.0, 0.01]).unwrap();
243        let total: f64 = pr.iter().sum();
244        assert!((total - 1.0).abs() < 1e-9);
245        assert!(
246            pr[1] > pr[2],
247            "heavy-edge target {} should beat light-edge {}",
248            pr[1],
249            pr[2]
250        );
251    }
252
253    #[test]
254    fn star_centre_has_higher_pagerank_than_leaves() {
255        let mut g = Graph::with_vertices(4);
256        for v in 1..4 {
257            g.add_edge(0, v).unwrap();
258        }
259        let pr = pagerank_weighted(&g, &[1.0; 3]).unwrap();
260        for &leaf in &pr[1..4] {
261            assert!(pr[0] > leaf, "centre {} not > leaf {}", pr[0], leaf);
262        }
263    }
264
265    #[test]
266    fn weights_size_mismatch_errors() {
267        let mut g = Graph::with_vertices(2);
268        g.add_edge(0, 1).unwrap();
269        assert!(pagerank_weighted(&g, &[]).is_err());
270    }
271
272    #[test]
273    fn negative_weight_errors() {
274        let mut g = Graph::with_vertices(2);
275        g.add_edge(0, 1).unwrap();
276        assert!(pagerank_weighted(&g, &[-1.0]).is_err());
277    }
278
279    #[test]
280    fn nan_weight_errors() {
281        let mut g = Graph::with_vertices(2);
282        g.add_edge(0, 1).unwrap();
283        assert!(pagerank_weighted(&g, &[f64::NAN]).is_err());
284    }
285}