rust_igraph/algorithms/properties/
spectral_metrics.rs1#![allow(
22 clippy::cast_possible_truncation,
23 clippy::cast_precision_loss,
24 clippy::many_single_char_names,
25 clippy::needless_range_loop,
26 clippy::similar_names,
27 clippy::too_many_lines
28)]
29
30use crate::core::{Graph, IgraphError, IgraphResult};
31
32fn dense_adjacency(graph: &Graph) -> Vec<f64> {
34 let n = graph.vcount() as usize;
35 let mut a = vec![0.0_f64; n * n];
36 for (u, v) in graph.edges() {
37 let ui = u as usize;
38 let vi = v as usize;
39 a[ui * n + vi] += 1.0;
40 if !graph.is_directed() && ui != vi {
41 a[vi * n + ui] += 1.0;
42 }
43 }
44 a
45}
46
47fn jacobi_eigen(mat: &mut [f64], n: usize, need_vectors: bool) -> (Vec<f64>, Vec<Vec<f64>>) {
54 if n == 0 {
55 return (Vec::new(), Vec::new());
56 }
57 if n == 1 {
58 let val = mat[0];
59 let vecs = if need_vectors {
60 vec![vec![1.0]]
61 } else {
62 Vec::new()
63 };
64 return (vec![val], vecs);
65 }
66
67 let mut v = vec![0.0_f64; n * n];
69 if need_vectors {
70 for i in 0..n {
71 v[i * n + i] = 1.0;
72 }
73 }
74
75 let max_sweeps = 100;
76 for _ in 0..max_sweeps {
77 let mut max_off = 0.0_f64;
79 let mut p = 0;
80 let mut q = 1;
81 for i in 0..n {
82 for j in (i + 1)..n {
83 let val = mat[i * n + j].abs();
84 if val > max_off {
85 max_off = val;
86 p = i;
87 q = j;
88 }
89 }
90 }
91
92 if max_off < 1e-14 {
93 break;
94 }
95
96 let app = mat[p * n + p];
98 let aqq = mat[q * n + q];
99 let apq = mat[p * n + q];
100
101 let (cos, sin) = if (app - aqq).abs() < 1e-300 {
102 let s = std::f64::consts::FRAC_1_SQRT_2;
103 (s, s)
104 } else {
105 let tau = (aqq - app) / (2.0 * apq);
106 let t = if tau >= 0.0 {
107 1.0 / (tau + (1.0 + tau * tau).sqrt())
108 } else {
109 -1.0 / (-tau + (1.0 + tau * tau).sqrt())
110 };
111 let c = 1.0 / (1.0 + t * t).sqrt();
112 (c, t * c)
113 };
114
115 for i in 0..n {
117 if i == p || i == q {
118 continue;
119 }
120 let aip = mat[i * n + p];
121 let aiq = mat[i * n + q];
122 mat[i * n + p] = cos * aip - sin * aiq;
123 mat[p * n + i] = mat[i * n + p];
124 mat[i * n + q] = sin * aip + cos * aiq;
125 mat[q * n + i] = mat[i * n + q];
126 }
127
128 let new_pp = cos * cos * app - 2.0 * sin * cos * apq + sin * sin * aqq;
129 let new_qq = sin * sin * app + 2.0 * sin * cos * apq + cos * cos * aqq;
130 mat[p * n + p] = new_pp;
131 mat[q * n + q] = new_qq;
132 mat[p * n + q] = 0.0;
133 mat[q * n + p] = 0.0;
134
135 if need_vectors {
137 for i in 0..n {
138 let vip = v[i * n + p];
139 let viq = v[i * n + q];
140 v[i * n + p] = cos * vip - sin * viq;
141 v[i * n + q] = sin * vip + cos * viq;
142 }
143 }
144 }
145
146 let mut eigenvalues: Vec<f64> = (0..n).map(|i| mat[i * n + i]).collect();
148
149 let mut indices: Vec<usize> = (0..n).collect();
151 indices.sort_by(|&a, &b| {
152 eigenvalues[b]
153 .partial_cmp(&eigenvalues[a])
154 .unwrap_or(std::cmp::Ordering::Equal)
155 });
156
157 let sorted_vals: Vec<f64> = indices.iter().map(|&i| eigenvalues[i]).collect();
158
159 let sorted_vecs = if need_vectors {
160 indices
161 .iter()
162 .map(|&idx| {
163 let mut col = vec![0.0_f64; n];
164 for i in 0..n {
165 col[i] = v[i * n + idx];
166 }
167 col
168 })
169 .collect()
170 } else {
171 Vec::new()
172 };
173
174 eigenvalues = sorted_vals;
175 (eigenvalues, sorted_vecs)
176}
177
178fn full_spectrum(graph: &Graph) -> Vec<f64> {
180 let n = graph.vcount() as usize;
181 if n == 0 {
182 return Vec::new();
183 }
184 let mut a = dense_adjacency(graph);
185 let (vals, _) = jacobi_eigen(&mut a, n, false);
186 vals
187}
188
189fn full_decomposition(graph: &Graph) -> (Vec<f64>, Vec<Vec<f64>>) {
191 let n = graph.vcount() as usize;
192 if n == 0 {
193 return (Vec::new(), Vec::new());
194 }
195 let mut a = dense_adjacency(graph);
196 jacobi_eigen(&mut a, n, true)
197}
198
199pub fn estrada_index(graph: &Graph) -> IgraphResult<f64> {
218 let spectrum = full_spectrum(graph);
219 Ok(spectrum.iter().map(|&lam| lam.exp()).sum())
220}
221
222pub fn graph_energy(graph: &Graph) -> IgraphResult<f64> {
238 let spectrum = full_spectrum(graph);
239 Ok(spectrum.iter().map(|&lam| lam.abs()).sum())
240}
241
242pub fn spectral_radius(graph: &Graph) -> IgraphResult<f64> {
260 let spectrum = full_spectrum(graph);
261 Ok(spectrum
262 .iter()
263 .map(|&lam| lam.abs())
264 .fold(0.0_f64, f64::max))
265}
266
267pub fn spectral_gap(graph: &Graph) -> IgraphResult<f64> {
286 let spectrum = full_spectrum(graph);
287 if spectrum.len() < 2 {
288 return Ok(0.0);
289 }
290 Ok(spectrum[0] - spectrum[1])
291}
292
293pub fn natural_connectivity(graph: &Graph) -> IgraphResult<f64> {
314 let n = graph.vcount() as usize;
315 if n == 0 {
316 return Ok(0.0);
317 }
318 let ee = estrada_index(graph)?;
319 Ok((ee / n as f64).ln())
320}
321
322pub fn subgraph_centrality(graph: &Graph) -> IgraphResult<Vec<f64>> {
342 let n = graph.vcount() as usize;
343 if n == 0 {
344 return Ok(Vec::new());
345 }
346
347 let (eigenvalues, eigenvectors) = full_decomposition(graph);
348
349 let mut sc = vec![0.0_f64; n];
350
351 for (j, &lam) in eigenvalues.iter().enumerate() {
352 let exp_lam = lam.exp();
353 let phi = &eigenvectors[j];
354 for v in 0..n {
355 sc[v] += phi[v] * phi[v] * exp_lam;
356 }
357 }
358
359 Ok(sc)
360}
361
362pub fn communicability_matrix(graph: &Graph) -> IgraphResult<Vec<f64>> {
387 let n = graph.vcount() as usize;
388 if n == 0 {
389 return Ok(Vec::new());
390 }
391
392 if n > 10_000 {
393 return Err(IgraphError::InvalidArgument(format!(
394 "communicability_matrix: graph has {n} vertices; n² = {} entries would be impractical",
395 (n as u64).saturating_mul(n as u64)
396 )));
397 }
398
399 let (eigenvalues, eigenvectors) = full_decomposition(graph);
400
401 let mut mat = vec![0.0_f64; n * n];
402
403 for (j, &lam) in eigenvalues.iter().enumerate() {
404 let exp_lam = lam.exp();
405 let phi = &eigenvectors[j];
406 for u in 0..n {
407 for v in u..n {
408 let contrib = phi[u] * phi[v] * exp_lam;
409 mat[u * n + v] += contrib;
410 if u != v {
411 mat[v * n + u] += contrib;
412 }
413 }
414 }
415 }
416
417 Ok(mat)
418}
419
420#[cfg(test)]
421mod tests {
422 use super::*;
423
424 fn k3() -> Graph {
425 Graph::from_edges(&[(0, 1), (1, 2), (0, 2)], false, Some(3)).unwrap()
426 }
427
428 fn path3() -> Graph {
429 Graph::from_edges(&[(0, 1), (1, 2)], false, Some(3)).unwrap()
430 }
431
432 fn k4() -> Graph {
433 Graph::from_edges(
434 &[(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)],
435 false,
436 Some(4),
437 )
438 .unwrap()
439 }
440
441 fn star4() -> Graph {
442 Graph::from_edges(&[(0, 1), (0, 2), (0, 3)], false, Some(4)).unwrap()
443 }
444
445 fn cycle4() -> Graph {
446 Graph::from_edges(&[(0, 1), (1, 2), (2, 3), (3, 0)], false, Some(4)).unwrap()
447 }
448
449 #[test]
452 fn estrada_empty() {
453 let g = Graph::with_vertices(0);
454 let ee = estrada_index(&g).unwrap();
455 assert!(ee.abs() < 1e-10);
456 }
457
458 #[test]
459 fn estrada_no_edges() {
460 let g = Graph::with_vertices(5);
461 let ee = estrada_index(&g).unwrap();
462 assert!((ee - 5.0).abs() < 0.1);
464 }
465
466 #[test]
467 fn estrada_k3() {
468 let g = k3();
469 let ee = estrada_index(&g).unwrap();
470 let expected = (2.0_f64).exp() + 2.0 * (-1.0_f64).exp();
471 assert!((ee - expected).abs() < 0.05);
472 }
473
474 #[test]
475 fn estrada_k4() {
476 let g = k4();
477 let ee = estrada_index(&g).unwrap();
478 let expected = (3.0_f64).exp() + 3.0 * (-1.0_f64).exp();
480 assert!((ee - expected).abs() < 0.1);
481 }
482
483 #[test]
484 fn estrada_positive() {
485 let g = cycle4();
486 let ee = estrada_index(&g).unwrap();
487 assert!(ee > 0.0);
488 }
489
490 #[test]
493 fn energy_empty() {
494 let g = Graph::with_vertices(0);
495 let e = graph_energy(&g).unwrap();
496 assert!(e.abs() < 1e-10);
497 }
498
499 #[test]
500 fn energy_no_edges() {
501 let g = Graph::with_vertices(3);
502 let e = graph_energy(&g).unwrap();
503 assert!(e.abs() < 0.1);
504 }
505
506 #[test]
507 fn energy_k3() {
508 let g = k3();
509 let e = graph_energy(&g).unwrap();
510 assert!((e - 4.0).abs() < 0.1);
512 }
513
514 #[test]
515 fn energy_k4() {
516 let g = k4();
517 let e = graph_energy(&g).unwrap();
518 assert!((e - 6.0).abs() < 0.1);
520 }
521
522 #[test]
523 fn energy_nonneg() {
524 let g = star4();
525 let e = graph_energy(&g).unwrap();
526 assert!(e >= -1e-10);
527 }
528
529 #[test]
532 fn radius_empty() {
533 let g = Graph::with_vertices(0);
534 let r = spectral_radius(&g).unwrap();
535 assert!(r.abs() < 1e-10);
536 }
537
538 #[test]
539 fn radius_k3() {
540 let g = k3();
541 let r = spectral_radius(&g).unwrap();
542 assert!((r - 2.0).abs() < 0.01);
543 }
544
545 #[test]
546 fn radius_complete_n() {
547 let g = k4();
549 let r = spectral_radius(&g).unwrap();
550 assert!((r - 3.0).abs() < 0.01);
551 }
552
553 #[test]
554 fn radius_path() {
555 let g = path3();
556 let r = spectral_radius(&g).unwrap();
557 assert!((r - std::f64::consts::SQRT_2).abs() < 0.01);
559 }
560
561 #[test]
564 fn gap_empty() {
565 let g = Graph::with_vertices(0);
566 let gap = spectral_gap(&g).unwrap();
567 assert!(gap.abs() < 1e-10);
568 }
569
570 #[test]
571 fn gap_single() {
572 let g = Graph::with_vertices(1);
573 let gap = spectral_gap(&g).unwrap();
574 assert!(gap.abs() < 1e-10);
575 }
576
577 #[test]
578 fn gap_k3() {
579 let g = k3();
580 let gap = spectral_gap(&g).unwrap();
581 assert!((gap - 3.0).abs() < 0.1);
583 }
584
585 #[test]
586 fn gap_complete_is_n() {
587 let g = k4();
589 let gap = spectral_gap(&g).unwrap();
590 assert!((gap - 4.0).abs() < 0.1);
591 }
592
593 #[test]
596 fn nc_empty() {
597 let g = Graph::with_vertices(0);
598 let nc = natural_connectivity(&g).unwrap();
599 assert!(nc.abs() < 1e-10);
600 }
601
602 #[test]
603 fn nc_no_edges() {
604 let g = Graph::with_vertices(4);
605 let nc = natural_connectivity(&g).unwrap();
606 assert!(nc.abs() < 0.1);
608 }
609
610 #[test]
611 fn nc_k3() {
612 let g = k3();
613 let nc = natural_connectivity(&g).unwrap();
614 let expected = ((2.0_f64).exp() + 2.0 * (-1.0_f64).exp()).ln() - (3.0_f64).ln();
615 assert!((nc - expected).abs() < 0.05);
616 }
617
618 #[test]
619 fn nc_more_edges_higher() {
620 let p = path3();
622 let k = k3();
623 let nc_p = natural_connectivity(&p).unwrap();
624 let nc_k = natural_connectivity(&k).unwrap();
625 assert!(nc_k > nc_p - 0.01);
626 }
627
628 #[test]
631 fn sc_empty() {
632 let g = Graph::with_vertices(0);
633 let sc = subgraph_centrality(&g).unwrap();
634 assert!(sc.is_empty());
635 }
636
637 #[test]
638 fn sc_no_edges() {
639 let g = Graph::with_vertices(3);
640 let sc = subgraph_centrality(&g).unwrap();
641 for &v in &sc {
643 assert!((v - 1.0).abs() < 0.1);
644 }
645 }
646
647 #[test]
648 fn sc_k3_symmetric() {
649 let g = k3();
650 let sc = subgraph_centrality(&g).unwrap();
651 assert!((sc[0] - sc[1]).abs() < 0.01);
653 assert!((sc[1] - sc[2]).abs() < 0.01);
654 }
655
656 #[test]
657 fn sc_star_center_highest() {
658 let g = star4();
659 let sc = subgraph_centrality(&g).unwrap();
660 assert!(sc[0] > sc[1]);
662 assert!(sc[0] > sc[2]);
663 assert!(sc[0] > sc[3]);
664 }
665
666 #[test]
667 fn sc_all_positive() {
668 let g = cycle4();
669 let sc = subgraph_centrality(&g).unwrap();
670 for &v in &sc {
671 assert!(v > 0.0);
672 }
673 }
674
675 #[test]
676 fn sc_sum_equals_estrada() {
677 let g = k3();
678 let sc = subgraph_centrality(&g).unwrap();
679 let ee = estrada_index(&g).unwrap();
680 let sc_sum: f64 = sc.iter().sum();
681 assert!((sc_sum - ee).abs() < 0.1);
682 }
683
684 #[test]
687 fn comm_empty() {
688 let g = Graph::with_vertices(0);
689 let c = communicability_matrix(&g).unwrap();
690 assert!(c.is_empty());
691 }
692
693 #[test]
694 fn comm_symmetric() {
695 let g = path3();
696 let c = communicability_matrix(&g).unwrap();
697 let n = 3;
698 for u in 0..n {
699 for v in 0..n {
700 assert!(
701 (c[u * n + v] - c[v * n + u]).abs() < 0.01,
702 "C({u},{v}) != C({v},{u})"
703 );
704 }
705 }
706 }
707
708 #[test]
709 fn comm_diagonal_is_sc() {
710 let g = k3();
711 let c = communicability_matrix(&g).unwrap();
712 let sc = subgraph_centrality(&g).unwrap();
713 let n = 3;
714 for v in 0..n {
715 assert!(
716 (c[v * n + v] - sc[v]).abs() < 0.1,
717 "Diagonal mismatch at vertex {v}"
718 );
719 }
720 }
721
722 #[test]
723 fn comm_k3_all_positive() {
724 let g = k3();
725 let c = communicability_matrix(&g).unwrap();
726 for &val in &c {
727 assert!(val > 0.0);
728 }
729 }
730
731 #[test]
732 fn comm_neighbors_higher_than_distant() {
733 let g = path3();
734 let c = communicability_matrix(&g).unwrap();
735 assert!(c[1] > c[2]);
737 }
738
739 #[test]
740 fn comm_too_large() {
741 assert!(communicability_matrix(&Graph::with_vertices(10_001)).is_err());
743 }
744}