rust_igraph/algorithms/properties/
pagerank_weighted.rs1use 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
31pub 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 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 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 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 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 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}