Skip to main content

rust_igraph/algorithms/properties/
betweenness_subset.rs

1//! Subset betweenness centrality (ALGO-PR-051).
2//!
3//! Counterpart of `igraph_betweenness_subset()` from
4//! `references/igraph/src/centrality/betweenness.c:1000`.
5//!
6//! Computes betweenness centrality considering only shortest paths
7//! originating from a given source subset and terminating at a given
8//! target subset. Uses a modified Brandes framework where only target
9//! vertices seed the back-propagation (+1 contribution).
10
11use std::collections::VecDeque;
12
13use crate::core::{Graph, IgraphResult, VertexId};
14
15fn all_neighbors(graph: &Graph, v: VertexId, directed: bool) -> IgraphResult<Vec<VertexId>> {
16    if directed && graph.is_directed() {
17        graph.neighbors(v)
18    } else if graph.is_directed() {
19        let mut out = graph.out_neighbors_vec(v)?;
20        out.extend(graph.in_neighbors_vec(v)?);
21        Ok(out)
22    } else {
23        graph.neighbors(v)
24    }
25}
26
27/// Subset betweenness centrality.
28///
29/// Computes betweenness for all vertices considering only shortest paths
30/// that start in `sources` and end in `targets`.
31///
32/// For undirected graphs (or when `directed = false`), the result is
33/// halved since each unordered pair is counted once.
34///
35/// # Parameters
36///
37/// * `graph` — the input graph.
38/// * `sources` — source vertices for the shortest paths.
39/// * `targets` — target vertices for the shortest paths.
40/// * `directed` — whether to follow edge directions (ignored for
41///   undirected graphs).
42///
43/// # Examples
44///
45/// ```
46/// use rust_igraph::{Graph, betweenness_subset};
47///
48/// // Path 0—1—2—3—4: sources={0,1}, targets={3,4}
49/// let mut g = Graph::with_vertices(5);
50/// for i in 0..4u32 { g.add_edge(i, i + 1).unwrap(); }
51/// let b = betweenness_subset(&g, &[0, 1], &[3, 4], true).unwrap();
52/// // Vertex 2 lies on paths (0,3),(0,4),(1,3),(1,4) — betweenness = 4/2 = 2
53/// // Vertex 3 lies on paths (0,4),(1,4) — betweenness = 2/2 = 1
54/// assert!((b[2] - 2.0).abs() < 1e-12);
55/// assert!((b[3] - 1.0).abs() < 1e-12);
56/// ```
57pub fn betweenness_subset(
58    graph: &Graph,
59    sources: &[VertexId],
60    targets: &[VertexId],
61    directed: bool,
62) -> IgraphResult<Vec<f64>> {
63    let n = graph.vcount();
64    let n_us = n as usize;
65    let mut betweenness = vec![0.0_f64; n_us];
66
67    if n == 0 || sources.is_empty() || targets.is_empty() {
68        return Ok(betweenness);
69    }
70
71    let mut is_target = vec![false; n_us];
72    for &t in targets {
73        if (t as usize) < n_us {
74            is_target[t as usize] = true;
75        }
76    }
77
78    let mut sigma = vec![0.0_f64; n_us];
79    let mut dist = vec![-1_i64; n_us];
80    let mut pred: Vec<Vec<VertexId>> = vec![Vec::new(); n_us];
81    let mut stack: Vec<VertexId> = Vec::with_capacity(n_us);
82    let mut delta = vec![0.0_f64; n_us];
83
84    for &s in sources {
85        if s >= n {
86            continue;
87        }
88
89        sigma.fill(0.0);
90        dist.fill(-1);
91        for slot in &mut pred {
92            slot.clear();
93        }
94        stack.clear();
95        delta.fill(0.0);
96
97        sigma[s as usize] = 1.0;
98        dist[s as usize] = 0;
99        let mut queue: VecDeque<VertexId> = VecDeque::new();
100        queue.push_back(s);
101
102        while let Some(v) = queue.pop_front() {
103            stack.push(v);
104            let v_dist = dist[v as usize];
105
106            for w in all_neighbors(graph, v, directed)? {
107                if dist[w as usize] < 0 {
108                    queue.push_back(w);
109                    dist[w as usize] = v_dist + 1;
110                }
111                if dist[w as usize] == v_dist + 1 {
112                    sigma[w as usize] += sigma[v as usize];
113                    pred[w as usize].push(v);
114                }
115            }
116        }
117
118        while let Some(w) = stack.pop() {
119            let coeff = if is_target[w as usize] {
120                (1.0 + delta[w as usize]) / sigma[w as usize]
121            } else {
122                delta[w as usize] / sigma[w as usize]
123            };
124
125            for &v in &pred[w as usize] {
126                delta[v as usize] += sigma[v as usize] * coeff;
127            }
128
129            if w != s {
130                betweenness[w as usize] += delta[w as usize];
131            }
132        }
133    }
134
135    if !directed || !graph.is_directed() {
136        for slot in &mut betweenness {
137            *slot /= 2.0;
138        }
139    }
140
141    Ok(betweenness)
142}
143
144#[cfg(test)]
145mod tests {
146    use super::*;
147
148    fn close(actual: &[f64], expected: &[f64], tol: f64) {
149        assert_eq!(actual.len(), expected.len(), "length mismatch");
150        for (i, (a, e)) in actual.iter().zip(expected.iter()).enumerate() {
151            assert!((a - e).abs() < tol, "vertex {i}: actual={a} expected={e}");
152        }
153    }
154
155    #[test]
156    fn empty_graph() {
157        let g = Graph::new(0, false).unwrap();
158        let b = betweenness_subset(&g, &[0], &[0], true).unwrap();
159        assert!(b.is_empty());
160    }
161
162    #[test]
163    fn no_sources() {
164        let mut g = Graph::new(3, false).unwrap();
165        g.add_edges(vec![(0, 1), (1, 2)]).unwrap();
166        let b = betweenness_subset(&g, &[], &[0, 1, 2], true).unwrap();
167        close(&b, &[0.0, 0.0, 0.0], 1e-12);
168    }
169
170    #[test]
171    fn no_targets() {
172        let mut g = Graph::new(3, false).unwrap();
173        g.add_edges(vec![(0, 1), (1, 2)]).unwrap();
174        let b = betweenness_subset(&g, &[0, 1, 2], &[], true).unwrap();
175        close(&b, &[0.0, 0.0, 0.0], 1e-12);
176    }
177
178    #[test]
179    fn all_sources_all_targets_undirected() {
180        // With all sources and targets, should equal regular betweenness
181        // Path 0—1—2—3
182        let mut g = Graph::new(4, false).unwrap();
183        g.add_edges(vec![(0, 1), (1, 2), (2, 3)]).unwrap();
184        let b = betweenness_subset(&g, &[0, 1, 2, 3], &[0, 1, 2, 3], true).unwrap();
185        // Regular betweenness for path: [0, 2, 2, 0]
186        close(&b, &[0.0, 2.0, 2.0, 0.0], 1e-12);
187    }
188
189    #[test]
190    fn path_subset_sources() {
191        // 0—1—2—3—4: sources={0}, targets=all
192        // BFS from 0 reaches all; paths (0,1),(0,2),(0,3),(0,4)
193        // v1 on (0,2),(0,3),(0,4) = 3; v2 on (0,3),(0,4) = 2; v3 on (0,4) = 1
194        // Undirected → /2
195        let mut g = Graph::new(5, false).unwrap();
196        g.add_edges(vec![(0, 1), (1, 2), (2, 3), (3, 4)]).unwrap();
197        let b = betweenness_subset(&g, &[0], &[0, 1, 2, 3, 4], true).unwrap();
198        close(&b, &[0.0, 1.5, 1.0, 0.5, 0.0], 1e-12);
199    }
200
201    #[test]
202    fn path_subset_targets() {
203        // 0—1—2—3—4: sources=all, targets={4}
204        // Only paths (s,4) count: (0,4) through 1,2,3; (1,4) through 2,3; (2,4) through 3; (3,4) direct
205        // v1: on (0,4) = 1; v2: on (0,4),(1,4) = 2; v3: on (0,4),(1,4),(2,4) = 3
206        // Undirected → /2
207        let mut g = Graph::new(5, false).unwrap();
208        g.add_edges(vec![(0, 1), (1, 2), (2, 3), (3, 4)]).unwrap();
209        let b = betweenness_subset(&g, &[0, 1, 2, 3, 4], &[4], true).unwrap();
210        close(&b, &[0.0, 0.5, 1.0, 1.5, 0.0], 1e-12);
211    }
212
213    #[test]
214    fn path_both_subsets() {
215        // 0—1—2—3—4: sources={0,1}, targets={3,4}
216        // From 0: paths to 3 (through 1,2) and 4 (through 1,2,3)
217        //   v1: on (0,3),(0,4) = 2; v2: on (0,3),(0,4) = 2; v3: on (0,4) = 1
218        // From 1: paths to 3 (through 2) and 4 (through 2,3)
219        //   v2: on (1,3),(1,4) = 2; v3: on (1,4) = 1
220        // Total: v1=2, v2=4, v3=2
221        // Undirected → /2: v1=1, v2=2, v3=1
222        let mut g = Graph::new(5, false).unwrap();
223        g.add_edges(vec![(0, 1), (1, 2), (2, 3), (3, 4)]).unwrap();
224        let b = betweenness_subset(&g, &[0, 1], &[3, 4], true).unwrap();
225        close(&b, &[0.0, 1.0, 2.0, 1.0, 0.0], 1e-12);
226    }
227
228    #[test]
229    fn directed_path() {
230        // 0→1→2→3: sources={0}, targets={2,3}, directed=true
231        // From 0: path to 2 (through 1), path to 3 (through 1,2)
232        // v1: on (0,2),(0,3) = 2; v2: on (0,3) = 1
233        // Directed → no halving
234        let mut g = Graph::new(4, true).unwrap();
235        g.add_edges(vec![(0, 1), (1, 2), (2, 3)]).unwrap();
236        let b = betweenness_subset(&g, &[0], &[2, 3], true).unwrap();
237        close(&b, &[0.0, 2.0, 1.0, 0.0], 1e-12);
238    }
239
240    #[test]
241    fn directed_as_undirected() {
242        // 0→1→2→3: sources={0}, targets={2,3}, directed=false
243        // Same as undirected path — halved
244        let mut g = Graph::new(4, true).unwrap();
245        g.add_edges(vec![(0, 1), (1, 2), (2, 3)]).unwrap();
246        let b = betweenness_subset(&g, &[0], &[2, 3], false).unwrap();
247        close(&b, &[0.0, 1.0, 0.5, 0.0], 1e-12);
248    }
249
250    #[test]
251    fn triangle() {
252        // Triangle 0—1—2—0: sources=all, targets=all → betweenness all 0
253        let mut g = Graph::new(3, false).unwrap();
254        g.add_edges(vec![(0, 1), (1, 2), (2, 0)]).unwrap();
255        let b = betweenness_subset(&g, &[0, 1, 2], &[0, 1, 2], true).unwrap();
256        close(&b, &[0.0, 0.0, 0.0], 1e-12);
257    }
258
259    #[test]
260    fn star_subset() {
261        // Star: 0 connected to 1,2,3,4
262        // sources={1,2}, targets={3,4}: paths (1,3),(1,4),(2,3),(2,4) all through 0
263        // v0 betweenness = 4 (directed=true keeps undirected → halve)
264        let mut g = Graph::new(5, false).unwrap();
265        g.add_edges(vec![(0, 1), (0, 2), (0, 3), (0, 4)]).unwrap();
266        let b = betweenness_subset(&g, &[1, 2], &[3, 4], true).unwrap();
267        close(&b, &[2.0, 0.0, 0.0, 0.0, 0.0], 1e-12);
268    }
269
270    #[test]
271    fn out_of_range_vertices_ignored() {
272        let mut g = Graph::new(3, false).unwrap();
273        g.add_edges(vec![(0, 1), (1, 2)]).unwrap();
274        // Source 99 is out of range — silently skipped
275        let b = betweenness_subset(&g, &[0, 99], &[2], true).unwrap();
276        // Only source=0, target=2: path 0-1-2, v1 gets 1 / 2 = 0.5
277        close(&b, &[0.0, 0.5, 0.0], 1e-12);
278    }
279}