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