Skip to main content

rust_igraph/algorithms/operators/
residual_graph.rs

1//! Residual and reverse-residual graphs (ALGO-OP-030).
2//!
3//! Counterpart of `igraph_residual_graph()` and
4//! `igraph_reverse_residual_graph()` from
5//! `references/igraph/src/flow/st-cuts.c:148-283`.
6//!
7//! The residual graph contains edges with remaining capacity
8//! (capacity − flow > 0). The reverse-residual graph contains both
9//! forward edges with positive flow and backward edges with remaining
10//! capacity — used in BFS-based min-cut algorithms.
11
12use crate::core::{Graph, IgraphError, IgraphResult};
13
14/// Result of the residual graph computation.
15#[derive(Debug, Clone)]
16pub struct ResidualGraphResult {
17    /// The residual directed graph (same vertex count, fewer edges).
18    pub graph: Graph,
19    /// Residual capacity of each edge in the new graph.
20    pub capacity: Vec<f64>,
21}
22
23/// Compute the residual graph given capacity and flow vectors.
24///
25/// For each edge `e` where `capacity[e] - flow[e] > 0`, the residual
26/// graph contains the same edge with residual capacity
27/// `capacity[e] - flow[e]`.
28///
29/// # Errors
30///
31/// Returns an error if `capacity` or `flow` length differs from the
32/// edge count.
33///
34/// # Examples
35///
36/// ```
37/// use rust_igraph::{Graph, residual_graph};
38///
39/// let mut g = Graph::new(3, true).unwrap();
40/// g.add_edge(0, 1).unwrap(); // edge 0
41/// g.add_edge(1, 2).unwrap(); // edge 1
42/// let capacity = vec![10.0, 5.0];
43/// let flow = vec![7.0, 5.0];
44/// let result = residual_graph(&g, &capacity, &flow).unwrap();
45/// // Edge 0 has residual 3.0, edge 1 is saturated (removed)
46/// assert_eq!(result.graph.vcount(), 3);
47/// assert_eq!(result.graph.ecount(), 1);
48/// assert_eq!(result.capacity, vec![3.0]);
49/// ```
50pub fn residual_graph(
51    graph: &Graph,
52    capacity: &[f64],
53    flow: &[f64],
54) -> IgraphResult<ResidualGraphResult> {
55    let m = graph.ecount();
56
57    if capacity.len() != m {
58        return Err(IgraphError::InvalidArgument(format!(
59            "residual_graph: capacity length {} != edge count {m}",
60            capacity.len()
61        )));
62    }
63    if flow.len() != m {
64        return Err(IgraphError::InvalidArgument(format!(
65            "residual_graph: flow length {} != edge count {m}",
66            flow.len()
67        )));
68    }
69
70    let n = graph.vcount();
71    let mut new_graph = Graph::new(n, true)?;
72    let mut res_capacity: Vec<f64> = Vec::new();
73
74    for eid in 0..m {
75        let residual = capacity[eid] - flow[eid];
76        if residual > 0.0 {
77            let eid32 = u32::try_from(eid).map_err(|_| {
78                IgraphError::InvalidArgument("residual_graph: edge id overflow".into())
79            })?;
80            let (from, to) = graph.edge(eid32)?;
81            new_graph.add_edge(from, to)?;
82            res_capacity.push(residual);
83        }
84    }
85
86    Ok(ResidualGraphResult {
87        graph: new_graph,
88        capacity: res_capacity,
89    })
90}
91
92/// Compute the reverse-residual graph given capacity and flow vectors.
93///
94/// For each edge `e` in the original graph:
95/// - If `flow[e] > 0`: adds forward edge `(from, to)`
96/// - If `flow[e] < capacity[e]`: adds backward edge `(to, from)`
97///
98/// If `capacity` is `None`, all edges are assumed to have capacity 1.0.
99///
100/// # Errors
101///
102/// Returns an error if vector lengths don't match the edge count.
103///
104/// # Examples
105///
106/// ```
107/// use rust_igraph::{Graph, reverse_residual_graph};
108///
109/// let mut g = Graph::new(3, true).unwrap();
110/// g.add_edge(0, 1).unwrap(); // edge 0
111/// g.add_edge(1, 2).unwrap(); // edge 1
112/// let capacity = vec![10.0, 5.0];
113/// let flow = vec![7.0, 5.0];
114/// let result = reverse_residual_graph(&g, Some(&capacity), &flow).unwrap();
115/// // Edge 0: flow>0 → forward (0,1); residual>0 → backward (1,0) = 2 edges
116/// // Edge 1: flow>0 → forward (1,2); saturated → no backward = 1 edge
117/// assert_eq!(result.vcount(), 3);
118/// assert_eq!(result.ecount(), 3);
119/// ```
120pub fn reverse_residual_graph(
121    graph: &Graph,
122    capacity: Option<&[f64]>,
123    flow: &[f64],
124) -> IgraphResult<Graph> {
125    let m = graph.ecount();
126
127    if let Some(cap) = capacity {
128        if cap.len() != m {
129            return Err(IgraphError::InvalidArgument(format!(
130                "reverse_residual_graph: capacity length {} != edge count {m}",
131                cap.len()
132            )));
133        }
134    }
135    if flow.len() != m {
136        return Err(IgraphError::InvalidArgument(format!(
137            "reverse_residual_graph: flow length {} != edge count {m}",
138            flow.len()
139        )));
140    }
141
142    let n = graph.vcount();
143    let mut new_graph = Graph::new(n, true)?;
144
145    for eid in 0..m {
146        let cap = capacity.map_or(1.0, |c| c[eid]);
147        let eid32 = u32::try_from(eid).map_err(|_| {
148            IgraphError::InvalidArgument("reverse_residual_graph: edge id overflow".into())
149        })?;
150        let (from, to) = graph.edge(eid32)?;
151
152        if flow[eid] > 0.0 {
153            new_graph.add_edge(from, to)?;
154        }
155        if flow[eid] < cap {
156            new_graph.add_edge(to, from)?;
157        }
158    }
159
160    Ok(new_graph)
161}
162
163#[cfg(test)]
164#[allow(clippy::float_cmp)]
165mod tests {
166    use super::*;
167
168    #[test]
169    fn residual_empty() {
170        let g = Graph::new(0, true).unwrap();
171        let result = residual_graph(&g, &[], &[]).unwrap();
172        assert_eq!(result.graph.vcount(), 0);
173        assert_eq!(result.graph.ecount(), 0);
174    }
175
176    #[test]
177    fn residual_no_flow() {
178        let mut g = Graph::new(3, true).unwrap();
179        g.add_edge(0, 1).unwrap();
180        g.add_edge(1, 2).unwrap();
181        let cap = vec![10.0, 5.0];
182        let flow = vec![0.0, 0.0];
183        let result = residual_graph(&g, &cap, &flow).unwrap();
184        assert_eq!(result.graph.ecount(), 2);
185        assert_eq!(result.capacity, vec![10.0, 5.0]);
186    }
187
188    #[test]
189    fn residual_saturated() {
190        let mut g = Graph::new(2, true).unwrap();
191        g.add_edge(0, 1).unwrap();
192        let cap = vec![5.0];
193        let flow = vec![5.0];
194        let result = residual_graph(&g, &cap, &flow).unwrap();
195        assert_eq!(result.graph.ecount(), 0);
196        assert!(result.capacity.is_empty());
197    }
198
199    #[test]
200    fn residual_partial_flow() {
201        let mut g = Graph::new(4, true).unwrap();
202        g.add_edge(0, 1).unwrap();
203        g.add_edge(1, 2).unwrap();
204        g.add_edge(0, 3).unwrap();
205        let cap = vec![10.0, 10.0, 5.0];
206        let flow = vec![7.0, 10.0, 3.0];
207        let result = residual_graph(&g, &cap, &flow).unwrap();
208        // Edge 0: residual 3, Edge 1: saturated, Edge 2: residual 2
209        assert_eq!(result.graph.ecount(), 2);
210        assert_eq!(result.capacity, vec![3.0, 2.0]);
211    }
212
213    #[test]
214    fn residual_invalid_capacity_len() {
215        let mut g = Graph::new(2, true).unwrap();
216        g.add_edge(0, 1).unwrap();
217        assert!(residual_graph(&g, &[1.0, 2.0], &[0.0]).is_err());
218    }
219
220    #[test]
221    fn residual_invalid_flow_len() {
222        let mut g = Graph::new(2, true).unwrap();
223        g.add_edge(0, 1).unwrap();
224        assert!(residual_graph(&g, &[1.0], &[0.0, 0.0]).is_err());
225    }
226
227    #[test]
228    fn reverse_residual_empty() {
229        let g = Graph::new(0, true).unwrap();
230        let result = reverse_residual_graph(&g, Some(&[]), &[]).unwrap();
231        assert_eq!(result.vcount(), 0);
232        assert_eq!(result.ecount(), 0);
233    }
234
235    #[test]
236    fn reverse_residual_no_flow() {
237        let mut g = Graph::new(2, true).unwrap();
238        g.add_edge(0, 1).unwrap();
239        let cap = vec![5.0];
240        let flow = vec![0.0];
241        let result = reverse_residual_graph(&g, Some(&cap), &flow).unwrap();
242        // No forward (flow=0), one backward (flow<cap)
243        assert_eq!(result.ecount(), 1);
244        assert_eq!(result.edge(0).unwrap(), (1, 0));
245    }
246
247    #[test]
248    fn reverse_residual_full_flow() {
249        let mut g = Graph::new(2, true).unwrap();
250        g.add_edge(0, 1).unwrap();
251        let cap = vec![5.0];
252        let flow = vec![5.0];
253        let result = reverse_residual_graph(&g, Some(&cap), &flow).unwrap();
254        // Forward only (flow>0, flow==cap so no backward)
255        assert_eq!(result.ecount(), 1);
256        assert_eq!(result.edge(0).unwrap(), (0, 1));
257    }
258
259    #[test]
260    fn reverse_residual_partial() {
261        let mut g = Graph::new(2, true).unwrap();
262        g.add_edge(0, 1).unwrap();
263        let cap = vec![10.0];
264        let flow = vec![7.0];
265        let result = reverse_residual_graph(&g, Some(&cap), &flow).unwrap();
266        // Both forward (flow>0) and backward (flow<cap)
267        assert_eq!(result.ecount(), 2);
268        assert_eq!(result.edge(0).unwrap(), (0, 1));
269        assert_eq!(result.edge(1).unwrap(), (1, 0));
270    }
271
272    #[test]
273    fn reverse_residual_no_capacity() {
274        let mut g = Graph::new(2, true).unwrap();
275        g.add_edge(0, 1).unwrap();
276        let flow = vec![0.5];
277        let result = reverse_residual_graph(&g, None, &flow).unwrap();
278        // cap=1.0 by default: flow>0 → forward, flow<1.0 → backward
279        assert_eq!(result.ecount(), 2);
280    }
281
282    #[test]
283    fn reverse_residual_invalid_len() {
284        let mut g = Graph::new(2, true).unwrap();
285        g.add_edge(0, 1).unwrap();
286        assert!(reverse_residual_graph(&g, Some(&[1.0, 2.0]), &[0.0]).is_err());
287        assert!(reverse_residual_graph(&g, Some(&[1.0]), &[0.0, 0.0]).is_err());
288    }
289}