Skip to main content

problemreductions/models/graph/
integral_flow_bundles.rs

1//! Integral Flow with Bundles problem implementation.
2//!
3//! Given a directed graph with overlapping bundle-capacity constraints on arcs,
4//! determine whether an integral flow can deliver a required amount to the sink.
5
6use crate::registry::{CreateSpec, ProblemSchemaEntry};
7use crate::topology::DirectedGraph;
8use crate::traits::Problem;
9use serde::{Deserialize, Serialize};
10use std::collections::BTreeSet;
11
12inventory::submit! {
13    ProblemSchemaEntry {
14        name: "IntegralFlowBundles",
15        display_name: "Integral Flow with Bundles",
16        aliases: &[],
17        dimensions: &[],
18        category: crate::registry::ProblemCategory::Graph,
19        module_path: module_path!(),
20        description: "Integral flow feasibility on a directed graph with overlapping bundle capacities",
21        fields: IntegralFlowBundlesCreateSpec::FIELDS,
22    }
23}
24
25/// Integral Flow with Bundles (Garey & Johnson ND36).
26#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct IntegralFlowBundles {
28    graph: DirectedGraph,
29    source: usize,
30    sink: usize,
31    bundles: Vec<Vec<usize>>,
32    bundle_capacities: Vec<i64>,
33    requirement: i64,
34}
35
36#[derive(Debug, Deserialize, crate::CreateSpec)]
37struct IntegralFlowBundlesCreateSpec {
38    #[create(codec = "arc-list")]
39    arcs: Vec<(usize, usize)>,
40    num_vertices: Option<usize>,
41    #[create(codec = "semicolon-separated")]
42    bundles: Vec<Vec<usize>>,
43    #[create(codec = "comma-separated")]
44    bundle_capacities: Vec<i64>,
45    source: usize,
46    sink: usize,
47    requirement: i64,
48}
49
50impl TryFrom<IntegralFlowBundlesCreateSpec> for IntegralFlowBundles {
51    type Error = crate::registry::ConstructionError;
52    fn try_from(
53        spec: IntegralFlowBundlesCreateSpec,
54    ) -> Result<Self, crate::registry::ConstructionError> {
55        if spec.arcs.is_empty() {
56            return Err("arcs must be non-empty".into());
57        }
58        let inferred = spec
59            .arcs
60            .iter()
61            .flat_map(|&(u, v)| [u, v])
62            .max()
63            .map(|v| v.checked_add(1).ok_or("vertex count overflows usize"))
64            .transpose()?
65            .unwrap_or(0);
66        let count = spec.num_vertices.unwrap_or(inferred);
67        if count < inferred {
68            return Err("num_vertices is too small".into());
69        }
70        if spec.source >= count || spec.sink >= count {
71            return Err("source and sink must be valid vertices".into());
72        }
73        if spec.source == spec.sink {
74            return Err("source and sink must be distinct".into());
75        }
76        if spec.bundles.len() != spec.bundle_capacities.len() {
77            return Err("bundles length must match bundle_capacities length".into());
78        }
79        if spec.requirement == 0 {
80            return Err("requirement must be positive".into());
81        }
82        let mut covered = vec![false; spec.arcs.len()];
83        let mut upper = vec![i64::MAX; spec.arcs.len()];
84        for (i, (bundle, &capacity)) in spec.bundles.iter().zip(&spec.bundle_capacities).enumerate()
85        {
86            if capacity == 0 {
87                return Err(format!("bundle capacity {i} must be positive").into());
88            }
89            let mut seen = BTreeSet::new();
90            for &arc in bundle {
91                if arc >= spec.arcs.len() {
92                    return Err(format!("bundle {i} arc is out of range").into());
93                }
94                if !seen.insert(arc) {
95                    return Err(format!("bundle {i} contains duplicate arc").into());
96                }
97                covered[arc] = true;
98                upper[arc] = upper[arc].min(capacity);
99            }
100        }
101        for (arc, &is_covered) in covered.iter().enumerate() {
102            if !is_covered {
103                return Err(format!("arc {arc} must belong to a bundle").into());
104            }
105            if usize::try_from(upper[arc])
106                .ok()
107                .and_then(|v| v.checked_add(1))
108                .is_none()
109            {
110                return Err(format!("arc {arc} upper bound is too large").into());
111            }
112        }
113        Ok(Self {
114            graph: DirectedGraph::new(count, spec.arcs),
115            source: spec.source,
116            sink: spec.sink,
117            bundles: spec.bundles,
118            bundle_capacities: spec.bundle_capacities,
119            requirement: spec.requirement,
120        })
121    }
122}
123
124impl IntegralFlowBundles {
125    /// Create a new Integral Flow with Bundles instance.
126    pub fn new(
127        graph: DirectedGraph,
128        source: usize,
129        sink: usize,
130        bundles: Vec<Vec<usize>>,
131        bundle_capacities: Vec<i64>,
132        requirement: i64,
133    ) -> Self {
134        let num_vertices = graph.num_vertices();
135        let num_arcs = graph.num_arcs();
136
137        assert!(
138            source < num_vertices,
139            "source ({source}) >= num_vertices ({num_vertices})"
140        );
141        assert!(
142            sink < num_vertices,
143            "sink ({sink}) >= num_vertices ({num_vertices})"
144        );
145        assert!(source != sink, "source and sink must be distinct");
146        assert_eq!(
147            bundles.len(),
148            bundle_capacities.len(),
149            "bundles length must match bundle_capacities length"
150        );
151        assert!(requirement > 0, "requirement must be positive");
152
153        let mut arc_covered = vec![false; num_arcs];
154        let mut arc_upper_bounds = vec![i64::MAX; num_arcs];
155
156        for (bundle_index, (bundle, &capacity)) in
157            bundles.iter().zip(&bundle_capacities).enumerate()
158        {
159            assert!(
160                capacity > 0,
161                "bundle capacity at index {bundle_index} must be positive"
162            );
163
164            let mut seen = BTreeSet::new();
165            for &arc_index in bundle {
166                assert!(
167                    arc_index < num_arcs,
168                    "bundle {bundle_index} references arc {arc_index}, but num_arcs is {num_arcs}"
169                );
170                assert!(
171                    seen.insert(arc_index),
172                    "bundle {bundle_index} contains duplicate arc index {arc_index}"
173                );
174                arc_covered[arc_index] = true;
175                arc_upper_bounds[arc_index] = arc_upper_bounds[arc_index].min(capacity);
176            }
177        }
178
179        for (arc_index, covered) in arc_covered.iter().copied().enumerate() {
180            assert!(
181                covered,
182                "arc {arc_index} must belong to at least one bundle"
183            );
184            let domain = usize::try_from(arc_upper_bounds[arc_index])
185                .ok()
186                .and_then(|bound| bound.checked_add(1));
187            assert!(
188                domain.is_some(),
189                "bundle-derived upper bound for arc {arc_index} must fit into usize for dims()"
190            );
191        }
192
193        Self {
194            graph,
195            source,
196            sink,
197            bundles,
198            bundle_capacities,
199            requirement,
200        }
201    }
202
203    /// Get the underlying directed graph.
204    pub fn graph(&self) -> &DirectedGraph {
205        &self.graph
206    }
207
208    /// Get the source vertex.
209    pub fn source(&self) -> usize {
210        self.source
211    }
212
213    /// Get the sink vertex.
214    pub fn sink(&self) -> usize {
215        self.sink
216    }
217
218    /// Get the bundles.
219    pub fn bundles(&self) -> &[Vec<usize>] {
220        &self.bundles
221    }
222
223    /// Get the bundle capacities.
224    pub fn bundle_capacities(&self) -> &[i64] {
225        &self.bundle_capacities
226    }
227
228    /// Get the required net inflow at the sink.
229    pub fn requirement(&self) -> i64 {
230        self.requirement
231    }
232
233    /// Get the number of vertices.
234    pub fn num_vertices(&self) -> usize {
235        self.graph.num_vertices()
236    }
237
238    /// Get the number of arcs.
239    pub fn num_arcs(&self) -> usize {
240        self.graph.num_arcs()
241    }
242
243    /// Get the number of bundles.
244    pub fn num_bundles(&self) -> usize {
245        self.bundles.len()
246    }
247
248    /// Check whether a configuration is feasible.
249    pub fn is_valid_solution(
250        &self,
251        config: &[usize],
252    ) -> Result<bool, crate::traits::EvaluationError> {
253        Ok(self.evaluate_solution(config)?.0)
254    }
255
256    fn arc_upper_bounds(&self) -> Vec<i64> {
257        let mut upper_bounds = vec![i64::MAX; self.num_arcs()];
258        for (bundle, &capacity) in self.bundles.iter().zip(&self.bundle_capacities) {
259            for &arc_index in bundle {
260                upper_bounds[arc_index] = upper_bounds[arc_index].min(capacity);
261            }
262        }
263        upper_bounds
264    }
265
266    fn vertex_balance(
267        &self,
268        config: &[usize],
269        vertex: usize,
270    ) -> Result<Option<i64>, crate::traits::EvaluationError> {
271        let mut balance = 0_i64;
272        for (arc_index, (u, v)) in self.graph.arcs().into_iter().enumerate() {
273            let Some(&raw_flow) = config.get(arc_index) else {
274                return Ok(None);
275            };
276            let flow = i64::try_from(raw_flow).map_err(|_| {
277                crate::traits::EvaluationError::IntegerOverflow(
278                    "converting bundled arc flow to i64".into(),
279                )
280            })?;
281            if vertex == u {
282                balance = balance.checked_sub(flow).ok_or_else(|| {
283                    crate::traits::EvaluationError::IntegerOverflow(
284                        "subtracting outgoing bundled flow".into(),
285                    )
286                })?;
287            }
288            if vertex == v {
289                balance = balance.checked_add(flow).ok_or_else(|| {
290                    crate::traits::EvaluationError::IntegerOverflow(
291                        "adding incoming bundled flow".into(),
292                    )
293                })?;
294            }
295        }
296        Ok(Some(balance))
297    }
298
299    fn evaluate_solution(
300        &self,
301        config: &[usize],
302    ) -> Result<crate::types::Or, crate::traits::EvaluationError> {
303        if config.len() != self.num_arcs() {
304            return Err(crate::traits::EvaluationError::InvalidConfiguration(
305                "flow vector length does not match the graph arcs".into(),
306            ));
307        }
308
309        let upper_bounds = self.arc_upper_bounds();
310        for (&value, &upper_bound) in config.iter().zip(&upper_bounds) {
311            if i64::try_from(value).map_or(true, |value| value > upper_bound) {
312                return Ok(crate::types::Or(false));
313            }
314        }
315
316        for (bundle, &capacity) in self.bundles.iter().zip(&self.bundle_capacities) {
317            let mut total = 0i64;
318            for &arc_index in bundle {
319                let Ok(flow) = i64::try_from(config[arc_index]) else {
320                    return Ok(crate::types::Or(false));
321                };
322                let Some(next_total) = total.checked_add(flow) else {
323                    return Ok(crate::types::Or(false));
324                };
325                total = next_total;
326            }
327            if total > capacity {
328                return Ok(crate::types::Or(false));
329            }
330        }
331
332        for vertex in 0..self.num_vertices() {
333            if vertex == self.source || vertex == self.sink {
334                continue;
335            }
336            if self.vertex_balance(config, vertex)? != Some(0) {
337                return Ok(crate::types::Or(false));
338            }
339        }
340
341        Ok(crate::types::Or(matches!(
342            self.vertex_balance(config, self.sink)?,
343            Some(balance) if balance >= self.requirement
344        )))
345    }
346}
347
348impl Problem for IntegralFlowBundles {
349    const NAME: &'static str = "IntegralFlowBundles";
350    type Solution = Vec<usize>;
351    type Value = crate::types::Or;
352
353    crate::problem_parameters![
354        ("num_arcs", num_arcs),
355        ("num_bundles", num_bundles),
356        ("num_vertices", num_vertices),
357    ];
358
359    fn evaluate(
360        &self,
361        config: &Self::Solution,
362    ) -> Result<crate::types::Or, crate::traits::EvaluationError> {
363        self.evaluate_solution(config)
364    }
365
366    fn variant() -> Vec<(&'static str, &'static str)> {
367        crate::variant_params![]
368    }
369}
370
371impl crate::solvers::BruteForceProblem for IntegralFlowBundles {
372    fn dimensions(&self) -> Vec<usize> {
373        self.arc_upper_bounds()
374            .into_iter()
375            .map(|bound| {
376                usize::try_from(bound)
377                    .ok()
378                    .and_then(|bound| bound.checked_add(1))
379                    .expect("bundle-derived arc upper bounds are validated in the constructor")
380            })
381            .collect()
382    }
383}
384
385crate::declare_variants! {
386    default IntegralFlowBundles => "2^num_arcs" create IntegralFlowBundlesCreateSpec,
387}
388
389crate::register_brute_force! {
390    IntegralFlowBundles,
391}
392
393#[cfg(feature = "example-db")]
394pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
395    vec![crate::example_db::specs::ModelExampleSpec {
396        id: "integral_flow_bundles",
397        instance: Box::new(IntegralFlowBundles::new(
398            DirectedGraph::new(4, vec![(0, 1), (0, 2), (1, 3), (2, 3), (1, 2), (2, 1)]),
399            0,
400            3,
401            vec![vec![0, 1], vec![2, 5], vec![3, 4]],
402            vec![1, 1, 1],
403            1,
404        )),
405        optimal_config: serde_json::json!(vec![1, 0, 1, 0, 0, 0]),
406        optimal_value: serde_json::json!(true),
407    }]
408}
409
410#[cfg(test)]
411#[path = "../../unit_tests/models/graph/integral_flow_bundles.rs"]
412mod tests;