use crate::graph::GraphRef;
#[derive(Debug, Clone)]
pub struct EigenvectorRun {
pub scores: Vec<f64>,
pub iterations: usize,
pub diff_l1: f64,
pub converged: bool,
}
pub fn eigenvector_centrality<G: GraphRef>(graph: &G) -> Vec<f64> {
eigenvector_centrality_run(graph, 100, 1e-6).scores
}
pub fn eigenvector_centrality_run<G: GraphRef>(
graph: &G,
max_iterations: usize,
tolerance: f64,
) -> EigenvectorRun {
let n = graph.node_count();
if n == 0 {
return EigenvectorRun {
scores: vec![],
iterations: 0,
diff_l1: 0.0,
converged: true,
};
}
let init = 1.0 / (n as f64).sqrt();
let mut scores = vec![init; n];
let mut new_scores = vec![0.0; n];
let mut diff_l1 = 0.0;
let mut iterations = 0;
for iter in 0..max_iterations {
iterations = iter + 1;
for v in 0..n {
let mut sum = scores[v]; for &u in graph.neighbors_ref(v) {
if u < n {
sum += scores[u];
}
}
new_scores[v] = sum;
}
let norm: f64 = new_scores.iter().map(|x| x * x).sum::<f64>().sqrt();
if norm > 0.0 {
let sign = if new_scores.iter().sum::<f64>() >= 0.0 {
1.0
} else {
-1.0
};
for x in &mut new_scores {
*x = *x * sign / norm;
}
}
diff_l1 = scores
.iter()
.zip(new_scores.iter())
.map(|(a, b)| (a - b).abs())
.sum();
std::mem::swap(&mut scores, &mut new_scores);
if diff_l1 < tolerance {
return EigenvectorRun {
scores,
iterations,
diff_l1,
converged: true,
};
}
}
EigenvectorRun {
scores,
iterations,
diff_l1,
converged: false,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::graph::GraphRef;
struct VecGraph {
adj: Vec<Vec<usize>>,
}
impl GraphRef for VecGraph {
fn node_count(&self) -> usize {
self.adj.len()
}
fn neighbors_ref(&self, node: usize) -> &[usize] {
&self.adj[node]
}
}
#[test]
fn triangle_all_equal() {
let g = VecGraph {
adj: vec![vec![1, 2], vec![0, 2], vec![0, 1]],
};
let run = eigenvector_centrality_run(&g, 100, 1e-8);
assert!(run.converged);
let expected = 1.0 / 3.0_f64.sqrt();
for &s in &run.scores {
assert!((s - expected).abs() < 1e-6, "score {s} != {expected}");
}
}
#[test]
fn star_center_highest() {
let g = VecGraph {
adj: vec![vec![1, 2, 3, 4], vec![0], vec![0], vec![0], vec![0]],
};
let scores = eigenvector_centrality(&g);
assert!(scores[0] > scores[1]);
assert!((scores[1] - scores[2]).abs() < 1e-6);
}
#[test]
fn empty_graph() {
let g = VecGraph { adj: vec![] };
let run = eigenvector_centrality_run(&g, 100, 1e-6);
assert!(run.scores.is_empty());
assert!(run.converged);
}
#[test]
fn isolated_nodes() {
let g = VecGraph {
adj: vec![vec![], vec![], vec![]],
};
let scores = eigenvector_centrality(&g);
for &s in &scores {
assert!(s.abs() < 1e-10 || (s - 1.0 / 3.0_f64.sqrt()).abs() < 1e-10);
}
}
#[test]
fn l2_normalized() {
let g = VecGraph {
adj: vec![vec![1, 2, 3], vec![0, 2], vec![0, 1, 3], vec![0, 2]],
};
let scores = eigenvector_centrality(&g);
let l2: f64 = scores.iter().map(|x| x * x).sum::<f64>().sqrt();
assert!((l2 - 1.0).abs() < 1e-6, "L2 norm = {l2}");
}
}