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)]
71pub struct DirectedTwoCommodityIntegralFlow {
72    /// The directed graph G = (V, A).
73    graph: DirectedGraph,
74    /// Capacity c(a) for each arc.
75    capacities: Vec<i64>,
76    /// Source vertex s_1 for commodity 1.
77    source_1: usize,
78    /// Sink vertex t_1 for commodity 1.
79    sink_1: usize,
80    /// Source vertex s_2 for commodity 2.
81    source_2: usize,
82    /// Sink vertex t_2 for commodity 2.
83    sink_2: usize,
84    /// Flow requirement R_1 for commodity 1.
85    requirement_1: i64,
86    /// Flow requirement R_2 for commodity 2.
87    requirement_2: i64,
88}
89
90impl DirectedTwoCommodityIntegralFlow {
91    /// Create a new Directed Two-Commodity Integral Flow problem.
92    ///
93    /// # Panics
94    ///
95    /// Panics if:
96    /// - `capacities.len() != graph.num_arcs()`
97    /// - Any terminal vertex index >= `graph.num_vertices()`
98    #[allow(clippy::too_many_arguments)]
99    pub fn new(
100        graph: DirectedGraph,
101        capacities: Vec<i64>,
102        source_1: usize,
103        sink_1: usize,
104        source_2: usize,
105        sink_2: usize,
106        requirement_1: i64,
107        requirement_2: i64,
108    ) -> Self {
109        let n = graph.num_vertices();
110        assert_eq!(
111            capacities.len(),
112            graph.num_arcs(),
113            "capacities length must match graph num_arcs"
114        );
115        assert!(
116            capacities.iter().all(|&capacity| capacity >= 0),
117            "capacities must be nonnegative"
118        );
119        assert!(
120            requirement_1 >= 0 && requirement_2 >= 0,
121            "flow requirements must be nonnegative"
122        );
123        assert!(source_1 < n, "source_1 ({source_1}) >= num_vertices ({n})");
124        assert!(sink_1 < n, "sink_1 ({sink_1}) >= num_vertices ({n})");
125        assert!(source_2 < n, "source_2 ({source_2}) >= num_vertices ({n})");
126        assert!(sink_2 < n, "sink_2 ({sink_2}) >= num_vertices ({n})");
127        Self {
128            graph,
129            capacities,
130            source_1,
131            sink_1,
132            source_2,
133            sink_2,
134            requirement_1,
135            requirement_2,
136        }
137    }
138
139    /// Get a reference to the underlying directed graph.
140    pub fn graph(&self) -> &DirectedGraph {
141        &self.graph
142    }
143
144    /// Get a reference to the capacities.
145    pub fn capacities(&self) -> &[i64] {
146        &self.capacities
147    }
148
149    /// Get source vertex for commodity 1.
150    pub fn source_1(&self) -> usize {
151        self.source_1
152    }
153
154    /// Get sink vertex for commodity 1.
155    pub fn sink_1(&self) -> usize {
156        self.sink_1
157    }
158
159    /// Get source vertex for commodity 2.
160    pub fn source_2(&self) -> usize {
161        self.source_2
162    }
163
164    /// Get sink vertex for commodity 2.
165    pub fn sink_2(&self) -> usize {
166        self.sink_2
167    }
168
169    /// Get requirement for commodity 1.
170    pub fn requirement_1(&self) -> i64 {
171        self.requirement_1
172    }
173
174    /// Get requirement for commodity 2.
175    pub fn requirement_2(&self) -> i64 {
176        self.requirement_2
177    }
178
179    /// Get the number of vertices.
180    pub fn num_vertices(&self) -> usize {
181        self.graph.num_vertices()
182    }
183
184    /// Get the number of arcs.
185    pub fn num_arcs(&self) -> usize {
186        self.graph.num_arcs()
187    }
188
189    /// Get the maximum capacity across all arcs.
190    pub fn max_capacity(&self) -> i64 {
191        self.capacities.iter().copied().max().unwrap_or(0)
192    }
193
194    /// Check whether a flow assignment is feasible.
195    ///
196    /// `config` has 2*|A| entries: first |A| for commodity 1, next |A| for commodity 2.
197    pub fn is_feasible(&self, config: &[usize]) -> Result<bool, crate::traits::EvaluationError> {
198        let m = self.graph.num_arcs();
199        if config.len() != 2 * m {
200            return Ok(false);
201        }
202        let arcs = self.graph.arcs();
203        // (1) Joint capacity constraint
204        for a in 0..m {
205            let f1 = i64::try_from(config[a]).map_err(|_| {
206                crate::traits::EvaluationError::IntegerOverflow(
207                    "converting first commodity flow to i64".into(),
208                )
209            })?;
210            let f2 = i64::try_from(config[m + a]).map_err(|_| {
211                crate::traits::EvaluationError::IntegerOverflow(
212                    "converting second commodity flow to i64".into(),
213                )
214            })?;
215            if f1.checked_add(f2).ok_or_else(|| {
216                crate::traits::EvaluationError::IntegerOverflow(
217                    "summing two-commodity arc flow".into(),
218                )
219            })? > self.capacities[a]
220            {
221                return Ok(false);
222            }
223        }
224
225        // (2) Flow conservation for each commodity at non-terminal vertices
226        let n = self.graph.num_vertices();
227        let mut balances = [vec![0_i64; n], vec![0_i64; n]];
228        for (a, &(u, w)) in arcs.iter().enumerate() {
229            let flow_1 = i64::try_from(config[a]).map_err(|_| {
230                crate::traits::EvaluationError::IntegerOverflow(
231                    "converting first commodity flow to i64".into(),
232                )
233            })?;
234            let flow_2 = i64::try_from(config[m + a]).map_err(|_| {
235                crate::traits::EvaluationError::IntegerOverflow(
236                    "converting second commodity flow to i64".into(),
237                )
238            })?;
239
240            for (commodity, flow) in [(0, flow_1), (1, flow_2)] {
241                balances[commodity][u] =
242                    balances[commodity][u].checked_sub(flow).ok_or_else(|| {
243                        crate::traits::EvaluationError::IntegerOverflow(
244                            "subtracting outgoing commodity flow".into(),
245                        )
246                    })?;
247                balances[commodity][w] =
248                    balances[commodity][w].checked_add(flow).ok_or_else(|| {
249                        crate::traits::EvaluationError::IntegerOverflow(
250                            "adding incoming commodity flow".into(),
251                        )
252                    })?;
253            }
254        }
255
256        for (commodity, commodity_balances) in balances.iter().enumerate() {
257            let src = if commodity == 0 {
258                self.source_1
259            } else {
260                self.source_2
261            };
262            for (v, &balance) in commodity_balances.iter().enumerate() {
263                let snk = if commodity == 0 {
264                    self.sink_1
265                } else {
266                    self.sink_2
267                };
268                if v != src && v != snk && balance != 0 {
269                    return Ok(false);
270                }
271            }
272
273            let snk = if commodity == 0 {
274                self.sink_1
275            } else {
276                self.sink_2
277            };
278            let req = if commodity == 0 {
279                self.requirement_1
280            } else {
281                self.requirement_2
282            };
283
284            if commodity_balances[snk] < req {
285                return Ok(false);
286            }
287        }
288
289        Ok(true)
290    }
291}
292
293impl Problem for DirectedTwoCommodityIntegralFlow {
294    const NAME: &'static str = "DirectedTwoCommodityIntegralFlow";
295    type Solution = Vec<usize>;
296    type Value = crate::types::Or;
297
298    crate::problem_parameters![
299        ("max_capacity", max_capacity),
300        ("num_arcs", num_arcs),
301        ("num_vertices", num_vertices),
302    ];
303
304    fn evaluate(
305        &self,
306        config: &Self::Solution,
307    ) -> Result<crate::types::Or, crate::traits::EvaluationError> {
308        if config.len() != 2 * self.graph.num_arcs() {
309            return Err(crate::traits::EvaluationError::InvalidConfiguration(
310                "two-commodity flow vector length does not match the graph arcs".into(),
311            ));
312        }
313        Ok(crate::types::Or(self.is_feasible(config)?))
314    }
315
316    fn variant() -> Vec<(&'static str, &'static str)> {
317        crate::variant_params![]
318    }
319}
320
321impl crate::solvers::BruteForceProblem for DirectedTwoCommodityIntegralFlow {
322    fn dimensions(&self) -> Vec<usize> {
323        self.capacities
324            .iter()
325            .chain(self.capacities.iter())
326            .map(|&c| (c as usize) + 1)
327            .collect()
328    }
329}
330
331crate::declare_variants! {
332    default DirectedTwoCommodityIntegralFlow => "(max_capacity + 1)^(2 * num_arcs)",
333}
334
335crate::register_brute_force! {
336    DirectedTwoCommodityIntegralFlow,
337}
338
339#[cfg(feature = "example-db")]
340pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
341    vec![crate::example_db::specs::ModelExampleSpec {
342        id: "directed_two_commodity_integral_flow",
343        instance: Box::new(DirectedTwoCommodityIntegralFlow::new(
344            DirectedGraph::new(
345                6,
346                vec![
347                    (0, 2),
348                    (0, 3),
349                    (1, 2),
350                    (1, 3),
351                    (2, 4),
352                    (2, 5),
353                    (3, 4),
354                    (3, 5),
355                ],
356            ),
357            vec![1; 8],
358            0,
359            4,
360            1,
361            5,
362            1,
363            1,
364        )),
365        optimal_config: serde_json::json!(vec![1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1]),
366        optimal_value: serde_json::json!(true),
367    }]
368}
369
370#[cfg(test)]
371#[path = "../../unit_tests/models/graph/directed_two_commodity_integral_flow.rs"]
372mod tests;