Skip to main content

rust_igraph/algorithms/connectivity/
reachability_scc.rs

1//! SCC-based reachability (ALGO-CC-023).
2//!
3//! Counterpart of `igraph_reachability()` from
4//! `references/igraph/src/connectivity/reachability.c:72-149`.
5//!
6//! Computes which vertices are reachable from each vertex via
7//! SCC-condensation + bitset propagation. For directed graphs the
8//! condensation DAG is traversed in reverse topological order; for
9//! undirected graphs (or `All` mode) each weak component trivially
10//! reaches itself.
11//!
12//! Time: O(|C|·|V|/w + |V| + |E|) where |C| is the SCC count and w is
13//! the machine word size.
14
15use crate::algorithms::connectivity::components::connected_components;
16use crate::algorithms::connectivity::strong::strongly_connected_components;
17use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
18
19/// Direction mode for reachability in directed graphs.
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum ReachabilityMode {
22    /// Follow edges forward (out-neighbours).
23    Out,
24    /// Follow edges backward (in-neighbours).
25    In,
26    /// Ignore direction (treat as undirected).
27    All,
28}
29
30/// Result of a reachability analysis.
31#[derive(Debug, Clone)]
32pub struct ReachabilityResult {
33    /// `membership[v]` — component id for vertex `v`.
34    pub membership: Vec<u32>,
35    /// `csize[c]` — number of vertices in component `c`.
36    pub csize: Vec<u32>,
37    /// Number of components.
38    pub num_components: u32,
39    /// `reach[c]` — bitvector of length `vcount`; bit `v` is set if
40    /// vertex `v` is reachable from any vertex in component `c`.
41    pub reach: Vec<Vec<u64>>,
42}
43
44impl ReachabilityResult {
45    /// Returns `true` if vertex `target` is reachable from vertex `source`.
46    pub fn is_reachable(&self, source: VertexId, target: VertexId) -> bool {
47        let comp = self.membership[source as usize] as usize;
48        let word = target as usize / 64;
49        let bit = target as usize % 64;
50        if word >= self.reach[comp].len() {
51            return false;
52        }
53        (self.reach[comp][word] >> bit) & 1 == 1
54    }
55
56    /// Count how many vertices are reachable from `v` (including `v`).
57    pub fn count_from(&self, v: VertexId) -> u32 {
58        let comp = self.membership[v as usize] as usize;
59        let mut count = 0u32;
60        for &word in &self.reach[comp] {
61            count = count.wrapping_add(word.count_ones());
62        }
63        count
64    }
65}
66
67fn bitvec_words(n: u32) -> usize {
68    (n as usize).div_ceil(64)
69}
70
71fn bitvec_set(bv: &mut [u64], bit: u32) {
72    let w = bit as usize / 64;
73    let b = bit as usize % 64;
74    bv[w] |= 1u64 << b;
75}
76
77fn bitvec_or(dst: &mut [u64], src: &[u64]) {
78    for (d, s) in dst.iter_mut().zip(src.iter()) {
79        *d |= *s;
80    }
81}
82
83/// Compute per-component reachability bitsets.
84///
85/// Counterpart of `igraph_reachability()`. For directed graphs with
86/// `Out` mode, vertex `v` reaches vertex `u` if there is a directed
87/// path from `v` to `u`. With `In` mode, edges are traversed in
88/// reverse. With `All` mode (or undirected graphs), direction is
89/// ignored.
90///
91/// # Examples
92///
93/// ```
94/// use rust_igraph::{Graph, reachability, ReachabilityMode};
95///
96/// // Directed chain 0→1→2
97/// let mut g = Graph::new(3, true).unwrap();
98/// g.add_edge(0, 1).unwrap();
99/// g.add_edge(1, 2).unwrap();
100///
101/// let r = reachability(&g, ReachabilityMode::Out).unwrap();
102/// assert!(r.is_reachable(0, 2));
103/// assert!(!r.is_reachable(2, 0));
104/// assert_eq!(r.count_from(0), 3);
105/// assert_eq!(r.count_from(2), 1);
106/// ```
107pub fn reachability(graph: &Graph, mode: ReachabilityMode) -> IgraphResult<ReachabilityResult> {
108    let n = graph.vcount();
109    let words = bitvec_words(n);
110
111    let effective_mode = if graph.is_directed() {
112        mode
113    } else {
114        ReachabilityMode::All
115    };
116
117    // Step 1: compute components
118    let (membership, num_components) = if effective_mode == ReachabilityMode::All {
119        let cc = connected_components(graph)?;
120        (cc.membership, cc.count)
121    } else {
122        let scc = strongly_connected_components(graph)?;
123        (scc.membership, scc.count)
124    };
125
126    // Step 2: compute component sizes
127    let mut csize = vec![0u32; num_components as usize];
128    for &m in &membership {
129        csize[m as usize] = csize[m as usize]
130            .checked_add(1)
131            .ok_or(IgraphError::Internal("component size overflow"))?;
132    }
133
134    // Step 3: initialise reach bitsets — each component contains its own vertices
135    let mut reach = vec![vec![0u64; words]; num_components as usize];
136    for v in 0..n {
137        bitvec_set(&mut reach[membership[v as usize] as usize], v);
138    }
139
140    // For undirected / All mode, components are self-contained — done
141    if effective_mode == ReachabilityMode::All {
142        return Ok(ReachabilityResult {
143            membership,
144            csize,
145            num_components,
146            reach,
147        });
148    }
149
150    // Step 4: build condensation DAG adjacency list
151    // For each SCC, collect which other SCCs it can reach directly
152    let mut dag: Vec<Vec<u32>> = vec![Vec::new(); num_components as usize];
153
154    for v in 0..n {
155        let v_comp = membership[v as usize];
156        let neighbors = match effective_mode {
157            ReachabilityMode::Out => graph.out_neighbors_vec(v)?,
158            ReachabilityMode::In => graph.in_neighbors_vec(v)?,
159            ReachabilityMode::All => unreachable!(),
160        };
161        for w in neighbors {
162            let w_comp = membership[w as usize];
163            if v_comp != w_comp {
164                dag[v_comp as usize].push(w_comp);
165            }
166        }
167    }
168
169    // Step 5: propagate in reverse topological order
170    // SCCs from strongly_connected_components are indexed in topological order
171    // (Kosaraju guarantees this). For Out mode, iterate from last to first;
172    // for In mode, iterate from first to last.
173    let nc = num_components as usize;
174    for i in 0..nc {
175        let comp = if effective_mode == ReachabilityMode::In {
176            i
177        } else {
178            nc - 1 - i
179        };
180
181        // Deduplicate DAG neighbors for this component
182        dag[comp].sort_unstable();
183        dag[comp].dedup();
184
185        // Collect neighbor bitsets to OR in
186        // We need to avoid borrowing reach mutably and immutably at the same time
187        let neighbor_comps: Vec<u32> = dag[comp].clone();
188        for &nc_id in &neighbor_comps {
189            // Copy the neighbor's bitset, then OR into ours
190            let src = reach[nc_id as usize].clone();
191            bitvec_or(&mut reach[comp], &src);
192        }
193    }
194
195    Ok(ReachabilityResult {
196        membership,
197        csize,
198        num_components,
199        reach,
200    })
201}
202
203#[cfg(test)]
204mod tests {
205    use super::*;
206
207    #[test]
208    fn empty_graph() {
209        let g = Graph::with_vertices(0);
210        let r = reachability(&g, ReachabilityMode::Out).unwrap();
211        assert_eq!(r.num_components, 0);
212        assert!(r.reach.is_empty());
213    }
214
215    #[test]
216    fn isolated_vertices() {
217        let g = Graph::with_vertices(5);
218        let r = reachability(&g, ReachabilityMode::Out).unwrap();
219        assert_eq!(r.num_components, 5);
220        for v in 0..5u32 {
221            assert_eq!(r.count_from(v), 1);
222            assert!(r.is_reachable(v, v));
223        }
224        assert!(!r.is_reachable(0, 1));
225    }
226
227    #[test]
228    fn undirected_path() {
229        let mut g = Graph::with_vertices(4);
230        for i in 0..3u32 {
231            g.add_edge(i, i + 1).unwrap();
232        }
233        let r = reachability(&g, ReachabilityMode::All).unwrap();
234        assert_eq!(r.num_components, 1);
235        for u in 0..4u32 {
236            for v in 0..4u32 {
237                assert!(r.is_reachable(u, v));
238            }
239        }
240    }
241
242    #[test]
243    fn undirected_two_components() {
244        let mut g = Graph::with_vertices(5);
245        g.add_edge(0, 1).unwrap();
246        g.add_edge(1, 2).unwrap();
247        g.add_edge(3, 4).unwrap();
248
249        let r = reachability(&g, ReachabilityMode::All).unwrap();
250        assert_eq!(r.num_components, 2);
251        assert!(r.is_reachable(0, 2));
252        assert!(r.is_reachable(2, 0));
253        assert!(r.is_reachable(3, 4));
254        assert!(!r.is_reachable(0, 3));
255        assert!(!r.is_reachable(3, 0));
256    }
257
258    #[test]
259    fn directed_chain_out() {
260        // 0→1→2→3
261        let mut g = Graph::new(4, true).unwrap();
262        for i in 0..3u32 {
263            g.add_edge(i, i + 1).unwrap();
264        }
265        let r = reachability(&g, ReachabilityMode::Out).unwrap();
266        assert_eq!(r.count_from(0), 4);
267        assert_eq!(r.count_from(1), 3);
268        assert_eq!(r.count_from(2), 2);
269        assert_eq!(r.count_from(3), 1);
270
271        assert!(r.is_reachable(0, 3));
272        assert!(!r.is_reachable(3, 0));
273    }
274
275    #[test]
276    fn directed_chain_in() {
277        // 0→1→2→3 with In mode = reverse edges
278        let mut g = Graph::new(4, true).unwrap();
279        for i in 0..3u32 {
280            g.add_edge(i, i + 1).unwrap();
281        }
282        let r = reachability(&g, ReachabilityMode::In).unwrap();
283        // In mode reverses: 3 can reach everything backwards
284        assert_eq!(r.count_from(3), 4);
285        assert_eq!(r.count_from(2), 3);
286        assert_eq!(r.count_from(1), 2);
287        assert_eq!(r.count_from(0), 1);
288
289        assert!(r.is_reachable(3, 0));
290        assert!(!r.is_reachable(0, 3));
291    }
292
293    #[test]
294    fn directed_cycle() {
295        // 0→1→2→0: all vertices in one SCC, all reach all
296        let mut g = Graph::new(3, true).unwrap();
297        g.add_edge(0, 1).unwrap();
298        g.add_edge(1, 2).unwrap();
299        g.add_edge(2, 0).unwrap();
300
301        let r = reachability(&g, ReachabilityMode::Out).unwrap();
302        assert_eq!(r.num_components, 1);
303        for u in 0..3u32 {
304            assert_eq!(r.count_from(u), 3);
305        }
306    }
307
308    #[test]
309    fn directed_diamond() {
310        // 0→1, 0→2, 1→3, 2→3
311        let mut g = Graph::new(4, true).unwrap();
312        g.add_edge(0, 1).unwrap();
313        g.add_edge(0, 2).unwrap();
314        g.add_edge(1, 3).unwrap();
315        g.add_edge(2, 3).unwrap();
316
317        let r = reachability(&g, ReachabilityMode::Out).unwrap();
318        assert_eq!(r.count_from(0), 4);
319        assert_eq!(r.count_from(1), 2); // 1, 3
320        assert_eq!(r.count_from(2), 2); // 2, 3
321        assert_eq!(r.count_from(3), 1); // 3
322
323        assert!(r.is_reachable(0, 3));
324        assert!(!r.is_reachable(3, 0));
325    }
326
327    #[test]
328    fn directed_two_sccs_connected() {
329        // SCC1: 0→1→0, SCC2: 2→3→2, bridge: 1→2
330        let mut g = Graph::new(4, true).unwrap();
331        g.add_edge(0, 1).unwrap();
332        g.add_edge(1, 0).unwrap();
333        g.add_edge(2, 3).unwrap();
334        g.add_edge(3, 2).unwrap();
335        g.add_edge(1, 2).unwrap(); // bridge SCC1→SCC2
336
337        let r = reachability(&g, ReachabilityMode::Out).unwrap();
338        // 0 and 1 reach everything
339        assert_eq!(r.count_from(0), 4);
340        assert_eq!(r.count_from(1), 4);
341        // 2 and 3 only reach SCC2
342        assert_eq!(r.count_from(2), 2);
343        assert_eq!(r.count_from(3), 2);
344    }
345
346    #[test]
347    fn self_loop_directed() {
348        let mut g = Graph::new(2, true).unwrap();
349        g.add_edge(0, 0).unwrap();
350        g.add_edge(0, 1).unwrap();
351
352        let r = reachability(&g, ReachabilityMode::Out).unwrap();
353        assert_eq!(r.count_from(0), 2);
354        assert_eq!(r.count_from(1), 1);
355    }
356
357    #[test]
358    fn mode_all_on_directed() {
359        // Directed chain 0→1→2, but All mode ignores direction
360        let mut g = Graph::new(3, true).unwrap();
361        g.add_edge(0, 1).unwrap();
362        g.add_edge(1, 2).unwrap();
363
364        let r = reachability(&g, ReachabilityMode::All).unwrap();
365        assert_eq!(r.num_components, 1);
366        for v in 0..3u32 {
367            assert_eq!(r.count_from(v), 3);
368        }
369    }
370
371    #[test]
372    fn large_bitvec_more_than_64_vertices() {
373        // 70 vertices in a chain: 0→1→2→...→69
374        let mut g = Graph::new(70, true).unwrap();
375        for i in 0..69u32 {
376            g.add_edge(i, i + 1).unwrap();
377        }
378
379        let r = reachability(&g, ReachabilityMode::Out).unwrap();
380        assert_eq!(r.count_from(0), 70);
381        assert_eq!(r.count_from(69), 1);
382        assert!(r.is_reachable(0, 69));
383        assert!(!r.is_reachable(69, 0));
384    }
385
386    #[test]
387    fn csize_correct() {
388        // Two weak components: {0,1,2} and {3,4}
389        let mut g = Graph::with_vertices(5);
390        g.add_edge(0, 1).unwrap();
391        g.add_edge(1, 2).unwrap();
392        g.add_edge(3, 4).unwrap();
393
394        let r = reachability(&g, ReachabilityMode::All).unwrap();
395        let mut sizes: Vec<u32> = r.csize.clone();
396        sizes.sort_unstable();
397        assert_eq!(sizes, vec![2, 3]);
398    }
399
400    #[test]
401    fn agrees_with_count_reachable() {
402        use crate::algorithms::connectivity::reachability::count_reachable;
403
404        let mut g = Graph::new(6, true).unwrap();
405        g.add_edge(0, 1).unwrap();
406        g.add_edge(1, 2).unwrap();
407        g.add_edge(2, 0).unwrap();
408        g.add_edge(2, 3).unwrap();
409        g.add_edge(3, 4).unwrap();
410        g.add_edge(5, 3).unwrap();
411
412        let r = reachability(&g, ReachabilityMode::Out).unwrap();
413        let cr = count_reachable(&g).unwrap();
414
415        for v in 0..6u32 {
416            assert_eq!(r.count_from(v), cr[v as usize], "mismatch at vertex {v}");
417        }
418    }
419}