Skip to main content

oxicuda_graph/centrality/
betweenness.rs

1//! Betweenness centrality (Brandes 2001).
2//!
3//! The betweenness centrality of a node `v` measures how often `v` lies on a
4//! shortest path between two other nodes:
5//! ```text
6//! C_B(v) = Σ_{s ≠ v ≠ t}  σ_{st}(v) / σ_{st}
7//! ```
8//! where `σ_{st}` is the number of shortest `s → t` paths and `σ_{st}(v)` is the
9//! number of those that pass through `v`.
10//!
11//! Brandes' algorithm computes all pairwise contributions in `O(n·m)` time (for
12//! unweighted graphs) without ever materialising the `O(n²)` pair table. For
13//! each source `s` it
14//! 1. runs a BFS that records, for every node, its shortest-path distance, the
15//!    number of shortest paths `σ` from `s`, and the set of *predecessors* on
16//!    those paths, and
17//! 2. accumulates dependencies `δ_s(v)` by walking the BFS-discovered nodes in
18//!    order of *non-increasing* distance, using the recurrence
19//!    ```text
20//!    δ_s(v) = Σ_{w : v ∈ pred(w)} (σ_v / σ_w) · (1 + δ_s(w)).
21//!    ```
22//!
23//! The adjacency list is interpreted as a **directed** graph. For an undirected
24//! graph supply a symmetric adjacency list; the returned scores then follow the
25//! usual convention of including each unordered pair twice (the values are *not*
26//! halved here, matching the directed accumulation).
27//!
28//! Reference: U. Brandes, "A faster algorithm for betweenness centrality",
29//! Journal of Mathematical Sociology, 2001.
30
31use std::collections::VecDeque;
32
33use crate::error::{GraphError, GraphResult};
34
35/// Compute the (shortest-path) betweenness centrality of every node.
36///
37/// `adj[u]` lists the out-neighbours of node `u`. The result has length
38/// `n_nodes`; `result[v]` is the betweenness score of node `v`. Scores are
39/// non-negative.
40///
41/// # Errors
42/// - [`GraphError::EmptyGraph`] when `n_nodes == 0`.
43/// - [`GraphError::InvalidPlan`] when an adjacency entry references a node
44///   `>= n_nodes`.
45pub fn betweenness_centrality(adj: &[Vec<usize>], n_nodes: usize) -> GraphResult<Vec<f64>> {
46    if n_nodes == 0 {
47        return Err(GraphError::EmptyGraph);
48    }
49    for (u, heads) in adj.iter().enumerate() {
50        for &w in heads {
51            if w >= n_nodes {
52                return Err(GraphError::InvalidPlan(format!(
53                    "adjacency edge {u} -> {w} references node >= n_nodes={n_nodes}"
54                )));
55            }
56        }
57    }
58
59    let mut centrality = vec![0.0f64; n_nodes];
60
61    // Reusable per-source scratch buffers.
62    let mut dist = vec![-1i64; n_nodes];
63    let mut sigma = vec![0.0f64; n_nodes];
64    let mut delta = vec![0.0f64; n_nodes];
65    let mut preds: Vec<Vec<usize>> = vec![Vec::new(); n_nodes];
66    // Stack of nodes in order of discovery (non-decreasing distance).
67    let mut order: Vec<usize> = Vec::with_capacity(n_nodes);
68    let mut queue: VecDeque<usize> = VecDeque::new();
69
70    for s in 0..n_nodes {
71        // Reset scratch.
72        for v in 0..n_nodes {
73            dist[v] = -1;
74            sigma[v] = 0.0;
75            delta[v] = 0.0;
76            preds[v].clear();
77        }
78        order.clear();
79        queue.clear();
80
81        dist[s] = 0;
82        sigma[s] = 1.0;
83        queue.push_back(s);
84
85        // BFS: shortest-path counting + predecessor recording.
86        while let Some(v) = queue.pop_front() {
87            order.push(v);
88            if let Some(neighbours) = adj.get(v) {
89                for &w in neighbours {
90                    // First time `w` is found: set its distance, enqueue it.
91                    if dist[w] < 0 {
92                        dist[w] = dist[v] + 1;
93                        queue.push_back(w);
94                    }
95                    // `v` lies on a shortest path to `w`.
96                    if dist[w] == dist[v] + 1 {
97                        sigma[w] += sigma[v];
98                        preds[w].push(v);
99                    }
100                }
101            }
102        }
103
104        // Dependency accumulation in reverse BFS order.
105        while let Some(w) = order.pop() {
106            let coeff = (1.0 + delta[w]) / sigma[w];
107            for &v in &preds[w] {
108                delta[v] += sigma[v] * coeff;
109            }
110            if w != s {
111                centrality[w] += delta[w];
112            }
113        }
114    }
115
116    Ok(centrality)
117}
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122
123    /// Build a symmetric (undirected) adjacency list from unordered edges.
124    fn undirected(n: usize, edges: &[(usize, usize)]) -> Vec<Vec<usize>> {
125        let mut adj = vec![Vec::new(); n];
126        for &(a, b) in edges {
127            adj[a].push(b);
128            adj[b].push(a);
129        }
130        adj
131    }
132
133    // 1. All scores are non-negative.
134    #[test]
135    fn nonneg() {
136        let adj = undirected(5, &[(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)]);
137        let c = betweenness_centrality(&adj, 5).expect("betweenness_centrality should succeed");
138        for &v in &c {
139            assert!(v >= 0.0, "negative betweenness {v}");
140        }
141    }
142
143    // 2. In a star, the centre has the strictly highest betweenness.
144    #[test]
145    fn star_center_highest() {
146        // Node 0 is the hub of a 4-leaf star.
147        let adj = undirected(5, &[(0, 1), (0, 2), (0, 3), (0, 4)]);
148        let c = betweenness_centrality(&adj, 5).expect("betweenness_centrality should succeed");
149        for leaf in 1..5 {
150            assert!(
151                c[0] > c[leaf],
152                "centre {} should exceed leaf {} = {}",
153                c[0],
154                leaf,
155                c[leaf]
156            );
157            assert!(
158                (c[leaf]).abs() < 1e-12,
159                "leaf {leaf} should be 0, got {}",
160                c[leaf]
161            );
162        }
163    }
164
165    // 3. In a path graph, the middle node has the highest betweenness.
166    #[test]
167    fn path_graph_middle_highest() {
168        // 0 - 1 - 2 - 3 - 4 ; node 2 is the centre.
169        let adj = undirected(5, &[(0, 1), (1, 2), (2, 3), (3, 4)]);
170        let c = betweenness_centrality(&adj, 5).expect("betweenness_centrality should succeed");
171        assert!(c[2] > c[1] && c[2] > c[3], "middle {} not the max", c[2]);
172        assert!(c[1] > c[0] && c[3] > c[4], "endpoints should be lowest");
173    }
174
175    // 4. In a complete graph every node has equal (zero) betweenness, since all
176    //    shortest paths are direct edges.
177    #[test]
178    fn complete_graph_zero() {
179        let n = 4;
180        let mut edges = Vec::new();
181        for a in 0..n {
182            for b in (a + 1)..n {
183                edges.push((a, b));
184            }
185        }
186        let adj = undirected(n, &edges);
187        let c = betweenness_centrality(&adj, n).expect("betweenness_centrality should succeed");
188        for &v in &c {
189            assert!(
190                v.abs() < 1e-12,
191                "complete-graph betweenness {v} should be 0"
192            );
193        }
194    }
195
196    // 5. n_nodes == 0 errors.
197    #[test]
198    fn n_nodes_0_error() {
199        let adj: Vec<Vec<usize>> = vec![];
200        let err = betweenness_centrality(&adj, 0);
201        assert!(matches!(err, Err(GraphError::EmptyGraph)), "got {err:?}");
202    }
203
204    // 6. An isolated node has zero betweenness.
205    #[test]
206    fn isolated_node_zero() {
207        // Node 3 has no edges.
208        let adj = undirected(4, &[(0, 1), (1, 2)]);
209        let c = betweenness_centrality(&adj, 4).expect("betweenness_centrality should succeed");
210        assert!(c[3].abs() < 1e-12, "isolated node betweenness {}", c[3]);
211    }
212
213    // 7. Output shape matches n_nodes.
214    #[test]
215    fn output_shape() {
216        let adj = undirected(6, &[(0, 1), (2, 3), (4, 5)]);
217        let c = betweenness_centrality(&adj, 6).expect("betweenness_centrality should succeed");
218        assert_eq!(c.len(), 6);
219    }
220
221    // 8. A single node has zero betweenness.
222    #[test]
223    fn single_node_zero() {
224        let adj = vec![vec![]];
225        let c = betweenness_centrality(&adj, 1).expect("betweenness_centrality should succeed");
226        assert_eq!(c.len(), 1);
227        assert!(c[0].abs() < 1e-12, "single-node betweenness {}", c[0]);
228    }
229
230    // 9. Symmetric structure yields symmetric values (the two arms of a path are
231    //    mirror images).
232    #[test]
233    fn symmetric_values() {
234        let adj = undirected(5, &[(0, 1), (1, 2), (2, 3), (3, 4)]);
235        let c = betweenness_centrality(&adj, 5).expect("betweenness_centrality should succeed");
236        assert!((c[0] - c[4]).abs() < 1e-9, "endpoints {} vs {}", c[0], c[4]);
237        assert!((c[1] - c[3]).abs() < 1e-9, "inner {} vs {}", c[1], c[3]);
238    }
239
240    // 10. Out-of-range adjacency reference errors.
241    #[test]
242    fn out_of_range_edge_error() {
243        let adj = vec![vec![9]]; // node 9 >= n_nodes = 3
244        let err = betweenness_centrality(&adj, 3);
245        assert!(
246            matches!(err, Err(GraphError::InvalidPlan(_))),
247            "got {err:?}"
248        );
249    }
250}