rust_igraph/algorithms/properties/
resistance.rs1#![allow(
14 clippy::cast_possible_truncation,
15 clippy::cast_precision_loss,
16 clippy::many_single_char_names,
17 clippy::needless_range_loop,
18 clippy::similar_names,
19 clippy::too_many_lines
20)]
21
22use crate::core::{Graph, IgraphError, IgraphResult};
23
24fn dense_laplacian(graph: &Graph) -> Vec<f64> {
26 let n = graph.vcount() as usize;
27 let mut lap = vec![0.0_f64; n * n];
28 for (u, v) in graph.edges() {
29 let ui = u as usize;
30 let vi = v as usize;
31 lap[ui * n + vi] -= 1.0;
32 lap[vi * n + ui] -= 1.0;
33 lap[ui * n + ui] += 1.0;
34 lap[vi * n + vi] += 1.0;
35 }
36 lap
37}
38
39fn jacobi_eigen_full(mat: &mut [f64], n: usize) -> (Vec<f64>, Vec<Vec<f64>>) {
42 if n == 0 {
43 return (Vec::new(), Vec::new());
44 }
45 if n == 1 {
46 return (vec![mat[0]], vec![vec![1.0]]);
47 }
48
49 let mut v = vec![0.0_f64; n * n];
50 for i in 0..n {
51 v[i * n + i] = 1.0;
52 }
53
54 let max_sweeps = 100;
55 for _ in 0..max_sweeps {
56 let mut max_off = 0.0_f64;
57 let mut p = 0;
58 let mut q = 1;
59 for i in 0..n {
60 for j in (i + 1)..n {
61 let val = mat[i * n + j].abs();
62 if val > max_off {
63 max_off = val;
64 p = i;
65 q = j;
66 }
67 }
68 }
69
70 if max_off < 1e-14 {
71 break;
72 }
73
74 let app = mat[p * n + p];
75 let aqq = mat[q * n + q];
76 let apq = mat[p * n + q];
77
78 let (cos, sin) = if (app - aqq).abs() < 1e-300 {
79 let s = std::f64::consts::FRAC_1_SQRT_2;
80 (s, s)
81 } else {
82 let tau = (aqq - app) / (2.0 * apq);
83 let t = if tau >= 0.0 {
84 1.0 / (tau + (1.0 + tau * tau).sqrt())
85 } else {
86 -1.0 / (-tau + (1.0 + tau * tau).sqrt())
87 };
88 let c = 1.0 / (1.0 + t * t).sqrt();
89 (c, t * c)
90 };
91
92 for i in 0..n {
93 if i == p || i == q {
94 continue;
95 }
96 let aip = mat[i * n + p];
97 let aiq = mat[i * n + q];
98 mat[i * n + p] = cos * aip - sin * aiq;
99 mat[p * n + i] = mat[i * n + p];
100 mat[i * n + q] = sin * aip + cos * aiq;
101 mat[q * n + i] = mat[i * n + q];
102 }
103
104 let new_pp = cos * cos * app - 2.0 * sin * cos * apq + sin * sin * aqq;
105 let new_qq = sin * sin * app + 2.0 * sin * cos * apq + cos * cos * aqq;
106 mat[p * n + p] = new_pp;
107 mat[q * n + q] = new_qq;
108 mat[p * n + q] = 0.0;
109 mat[q * n + p] = 0.0;
110
111 for i in 0..n {
112 let vip = v[i * n + p];
113 let viq = v[i * n + q];
114 v[i * n + p] = cos * vip - sin * viq;
115 v[i * n + q] = sin * vip + cos * viq;
116 }
117 }
118
119 let eigenvalues: Vec<f64> = (0..n).map(|i| mat[i * n + i]).collect();
120 let mut indices: Vec<usize> = (0..n).collect();
121 indices.sort_by(|&a, &b| {
122 eigenvalues[b]
123 .partial_cmp(&eigenvalues[a])
124 .unwrap_or(std::cmp::Ordering::Equal)
125 });
126
127 let sorted_vals: Vec<f64> = indices.iter().map(|&i| eigenvalues[i]).collect();
128 let sorted_vecs: Vec<Vec<f64>> = indices
129 .iter()
130 .map(|&idx| {
131 let mut col = vec![0.0_f64; n];
132 for i in 0..n {
133 col[i] = v[i * n + idx];
134 }
135 col
136 })
137 .collect();
138
139 (sorted_vals, sorted_vecs)
140}
141
142fn laplacian_pseudoinverse(graph: &Graph) -> Vec<f64> {
147 let n = graph.vcount() as usize;
148 if n == 0 {
149 return Vec::new();
150 }
151
152 let mut lap = dense_laplacian(graph);
153 let (eigenvalues, eigenvectors) = jacobi_eigen_full(&mut lap, n);
154
155 let mut lpinv = vec![0.0_f64; n * n];
156 let eps = 1e-10;
157
158 for (j, &lam) in eigenvalues.iter().enumerate() {
159 if lam.abs() < eps {
160 continue;
161 }
162 let inv_lam = 1.0 / lam;
163 let phi = &eigenvectors[j];
164 for u in 0..n {
165 for v in u..n {
166 let contrib = inv_lam * phi[u] * phi[v];
167 lpinv[u * n + v] += contrib;
168 if u != v {
169 lpinv[v * n + u] += contrib;
170 }
171 }
172 }
173 }
174
175 lpinv
176}
177
178pub fn effective_resistance(graph: &Graph, u: u32, v: u32) -> IgraphResult<f64> {
197 let n = graph.vcount();
198 if u >= n || v >= n {
199 return Err(IgraphError::InvalidArgument(format!(
200 "vertex index out of range: u={u}, v={v}, vcount={n}"
201 )));
202 }
203 if graph.is_directed() {
204 return Err(IgraphError::InvalidArgument(
205 "effective_resistance is defined for undirected graphs only".into(),
206 ));
207 }
208 if u == v {
209 return Ok(0.0);
210 }
211
212 let n = n as usize;
213 let lpinv = laplacian_pseudoinverse(graph);
214 let ui = u as usize;
215 let vi = v as usize;
216
217 Ok(lpinv[ui * n + ui] + lpinv[vi * n + vi] - 2.0 * lpinv[ui * n + vi])
218}
219
220pub fn effective_resistance_matrix(graph: &Graph) -> IgraphResult<Vec<f64>> {
237 let n = graph.vcount() as usize;
238 if graph.is_directed() {
239 return Err(IgraphError::InvalidArgument(
240 "effective_resistance_matrix is defined for undirected graphs only".into(),
241 ));
242 }
243
244 let lpinv = laplacian_pseudoinverse(graph);
245 let mut result = vec![0.0_f64; n * n];
246
247 for u in 0..n {
248 for v in (u + 1)..n {
249 let r = lpinv[u * n + u] + lpinv[v * n + v] - 2.0 * lpinv[u * n + v];
250 result[u * n + v] = r;
251 result[v * n + u] = r;
252 }
253 }
254
255 Ok(result)
256}
257
258pub fn kirchhoff_index(graph: &Graph) -> IgraphResult<f64> {
276 let n = graph.vcount() as usize;
277 if graph.is_directed() {
278 return Err(IgraphError::InvalidArgument(
279 "kirchhoff_index is defined for undirected graphs only".into(),
280 ));
281 }
282 if n <= 1 {
283 return Ok(0.0);
284 }
285
286 let mut lap = dense_laplacian(graph);
287 let (eigenvalues, _) = jacobi_eigen_full(&mut lap, n);
288
289 let eps = 1e-10;
290 let mut kf = 0.0_f64;
291 for &lam in &eigenvalues {
292 if lam.abs() > eps {
293 kf += 1.0 / lam;
294 }
295 }
296 kf *= n as f64;
297
298 Ok(kf)
299}
300
301pub fn resistance_centrality(graph: &Graph) -> IgraphResult<Vec<f64>> {
320 let n = graph.vcount() as usize;
321 if graph.is_directed() {
322 return Err(IgraphError::InvalidArgument(
323 "resistance_centrality is defined for undirected graphs only".into(),
324 ));
325 }
326 if n <= 1 {
327 return Ok(vec![0.0; n]);
328 }
329
330 let rmat = effective_resistance_matrix(graph)?;
331 let mut centrality = vec![0.0_f64; n];
332
333 for v in 0..n {
334 let total_r: f64 = (0..n).filter(|&u| u != v).map(|u| rmat[v * n + u]).sum();
335 if total_r > 1e-300 {
336 centrality[v] = (n - 1) as f64 / total_r;
337 }
338 }
339
340 Ok(centrality)
341}
342
343#[cfg(test)]
344mod tests {
345 use super::*;
346
347 fn path3() -> Graph {
348 Graph::from_edges(&[(0, 1), (1, 2)], false, Some(3)).unwrap()
349 }
350
351 fn k3() -> Graph {
352 Graph::from_edges(&[(0, 1), (1, 2), (0, 2)], false, Some(3)).unwrap()
353 }
354
355 fn k4() -> Graph {
356 Graph::from_edges(
357 &[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)],
358 false,
359 Some(4),
360 )
361 .unwrap()
362 }
363
364 fn star4() -> Graph {
365 Graph::from_edges(&[(0, 1), (0, 2), (0, 3)], false, Some(4)).unwrap()
366 }
367
368 fn cycle4() -> Graph {
369 Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 0)], false, Some(4)).unwrap()
370 }
371
372 #[test]
375 fn er_self() {
376 let g = k3();
377 let r = effective_resistance(&g, 0, 0).unwrap();
378 assert!(r.abs() < 1e-10);
379 }
380
381 #[test]
382 fn er_path_adjacent() {
383 let g = path3();
384 let r = effective_resistance(&g, 0, 1).unwrap();
385 assert!((r - 1.0).abs() < 0.01);
386 }
387
388 #[test]
389 fn er_path_ends() {
390 let g = path3();
391 let r = effective_resistance(&g, 0, 2).unwrap();
392 assert!((r - 2.0).abs() < 0.01);
393 }
394
395 #[test]
396 fn er_complete_graph() {
397 let g = k4();
399 let r = effective_resistance(&g, 0, 1).unwrap();
400 assert!((r - 0.5).abs() < 0.01); }
402
403 #[test]
404 fn er_symmetric() {
405 let g = star4();
406 let r01 = effective_resistance(&g, 0, 1).unwrap();
407 let r10 = effective_resistance(&g, 1, 0).unwrap();
408 assert!((r01 - r10).abs() < 1e-10);
409 }
410
411 #[test]
412 fn er_triangle_inequality() {
413 let g = path3();
414 let r01 = effective_resistance(&g, 0, 1).unwrap();
415 let r12 = effective_resistance(&g, 1, 2).unwrap();
416 let r02 = effective_resistance(&g, 0, 2).unwrap();
417 assert!(r02 <= r01 + r12 + 1e-10);
418 }
419
420 #[test]
421 fn er_nonneg() {
422 let g = cycle4();
423 for u in 0..4_u32 {
424 for v in 0..4_u32 {
425 let r = effective_resistance(&g, u, v).unwrap();
426 assert!(r >= -1e-10, "R({u},{v}) = {r} < 0");
427 }
428 }
429 }
430
431 #[test]
432 fn er_out_of_range() {
433 let g = k3();
434 assert!(effective_resistance(&g, 0, 5).is_err());
435 }
436
437 #[test]
438 fn er_directed_error() {
439 let g = Graph::from_edges(&[(0, 1)], true, Some(2)).unwrap();
440 assert!(effective_resistance(&g, 0, 1).is_err());
441 }
442
443 #[test]
446 fn erm_symmetric() {
447 let g = star4();
448 let r = effective_resistance_matrix(&g).unwrap();
449 let n = 4;
450 for u in 0..n {
451 for v in 0..n {
452 assert!(
453 (r[u * n + v] - r[v * n + u]).abs() < 1e-10,
454 "R({u},{v}) != R({v},{u})"
455 );
456 }
457 }
458 }
459
460 #[test]
461 fn erm_diagonal_zero() {
462 let g = k3();
463 let r = effective_resistance_matrix(&g).unwrap();
464 let n = 3;
465 for v in 0..n {
466 assert!(r[v * n + v].abs() < 1e-10);
467 }
468 }
469
470 #[test]
471 fn erm_path3() {
472 let g = path3();
473 let r = effective_resistance_matrix(&g).unwrap();
474 let n = 3;
475 assert!((r[1] - 1.0).abs() < 0.01); assert!((r[2] - 2.0).abs() < 0.01); assert!((r[n + 2] - 1.0).abs() < 0.01); }
479
480 #[test]
483 fn kf_empty() {
484 let g = Graph::with_vertices(1);
485 let kf = kirchhoff_index(&g).unwrap();
486 assert!(kf.abs() < 1e-10);
487 }
488
489 #[test]
490 fn kf_path3() {
491 let g = path3();
492 let kf = kirchhoff_index(&g).unwrap();
493 assert!((kf - 4.0).abs() < 0.1);
495 }
496
497 #[test]
498 fn kf_complete() {
499 let g = k4();
501 let kf = kirchhoff_index(&g).unwrap();
502 assert!((kf - 3.0).abs() < 0.1);
503 }
504
505 #[test]
506 fn kf_cycle4() {
507 let g = cycle4();
509 let kf = kirchhoff_index(&g).unwrap();
510 assert!((kf - 5.0).abs() < 0.1);
511 }
512
513 #[test]
514 fn kf_equals_sum_of_resistances() {
515 let g = star4();
516 let rmat = effective_resistance_matrix(&g).unwrap();
517 let n = 4;
518 let mut sum = 0.0_f64;
519 for u in 0..n {
520 for v in (u + 1)..n {
521 sum += rmat[u * n + v];
522 }
523 }
524 let kf = kirchhoff_index(&g).unwrap();
525 assert!((kf - sum).abs() < 0.1);
526 }
527
528 #[test]
529 fn kf_directed_error() {
530 let g = Graph::from_edges(&[(0, 1)], true, Some(2)).unwrap();
531 assert!(kirchhoff_index(&g).is_err());
532 }
533
534 #[test]
537 fn rc_k3_symmetric() {
538 let g = k3();
539 let c = resistance_centrality(&g).unwrap();
540 assert!((c[0] - c[1]).abs() < 0.01);
541 assert!((c[1] - c[2]).abs() < 0.01);
542 }
543
544 #[test]
545 fn rc_star_center_highest() {
546 let g = star4();
547 let c = resistance_centrality(&g).unwrap();
548 assert!(c[0] > c[1]);
549 assert!(c[0] > c[2]);
550 assert!(c[0] > c[3]);
551 }
552
553 #[test]
554 fn rc_path_center_highest() {
555 let g = path3();
556 let c = resistance_centrality(&g).unwrap();
557 assert!(c[1] > c[0]);
559 assert!(c[1] > c[2]);
560 }
561
562 #[test]
563 fn rc_all_positive() {
564 let g = k4();
565 let c = resistance_centrality(&g).unwrap();
566 for &v in &c {
567 assert!(v > 0.0);
568 }
569 }
570
571 #[test]
572 fn rc_single_vertex() {
573 let g = Graph::with_vertices(1);
574 let c = resistance_centrality(&g).unwrap();
575 assert!(c[0].abs() < 1e-10);
576 }
577}