1use crate::graph::GraphRef;
11
12#[derive(Debug, Clone)]
14pub struct EigenvectorRun {
15 pub scores: Vec<f64>,
17 pub iterations: usize,
19 pub diff_l1: f64,
21 pub converged: bool,
23}
24
25pub fn eigenvector_centrality<G: GraphRef>(graph: &G) -> Vec<f64> {
27 eigenvector_centrality_run(graph, 100, 1e-6).scores
28}
29
30pub fn eigenvector_centrality_run<G: GraphRef>(
50 graph: &G,
51 max_iterations: usize,
52 tolerance: f64,
53) -> EigenvectorRun {
54 let n = graph.node_count();
55 if n == 0 {
56 return EigenvectorRun {
57 scores: vec![],
58 iterations: 0,
59 diff_l1: 0.0,
60 converged: true,
61 };
62 }
63
64 let init = 1.0 / (n as f64).sqrt();
66 let mut scores = vec![init; n];
67 let mut new_scores = vec![0.0; n];
68 let mut diff_l1 = 0.0;
69 let mut iterations = 0;
70
71 for iter in 0..max_iterations {
72 iterations = iter + 1;
73
74 for v in 0..n {
77 let mut sum = scores[v]; for &u in graph.neighbors_ref(v) {
79 if u < n {
80 sum += scores[u];
81 }
82 }
83 new_scores[v] = sum;
84 }
85
86 let norm: f64 = new_scores.iter().map(|x| x * x).sum::<f64>().sqrt();
88 if norm > 0.0 {
89 let sign = if new_scores.iter().sum::<f64>() >= 0.0 {
91 1.0
92 } else {
93 -1.0
94 };
95 for x in &mut new_scores {
96 *x = *x * sign / norm;
97 }
98 }
99
100 diff_l1 = scores
102 .iter()
103 .zip(new_scores.iter())
104 .map(|(a, b)| (a - b).abs())
105 .sum();
106
107 std::mem::swap(&mut scores, &mut new_scores);
108
109 if diff_l1 < tolerance {
110 return EigenvectorRun {
111 scores,
112 iterations,
113 diff_l1,
114 converged: true,
115 };
116 }
117 }
118
119 EigenvectorRun {
120 scores,
121 iterations,
122 diff_l1,
123 converged: false,
124 }
125}
126
127#[cfg(test)]
128mod tests {
129 use super::*;
130 use crate::graph::GraphRef;
131
132 struct VecGraph {
133 adj: Vec<Vec<usize>>,
134 }
135
136 impl GraphRef for VecGraph {
137 fn node_count(&self) -> usize {
138 self.adj.len()
139 }
140 fn neighbors_ref(&self, node: usize) -> &[usize] {
141 &self.adj[node]
142 }
143 }
144
145 #[test]
146 fn triangle_all_equal() {
147 let g = VecGraph {
148 adj: vec![vec![1, 2], vec![0, 2], vec![0, 1]],
149 };
150 let run = eigenvector_centrality_run(&g, 100, 1e-8);
151 assert!(run.converged);
152 let expected = 1.0 / 3.0_f64.sqrt();
153 for &s in &run.scores {
154 assert!((s - expected).abs() < 1e-6, "score {s} != {expected}");
155 }
156 }
157
158 #[test]
159 fn star_center_highest() {
160 let g = VecGraph {
162 adj: vec![vec![1, 2, 3, 4], vec![0], vec![0], vec![0], vec![0]],
163 };
164 let scores = eigenvector_centrality(&g);
165 assert!(scores[0] > scores[1]);
166 assert!((scores[1] - scores[2]).abs() < 1e-6);
167 }
168
169 #[test]
170 fn empty_graph() {
171 let g = VecGraph { adj: vec![] };
172 let run = eigenvector_centrality_run(&g, 100, 1e-6);
173 assert!(run.scores.is_empty());
174 assert!(run.converged);
175 }
176
177 #[test]
178 fn isolated_nodes() {
179 let g = VecGraph {
180 adj: vec![vec![], vec![], vec![]],
181 };
182 let scores = eigenvector_centrality(&g);
183 for &s in &scores {
185 assert!(s.abs() < 1e-10 || (s - 1.0 / 3.0_f64.sqrt()).abs() < 1e-10);
186 }
187 }
188
189 #[test]
190 fn l2_normalized() {
191 let g = VecGraph {
192 adj: vec![vec![1, 2, 3], vec![0, 2], vec![0, 1, 3], vec![0, 2]],
193 };
194 let scores = eigenvector_centrality(&g);
195 let l2: f64 = scores.iter().map(|x| x * x).sum::<f64>().sqrt();
196 assert!((l2 - 1.0).abs() < 1e-6, "L2 norm = {l2}");
197 }
198}