oxicuda_graph/centrality/
betweenness.rs1use std::collections::VecDeque;
32
33use crate::error::{GraphError, GraphResult};
34
35pub 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 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 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 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 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 if dist[w] < 0 {
92 dist[w] = dist[v] + 1;
93 queue.push_back(w);
94 }
95 if dist[w] == dist[v] + 1 {
97 sigma[w] += sigma[v];
98 preds[w].push(v);
99 }
100 }
101 }
102 }
103
104 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 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 #[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 #[test]
145 fn star_center_highest() {
146 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 #[test]
167 fn path_graph_middle_highest() {
168 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 #[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 #[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 #[test]
206 fn isolated_node_zero() {
207 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 #[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 #[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 #[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 #[test]
242 fn out_of_range_edge_error() {
243 let adj = vec![vec![9]]; let err = betweenness_centrality(&adj, 3);
245 assert!(
246 matches!(err, Err(GraphError::InvalidPlan(_))),
247 "got {err:?}"
248 );
249 }
250}