rust_igraph/algorithms/properties/
pagerank_linsys.rs1use crate::core::{Graph, IgraphResult};
47
48const ALPHA: f64 = 0.85;
50const RESTART_DIM: usize = 30;
52const MAX_RESTARTS: usize = 50;
54const TOL: f64 = 1e-13;
59
60pub fn pagerank_linsys(graph: &Graph) -> IgraphResult<Vec<f64>> {
100 let n = graph.vcount();
101 let n_us = n as usize;
102 if n == 0 {
103 return Ok(Vec::new());
104 }
105 if n == 1 {
106 return Ok(vec![1.0]);
107 }
108
109 let n_f = f64::from(n);
110 let adj = build_adjacency(graph)?;
111
112 let rhs_val = (1.0 - ALPHA) / n_f;
114 let rhs_norm = rhs_val * (n_f.sqrt());
115 let stop = TOL * rhs_norm.max(1e-300);
116
117 let mut x_iter = vec![1.0 / n_f; n_us];
119
120 let mut residual = vec![0.0_f64; n_us];
122 let mut ax = vec![0.0_f64; n_us];
123 let mut basis_flat = vec![0.0_f64; (RESTART_DIM + 1) * n_us];
124 let mut hcols_flat = vec![0.0_f64; RESTART_DIM * (RESTART_DIM + 1)];
125 let mut w_scratch = vec![0.0_f64; n_us];
126 let mut cos_v = vec![0.0_f64; RESTART_DIM];
127 let mut sin_v = vec![0.0_f64; RESTART_DIM];
128 let mut g_rot = vec![0.0_f64; RESTART_DIM + 1];
129 let mut y_solve = vec![0.0_f64; RESTART_DIM];
130
131 for _restart in 0..MAX_RESTARTS {
132 apply_a(&x_iter, &mut ax, &adj, n_f);
133 for i in 0..n_us {
134 residual[i] = rhs_val - ax[i];
135 }
136 let beta = norm2(&residual);
137 if beta < stop {
138 break;
139 }
140 let converged = gmres_inner(
141 &mut x_iter,
142 &residual,
143 beta,
144 stop,
145 &adj,
146 n_f,
147 &mut basis_flat,
148 &mut hcols_flat,
149 &mut w_scratch,
150 &mut cos_v,
151 &mut sin_v,
152 &mut g_rot,
153 &mut y_solve,
154 );
155 if converged {
156 break;
157 }
158 }
159
160 Ok(x_iter)
161}
162
163struct Adj {
164 incoming: Vec<Vec<u32>>,
165 inv_out_deg: Vec<f64>,
166 dangling_ids: Vec<u32>,
167}
168
169fn build_adjacency(graph: &Graph) -> IgraphResult<Adj> {
170 let n = graph.vcount();
171 let n_us = n as usize;
172 let directed = graph.is_directed();
173
174 let mut out_deg = vec![0u64; n_us];
175 for vid in 0..n {
176 let nbrs = graph.neighbors(vid)?;
177 out_deg[vid as usize] = nbrs.len() as u64;
178 }
179
180 let mut in_adj: Vec<Vec<u32>> = vec![Vec::new(); n_us];
181 let edge_count = u32::try_from(graph.ecount())
182 .map_err(|_| crate::core::IgraphError::Internal("ecount overflows u32"))?;
183 if directed {
184 for eid in 0..edge_count {
185 let (src, dst) = graph.edge(eid)?;
186 in_adj[dst as usize].push(src);
187 }
188 } else {
189 for eid in 0..edge_count {
190 let (src, dst) = graph.edge(eid)?;
191 if src == dst {
192 in_adj[dst as usize].push(src);
193 in_adj[dst as usize].push(src);
194 } else {
195 in_adj[src as usize].push(dst);
196 in_adj[dst as usize].push(src);
197 }
198 }
199 }
200
201 let mut inv_out_deg = vec![0.0_f64; n_us];
205 for v in 0..n_us {
206 if out_deg[v] > 0 {
207 #[allow(clippy::cast_precision_loss)]
208 let d = out_deg[v] as f64;
209 inv_out_deg[v] = 1.0 / d;
210 }
211 }
212
213 let dangling_ids: Vec<u32> = (0..n).filter(|&v| out_deg[v as usize] == 0).collect();
214 Ok(Adj {
215 incoming: in_adj,
216 inv_out_deg,
217 dangling_ids,
218 })
219}
220
221fn apply_a(x: &[f64], y: &mut [f64], adj: &Adj, n_f: f64) {
222 let mut dangling_sum = 0.0_f64;
223 for &sid in &adj.dangling_ids {
224 dangling_sum += x[sid as usize];
225 }
226 let dangling_share = dangling_sum / n_f;
227 let off = ALPHA * dangling_share;
228 for v in 0..y.len() {
229 let mut acc = 0.0_f64;
230 for &u in &adj.incoming[v] {
231 acc += x[u as usize] * adj.inv_out_deg[u as usize];
235 }
236 y[v] = x[v] - ALPHA * acc - off;
237 }
238}
239
240#[allow(clippy::too_many_arguments)]
243fn gmres_inner(
244 x_iter: &mut [f64],
245 r0: &[f64],
246 beta: f64,
247 stop: f64,
248 adj: &Adj,
249 n_f: f64,
250 basis_flat: &mut [f64],
251 hcols_flat: &mut [f64],
252 w: &mut [f64],
253 cos_v: &mut [f64],
254 sin_v: &mut [f64],
255 g_rot: &mut [f64],
256 y_solve: &mut [f64],
257) -> bool {
258 let n_us = x_iter.len();
259 let inv_beta = 1.0 / beta;
260
261 for i in 0..n_us {
263 basis_flat[i] = r0[i] * inv_beta;
264 }
265
266 for g in g_rot.iter_mut() {
267 *g = 0.0;
268 }
269 g_rot[0] = beta;
270
271 let col_stride = RESTART_DIM + 1;
272 let mut steps = 0_usize;
273 let mut converged = false;
274
275 for j in 0..RESTART_DIM {
276 let (vj, _rest) = basis_flat[j * n_us..].split_at(n_us);
278 apply_a(vj, w, adj, n_f);
279
280 let h_col_start = j * col_stride;
282 for i in 0..=j {
283 let vi = &basis_flat[i * n_us..(i + 1) * n_us];
284 let h_ij = dot(w, vi);
285 hcols_flat[h_col_start + i] = h_ij;
286 for k in 0..n_us {
287 w[k] -= h_ij * vi[k];
288 }
289 }
290 let h_next = norm2(w);
291 hcols_flat[h_col_start + j + 1] = h_next;
292
293 for i in 0..j {
295 let a = hcols_flat[h_col_start + i];
296 let b = hcols_flat[h_col_start + i + 1];
297 hcols_flat[h_col_start + i] = cos_v[i] * a + sin_v[i] * b;
298 hcols_flat[h_col_start + i + 1] = -sin_v[i] * a + cos_v[i] * b;
299 }
300
301 let diag = hcols_flat[h_col_start + j];
303 let subdiag = hcols_flat[h_col_start + j + 1];
304 let denom = (diag * diag + subdiag * subdiag).sqrt();
305 let (c_new, s_new) = if denom > 0.0 {
306 (diag / denom, subdiag / denom)
307 } else {
308 (1.0_f64, 0.0_f64)
309 };
310 cos_v[j] = c_new;
311 sin_v[j] = s_new;
312
313 hcols_flat[h_col_start + j] = c_new * diag + s_new * subdiag;
314 hcols_flat[h_col_start + j + 1] = 0.0;
315 let g_temp = c_new * g_rot[j] + s_new * g_rot[j + 1];
316 g_rot[j + 1] = -s_new * g_rot[j] + c_new * g_rot[j + 1];
317 g_rot[j] = g_temp;
318
319 steps = j + 1;
320
321 if g_rot[j + 1].abs() < stop {
322 converged = true;
323 break;
324 }
325 if h_next.abs() <= 1e-300 {
326 break;
327 }
328 if j + 1 < RESTART_DIM {
329 let inv_h_next = 1.0 / h_next;
330 let dst_start = (j + 1) * n_us;
331 for k in 0..n_us {
332 basis_flat[dst_start + k] = w[k] * inv_h_next;
333 }
334 }
335 }
336
337 for i in (0..steps).rev() {
339 let mut sum = g_rot[i];
340 for k in (i + 1)..steps {
341 sum -= hcols_flat[k * col_stride + i] * y_solve[k];
342 }
343 y_solve[i] = sum / hcols_flat[i * col_stride + i];
346 }
347
348 for k in 0..steps {
350 let yk = y_solve[k];
351 let vk = &basis_flat[k * n_us..(k + 1) * n_us];
352 for i in 0..n_us {
353 x_iter[i] += yk * vk[i];
354 }
355 }
356
357 converged
358}
359
360#[inline]
361fn dot(a: &[f64], b: &[f64]) -> f64 {
362 let mut s = 0.0_f64;
363 for i in 0..a.len() {
364 s += a[i] * b[i];
365 }
366 s
367}
368
369#[inline]
370fn norm2(a: &[f64]) -> f64 {
371 dot(a, a).sqrt()
372}
373
374#[cfg(test)]
375mod tests {
376 use super::*;
377
378 fn close(actual: &[f64], expected: &[f64], tol: f64) {
379 assert_eq!(actual.len(), expected.len(), "length mismatch");
380 for (i, (a, e)) in actual.iter().zip(expected.iter()).enumerate() {
381 assert!((a - e).abs() < tol, "vertex {i}: actual={a} expected={e}");
382 }
383 }
384
385 #[test]
386 fn empty_graph_yields_empty() {
387 let g = Graph::with_vertices(0);
388 assert!(pagerank_linsys(&g).unwrap().is_empty());
389 }
390
391 #[test]
392 fn singleton_yields_one() {
393 let g = Graph::with_vertices(1);
394 assert_eq!(pagerank_linsys(&g).unwrap(), vec![1.0]);
395 }
396
397 #[test]
398 fn isolated_vertices_uniform() {
399 let g = Graph::with_vertices(4);
400 let pr = pagerank_linsys(&g).unwrap();
401 close(&pr, &[0.25, 0.25, 0.25, 0.25], 1e-12);
402 }
403
404 #[test]
405 fn triangle_uniform_one_third() {
406 let mut g = Graph::with_vertices(3);
407 g.add_edge(0, 1).unwrap();
408 g.add_edge(1, 2).unwrap();
409 g.add_edge(2, 0).unwrap();
410 let pr = pagerank_linsys(&g).unwrap();
411 close(&pr, &[1.0 / 3.0, 1.0 / 3.0, 1.0 / 3.0], 1e-12);
412 }
413
414 #[test]
415 fn directed_4cycle_uniform_quarter() {
416 let mut g = Graph::new(4, true).unwrap();
417 for i in 0..4u32 {
418 g.add_edge(i, (i + 1) % 4).unwrap();
419 }
420 let pr = pagerank_linsys(&g).unwrap();
421 close(&pr, &[0.25, 0.25, 0.25, 0.25], 1e-12);
422 }
423
424 #[test]
425 fn matches_power_iter_k4_minus_edge() {
426 use crate::pagerank;
427 let mut g = Graph::with_vertices(4);
428 g.add_edge(0, 1).unwrap();
429 g.add_edge(0, 2).unwrap();
430 g.add_edge(1, 2).unwrap();
431 g.add_edge(1, 3).unwrap();
432 g.add_edge(2, 3).unwrap();
433 let a = pagerank(&g).unwrap();
434 let b = pagerank_linsys(&g).unwrap();
435 for i in 0..4 {
436 assert!((a[i] - b[i]).abs() < 1e-9, "i={i}: {} vs {}", a[i], b[i]);
437 }
438 }
439
440 #[test]
441 fn matches_power_iter_directed_dangling() {
442 use crate::pagerank;
443 let mut g = Graph::new(2, true).unwrap();
444 g.add_edge(0, 1).unwrap();
445 let a = pagerank(&g).unwrap();
446 let b = pagerank_linsys(&g).unwrap();
447 for i in 0..2 {
448 assert!((a[i] - b[i]).abs() < 1e-9, "i={i}: {} vs {}", a[i], b[i]);
449 }
450 }
451
452 #[test]
453 fn matches_power_iter_star() {
454 use crate::pagerank;
455 let mut g = Graph::with_vertices(8);
456 for v in 1..8u32 {
457 g.add_edge(0, v).unwrap();
458 }
459 let a = pagerank(&g).unwrap();
460 let b = pagerank_linsys(&g).unwrap();
461 for i in 0..8 {
462 assert!((a[i] - b[i]).abs() < 1e-9, "i={i}: {} vs {}", a[i], b[i]);
463 }
464 }
465
466 #[test]
467 fn sums_to_one() {
468 let mut g = Graph::with_vertices(6);
469 g.add_edge(0, 1).unwrap();
470 g.add_edge(1, 2).unwrap();
471 g.add_edge(2, 3).unwrap();
472 g.add_edge(3, 4).unwrap();
473 g.add_edge(4, 5).unwrap();
474 g.add_edge(5, 0).unwrap();
475 let pr = pagerank_linsys(&g).unwrap();
476 let total: f64 = pr.iter().sum();
477 assert!((total - 1.0).abs() < 1e-9, "sum {total} != 1.0");
478 }
479}