Skip to main content

problemreductions/models/graph/
directed_two_commodity_integral_flow.rs

1//! Directed Two-Commodity Integral Flow problem implementation.
2//!
3//! Given a directed graph with arc capacities and two source-sink pairs with
4//! flow requirements, determine whether two integral flow functions exist that
5//! jointly satisfy capacity, conservation, and requirement constraints.
6//!
7//! NP-complete even with unit capacities (Even, Itai, and Shamir, 1976).
8
9use crate::registry::{FieldInfo, ProblemSchemaEntry};
10use crate::topology::DirectedGraph;
11use crate::traits::Problem;
12use serde::{Deserialize, Serialize};
13
14inventory::submit! {
15    ProblemSchemaEntry {
16        name: "DirectedTwoCommodityIntegralFlow",
17        display_name: "Directed Two-Commodity Integral Flow",
18        aliases: &["D2CIF"],
19        dimensions: &[],
20        category: crate::registry::ProblemCategory::Graph,
21        module_path: module_path!(),
22        description: "Two-commodity integral flow feasibility on a directed graph",
23        fields: &[
24            FieldInfo { name: "graph", type_name: "DirectedGraph", description: "Directed graph G = (V, A)" },
25            FieldInfo { name: "capacities", type_name: "Vec<i64>", description: "Capacity c(a) for each arc" },
26            FieldInfo { name: "source_1", type_name: "usize", description: "Source vertex s_1 for commodity 1" },
27            FieldInfo { name: "sink_1", type_name: "usize", description: "Sink vertex t_1 for commodity 1" },
28            FieldInfo { name: "source_2", type_name: "usize", description: "Source vertex s_2 for commodity 2" },
29            FieldInfo { name: "sink_2", type_name: "usize", description: "Sink vertex t_2 for commodity 2" },
30            FieldInfo { name: "requirement_1", type_name: "i64", description: "Flow requirement R_1 for commodity 1" },
31            FieldInfo { name: "requirement_2", type_name: "i64", description: "Flow requirement R_2 for commodity 2" },
32        ],
33    }
34}
35
36/// Directed Two-Commodity Integral Flow problem.
37///
38/// Given a directed graph G = (V, A) with arc capacities c(a), two source-sink
39/// pairs (s_1, t_1) and (s_2, t_2), and requirements R_1, R_2, determine
40/// whether two integral flow functions f_1, f_2: A -> Z_0^+ exist such that:
41/// 1. Joint capacity: f_1(a) + f_2(a) <= c(a) for all a in A
42/// 2. Flow conservation: for each commodity i, flow is conserved at every
43///    vertex except its own source and sink
44/// 3. Requirements: net flow into t_i under f_i is at least R_i
45///
46/// # Variables
47///
48/// 2|A| variables: first |A| for commodity 1's flow on each arc,
49/// next |A| for commodity 2's flow on each arc. Variable j for arc a
50/// of commodity i ranges over {0, ..., c(a)}.
51///
52/// # Example
53///
54/// ```
55/// use problemreductions::models::graph::DirectedTwoCommodityIntegralFlow;
56/// use problemreductions::topology::DirectedGraph;
57/// use problemreductions::{Problem, BruteForce};
58///
59/// // 6-vertex network: s1=0, s2=1, t1=4, t2=5
60/// let graph = DirectedGraph::new(6, vec![
61///     (0, 2), (0, 3), (1, 2), (1, 3),
62///     (2, 4), (2, 5), (3, 4), (3, 5),
63/// ]);
64/// let problem = DirectedTwoCommodityIntegralFlow::new(
65///     graph, vec![1; 8], 0, 4, 1, 5, 1, 1,
66/// );
67/// let solver = BruteForce::new();
68/// assert!(solver.solve(&problem).unwrap().is_some());
69/// ```
70#[derive(Debug, Clone, Serialize, Deserialize)]
71#[serde(try_from = "DirectedTwoCommodityIntegralFlowData")]
72pub struct DirectedTwoCommodityIntegralFlow {
73    /// The directed graph G = (V, A).
74    graph: DirectedGraph,
75    /// Capacity c(a) for each arc.
76    capacities: Vec<i64>,
77    /// Source vertex s_1 for commodity 1.
78    source_1: usize,
79    /// Sink vertex t_1 for commodity 1.
80    sink_1: usize,
81    /// Source vertex s_2 for commodity 2.
82    source_2: usize,
83    /// Sink vertex t_2 for commodity 2.
84    sink_2: usize,
85    /// Flow requirement R_1 for commodity 1.
86    requirement_1: i64,
87    /// Flow requirement R_2 for commodity 2.
88    requirement_2: i64,
89}
90
91#[derive(Deserialize)]
92struct DirectedTwoCommodityIntegralFlowData {
93    graph: DirectedGraph,
94    capacities: Vec<i64>,
95    source_1: usize,
96    sink_1: usize,
97    source_2: usize,
98    sink_2: usize,
99    requirement_1: i64,
100    requirement_2: i64,
101}
102
103impl TryFrom<DirectedTwoCommodityIntegralFlowData> for DirectedTwoCommodityIntegralFlow {
104    type Error = crate::registry::ConstructionError;
105    fn try_from(data: DirectedTwoCommodityIntegralFlowData) -> Result<Self, Self::Error> {
106        Self::try_new(
107            data.graph,
108            data.capacities,
109            data.source_1,
110            data.sink_1,
111            data.source_2,
112            data.sink_2,
113            data.requirement_1,
114            data.requirement_2,
115        )
116    }
117}
118
119impl DirectedTwoCommodityIntegralFlow {
120    /// Create a new Directed Two-Commodity Integral Flow problem.
121    ///
122    /// # Panics
123    ///
124    /// Panics if:
125    /// - `capacities.len() != graph.num_arcs()`
126    /// - Any terminal vertex index >= `graph.num_vertices()`
127    #[allow(clippy::too_many_arguments)]
128    pub fn new(
129        graph: DirectedGraph,
130        capacities: Vec<i64>,
131        source_1: usize,
132        sink_1: usize,
133        source_2: usize,
134        sink_2: usize,
135        requirement_1: i64,
136        requirement_2: i64,
137    ) -> Self {
138        Self::try_new(
139            graph,
140            capacities,
141            source_1,
142            sink_1,
143            source_2,
144            sink_2,
145            requirement_1,
146            requirement_2,
147        )
148        .unwrap_or_else(|error| panic!("{error}"))
149    }
150
151    #[allow(clippy::too_many_arguments)]
152    fn try_new(
153        graph: DirectedGraph,
154        capacities: Vec<i64>,
155        source_1: usize,
156        sink_1: usize,
157        source_2: usize,
158        sink_2: usize,
159        requirement_1: i64,
160        requirement_2: i64,
161    ) -> Result<Self, crate::registry::ConstructionError> {
162        let n = graph.num_vertices();
163        if capacities.len() != graph.num_arcs() {
164            return Err(crate::registry::ConstructionError::length_mismatch(
165                "capacities",
166                capacities.len(),
167                graph.num_arcs(),
168            ));
169        }
170        if capacities.iter().any(|&capacity| capacity < 0) {
171            return Err("capacities must be nonnegative".into());
172        }
173        if requirement_1 < 0 || requirement_2 < 0 {
174            return Err("flow requirements must be nonnegative".into());
175        }
176        if source_1 >= n {
177            return Err(format!("source_1 ({source_1}) >= num_vertices ({n})").into());
178        }
179        if sink_1 >= n {
180            return Err(format!("sink_1 ({sink_1}) >= num_vertices ({n})").into());
181        }
182        if source_2 >= n {
183            return Err(format!("source_2 ({source_2}) >= num_vertices ({n})").into());
184        }
185        if sink_2 >= n {
186            return Err(format!("sink_2 ({sink_2}) >= num_vertices ({n})").into());
187        }
188        Ok(Self {
189            graph,
190            capacities,
191            source_1,
192            sink_1,
193            source_2,
194            sink_2,
195            requirement_1,
196            requirement_2,
197        })
198    }
199
200    /// Get a reference to the underlying directed graph.
201    pub fn graph(&self) -> &DirectedGraph {
202        &self.graph
203    }
204
205    /// Get a reference to the capacities.
206    pub fn capacities(&self) -> &[i64] {
207        &self.capacities
208    }
209
210    /// Get source vertex for commodity 1.
211    pub fn source_1(&self) -> usize {
212        self.source_1
213    }
214
215    /// Get sink vertex for commodity 1.
216    pub fn sink_1(&self) -> usize {
217        self.sink_1
218    }
219
220    /// Get source vertex for commodity 2.
221    pub fn source_2(&self) -> usize {
222        self.source_2
223    }
224
225    /// Get sink vertex for commodity 2.
226    pub fn sink_2(&self) -> usize {
227        self.sink_2
228    }
229
230    /// Get requirement for commodity 1.
231    pub fn requirement_1(&self) -> i64 {
232        self.requirement_1
233    }
234
235    /// Get requirement for commodity 2.
236    pub fn requirement_2(&self) -> i64 {
237        self.requirement_2
238    }
239
240    /// Get the number of vertices.
241    pub fn num_vertices(&self) -> usize {
242        self.graph.num_vertices()
243    }
244
245    /// Get the number of arcs.
246    pub fn num_arcs(&self) -> usize {
247        self.graph.num_arcs()
248    }
249
250    /// Get the maximum capacity across all arcs.
251    pub fn max_capacity(&self) -> i64 {
252        self.capacities.iter().copied().max().unwrap_or(0)
253    }
254
255    /// Check whether a flow assignment is feasible.
256    ///
257    /// `config` has 2*|A| entries: first |A| for commodity 1, next |A| for commodity 2.
258    pub fn is_feasible(&self, config: &[usize]) -> Result<bool, crate::traits::EvaluationError> {
259        let m = self.graph.num_arcs();
260        if config.len() != 2 * m {
261            return Ok(false);
262        }
263        let arcs = self.graph.arcs();
264        // (1) Joint capacity constraint
265        for a in 0..m {
266            let f1 = i64::try_from(config[a]).map_err(|_| {
267                crate::traits::EvaluationError::IntegerOverflow(
268                    "converting first commodity flow to i64".into(),
269                )
270            })?;
271            let f2 = i64::try_from(config[m + a]).map_err(|_| {
272                crate::traits::EvaluationError::IntegerOverflow(
273                    "converting second commodity flow to i64".into(),
274                )
275            })?;
276            if f1.checked_add(f2).ok_or_else(|| {
277                crate::traits::EvaluationError::IntegerOverflow(
278                    "summing two-commodity arc flow".into(),
279                )
280            })? > self.capacities[a]
281            {
282                return Ok(false);
283            }
284        }
285
286        // (2) Flow conservation for each commodity at non-terminal vertices
287        let n = self.graph.num_vertices();
288        let mut balances = [vec![0_i64; n], vec![0_i64; n]];
289        for (a, &(u, w)) in arcs.iter().enumerate() {
290            let flow_1 = i64::try_from(config[a]).map_err(|_| {
291                crate::traits::EvaluationError::IntegerOverflow(
292                    "converting first commodity flow to i64".into(),
293                )
294            })?;
295            let flow_2 = i64::try_from(config[m + a]).map_err(|_| {
296                crate::traits::EvaluationError::IntegerOverflow(
297                    "converting second commodity flow to i64".into(),
298                )
299            })?;
300
301            for (commodity, flow) in [(0, flow_1), (1, flow_2)] {
302                balances[commodity][u] =
303                    balances[commodity][u].checked_sub(flow).ok_or_else(|| {
304                        crate::traits::EvaluationError::IntegerOverflow(
305                            "subtracting outgoing commodity flow".into(),
306                        )
307                    })?;
308                balances[commodity][w] =
309                    balances[commodity][w].checked_add(flow).ok_or_else(|| {
310                        crate::traits::EvaluationError::IntegerOverflow(
311                            "adding incoming commodity flow".into(),
312                        )
313                    })?;
314            }
315        }
316
317        for (commodity, commodity_balances) in balances.iter().enumerate() {
318            let src = if commodity == 0 {
319                self.source_1
320            } else {
321                self.source_2
322            };
323            for (v, &balance) in commodity_balances.iter().enumerate() {
324                let snk = if commodity == 0 {
325                    self.sink_1
326                } else {
327                    self.sink_2
328                };
329                if v != src && v != snk && balance != 0 {
330                    return Ok(false);
331                }
332            }
333
334            let snk = if commodity == 0 {
335                self.sink_1
336            } else {
337                self.sink_2
338            };
339            let req = if commodity == 0 {
340                self.requirement_1
341            } else {
342                self.requirement_2
343            };
344
345            if commodity_balances[snk] < req {
346                return Ok(false);
347            }
348        }
349
350        Ok(true)
351    }
352}
353
354impl Problem for DirectedTwoCommodityIntegralFlow {
355    const NAME: &'static str = "DirectedTwoCommodityIntegralFlow";
356    type Solution = Vec<usize>;
357    type Value = crate::types::Or;
358
359    crate::problem_parameters![
360        ("max_capacity", max_capacity),
361        ("num_arcs", num_arcs),
362        ("num_vertices", num_vertices),
363    ];
364
365    fn evaluate(
366        &self,
367        config: &Self::Solution,
368    ) -> Result<crate::types::Or, crate::traits::EvaluationError> {
369        if config.len() != 2 * self.graph.num_arcs() {
370            return Err(crate::traits::EvaluationError::InvalidConfiguration(
371                "two-commodity flow vector length does not match the graph arcs".into(),
372            ));
373        }
374        Ok(crate::types::Or(self.is_feasible(config)?))
375    }
376
377    fn variant() -> Vec<(&'static str, &'static str)> {
378        crate::variant_params![]
379    }
380}
381
382impl crate::solvers::BruteForceProblem for DirectedTwoCommodityIntegralFlow {
383    fn dimensions(&self) -> Vec<usize> {
384        self.capacities
385            .iter()
386            .chain(self.capacities.iter())
387            .map(|&c| (c as usize) + 1)
388            .collect()
389    }
390}
391
392crate::declare_variants! {
393    default DirectedTwoCommodityIntegralFlow => "(max_capacity + 1)^(2 * num_arcs)",
394}
395
396crate::register_brute_force! {
397    DirectedTwoCommodityIntegralFlow,
398}
399
400#[cfg(feature = "example-db")]
401pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
402    vec![crate::example_db::specs::ModelExampleSpec {
403        id: "directed_two_commodity_integral_flow",
404        instance: Box::new(DirectedTwoCommodityIntegralFlow::new(
405            DirectedGraph::new(
406                6,
407                vec![
408                    (0, 2),
409                    (0, 3),
410                    (1, 2),
411                    (1, 3),
412                    (2, 4),
413                    (2, 5),
414                    (3, 4),
415                    (3, 5),
416                ],
417            ),
418            vec![1; 8],
419            0,
420            4,
421            1,
422            5,
423            1,
424            1,
425        )),
426        optimal_config: serde_json::json!(vec![1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1]),
427        optimal_value: serde_json::json!(true),
428    }]
429}
430
431#[cfg(test)]
432#[path = "../../unit_tests/models/graph/directed_two_commodity_integral_flow.rs"]
433mod tests;