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}