use std::collections::VecDeque;
use crate::error::{GraphError, GraphResult};
pub fn closeness_centrality(adj: &[Vec<usize>], n_nodes: usize) -> GraphResult<Vec<f64>> {
if n_nodes == 0 {
return Err(GraphError::EmptyGraph);
}
for (u, heads) in adj.iter().enumerate() {
for &w in heads {
if w >= n_nodes {
return Err(GraphError::InvalidPlan(format!(
"adjacency edge {u} -> {w} references node >= n_nodes={n_nodes}"
)));
}
}
}
let mut centrality = vec![0.0f64; n_nodes];
let mut dist = vec![-1i64; n_nodes];
let mut queue: VecDeque<usize> = VecDeque::new();
for s in 0..n_nodes {
for d in dist.iter_mut() {
*d = -1;
}
queue.clear();
dist[s] = 0;
queue.push_back(s);
let mut total_dist: f64 = 0.0;
let mut reachable: usize = 1;
while let Some(v) = queue.pop_front() {
if let Some(neighbours) = adj.get(v) {
for &w in neighbours {
if dist[w] < 0 {
dist[w] = dist[v] + 1;
total_dist += dist[w] as f64;
reachable += 1;
queue.push_back(w);
}
}
}
}
if reachable > 1 && total_dist > 0.0 {
let inv_mean = (reachable as f64 - 1.0) / total_dist;
let wf = (reachable as f64 - 1.0) / (n_nodes as f64 - 1.0);
centrality[s] = inv_mean * wf;
}
}
Ok(centrality)
}
#[cfg(test)]
mod tests {
use super::*;
fn undirected(n: usize, edges: &[(usize, usize)]) -> Vec<Vec<usize>> {
let mut adj = vec![Vec::new(); n];
for &(a, b) in edges {
adj[a].push(b);
adj[b].push(a);
}
adj
}
#[test]
fn in_unit_range() {
let adj = undirected(5, &[(0, 1), (1, 2), (2, 3), (3, 4)]);
let c = closeness_centrality(&adj, 5).expect("closeness_centrality should succeed");
for &v in &c {
assert!(
(0.0..=1.0 + 1e-12).contains(&v),
"out-of-range closeness {v}"
);
}
}
#[test]
fn star_center_highest() {
let adj = undirected(5, &[(0, 1), (0, 2), (0, 3), (0, 4)]);
let c = closeness_centrality(&adj, 5).expect("closeness_centrality should succeed");
for leaf in 1..5 {
assert!(
c[0] > c[leaf],
"centre {} should exceed leaf {} = {}",
c[0],
leaf,
c[leaf]
);
}
assert!((c[0] - 1.0).abs() < 1e-12, "centre closeness {} != 1", c[0]);
}
#[test]
fn path_graph_middle_highest() {
let adj = undirected(5, &[(0, 1), (1, 2), (2, 3), (3, 4)]);
let c = closeness_centrality(&adj, 5).expect("closeness_centrality should succeed");
assert!(c[2] > c[1] && c[2] > c[3], "middle {} not the max", c[2]);
assert!(c[1] > c[0] && c[3] > c[4], "endpoints should be lowest");
}
#[test]
fn complete_graph_uniform() {
let n = 4;
let mut edges = Vec::new();
for a in 0..n {
for b in (a + 1)..n {
edges.push((a, b));
}
}
let adj = undirected(n, &edges);
let c = closeness_centrality(&adj, n).expect("closeness_centrality should succeed");
for &v in &c {
assert!((v - 1.0).abs() < 1e-12, "complete-graph closeness {v} != 1");
}
}
#[test]
fn n_nodes_0_error() {
let adj: Vec<Vec<usize>> = vec![];
let err = closeness_centrality(&adj, 0);
assert!(matches!(err, Err(GraphError::EmptyGraph)), "got {err:?}");
}
#[test]
fn isolated_node_zero() {
let adj = undirected(4, &[(0, 1), (1, 2)]);
let c = closeness_centrality(&adj, 4).expect("closeness_centrality should succeed");
assert!(c[3].abs() < 1e-12, "isolated node closeness {}", c[3]);
}
#[test]
fn output_shape() {
let adj = undirected(6, &[(0, 1), (2, 3), (4, 5)]);
let c = closeness_centrality(&adj, 6).expect("closeness_centrality should succeed");
assert_eq!(c.len(), 6);
}
#[test]
fn single_node_zero() {
let adj = vec![vec![]];
let c = closeness_centrality(&adj, 1).expect("closeness_centrality should succeed");
assert_eq!(c.len(), 1);
assert!(c[0].abs() < 1e-12, "single-node closeness {}", c[0]);
}
#[test]
fn symmetric_values() {
let adj = undirected(5, &[(0, 1), (1, 2), (2, 3), (3, 4)]);
let c = closeness_centrality(&adj, 5).expect("closeness_centrality should succeed");
assert!((c[0] - c[4]).abs() < 1e-9, "endpoints {} vs {}", c[0], c[4]);
assert!((c[1] - c[3]).abs() < 1e-9, "inner {} vs {}", c[1], c[3]);
}
#[test]
fn disconnected_components_normalised() {
let adj = undirected(5, &[(0, 1), (1, 2), (3, 4)]);
let c = closeness_centrality(&adj, 5).expect("closeness_centrality should succeed");
assert!(
c[1] > c[3],
"large-component hub {} <= small-component {}",
c[1],
c[3]
);
for &v in &c {
assert!(v >= 0.0, "negative closeness {v}");
}
}
#[test]
fn out_of_range_edge_error() {
let adj = vec![vec![7]];
let err = closeness_centrality(&adj, 3);
assert!(
matches!(err, Err(GraphError::InvalidPlan(_))),
"got {err:?}"
);
}
}