Skip to main content

rust_igraph/algorithms/properties/
edge_betweenness_subset.rs

1//! Subset edge betweenness centrality (ALGO-PR-052).
2//!
3//! Counterpart of `igraph_edge_betweenness_subset()` from
4//! `references/igraph/src/centrality/betweenness.c:1227`.
5//!
6//! Computes edge betweenness considering only shortest paths from a
7//! given source subset to a given target subset. Uses a modified
8//! Brandes framework where only target vertices seed the +1 in
9//! back-propagation.
10
11use std::collections::VecDeque;
12
13use crate::core::graph::EdgeId;
14use crate::core::{Graph, IgraphResult, VertexId};
15
16/// Subset edge betweenness centrality.
17///
18/// Computes edge betweenness considering only shortest paths that
19/// originate in `sources` and terminate in `targets`. The result is a
20/// vector indexed by edge id.
21///
22/// For undirected graphs (or when `directed = false`), the result is
23/// halved.
24///
25/// # Parameters
26///
27/// * `graph` — the input graph.
28/// * `sources` — source vertices.
29/// * `targets` — target vertices.
30/// * `directed` — whether to follow edge directions (ignored for
31///   undirected graphs).
32///
33/// # Examples
34///
35/// ```
36/// use rust_igraph::{Graph, edge_betweenness_subset};
37///
38/// // Path 0—1—2—3: sources={0}, targets={2,3}
39/// let mut g = Graph::with_vertices(4);
40/// for i in 0..3u32 { g.add_edge(i, i + 1).unwrap(); }
41/// let eb = edge_betweenness_subset(&g, &[0], &[2, 3], true).unwrap();
42/// // Edge 0 (0-1): on paths (0,2),(0,3) → 2
43/// // Edge 1 (1-2): on paths (0,2),(0,3) → 2
44/// // Edge 2 (2-3): on path (0,3) → 1
45/// // Undirected → halved: [1.0, 1.0, 0.5]
46/// assert!((eb[0] - 1.0).abs() < 1e-12);
47/// assert!((eb[1] - 1.0).abs() < 1e-12);
48/// assert!((eb[2] - 0.5).abs() < 1e-12);
49/// ```
50pub fn edge_betweenness_subset(
51    graph: &Graph,
52    sources: &[VertexId],
53    targets: &[VertexId],
54    directed: bool,
55) -> IgraphResult<Vec<f64>> {
56    let n = graph.vcount();
57    let n_us = n as usize;
58    let m = graph.ecount();
59    let mut betweenness_e = vec![0.0_f64; m];
60
61    if n == 0 || m == 0 || sources.is_empty() || targets.is_empty() {
62        return Ok(betweenness_e);
63    }
64
65    let mut is_target = vec![false; n_us];
66    for &t in targets {
67        if (t as usize) < n_us {
68            is_target[t as usize] = true;
69        }
70    }
71
72    let mut sigma = vec![0.0_f64; n_us];
73    let mut dist = vec![-1_i64; n_us];
74    let mut pred: Vec<Vec<(VertexId, EdgeId)>> = vec![Vec::new(); n_us];
75    let mut stack: Vec<VertexId> = Vec::with_capacity(n_us);
76    let mut delta_v = vec![0.0_f64; n_us];
77    let use_directed = directed && graph.is_directed();
78
79    for &s in sources {
80        if s >= n {
81            continue;
82        }
83
84        sigma.fill(0.0);
85        dist.fill(-1);
86        for slot in &mut pred {
87            slot.clear();
88        }
89        stack.clear();
90        delta_v.fill(0.0);
91
92        sigma[s as usize] = 1.0;
93        dist[s as usize] = 0;
94        let mut queue: VecDeque<VertexId> = VecDeque::new();
95        queue.push_back(s);
96
97        while let Some(v) = queue.pop_front() {
98            stack.push(v);
99            let v_dist = dist[v as usize];
100
101            let edges = if use_directed {
102                graph.incident(v)?
103            } else if graph.is_directed() {
104                let mut e = graph.incident(v)?;
105                e.extend(graph.incident_in(v)?);
106                e
107            } else {
108                graph.incident(v)?
109            };
110
111            for e in edges {
112                let w = graph.edge_other(e, v)?;
113                if dist[w as usize] < 0 {
114                    queue.push_back(w);
115                    dist[w as usize] = v_dist + 1;
116                }
117                if dist[w as usize] == v_dist + 1 {
118                    sigma[w as usize] += sigma[v as usize];
119                    pred[w as usize].push((v, e));
120                }
121            }
122        }
123
124        while let Some(w) = stack.pop() {
125            let coeff = if is_target[w as usize] {
126                (1.0 + delta_v[w as usize]) / sigma[w as usize]
127            } else {
128                delta_v[w as usize] / sigma[w as usize]
129            };
130
131            for &(v, e) in &pred[w as usize] {
132                let c = sigma[v as usize] * coeff;
133                delta_v[v as usize] += c;
134                betweenness_e[e as usize] += c;
135            }
136        }
137    }
138
139    if !directed || !graph.is_directed() {
140        for slot in &mut betweenness_e {
141            *slot /= 2.0;
142        }
143    }
144
145    Ok(betweenness_e)
146}
147
148#[cfg(test)]
149mod tests {
150    use super::*;
151
152    fn close(actual: &[f64], expected: &[f64], tol: f64) {
153        assert_eq!(actual.len(), expected.len(), "length mismatch");
154        for (i, (a, e)) in actual.iter().zip(expected.iter()).enumerate() {
155            assert!((a - e).abs() < tol, "edge {i}: actual={a} expected={e}");
156        }
157    }
158
159    #[test]
160    fn empty_graph() {
161        let g = Graph::new(0, false).unwrap();
162        let eb = edge_betweenness_subset(&g, &[0], &[0], true).unwrap();
163        assert!(eb.is_empty());
164    }
165
166    #[test]
167    fn no_edges() {
168        let g = Graph::new(3, false).unwrap();
169        let eb = edge_betweenness_subset(&g, &[0], &[1, 2], true).unwrap();
170        assert!(eb.is_empty());
171    }
172
173    #[test]
174    fn no_sources() {
175        let mut g = Graph::new(3, false).unwrap();
176        g.add_edges(vec![(0, 1), (1, 2)]).unwrap();
177        let eb = edge_betweenness_subset(&g, &[], &[0, 1, 2], true).unwrap();
178        close(&eb, &[0.0, 0.0], 1e-12);
179    }
180
181    #[test]
182    fn no_targets() {
183        let mut g = Graph::new(3, false).unwrap();
184        g.add_edges(vec![(0, 1), (1, 2)]).unwrap();
185        let eb = edge_betweenness_subset(&g, &[0, 1, 2], &[], true).unwrap();
186        close(&eb, &[0.0, 0.0], 1e-12);
187    }
188
189    #[test]
190    fn all_sources_all_targets_path() {
191        // 0—1—2—3 with all vertices as sources and targets → regular edge betweenness
192        let mut g = Graph::new(4, false).unwrap();
193        g.add_edges(vec![(0, 1), (1, 2), (2, 3)]).unwrap();
194        let eb = edge_betweenness_subset(&g, &[0, 1, 2, 3], &[0, 1, 2, 3], true).unwrap();
195        close(&eb, &[3.0, 4.0, 3.0], 1e-12);
196    }
197
198    #[test]
199    fn path_subset_sources_targets() {
200        // 0—1—2—3: sources={0}, targets={2,3}
201        // From 0: path to 2 uses edges 0,1; path to 3 uses edges 0,1,2
202        // Edge 0: 2 paths; Edge 1: 2 paths; Edge 2: 1 path
203        // Undirected → /2: [1.0, 1.0, 0.5]
204        let mut g = Graph::new(4, false).unwrap();
205        g.add_edges(vec![(0, 1), (1, 2), (2, 3)]).unwrap();
206        let eb = edge_betweenness_subset(&g, &[0], &[2, 3], true).unwrap();
207        close(&eb, &[1.0, 1.0, 0.5], 1e-12);
208    }
209
210    #[test]
211    fn directed_path() {
212        // 0→1→2→3: sources={0}, targets={2,3}, directed
213        // From 0: path to 2 uses edges 0,1; path to 3 uses edges 0,1,2
214        // Edge 0: 2; Edge 1: 2; Edge 2: 1
215        // Directed → no halving
216        let mut g = Graph::new(4, true).unwrap();
217        g.add_edges(vec![(0, 1), (1, 2), (2, 3)]).unwrap();
218        let eb = edge_betweenness_subset(&g, &[0], &[2, 3], true).unwrap();
219        close(&eb, &[2.0, 2.0, 1.0], 1e-12);
220    }
221
222    #[test]
223    fn triangle() {
224        // Triangle 0—1—2: sources=all, targets=all
225        // All shortest paths have length 1, so each edge carries 1 pair
226        let mut g = Graph::new(3, false).unwrap();
227        g.add_edges(vec![(0, 1), (1, 2), (2, 0)]).unwrap();
228        let eb = edge_betweenness_subset(&g, &[0, 1, 2], &[0, 1, 2], true).unwrap();
229        close(&eb, &[1.0, 1.0, 1.0], 1e-12);
230    }
231
232    #[test]
233    fn star_subset() {
234        // Star: 0 connected to 1,2,3,4
235        // sources={1,2}, targets={3,4}
236        // Paths: (1,3),(1,4),(2,3),(2,4) all go through centre
237        // Edge (0-1): on (1,3),(1,4) = 2; Edge (0-2): on (2,3),(2,4) = 2
238        // Edge (0-3): on (1,3),(2,3) = 2; Edge (0-4): on (1,4),(2,4) = 2
239        // Undirected → /2: all 1.0
240        let mut g = Graph::new(5, false).unwrap();
241        g.add_edges(vec![(0, 1), (0, 2), (0, 3), (0, 4)]).unwrap();
242        let eb = edge_betweenness_subset(&g, &[1, 2], &[3, 4], true).unwrap();
243        close(&eb, &[1.0, 1.0, 1.0, 1.0], 1e-12);
244    }
245
246    #[test]
247    fn out_of_range_ignored() {
248        let mut g = Graph::new(3, false).unwrap();
249        g.add_edges(vec![(0, 1), (1, 2)]).unwrap();
250        let eb = edge_betweenness_subset(&g, &[0, 99], &[2], true).unwrap();
251        // Only source=0, target=2: edges 0,1 both on path
252        // Undirected → /2: [0.5, 0.5]
253        close(&eb, &[0.5, 0.5], 1e-12);
254    }
255}