Skip to main content

problemreductions/models/graph/
undirected_two_commodity_integral_flow.rs

1//! Undirected two-commodity integral flow problem implementation.
2//!
3//! The problem asks whether two integral commodities can be routed through an
4//! undirected capacitated graph while sharing edge capacities.
5
6use crate::registry::{CreateSpec, ProblemSchemaEntry};
7use crate::topology::{Graph, SimpleGraph};
8use crate::traits::Problem;
9use serde::{Deserialize, Serialize};
10
11inventory::submit! {
12    ProblemSchemaEntry {
13        name: "UndirectedTwoCommodityIntegralFlow",
14        display_name: "Undirected Two-Commodity Integral Flow",
15        aliases: &[],
16        dimensions: &[],
17        category: crate::registry::ProblemCategory::Graph,
18        module_path: module_path!(),
19        description: "Determine whether two integral commodities can satisfy sink demands in an undirected capacitated graph",
20        fields: UndirectedTwoCommodityIntegralFlowCreateSpec::FIELDS,
21    }
22}
23
24/// Undirected two-commodity integral flow on a capacitated graph.
25///
26/// For each undirected edge `{u, v}`, a configuration stores four variables in
27/// the graph's edge order:
28/// - `f1(u, v)`
29/// - `f1(v, u)`
30/// - `f2(u, v)`
31/// - `f2(v, u)`
32#[derive(Debug, Clone, Serialize, Deserialize)]
33#[serde(try_from = "UndirectedTwoCommodityIntegralFlowData")]
34pub struct UndirectedTwoCommodityIntegralFlow {
35    graph: SimpleGraph,
36    capacities: Vec<i64>,
37    source_1: usize,
38    sink_1: usize,
39    source_2: usize,
40    sink_2: usize,
41    requirement_1: i64,
42    requirement_2: i64,
43}
44
45#[derive(Deserialize)]
46struct UndirectedTwoCommodityIntegralFlowData {
47    graph: SimpleGraph,
48    capacities: Vec<i64>,
49    source_1: usize,
50    sink_1: usize,
51    source_2: usize,
52    sink_2: usize,
53    requirement_1: i64,
54    requirement_2: i64,
55}
56
57impl TryFrom<UndirectedTwoCommodityIntegralFlowData> for UndirectedTwoCommodityIntegralFlow {
58    type Error = crate::registry::ConstructionError;
59    fn try_from(data: UndirectedTwoCommodityIntegralFlowData) -> Result<Self, Self::Error> {
60        Self::try_new(
61            data.graph,
62            data.capacities,
63            data.source_1,
64            data.sink_1,
65            data.source_2,
66            data.sink_2,
67            data.requirement_1,
68            data.requirement_2,
69        )
70    }
71}
72
73#[derive(Debug, Deserialize, crate::CreateSpec)]
74struct UndirectedTwoCommodityIntegralFlowCreateSpec {
75    /// Undirected graph edges.
76    #[create(codec = "edge-list")]
77    graph: Vec<(usize, usize)>,
78    /// Vertex count, needed for isolated vertices.
79    num_vertices: Option<usize>,
80    /// Edge capacities.
81    #[create(codec = "comma-separated")]
82    capacities: Vec<i64>,
83    source_1: usize,
84    sink_1: usize,
85    source_2: usize,
86    sink_2: usize,
87    requirement_1: i64,
88    requirement_2: i64,
89}
90
91impl TryFrom<UndirectedTwoCommodityIntegralFlowCreateSpec> for UndirectedTwoCommodityIntegralFlow {
92    type Error = crate::registry::ConstructionError;
93    fn try_from(spec: UndirectedTwoCommodityIntegralFlowCreateSpec) -> Result<Self, Self::Error> {
94        if spec.graph.is_empty() && spec.num_vertices.is_none() {
95            return Err("num_vertices is required for an empty graph".into());
96        }
97        for &(u, v) in &spec.graph {
98            if u == v {
99                return Err(format!("self-loop {u}-{v} is not allowed").into());
100            }
101        }
102        let inferred = spec
103            .graph
104            .iter()
105            .flat_map(|&(u, v)| [u, v])
106            .max()
107            .map(|v| v.checked_add(1).ok_or("vertex count overflows usize"))
108            .transpose()?
109            .unwrap_or(0);
110        let count = spec.num_vertices.unwrap_or(inferred);
111        if count < inferred {
112            return Err("num_vertices is too small for graph endpoints".into());
113        }
114        Self::try_new(
115            SimpleGraph::new(count, spec.graph),
116            spec.capacities,
117            spec.source_1,
118            spec.sink_1,
119            spec.source_2,
120            spec.sink_2,
121            spec.requirement_1,
122            spec.requirement_2,
123        )
124    }
125}
126
127impl UndirectedTwoCommodityIntegralFlow {
128    #[allow(clippy::too_many_arguments)]
129    pub fn new(
130        graph: SimpleGraph,
131        capacities: Vec<i64>,
132        source_1: usize,
133        sink_1: usize,
134        source_2: usize,
135        sink_2: usize,
136        requirement_1: i64,
137        requirement_2: i64,
138    ) -> Self {
139        Self::try_new(
140            graph,
141            capacities,
142            source_1,
143            sink_1,
144            source_2,
145            sink_2,
146            requirement_1,
147            requirement_2,
148        )
149        .unwrap_or_else(|error| panic!("{error}"))
150    }
151
152    #[allow(clippy::too_many_arguments)]
153    fn try_new(
154        graph: SimpleGraph,
155        capacities: Vec<i64>,
156        source_1: usize,
157        sink_1: usize,
158        source_2: usize,
159        sink_2: usize,
160        requirement_1: i64,
161        requirement_2: i64,
162    ) -> Result<Self, crate::registry::ConstructionError> {
163        if capacities.len() != graph.num_edges() {
164            return Err(crate::registry::ConstructionError::length_mismatch(
165                "capacities",
166                capacities.len(),
167                graph.num_edges(),
168            ));
169        }
170
171        let num_vertices = graph.num_vertices();
172        for (label, vertex) in [
173            ("source_1", source_1),
174            ("sink_1", sink_1),
175            ("source_2", source_2),
176            ("sink_2", sink_2),
177        ] {
178            if vertex >= num_vertices {
179                return Err(
180                    format!("{label} must be less than num_vertices ({num_vertices})").into(),
181                );
182            }
183        }
184
185        for &capacity in &capacities {
186            if usize::try_from(capacity)
187                .ok()
188                .and_then(|value| value.checked_add(1))
189                .is_none()
190            {
191                return Err("edge capacities must fit into usize for dims()".into());
192            }
193        }
194
195        Ok(Self {
196            graph,
197            capacities,
198            source_1,
199            sink_1,
200            source_2,
201            sink_2,
202            requirement_1,
203            requirement_2,
204        })
205    }
206
207    pub fn graph(&self) -> &SimpleGraph {
208        &self.graph
209    }
210
211    pub fn capacities(&self) -> &[i64] {
212        &self.capacities
213    }
214
215    /// Maximum bit length of the edge capacities, with a minimum of one.
216    pub fn max_capacity_bits(&self) -> u64 {
217        crate::types::max_numeric_magnitude_bits(self.capacities.iter().copied())
218    }
219
220    pub fn source_1(&self) -> usize {
221        self.source_1
222    }
223
224    pub fn sink_1(&self) -> usize {
225        self.sink_1
226    }
227
228    pub fn source_2(&self) -> usize {
229        self.source_2
230    }
231
232    pub fn sink_2(&self) -> usize {
233        self.sink_2
234    }
235
236    pub fn requirement_1(&self) -> i64 {
237        self.requirement_1
238    }
239
240    pub fn requirement_2(&self) -> i64 {
241        self.requirement_2
242    }
243
244    pub fn num_vertices(&self) -> usize {
245        self.graph.num_vertices()
246    }
247
248    pub fn num_edges(&self) -> usize {
249        self.graph.num_edges()
250    }
251
252    pub fn num_conservation_constraints(&self) -> usize {
253        [(self.source_1, self.sink_1), (self.source_2, self.sink_2)]
254            .into_iter()
255            .map(|(source, sink)| self.num_vertices() - if source == sink { 1 } else { 2 })
256            .sum()
257    }
258
259    pub fn is_valid_solution(
260        &self,
261        config: &[usize],
262    ) -> Result<bool, crate::traits::EvaluationError> {
263        Ok(self.evaluate_solution(config)?.0)
264    }
265
266    fn config_len(&self) -> usize {
267        self.num_edges() * 4
268    }
269
270    fn domain_size(capacity: i64) -> usize {
271        usize::try_from(capacity)
272            .ok()
273            .and_then(|value| value.checked_add(1))
274            .expect("capacity already validated to fit into usize")
275    }
276
277    fn edge_flows(&self, config: &[usize], edge_index: usize) -> Option<[usize; 4]> {
278        let start = edge_index.checked_mul(4)?;
279        Some([
280            *config.get(start)?,
281            *config.get(start + 1)?,
282            *config.get(start + 2)?,
283            *config.get(start + 3)?,
284        ])
285    }
286
287    fn flow_pair_for_commodity(flows: [usize; 4], commodity: usize) -> (usize, usize) {
288        match commodity {
289            1 => (flows[0], flows[1]),
290            2 => (flows[2], flows[3]),
291            _ => unreachable!("commodity must be 1 or 2"),
292        }
293    }
294
295    fn commodity_balance(
296        &self,
297        config: &[usize],
298        commodity: usize,
299        vertex: usize,
300    ) -> Result<Option<i64>, crate::traits::EvaluationError> {
301        let mut balance = 0_i64;
302        for (edge_index, (u, v)) in self.graph.edges().into_iter().enumerate() {
303            // A self-loop has equal incoming and outgoing flow at its vertex.
304            if u == v {
305                continue;
306            }
307            let Some(flows) = self.edge_flows(config, edge_index) else {
308                return Ok(None);
309            };
310            let (uv, vu) = Self::flow_pair_for_commodity(flows, commodity);
311            let uv = i64::try_from(uv).map_err(|_| {
312                crate::traits::EvaluationError::IntegerOverflow(
313                    "converting forward commodity flow to i64".into(),
314                )
315            })?;
316            let vu = i64::try_from(vu).map_err(|_| {
317                crate::traits::EvaluationError::IntegerOverflow(
318                    "converting reverse commodity flow to i64".into(),
319                )
320            })?;
321
322            if vertex == u {
323                balance = balance.checked_sub(uv).ok_or_else(|| {
324                    crate::traits::EvaluationError::IntegerOverflow(
325                        "subtracting forward commodity flow".into(),
326                    )
327                })?;
328                balance = balance.checked_add(vu).ok_or_else(|| {
329                    crate::traits::EvaluationError::IntegerOverflow(
330                        "adding reverse commodity flow".into(),
331                    )
332                })?;
333            } else if vertex == v {
334                balance = balance.checked_add(uv).ok_or_else(|| {
335                    crate::traits::EvaluationError::IntegerOverflow(
336                        "adding forward commodity flow".into(),
337                    )
338                })?;
339                balance = balance.checked_sub(vu).ok_or_else(|| {
340                    crate::traits::EvaluationError::IntegerOverflow(
341                        "subtracting reverse commodity flow".into(),
342                    )
343                })?;
344            }
345        }
346        Ok(Some(balance))
347    }
348
349    fn net_flow_into_sink(
350        &self,
351        config: &[usize],
352        commodity: usize,
353    ) -> Result<Option<i64>, crate::traits::EvaluationError> {
354        let sink = match commodity {
355            1 => self.sink_1,
356            2 => self.sink_2,
357            _ => unreachable!("commodity must be 1 or 2"),
358        };
359        self.commodity_balance(config, commodity, sink)
360    }
361
362    fn evaluate_solution(
363        &self,
364        config: &[usize],
365    ) -> Result<crate::types::Or, crate::traits::EvaluationError> {
366        if config.len() != self.config_len() {
367            return Err(crate::traits::EvaluationError::InvalidConfiguration(
368                "flow representation length does not match the graph".into(),
369            ));
370        }
371
372        for (edge_index, &capacity) in self.capacities.iter().enumerate() {
373            let Some(flows) = self.edge_flows(config, edge_index) else {
374                return Ok(crate::types::Or(false));
375            };
376
377            if flows
378                .iter()
379                .any(|&value| i64::try_from(value).map_or(true, |value| value > capacity))
380            {
381                return Ok(crate::types::Or(false));
382            }
383            if flows[0] > 0 && flows[1] > 0 || flows[2] > 0 && flows[3] > 0 {
384                return Ok(crate::types::Or(false));
385            }
386
387            let commodity_1 = i64::try_from(std::cmp::max(flows[0], flows[1]))
388                .expect("flow values already validated against i64 capacities");
389            let commodity_2 = i64::try_from(std::cmp::max(flows[2], flows[3]))
390                .expect("flow values already validated against i64 capacities");
391            let shared = commodity_1.checked_add(commodity_2).ok_or_else(|| {
392                crate::traits::EvaluationError::IntegerOverflow(
393                    "summing two commodities on an undirected edge".into(),
394                )
395            })?;
396            if shared > capacity {
397                return Ok(crate::types::Or(false));
398            }
399        }
400
401        for (commodity, source, sink) in [
402            (1, self.source_1, self.sink_1),
403            (2, self.source_2, self.sink_2),
404        ] {
405            for vertex in 0..self.num_vertices() {
406                if vertex != source
407                    && vertex != sink
408                    && self.commodity_balance(config, commodity, vertex)? != Some(0)
409                {
410                    return Ok(crate::types::Or(false));
411                }
412            }
413        }
414
415        Ok(crate::types::Or(
416            self.net_flow_into_sink(config, 1)?
417                .is_some_and(|flow| flow >= self.requirement_1)
418                && self
419                    .net_flow_into_sink(config, 2)?
420                    .is_some_and(|flow| flow >= self.requirement_2),
421        ))
422    }
423}
424
425impl Problem for UndirectedTwoCommodityIntegralFlow {
426    const NAME: &'static str = "UndirectedTwoCommodityIntegralFlow";
427    type Solution = Vec<usize>;
428    type Value = crate::types::Or;
429
430    crate::problem_parameters![
431        ("max_capacity_bits", max_capacity_bits),
432        ("num_edges", num_edges),
433        ("num_conservation_constraints", num_conservation_constraints),
434        ("num_vertices", num_vertices),
435    ];
436
437    fn variant() -> Vec<(&'static str, &'static str)> {
438        crate::variant_params![]
439    }
440
441    fn evaluate(
442        &self,
443        config: &Self::Solution,
444    ) -> Result<crate::types::Or, crate::traits::EvaluationError> {
445        self.evaluate_solution(config)
446    }
447}
448
449impl crate::solvers::BruteForceProblem for UndirectedTwoCommodityIntegralFlow {
450    fn dimensions(&self) -> Vec<usize> {
451        self.capacities
452            .iter()
453            .flat_map(|&capacity| {
454                let domain = Self::domain_size(capacity);
455                std::iter::repeat_n(domain, 4)
456            })
457            .collect()
458    }
459}
460
461crate::declare_variants! {
462    default UndirectedTwoCommodityIntegralFlow => "5^num_edges" create UndirectedTwoCommodityIntegralFlowCreateSpec,
463}
464
465crate::register_brute_force! {
466    UndirectedTwoCommodityIntegralFlow,
467}
468
469#[cfg(feature = "example-db")]
470pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
471    vec![crate::example_db::specs::ModelExampleSpec {
472        id: "undirected_two_commodity_integral_flow",
473        instance: Box::new(UndirectedTwoCommodityIntegralFlow::new(
474            SimpleGraph::new(4, vec![(0, 2), (1, 2), (2, 3)]),
475            vec![1, 1, 2],
476            0,
477            3,
478            1,
479            3,
480            1,
481            1,
482        )),
483        optimal_config: serde_json::json!(vec![1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0]),
484        optimal_value: serde_json::json!(true),
485    }]
486}
487
488#[cfg(test)]
489#[path = "../../unit_tests/models/graph/undirected_two_commodity_integral_flow.rs"]
490mod tests;