Skip to main content

rust_igraph/algorithms/flow/
edge_disjoint_paths.rs

1//! `edge_disjoint_paths` (ALGO-FL-012) — maximum number of
2//! edge-disjoint `source → target` paths.
3//!
4//! Counterpart of `igraph_edge_disjoint_paths` in
5//! `references/igraph/src/flow/flow.c` (lines 2326-2343). The C source
6//! is a 15-line wrapper that rejects `source == target` and then
7//! delegates to `igraph_maxflow_value` with `NULL` capacity (unit
8//! capacities per edge), casting the resulting `igraph_real_t` to
9//! `igraph_int_t` (int64). By Menger's theorem (1927) the maximum
10//! number of pairwise edge-disjoint `s → t` paths equals the
11//! cardinality of the minimum s-t edge cut, which on unit capacities
12//! equals the s-t maximum flow.
13//!
14//! Note that `edge_disjoint_paths(g, s, t) == st_edge_connectivity(g,
15//! s, t)` whenever both functions are defined; both are aliases for
16//! `round(max_flow_value(g, s, t, None))`. We keep them as separate
17//! Rust functions for naming parity with igraph C (so call sites can
18//! pick the name that matches their intent — "paths" vs.
19//! "connectivity").
20
21use crate::core::{Graph, IgraphError, IgraphResult, VertexId};
22
23use super::max_flow::max_flow_value;
24
25/// Maximum number of pairwise edge-disjoint paths from `source` to
26/// `target`.
27///
28/// Counterpart of `igraph_edge_disjoint_paths` in
29/// `references/igraph/src/flow/flow.c:2326`. By Menger's theorem
30/// (1927) this equals the s-t edge connectivity (and, on unit
31/// capacities, the s-t maximum flow); see
32/// [`st_edge_connectivity`](super::st_edge_connectivity::st_edge_connectivity)
33/// for the connectivity-flavoured spelling.
34///
35/// # Arguments
36///
37/// * `graph` — input graph (directed or undirected).
38/// * `source` — source vertex id (`0 ≤ source < vcount()`).
39/// * `target` — sink vertex id (`0 ≤ target < vcount()`,
40///   `target != source`).
41///
42/// # Returns
43///
44/// The maximum number of edge-disjoint `source → target` paths as
45/// `i64`. Returns `0` when no `source → target` path exists.
46///
47/// # Errors
48///
49/// Same as [`max_flow_value`](super::max_flow::max_flow_value):
50///
51/// * [`IgraphError::VertexOutOfRange`] if `source` or `target` is
52///   outside `[0, vcount())`.
53/// * [`IgraphError::InvalidArgument`] if `source == target`. (igraph
54///   C returns `IGRAPH_UNIMPLEMENTED` for this case; we use
55///   `InvalidArgument` for parity with the rest of our flow module.)
56/// * [`IgraphError::Internal`] if the unit-capacity max-flow value is
57///   not representable as `i64` (unreachable in practice).
58///
59/// [`IgraphError::VertexOutOfRange`]: crate::core::IgraphError::VertexOutOfRange
60/// [`IgraphError::InvalidArgument`]: crate::core::IgraphError::InvalidArgument
61/// [`IgraphError::Internal`]: crate::core::IgraphError::Internal
62///
63/// # Examples
64///
65/// ```
66/// use rust_igraph::{Graph, edge_disjoint_paths};
67///
68/// // Two parallel unit-cap paths 0→1→3 and 0→2→3 → 2 edge-disjoint paths.
69/// let mut g = Graph::new(4, true).unwrap();
70/// g.add_edge(0, 1).unwrap();
71/// g.add_edge(1, 3).unwrap();
72/// g.add_edge(0, 2).unwrap();
73/// g.add_edge(2, 3).unwrap();
74/// assert_eq!(edge_disjoint_paths(&g, 0, 3).unwrap(), 2);
75/// ```
76pub fn edge_disjoint_paths(graph: &Graph, source: VertexId, target: VertexId) -> IgraphResult<i64> {
77    let flow = max_flow_value(graph, source, target, None)?;
78    // Unit-capacity max-flow is integral and bounded above by `ecount()`
79    // (≤ u32::MAX), so the f64 → i64 round + cast is lossless in every
80    // reachable case. The clippy allows below are *known* properties of
81    // this conversion, not bugs.
82    #[allow(
83        clippy::cast_precision_loss,
84        clippy::cast_possible_truncation,
85        clippy::cast_sign_loss
86    )]
87    {
88        if !flow.is_finite() || flow < 0.0 || flow > i64::MAX as f64 {
89            return Err(IgraphError::Internal(
90                "unit-capacity max-flow value is not representable as i64",
91            ));
92        }
93        Ok(flow.round() as i64)
94    }
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100    use crate::core::IgraphError;
101
102    #[test]
103    fn rejects_source_equals_target() {
104        let mut g = Graph::new(2, true).expect("graph");
105        g.add_edge(0, 1).expect("edge");
106        let err = edge_disjoint_paths(&g, 0, 0).unwrap_err();
107        match err {
108            IgraphError::InvalidArgument(_) => {}
109            other => panic!("expected InvalidArgument, got {other:?}"),
110        }
111    }
112
113    #[test]
114    fn rejects_out_of_range_source() {
115        let g = Graph::new(2, true).expect("graph");
116        let err = edge_disjoint_paths(&g, 5, 0).unwrap_err();
117        match err {
118            IgraphError::VertexOutOfRange { id, n } => {
119                assert_eq!(id, 5);
120                assert_eq!(n, 2);
121            }
122            other => panic!("expected VertexOutOfRange, got {other:?}"),
123        }
124    }
125
126    #[test]
127    fn rejects_out_of_range_target() {
128        let g = Graph::new(2, true).expect("graph");
129        let err = edge_disjoint_paths(&g, 0, 5).unwrap_err();
130        match err {
131            IgraphError::VertexOutOfRange { id, n } => {
132                assert_eq!(id, 5);
133                assert_eq!(n, 2);
134            }
135            other => panic!("expected VertexOutOfRange, got {other:?}"),
136        }
137    }
138
139    #[test]
140    fn isolated_endpoints_have_no_paths() {
141        let g = Graph::new(4, true).expect("graph");
142        assert_eq!(edge_disjoint_paths(&g, 0, 3).expect("paths"), 0);
143    }
144
145    #[test]
146    fn single_edge_one_path() {
147        let mut g = Graph::new(2, true).expect("graph");
148        g.add_edge(0, 1).expect("edge");
149        assert_eq!(edge_disjoint_paths(&g, 0, 1).expect("paths"), 1);
150    }
151
152    #[test]
153    fn two_parallel_paths() {
154        // 0→1→3 and 0→2→3 → 2 edge-disjoint paths.
155        let mut g = Graph::new(4, true).expect("graph");
156        for (s, t) in [(0u32, 1u32), (1, 3), (0, 2), (2, 3)] {
157            g.add_edge(s, t).expect("edge");
158        }
159        assert_eq!(edge_disjoint_paths(&g, 0, 3).expect("paths"), 2);
160    }
161
162    #[test]
163    fn parallel_arcs_count_each_as_path() {
164        let mut g = Graph::new(2, true).expect("graph");
165        for _ in 0..4 {
166            g.add_edge(0, 1).expect("edge");
167        }
168        assert_eq!(edge_disjoint_paths(&g, 0, 1).expect("paths"), 4);
169    }
170
171    #[test]
172    fn directed_no_reverse_path() {
173        // 0 → 1 → 2 → 3 directed chain.
174        let mut g = Graph::new(4, true).expect("graph");
175        for (s, t) in [(0u32, 1u32), (1, 2), (2, 3)] {
176            g.add_edge(s, t).expect("edge");
177        }
178        assert_eq!(edge_disjoint_paths(&g, 0, 3).expect("paths"), 1);
179        // Reverse direction has no paths.
180        assert_eq!(edge_disjoint_paths(&g, 3, 0).expect("paths"), 0);
181    }
182
183    #[test]
184    fn c_unit_fixture_directed_0_to_5() {
185        // Mirrors igraph_edge_disjoint_paths.c — directed 6-vertex
186        // graph with a self-loop at vertex 3. ep(0→5) = 2.
187        let mut g = Graph::new(6, true).expect("graph");
188        let arcs = [
189            (0u32, 1u32),
190            (0, 2),
191            (1, 2),
192            (1, 3),
193            (2, 4),
194            (3, 4),
195            (3, 5),
196            (4, 5),
197            (3, 3),
198        ];
199        for (s, t) in arcs {
200            g.add_edge(s, t).expect("edge");
201        }
202        assert_eq!(edge_disjoint_paths(&g, 0, 5).expect("paths"), 2);
203        assert_eq!(edge_disjoint_paths(&g, 0, 3).expect("paths"), 1);
204        assert_eq!(edge_disjoint_paths(&g, 3, 0).expect("paths"), 0);
205        assert_eq!(edge_disjoint_paths(&g, 3, 5).expect("paths"), 2);
206    }
207
208    #[test]
209    fn c_unit_fixture_undirected_4_to_3() {
210        // After igraph_to_undirected the same fixture has 3 vertex-paths
211        // 4→3 (direct, via 1, via 2). ep(4, 3) = 3.
212        let mut g = Graph::new(6, false).expect("graph");
213        for (s, t) in [
214            (0u32, 1u32),
215            (0, 2),
216            (1, 2),
217            (1, 3),
218            (2, 4),
219            (3, 4),
220            (3, 5),
221            (4, 5),
222            (3, 3),
223        ] {
224            g.add_edge(s, t).expect("edge");
225        }
226        assert_eq!(edge_disjoint_paths(&g, 4, 3).expect("paths"), 3);
227    }
228
229    #[test]
230    fn matches_st_edge_connectivity() {
231        // Belt-and-suspenders: every fixture should agree with
232        // st_edge_connectivity by Menger's theorem.
233        use super::super::st_edge_connectivity::st_edge_connectivity;
234        let mut g = Graph::new(5, false).expect("graph");
235        for i in 0u32..5 {
236            for j in (i + 1)..5 {
237                g.add_edge(i, j).expect("edge");
238            }
239        }
240        assert_eq!(
241            edge_disjoint_paths(&g, 0, 4).expect("paths"),
242            st_edge_connectivity(&g, 0, 4).expect("ec")
243        );
244    }
245}
246
247#[cfg(all(test, feature = "proptest-harness"))]
248mod proptests {
249    //! Proptest cross-validates the Menger equality:
250    //! `edge_disjoint_paths(g, s, t) == st_edge_connectivity(g, s, t)`
251    //! for every random unit-cap graph. Both functions are themselves
252    //! cross-validated against `max_flow_value` (see `max_flow.rs`
253    //! proptests), so this transitively inherits the Edmonds-Karp
254    //! independent reference.
255
256    use super::super::st_edge_connectivity::st_edge_connectivity;
257    use super::*;
258    use crate::core::Graph;
259    use proptest::prelude::*;
260
261    fn xorshift(mut r: u64) -> u64 {
262        r ^= r << 13;
263        r ^= r >> 7;
264        r ^= r << 17;
265        r
266    }
267
268    fn build_random(seed: u64, n: u32, m_max: u32, directed: bool) -> Graph {
269        let mut g = Graph::new(n, directed).expect("graph");
270        let mut state = seed | 1;
271        for _ in 0..m_max {
272            state = xorshift(state);
273            let u = u32::try_from(state % u64::from(n)).expect("modulo fits");
274            state = xorshift(state);
275            let v = u32::try_from(state % u64::from(n)).expect("modulo fits");
276            if u == v {
277                continue;
278            }
279            g.add_edge(u, v).expect("edge");
280        }
281        g
282    }
283
284    proptest! {
285        #[test]
286        fn menger_equals_st_edge_connectivity(
287            seed in any::<u64>(),
288            n in 2u32..8,
289            m in 1u32..16,
290            directed in any::<bool>(),
291        ) {
292            let g = build_random(seed, n, m, directed);
293            let s = u32::try_from(seed % u64::from(n)).expect("modulo fits");
294            let t_raw = u32::try_from(xorshift(seed) % u64::from(n)).expect("modulo fits");
295            let t = if t_raw == s { (s + 1) % n } else { t_raw };
296            prop_assume!(s != t);
297
298            let paths = edge_disjoint_paths(&g, s, t).expect("paths");
299            let ec = st_edge_connectivity(&g, s, t).expect("ec");
300            prop_assert_eq!(
301                paths,
302                ec,
303                "Menger violated: paths={} ec={} (n={}, m={}, directed={}, seed={})",
304                paths, ec, n, m, directed, seed
305            );
306        }
307    }
308}