rust_igraph/algorithms/properties/
betweenness_weighted.rs1use std::cmp::Ordering;
22use std::collections::BinaryHeap;
23
24use crate::core::graph::EdgeId;
25use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
26
27#[derive(Copy, Clone)]
31struct Frontier(f64, VertexId);
32
33impl PartialEq for Frontier {
34 fn eq(&self, other: &Self) -> bool {
35 self.0 == other.0 && self.1 == other.1
36 }
37}
38impl Eq for Frontier {}
39impl Ord for Frontier {
40 fn cmp(&self, other: &Self) -> Ordering {
41 other.0.total_cmp(&self.0).then(other.1.cmp(&self.1))
42 }
43}
44impl PartialOrd for Frontier {
45 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
46 Some(self.cmp(other))
47 }
48}
49
50pub fn betweenness_weighted(graph: &Graph, weights: &[f64]) -> IgraphResult<Vec<f64>> {
72 let n = graph.vcount();
73 let n_us = n as usize;
74 let mut bc = vec![0.0_f64; n_us];
75
76 if n == 0 {
77 return Ok(bc);
78 }
79
80 let m = graph.ecount();
81 if weights.len() != m {
82 return Err(IgraphError::InvalidArgument(format!(
83 "weights vector size ({}) differs from edge count ({})",
84 weights.len(),
85 m
86 )));
87 }
88 for (e, &w) in weights.iter().enumerate() {
89 if w.is_nan() || w < 0.0 || !w.is_finite() {
90 return Err(IgraphError::InvalidArgument(format!(
91 "weight at edge {e} must be non-negative and finite (got {w})"
92 )));
93 }
94 }
95
96 let mut sigma = vec![0.0_f64; n_us];
98 let mut dist = vec![f64::INFINITY; n_us];
99 let mut visited = vec![false; n_us];
100 let mut pred: Vec<Vec<VertexId>> = vec![Vec::new(); n_us];
101 let mut stack: Vec<VertexId> = Vec::with_capacity(n_us);
102 let mut delta = vec![0.0_f64; n_us];
103
104 for s in 0..n {
109 sigma.fill(0.0);
111 dist.fill(f64::INFINITY);
112 visited.fill(false);
113 for slot in &mut pred {
114 slot.clear();
115 }
116 stack.clear();
117 delta.fill(0.0);
118
119 sigma[s as usize] = 1.0;
120 dist[s as usize] = 0.0;
121 let mut heap: BinaryHeap<Frontier> = BinaryHeap::new();
122 heap.push(Frontier(0.0, s));
123
124 while let Some(Frontier(d, v)) = heap.pop() {
125 let v_us = v as usize;
126 if visited[v_us] {
127 continue;
128 }
129 visited[v_us] = true;
130 stack.push(v);
131
132 for eid in graph.incident(v)? {
133 let w_edge = weights[eid as usize];
134 let other = graph.edge_other(eid as EdgeId, v)?;
135 let other_us = other as usize;
136 let nd = d + w_edge;
137 match nd.partial_cmp(&dist[other_us]) {
138 Some(Ordering::Less) => {
139 dist[other_us] = nd;
140 sigma[other_us] = sigma[v_us];
141 pred[other_us].clear();
142 pred[other_us].push(v);
143 heap.push(Frontier(nd, other));
144 }
145 Some(Ordering::Equal) => {
146 sigma[other_us] += sigma[v_us];
147 pred[other_us].push(v);
148 }
149 _ => {} }
151 }
152 }
153
154 while let Some(w) = stack.pop() {
156 let w_us = w as usize;
157 for &v in &pred[w_us] {
158 delta[v as usize] += (sigma[v as usize] / sigma[w_us]) * (1.0 + delta[w_us]);
159 }
160 if w != s {
161 bc[w_us] += delta[w_us];
162 }
163 }
164 }
165
166 if !graph.is_directed() {
167 for slot in &mut bc {
168 *slot /= 2.0;
169 }
170 }
171
172 Ok(bc)
173}
174
175#[cfg(test)]
176mod tests {
177 use super::*;
178
179 fn close(actual: &[f64], expected: &[f64], tol: f64) {
180 assert_eq!(actual.len(), expected.len(), "length mismatch");
181 for (i, (a, e)) in actual.iter().zip(expected.iter()).enumerate() {
182 assert!((a - e).abs() < tol, "vertex {i}: actual={a} expected={e}");
183 }
184 }
185
186 #[test]
187 fn empty_graph_yields_empty() {
188 let g = Graph::with_vertices(0);
189 assert!(betweenness_weighted(&g, &[]).unwrap().is_empty());
190 }
191
192 #[test]
193 fn isolated_vertices_all_zero() {
194 let g = Graph::with_vertices(5);
195 let b = betweenness_weighted(&g, &[]).unwrap();
196 assert_eq!(b, vec![0.0; 5]);
197 }
198
199 #[test]
200 fn unit_weights_match_unweighted_path_5() {
201 let mut g = Graph::with_vertices(5);
203 for i in 0..4u32 {
204 g.add_edge(i, i + 1).unwrap();
205 }
206 let b = betweenness_weighted(&g, &[1.0; 4]).unwrap();
207 close(&b, &[0.0, 3.0, 4.0, 3.0, 0.0], 1e-12);
208 }
209
210 #[test]
211 fn weighted_path_swaps_route_via_higher_weight() {
212 let mut g = Graph::with_vertices(3);
218 g.add_edge(0, 1).unwrap();
219 g.add_edge(1, 2).unwrap();
220 g.add_edge(0, 2).unwrap();
221 let b = betweenness_weighted(&g, &[1.0, 1.0, 5.0]).unwrap();
222 close(&b, &[0.0, 1.0, 0.0], 1e-12);
223 }
224
225 #[test]
226 fn weighted_path_keeps_direct_when_cheaper() {
227 let mut g = Graph::with_vertices(3);
230 g.add_edge(0, 1).unwrap();
231 g.add_edge(1, 2).unwrap();
232 g.add_edge(0, 2).unwrap();
233 let b = betweenness_weighted(&g, &[5.0, 5.0, 1.0]).unwrap();
234 close(&b, &[0.0, 0.0, 0.0], 1e-12);
235 }
236
237 #[test]
238 fn directed_unit_weights_match_unweighted() {
239 let mut g = Graph::new(3, true).unwrap();
241 g.add_edge(0, 1).unwrap();
242 g.add_edge(1, 2).unwrap();
243 let b = betweenness_weighted(&g, &[1.0, 1.0]).unwrap();
244 close(&b, &[0.0, 1.0, 0.0], 1e-12);
245 }
246
247 #[test]
248 fn k4_complete_unit_weights_all_zero() {
249 let mut g = Graph::with_vertices(4);
251 for u in 0..4u32 {
252 for v in (u + 1)..4 {
253 g.add_edge(u, v).unwrap();
254 }
255 }
256 let b = betweenness_weighted(&g, &[1.0; 6]).unwrap();
257 close(&b, &[0.0; 4], 1e-12);
258 }
259
260 #[test]
261 fn weights_size_mismatch_errors() {
262 let mut g = Graph::with_vertices(2);
263 g.add_edge(0, 1).unwrap();
264 assert!(betweenness_weighted(&g, &[]).is_err());
265 }
266
267 #[test]
268 fn negative_weight_errors() {
269 let mut g = Graph::with_vertices(2);
270 g.add_edge(0, 1).unwrap();
271 assert!(betweenness_weighted(&g, &[-1.0]).is_err());
272 }
273
274 #[test]
275 fn nan_weight_errors() {
276 let mut g = Graph::with_vertices(2);
277 g.add_edge(0, 1).unwrap();
278 assert!(betweenness_weighted(&g, &[f64::NAN]).is_err());
279 }
280}