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