Skip to main content

rust_igraph/algorithms/properties/
edge_betweenness_cutoff.rs

1//! Range-limited edge betweenness centrality (ALGO-PR-050).
2//!
3//! Counterpart of `igraph_edge_betweenness_cutoff()` from
4//! `references/igraph/src/centrality/betweenness.c:813+`.
5//!
6//! Same Brandes framework as edge betweenness but with a BFS depth
7//! bound. Only shortest paths of length at most `cutoff` are counted.
8
9use std::collections::VecDeque;
10
11use crate::core::graph::EdgeId;
12use crate::core::{Graph, IgraphResult, VertexId};
13
14/// Range-limited edge betweenness centrality.
15///
16/// Computes edge betweenness considering only shortest paths of length
17/// at most `cutoff`. The result is a vector indexed by edge id.
18///
19/// For undirected graphs the result is halved (each unordered pair
20/// counted once).
21///
22/// # Parameters
23///
24/// * `graph` — the input graph.
25/// * `cutoff` — maximum shortest-path length to consider.
26///
27/// # Examples
28///
29/// ```
30/// use rust_igraph::{Graph, edge_betweenness_cutoff};
31///
32/// // Path 0—1—2—3 with cutoff=2
33/// let mut g = Graph::with_vertices(4);
34/// for i in 0..3u32 { g.add_edge(i, i + 1).unwrap(); }
35/// let eb = edge_betweenness_cutoff(&g, 2).unwrap();
36/// // Edge 0 (0-1): on paths (0,1) len 1 and (0,2) len 2 → 2 pairs
37/// // Edge 1 (1-2): on paths (1,2) len 1 and (0,2) len 2 and (1,3) len 2 → 3 pairs
38/// // Edge 2 (2-3): on paths (2,3) len 1 and (1,3) len 2 → 2 pairs
39/// assert_eq!(eb, vec![2.0, 3.0, 2.0]);
40/// ```
41pub fn edge_betweenness_cutoff(graph: &Graph, cutoff: u32) -> IgraphResult<Vec<f64>> {
42    let n = graph.vcount();
43    let n_us = n as usize;
44    let m = graph.ecount();
45    let mut betweenness_e = vec![0.0_f64; m];
46
47    if n == 0 || m == 0 {
48        return Ok(betweenness_e);
49    }
50
51    let mut sigma = vec![0.0_f64; n_us];
52    let mut dist = vec![-1_i64; n_us];
53    let mut pred: Vec<Vec<(VertexId, EdgeId)>> = vec![Vec::new(); n_us];
54    let mut stack: Vec<VertexId> = Vec::with_capacity(n_us);
55    let mut delta_v = vec![0.0_f64; n_us];
56    let cutoff_i64 = i64::from(cutoff);
57
58    for s in 0..n {
59        sigma.fill(0.0);
60        dist.fill(-1);
61        for slot in &mut pred {
62            slot.clear();
63        }
64        stack.clear();
65        delta_v.fill(0.0);
66
67        sigma[s as usize] = 1.0;
68        dist[s as usize] = 0;
69        let mut queue: VecDeque<VertexId> = VecDeque::new();
70        queue.push_back(s);
71
72        while let Some(v) = queue.pop_front() {
73            stack.push(v);
74            let v_dist = dist[v as usize];
75
76            if v_dist >= cutoff_i64 {
77                continue;
78            }
79
80            for e in graph.incident(v)? {
81                let w = graph.edge_other(e, v)?;
82                if dist[w as usize] < 0 {
83                    queue.push_back(w);
84                    dist[w as usize] = v_dist + 1;
85                }
86                if dist[w as usize] == v_dist + 1 {
87                    sigma[w as usize] += sigma[v as usize];
88                    pred[w as usize].push((v, e));
89                }
90            }
91        }
92
93        while let Some(w) = stack.pop() {
94            for &(v, e) in &pred[w as usize] {
95                let c = (sigma[v as usize] / sigma[w as usize]) * (1.0 + delta_v[w as usize]);
96                delta_v[v as usize] += c;
97                betweenness_e[e as usize] += c;
98            }
99        }
100    }
101
102    if !graph.is_directed() {
103        for slot in &mut betweenness_e {
104            *slot /= 2.0;
105        }
106    }
107
108    Ok(betweenness_e)
109}
110
111#[cfg(test)]
112mod tests {
113    use super::*;
114
115    fn close(actual: &[f64], expected: &[f64], tol: f64) {
116        assert_eq!(actual.len(), expected.len(), "length mismatch");
117        for (i, (a, e)) in actual.iter().zip(expected.iter()).enumerate() {
118            assert!((a - e).abs() < tol, "edge {i}: actual={a} expected={e}");
119        }
120    }
121
122    #[test]
123    fn empty_graph() {
124        let g = Graph::new(0, false).unwrap();
125        let eb = edge_betweenness_cutoff(&g, 1).unwrap();
126        assert!(eb.is_empty());
127    }
128
129    #[test]
130    fn no_edges() {
131        let g = Graph::new(3, false).unwrap();
132        let eb = edge_betweenness_cutoff(&g, 5).unwrap();
133        assert!(eb.is_empty());
134    }
135
136    #[test]
137    fn path_full_cutoff() {
138        // 0—1—2—3: large cutoff → same as regular edge betweenness
139        let mut g = Graph::new(4, false).unwrap();
140        g.add_edges(vec![(0, 1), (1, 2), (2, 3)]).unwrap();
141        let eb = edge_betweenness_cutoff(&g, 100).unwrap();
142        close(&eb, &[3.0, 4.0, 3.0], 1e-12);
143    }
144
145    #[test]
146    fn path_cutoff_1() {
147        // 0—1—2—3 with cutoff=1: only direct edges, each with weight 1
148        let mut g = Graph::new(4, false).unwrap();
149        g.add_edges(vec![(0, 1), (1, 2), (2, 3)]).unwrap();
150        let eb = edge_betweenness_cutoff(&g, 1).unwrap();
151        // Each edge carries exactly 1 pair of length 1
152        close(&eb, &[1.0, 1.0, 1.0], 1e-12);
153    }
154
155    #[test]
156    fn path_cutoff_2() {
157        // 0—1—2—3 with cutoff=2
158        // Edge 0 (0-1): paths (0,1) len1, (0,2) len2 → 2
159        // Edge 1 (1-2): paths (1,2) len1, (0,2) len2, (1,3) len2 → 3
160        // Edge 2 (2-3): paths (2,3) len1, (1,3) len2 → 2
161        let mut g = Graph::new(4, false).unwrap();
162        g.add_edges(vec![(0, 1), (1, 2), (2, 3)]).unwrap();
163        let eb = edge_betweenness_cutoff(&g, 2).unwrap();
164        close(&eb, &[2.0, 3.0, 2.0], 1e-12);
165    }
166
167    #[test]
168    fn triangle() {
169        // Triangle: each edge carries 1 pair
170        let mut g = Graph::new(3, false).unwrap();
171        g.add_edges(vec![(0, 1), (1, 2), (2, 0)]).unwrap();
172        let eb = edge_betweenness_cutoff(&g, 10).unwrap();
173        close(&eb, &[1.0, 1.0, 1.0], 1e-12);
174    }
175
176    #[test]
177    fn star_cutoff_2() {
178        // Star: 0 connected to 1,2,3,4
179        // Each spoke carries: 1 direct pair + 3 pairs through centre to other leaves
180        // = 4 pairs per spoke
181        let mut g = Graph::new(5, false).unwrap();
182        g.add_edges(vec![(0, 1), (0, 2), (0, 3), (0, 4)]).unwrap();
183        let eb = edge_betweenness_cutoff(&g, 2).unwrap();
184        close(&eb, &[4.0, 4.0, 4.0, 4.0], 1e-12);
185    }
186
187    #[test]
188    fn directed_path() {
189        // 0→1→2→3 with large cutoff
190        let mut g = Graph::new(4, true).unwrap();
191        g.add_edges(vec![(0, 1), (1, 2), (2, 3)]).unwrap();
192        let eb = edge_betweenness_cutoff(&g, 100).unwrap();
193        // directed: edge 0→1 on (0,1),(0,2),(0,3) = 3
194        // edge 1→2 on (0,2),(0,3),(1,2),(1,3) = 4
195        // edge 2→3 on (0,3),(1,3),(2,3) = 3
196        close(&eb, &[3.0, 4.0, 3.0], 1e-12);
197    }
198
199    #[test]
200    fn directed_cutoff_2() {
201        // 0→1→2→3 with cutoff=2
202        // Edge 0→1: (0,1) len1, (0,2) len2 → 2
203        // Edge 1→2: (1,2) len1, (0,2) len2, (1,3) len2 → 3
204        // Edge 2→3: (2,3) len1, (1,3) len2 → 2
205        let mut g = Graph::new(4, true).unwrap();
206        g.add_edges(vec![(0, 1), (1, 2), (2, 3)]).unwrap();
207        let eb = edge_betweenness_cutoff(&g, 2).unwrap();
208        close(&eb, &[2.0, 3.0, 2.0], 1e-12);
209    }
210
211    #[test]
212    fn cutoff_zero() {
213        let mut g = Graph::new(3, false).unwrap();
214        g.add_edges(vec![(0, 1), (1, 2)]).unwrap();
215        let eb = edge_betweenness_cutoff(&g, 0).unwrap();
216        close(&eb, &[0.0, 0.0], 1e-12);
217    }
218}