Skip to main content

rust_igraph/algorithms/properties/
betweenness.rs

1//! Betweenness centrality (ALGO-PR-008).
2//!
3//! Counterpart of `igraph_betweenness()` from
4//! `references/igraph/src/centrality/betweenness.c:504+`.
5//!
6//! Implements Brandes' (2001) BFS-based algorithm for unweighted graphs.
7//! For each source vertex `s`:
8//! 1. BFS that records `sigma[v]` = number of shortest paths from `s` to
9//!    `v`, and `P[v]` = predecessor list of `v` on shortest paths.
10//! 2. Process vertices in non-increasing distance from `s` (LIFO stack
11//!    `S`); accumulate dependencies `delta[v] = Σ (sigma[v]/sigma[w]) *
12//!    (1 + delta[w])` over each child `w` of `v`.
13//! 3. For every `w != s`, add `delta[w]` to the running betweenness sum.
14//!
15//! For undirected graphs the result is divided by 2 (each unordered
16//! pair is counted from both endpoints).
17//!
18//! Phase-1 minimal slice: undirected / `IGRAPH_OUT`, unweighted, raw counts
19//! (`normalized = false`). Weighted version (Dijkstra) and per-vertex
20//! selection ship in PR-008b.
21
22use std::collections::VecDeque;
23
24use crate::core::{Graph, IgraphResult, VertexId};
25
26/// Per-vertex (unweighted) betweenness centrality.
27///
28/// `result[v]` is the raw number of shortest paths between *all*
29/// pairs `(s, t)` (`s != v != t`) that pass through `v`. For
30/// undirected graphs the result is halved (each unordered pair `(s, t)`
31/// is counted once).
32///
33/// Counterpart of
34/// `igraph_betweenness(_, _, vss_all(), /*directed=*/g.is_directed(), NULL)`.
35///
36/// # Examples
37///
38/// ```
39/// use rust_igraph::{Graph, betweenness};
40///
41/// // Path 0-1-2-3-4: only inner vertices have nonzero betweenness.
42/// // Vertex 1 lies on shortest paths (0,2),(0,3),(0,4) → 3 pairs.
43/// // Vertex 2: (0,3),(0,4),(1,3),(1,4) → 4 pairs.
44/// // Vertex 3: 3 pairs. Endpoints 0, 4: 0 pairs.
45/// let mut g = Graph::with_vertices(5);
46/// for i in 0..4u32 { g.add_edge(i, i + 1).unwrap(); }
47/// let b = betweenness(&g).unwrap();
48/// assert_eq!(b, vec![0.0, 3.0, 4.0, 3.0, 0.0]);
49/// ```
50pub fn betweenness(graph: &Graph) -> IgraphResult<Vec<f64>> {
51    let n = graph.vcount();
52    let n_us = n as usize;
53    let mut betweenness = vec![0.0_f64; n_us];
54
55    if n == 0 {
56        return Ok(betweenness);
57    }
58
59    // Per-vertex BFS using `neighbors()` (out-edges for directed, all for undirected).
60    // sigma[v] = number of shortest paths from s to v
61    // dist[v] = -1 (unvisited) initially, else the BFS layer index from s
62    // pred[v] = list of predecessors of v on a shortest path from s
63    let mut sigma = vec![0.0_f64; n_us];
64    let mut dist = vec![-1i64; n_us];
65    let mut pred: Vec<Vec<VertexId>> = vec![Vec::new(); n_us];
66    let mut stack: Vec<VertexId> = Vec::with_capacity(n_us);
67    let mut delta = vec![0.0_f64; n_us];
68
69    for s in 0..n {
70        // Reset per-source state.
71        sigma.fill(0.0);
72        dist.fill(-1);
73        for slot in &mut pred {
74            slot.clear();
75        }
76        stack.clear();
77        delta.fill(0.0);
78
79        sigma[s as usize] = 1.0;
80        dist[s as usize] = 0;
81        let mut queue: VecDeque<VertexId> = VecDeque::new();
82        queue.push_back(s);
83
84        while let Some(v) = queue.pop_front() {
85            stack.push(v);
86            for w in graph.neighbors_iter(v)? {
87                if dist[w as usize] < 0 {
88                    queue.push_back(w);
89                    dist[w as usize] = dist[v as usize] + 1;
90                }
91                if dist[w as usize] == dist[v as usize] + 1 {
92                    sigma[w as usize] += sigma[v as usize];
93                    pred[w as usize].push(v);
94                }
95            }
96        }
97
98        // Accumulate dependencies in reverse-BFS order.
99        while let Some(w) = stack.pop() {
100            for &v in &pred[w as usize] {
101                delta[v as usize] +=
102                    (sigma[v as usize] / sigma[w as usize]) * (1.0 + delta[w as usize]);
103            }
104            if w != s {
105                betweenness[w as usize] += delta[w as usize];
106            }
107        }
108    }
109
110    // Undirected: divide by 2 (every unordered pair (s,t) was counted twice).
111    if !graph.is_directed() {
112        for slot in &mut betweenness {
113            *slot /= 2.0;
114        }
115    }
116
117    Ok(betweenness)
118}
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123
124    fn close(actual: &[f64], expected: &[f64], tol: f64) {
125        assert_eq!(actual.len(), expected.len(), "length mismatch");
126        for (i, (a, e)) in actual.iter().zip(expected.iter()).enumerate() {
127            assert!((a - e).abs() < tol, "vertex {i}: actual={a} expected={e}");
128        }
129    }
130
131    #[test]
132    fn empty_graph_yields_empty() {
133        let g = Graph::with_vertices(0);
134        assert!(betweenness(&g).unwrap().is_empty());
135    }
136
137    #[test]
138    fn isolated_vertices_all_zero() {
139        let g = Graph::with_vertices(5);
140        let b = betweenness(&g).unwrap();
141        assert_eq!(b, vec![0.0; 5]);
142    }
143
144    #[test]
145    fn k3_triangle_all_zero() {
146        // Every pair of vertices has a direct edge — no intermediates.
147        let mut g = Graph::with_vertices(3);
148        g.add_edge(0, 1).unwrap();
149        g.add_edge(1, 2).unwrap();
150        g.add_edge(2, 0).unwrap();
151        let b = betweenness(&g).unwrap();
152        close(&b, &[0.0, 0.0, 0.0], 1e-12);
153    }
154
155    #[test]
156    fn path_5_betweenness() {
157        // Pairs through vertex 1: (0,2),(0,3),(0,4) = 3.
158        // Through 2: (0,3),(0,4),(1,3),(1,4) = 4.
159        // Through 3: (0,4),(1,4),(2,4) = 3.
160        // Endpoints 0, 4 = 0.
161        let mut g = Graph::with_vertices(5);
162        for i in 0..4u32 {
163            g.add_edge(i, i + 1).unwrap();
164        }
165        let b = betweenness(&g).unwrap();
166        close(&b, &[0.0, 3.0, 4.0, 3.0, 0.0], 1e-12);
167    }
168
169    #[test]
170    fn star_centre_has_max() {
171        // 4-star: centre on every (leaf, leaf) shortest path → 3 pairs (C(3,2)).
172        let mut g = Graph::with_vertices(4);
173        for v in 1..4 {
174            g.add_edge(0, v).unwrap();
175        }
176        let b = betweenness(&g).unwrap();
177        close(&b, &[3.0, 0.0, 0.0, 0.0], 1e-12);
178    }
179
180    #[test]
181    fn k4_complete_all_zero() {
182        let mut g = Graph::with_vertices(4);
183        for u in 0..4u32 {
184            for v in (u + 1)..4 {
185                g.add_edge(u, v).unwrap();
186            }
187        }
188        let b = betweenness(&g).unwrap();
189        close(&b, &[0.0; 4], 1e-12);
190    }
191
192    #[test]
193    fn cycle_4_all_one() {
194        // 0-1-2-3-0. Pairs (0,2) and (1,3) are at distance 2 — there are two
195        // shortest paths each (clockwise + counterclockwise). Each interior
196        // vertex on one of those paths counts 0.5. Each vertex sits on
197        // exactly one antipodal path → betweenness 0.5.
198        let mut g = Graph::with_vertices(4);
199        for i in 0..4u32 {
200            g.add_edge(i, (i + 1) % 4).unwrap();
201        }
202        let b = betweenness(&g).unwrap();
203        close(&b, &[0.5, 0.5, 0.5, 0.5], 1e-12);
204    }
205
206    #[test]
207    fn directed_path_3_uses_out_edges() {
208        // 0->1->2: only (0,2) has a path; goes through 1.
209        // For directed unnormalized: betweenness[1] = 1.0 (1 ordered pair
210        // (0,2) with the single shortest path passing through 1).
211        let mut g = Graph::new(3, true).unwrap();
212        g.add_edge(0, 1).unwrap();
213        g.add_edge(1, 2).unwrap();
214        let b = betweenness(&g).unwrap();
215        close(&b, &[0.0, 1.0, 0.0], 1e-12);
216    }
217}