rust_igraph/algorithms/properties/
edge_betweenness_weighted.rs1use std::cmp::Ordering;
13use std::collections::BinaryHeap;
14
15use crate::core::graph::EdgeId;
16use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
17
18#[derive(Copy, Clone)]
22struct Frontier(f64, VertexId);
23
24impl PartialEq for Frontier {
25 fn eq(&self, other: &Self) -> bool {
26 self.0 == other.0 && self.1 == other.1
27 }
28}
29impl Eq for Frontier {}
30impl Ord for Frontier {
31 fn cmp(&self, other: &Self) -> Ordering {
32 other.0.total_cmp(&self.0).then(other.1.cmp(&self.1))
33 }
34}
35impl PartialOrd for Frontier {
36 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
37 Some(self.cmp(other))
38 }
39}
40
41pub fn edge_betweenness_weighted(graph: &Graph, weights: &[f64]) -> IgraphResult<Vec<f64>> {
61 let n = graph.vcount();
62 let n_us = n as usize;
63 let m = graph.ecount();
64 let mut bc = vec![0.0_f64; m];
65
66 if n == 0 || m == 0 {
67 return Ok(bc);
68 }
69
70 if weights.len() != m {
71 return Err(IgraphError::InvalidArgument(format!(
72 "weights vector size ({}) differs from edge count ({})",
73 weights.len(),
74 m
75 )));
76 }
77 for (e, &w) in weights.iter().enumerate() {
78 if w.is_nan() || w < 0.0 || !w.is_finite() {
79 return Err(IgraphError::InvalidArgument(format!(
80 "weight at edge {e} must be non-negative and finite (got {w})"
81 )));
82 }
83 }
84
85 let mut sigma = vec![0.0_f64; n_us];
86 let mut dist = vec![f64::INFINITY; n_us];
87 let mut visited = vec![false; n_us];
88 let mut pred: Vec<Vec<(VertexId, EdgeId)>> = vec![Vec::new(); n_us];
90 let mut stack: Vec<VertexId> = Vec::with_capacity(n_us);
91 let mut delta_v = vec![0.0_f64; n_us];
92
93 for s in 0..n {
94 sigma.fill(0.0);
95 dist.fill(f64::INFINITY);
96 visited.fill(false);
97 for slot in &mut pred {
98 slot.clear();
99 }
100 stack.clear();
101 delta_v.fill(0.0);
102
103 sigma[s as usize] = 1.0;
104 dist[s as usize] = 0.0;
105 let mut heap: BinaryHeap<Frontier> = BinaryHeap::new();
106 heap.push(Frontier(0.0, s));
107
108 while let Some(Frontier(d, v)) = heap.pop() {
109 let v_us = v as usize;
110 if visited[v_us] {
111 continue;
112 }
113 visited[v_us] = true;
114 stack.push(v);
115
116 for eid in graph.incident(v)? {
117 let w_edge = weights[eid as usize];
118 let other = graph.edge_other(eid as EdgeId, v)?;
119 let other_us = other as usize;
120 let nd = d + w_edge;
121 match nd.partial_cmp(&dist[other_us]) {
122 Some(Ordering::Less) => {
123 dist[other_us] = nd;
124 sigma[other_us] = sigma[v_us];
125 pred[other_us].clear();
126 pred[other_us].push((v, eid as EdgeId));
127 heap.push(Frontier(nd, other));
128 }
129 Some(Ordering::Equal) => {
130 sigma[other_us] += sigma[v_us];
131 pred[other_us].push((v, eid as EdgeId));
132 }
133 _ => {}
134 }
135 }
136 }
137
138 while let Some(w) = stack.pop() {
140 let w_us = w as usize;
141 for &(v, e) in &pred[w_us] {
142 let c = (sigma[v as usize] / sigma[w_us]) * (1.0 + delta_v[w_us]);
143 delta_v[v as usize] += c;
144 bc[e as usize] += c;
145 }
146 }
147 }
148
149 if !graph.is_directed() {
150 for slot in &mut bc {
151 *slot /= 2.0;
152 }
153 }
154
155 Ok(bc)
156}
157
158#[cfg(test)]
159mod tests {
160 use super::*;
161
162 fn close(actual: &[f64], expected: &[f64], tol: f64) {
163 assert_eq!(actual.len(), expected.len(), "length mismatch");
164 for (i, (a, e)) in actual.iter().zip(expected.iter()).enumerate() {
165 assert!((a - e).abs() < tol, "edge {i}: actual={a} expected={e}");
166 }
167 }
168
169 #[test]
170 fn empty_graph_yields_empty() {
171 let g = Graph::with_vertices(0);
172 assert!(edge_betweenness_weighted(&g, &[]).unwrap().is_empty());
173 }
174
175 #[test]
176 fn no_edges_yields_empty() {
177 let g = Graph::with_vertices(5);
178 assert!(edge_betweenness_weighted(&g, &[]).unwrap().is_empty());
179 }
180
181 #[test]
182 fn unit_weights_match_unweighted_path_4() {
183 let mut g = Graph::with_vertices(4);
185 for i in 0..3u32 {
186 g.add_edge(i, i + 1).unwrap();
187 }
188 let eb = edge_betweenness_weighted(&g, &[1.0, 1.0, 1.0]).unwrap();
189 close(&eb, &[3.0, 4.0, 3.0], 1e-12);
190 }
191
192 #[test]
193 fn weighted_triangle_swaps_route() {
194 let mut g = Graph::with_vertices(3);
200 g.add_edge(0, 1).unwrap();
201 g.add_edge(1, 2).unwrap();
202 g.add_edge(0, 2).unwrap();
203 let eb = edge_betweenness_weighted(&g, &[1.0, 1.0, 5.0]).unwrap();
204 close(&eb, &[2.0, 2.0, 0.0], 1e-12);
205 }
206
207 #[test]
208 fn weighted_triangle_keeps_direct() {
209 let mut g = Graph::with_vertices(3);
213 g.add_edge(0, 1).unwrap();
214 g.add_edge(1, 2).unwrap();
215 g.add_edge(0, 2).unwrap();
216 let eb = edge_betweenness_weighted(&g, &[5.0, 5.0, 1.0]).unwrap();
217 close(&eb, &[1.0, 1.0, 1.0], 1e-12);
218 }
219
220 #[test]
221 fn directed_unit_weights_match_unweighted_path_3() {
222 let mut g = Graph::new(3, true).unwrap();
223 g.add_edge(0, 1).unwrap();
224 g.add_edge(1, 2).unwrap();
225 let eb = edge_betweenness_weighted(&g, &[1.0, 1.0]).unwrap();
226 close(&eb, &[2.0, 2.0], 1e-12);
227 }
228
229 #[test]
230 fn k4_unit_weights_each_edge_one() {
231 let mut g = Graph::with_vertices(4);
235 for u in 0..4u32 {
236 for v in (u + 1)..4 {
237 g.add_edge(u, v).unwrap();
238 }
239 }
240 let eb = edge_betweenness_weighted(&g, &[1.0; 6]).unwrap();
241 close(&eb, &[1.0; 6], 1e-12);
242 }
243
244 #[test]
245 fn weights_size_mismatch_errors() {
246 let mut g = Graph::with_vertices(2);
247 g.add_edge(0, 1).unwrap();
248 assert!(edge_betweenness_weighted(&g, &[]).is_err());
249 }
250
251 #[test]
252 fn negative_weight_errors() {
253 let mut g = Graph::with_vertices(2);
254 g.add_edge(0, 1).unwrap();
255 assert!(edge_betweenness_weighted(&g, &[-1.0]).is_err());
256 }
257
258 #[test]
259 fn nan_weight_errors() {
260 let mut g = Graph::with_vertices(2);
261 g.add_edge(0, 1).unwrap();
262 assert!(edge_betweenness_weighted(&g, &[f64::NAN]).is_err());
263 }
264}