Skip to main content

oxicuda_graphalg/centrality/
harmonic.rs

1//! Harmonic centrality `Σ_{u ≠ v} 1 / d(v, u)` (unweighted graph).
2//!
3//! Harmonic centrality (Marchiori & Latora 2000) is the sum of the reciprocals
4//! of the shortest-path distances from a vertex to every other vertex. Unlike
5//! closeness centrality — which sums distances and therefore requires a
6//! connected graph — unreachable vertices simply contribute `1/∞ = 0`, so
7//! harmonic centrality is well-defined on **disconnected** graphs.
8//!
9//! ```text
10//! H(v) = Σ_{u ≠ v, d(v,u) < ∞} 1 / d(v, u)
11//! ```
12//!
13//! Distances are computed by a BFS from each source (treating the graph as
14//! unweighted). The optional normalised variant divides by `n − 1` so values
15//! lie in `[0, 1]`.
16//!
17//! ## References
18//! - Marchiori, M. & Latora, V. (2000). "Harmony in the small-world."
19//!   Physica A 285(3–4), 539–546.
20//! - Boldi, P. & Vigna, S. (2014). "Axioms for centrality." Internet Math.
21
22use std::collections::VecDeque;
23
24use crate::error::GraphalgResult;
25use crate::repr::adjacency_list::AdjacencyList;
26
27/// Compute the (raw) harmonic centrality of every vertex.
28///
29/// Returns a length-`n` vector `H` where `H[v] = Σ_{u≠v} 1/d(v,u)` over
30/// vertices `u` reachable from `v` (BFS distance, unweighted).
31///
32/// # Errors
33/// Propagates [`AdjacencyList::neighbors`] index errors (cannot occur for a
34/// well-formed graph; surfaced for robustness).
35pub fn harmonic_centrality(g: &AdjacencyList) -> GraphalgResult<Vec<f64>> {
36    let n = g.n;
37    let mut hc = vec![0.0f64; n];
38    for s in 0..n {
39        let mut dist = vec![-1i64; n];
40        dist[s] = 0;
41        let mut q: VecDeque<usize> = VecDeque::new();
42        q.push_back(s);
43        while let Some(u) = q.pop_front() {
44            for &v in g.neighbors(u)? {
45                if dist[v] < 0 {
46                    dist[v] = dist[u] + 1;
47                    q.push_back(v);
48                }
49            }
50        }
51        let mut acc = 0.0f64;
52        for &d in dist.iter() {
53            if d > 0 {
54                acc += 1.0 / d as f64;
55            }
56        }
57        hc[s] = acc;
58    }
59    Ok(hc)
60}
61
62/// Compute the **normalised** harmonic centrality (`H(v) / (n − 1)`).
63///
64/// Values lie in `[0, 1]`; `1.0` is attained by a vertex adjacent to every
65/// other vertex. For `n ≤ 1` all entries are `0`.
66///
67/// # Errors
68/// As [`harmonic_centrality`].
69pub fn harmonic_centrality_normalized(g: &AdjacencyList) -> GraphalgResult<Vec<f64>> {
70    let n = g.n;
71    let mut hc = harmonic_centrality(g)?;
72    if n > 1 {
73        let denom = (n - 1) as f64;
74        for v in hc.iter_mut() {
75            *v /= denom;
76        }
77    }
78    Ok(hc)
79}
80
81#[cfg(test)]
82mod tests {
83    use super::*;
84
85    #[test]
86    fn path_graph_centre_value() {
87        // Path 0 - 1 - 2. From node 1: 1/1 + 1/1 = 2.
88        let mut g = AdjacencyList::new(3);
89        g.add_undirected_edge(0, 1).expect("ok");
90        g.add_undirected_edge(1, 2).expect("ok");
91        let h = harmonic_centrality(&g).expect("ok");
92        assert!((h[1] - 2.0).abs() < 1e-12, "h[1] = {}", h[1]);
93        // From node 0: 1/1 (to 1) + 1/2 (to 2) = 1.5.
94        assert!((h[0] - 1.5).abs() < 1e-12, "h[0] = {}", h[0]);
95    }
96
97    #[test]
98    fn complete_graph_is_n_minus_1() {
99        // K_4: every vertex is at distance 1 from the other 3 → H = 3.
100        let n = 4;
101        let mut g = AdjacencyList::new(n);
102        for i in 0..n {
103            for j in i + 1..n {
104                g.add_undirected_edge(i, j).expect("ok");
105            }
106        }
107        let h = harmonic_centrality(&g).expect("ok");
108        for (v, &val) in h.iter().enumerate() {
109            assert!((val - 3.0).abs() < 1e-12, "vertex {v}: {val}");
110        }
111    }
112
113    #[test]
114    fn disconnected_graph_well_defined() {
115        // Two disjoint edges: 0-1 and 2-3. Each vertex reaches exactly one other
116        // at distance 1 → H = 1 everywhere (no infinities).
117        let mut g = AdjacencyList::new(4);
118        g.add_undirected_edge(0, 1).expect("ok");
119        g.add_undirected_edge(2, 3).expect("ok");
120        let h = harmonic_centrality(&g).expect("ok");
121        for (v, &val) in h.iter().enumerate() {
122            assert!((val - 1.0).abs() < 1e-12, "vertex {v}: {val}");
123            assert!(val.is_finite());
124        }
125    }
126
127    #[test]
128    fn isolated_vertex_is_zero() {
129        // Vertex 2 is isolated.
130        let mut g = AdjacencyList::new(3);
131        g.add_undirected_edge(0, 1).expect("ok");
132        let h = harmonic_centrality(&g).expect("ok");
133        assert!((h[2] - 0.0).abs() < 1e-12, "isolated h[2] = {}", h[2]);
134    }
135
136    #[test]
137    fn empty_graph() {
138        let g = AdjacencyList::new(0);
139        let h = harmonic_centrality(&g).expect("ok");
140        assert!(h.is_empty());
141    }
142
143    #[test]
144    fn single_vertex() {
145        let g = AdjacencyList::new(1);
146        let h = harmonic_centrality(&g).expect("ok");
147        assert_eq!(h.len(), 1);
148        assert!((h[0] - 0.0).abs() < 1e-12);
149    }
150
151    #[test]
152    fn normalized_in_unit_range() {
153        let n = 5;
154        let mut g = AdjacencyList::new(n);
155        for i in 0..n {
156            for j in i + 1..n {
157                g.add_undirected_edge(i, j).expect("ok");
158            }
159        }
160        let h = harmonic_centrality_normalized(&g).expect("ok");
161        // Complete graph → all normalised values are exactly 1.
162        for &val in &h {
163            assert!((val - 1.0).abs() < 1e-12, "val = {val}");
164            assert!((0.0..=1.0).contains(&val));
165        }
166    }
167
168    #[test]
169    fn directed_asymmetry() {
170        // Directed star out of 0: 0->1, 0->2. From 0 both are reachable (H=2);
171        // from 1 nothing is reachable (H=0).
172        let mut g = AdjacencyList::new(3);
173        g.add_edge(0, 1).expect("ok");
174        g.add_edge(0, 2).expect("ok");
175        let h = harmonic_centrality(&g).expect("ok");
176        assert!((h[0] - 2.0).abs() < 1e-12, "h[0] = {}", h[0]);
177        assert!((h[1] - 0.0).abs() < 1e-12, "h[1] = {}", h[1]);
178        assert!((h[2] - 0.0).abs() < 1e-12, "h[2] = {}", h[2]);
179    }
180
181    #[test]
182    fn deterministic() {
183        let mut g = AdjacencyList::new(6);
184        g.add_undirected_edge(0, 1).expect("ok");
185        g.add_undirected_edge(1, 2).expect("ok");
186        g.add_undirected_edge(2, 3).expect("ok");
187        g.add_undirected_edge(3, 4).expect("ok");
188        g.add_undirected_edge(4, 5).expect("ok");
189        let a = harmonic_centrality(&g).expect("ok");
190        let b = harmonic_centrality(&g).expect("ok");
191        assert_eq!(a, b);
192    }
193
194    #[test]
195    fn all_values_finite_and_nonneg() {
196        // A graph with a mix of reachable and unreachable pairs.
197        let mut g = AdjacencyList::new(5);
198        g.add_undirected_edge(0, 1).expect("ok");
199        g.add_undirected_edge(1, 2).expect("ok");
200        g.add_undirected_edge(3, 4).expect("ok");
201        let h = harmonic_centrality(&g).expect("ok");
202        assert!(h.iter().all(|&v| v.is_finite() && v >= 0.0));
203    }
204
205    #[test]
206    fn star_centre_higher_than_leaf() {
207        // Undirected star: centre 0 connected to 1,2,3.
208        let mut g = AdjacencyList::new(4);
209        g.add_undirected_edge(0, 1).expect("ok");
210        g.add_undirected_edge(0, 2).expect("ok");
211        g.add_undirected_edge(0, 3).expect("ok");
212        let h = harmonic_centrality(&g).expect("ok");
213        // Centre: 1/1 * 3 = 3. Leaf: 1/1 (centre) + 1/2 + 1/2 = 2.
214        assert!((h[0] - 3.0).abs() < 1e-12, "centre {}", h[0]);
215        assert!((h[1] - 2.0).abs() < 1e-12, "leaf {}", h[1]);
216        assert!(h[0] > h[1]);
217    }
218}