Skip to main content

problemreductions/models/graph/
integral_flow_with_multipliers.rs

1//! Integral Flow With Multipliers problem implementation.
2//!
3//! Given a directed graph with arc capacities, vertex multipliers on
4//! non-terminals, and a sink demand, determine whether there exists an
5//! integral flow satisfying multiplier-scaled conservation.
6
7use crate::registry::{CreateSpec, ProblemSchemaEntry};
8use crate::topology::DirectedGraph;
9use crate::traits::Problem;
10use serde::{Deserialize, Serialize};
11
12inventory::submit! {
13    ProblemSchemaEntry {
14        name: "IntegralFlowWithMultipliers",
15        display_name: "Integral Flow With Multipliers",
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 multiplier-scaled conservation at non-terminal vertices",
21        fields: IntegralFlowWithMultipliersCreateSpec::FIELDS,
22    }
23}
24
25#[derive(Debug, Clone, Serialize, Deserialize)]
26pub struct IntegralFlowWithMultipliers {
27    graph: DirectedGraph,
28    source: usize,
29    sink: usize,
30    multipliers: Vec<i64>,
31    capacities: Vec<i64>,
32    requirement: i64,
33}
34
35#[derive(Debug, Deserialize, crate::CreateSpec)]
36struct IntegralFlowWithMultipliersCreateSpec {
37    #[create(codec = "arc-list")]
38    arcs: Vec<(usize, usize)>,
39    num_vertices: Option<usize>,
40    #[create(codec = "comma-separated")]
41    capacities: Vec<i64>,
42    source: usize,
43    sink: usize,
44    #[create(codec = "comma-separated")]
45    multipliers: Vec<i64>,
46    requirement: i64,
47}
48
49impl TryFrom<IntegralFlowWithMultipliersCreateSpec> for IntegralFlowWithMultipliers {
50    type Error = crate::registry::ConstructionError;
51    fn try_from(
52        spec: IntegralFlowWithMultipliersCreateSpec,
53    ) -> Result<Self, crate::registry::ConstructionError> {
54        if spec.arcs.is_empty() {
55            return Err("arcs must be non-empty".into());
56        }
57        let inferred = spec
58            .arcs
59            .iter()
60            .flat_map(|&(u, v)| [u, v])
61            .max()
62            .map(|v| v.checked_add(1).ok_or("vertex count overflows usize"))
63            .transpose()?
64            .unwrap_or(0);
65        let count = spec.num_vertices.unwrap_or(inferred);
66        if count < inferred {
67            return Err("num_vertices is too small".into());
68        }
69        if spec.capacities.len() != spec.arcs.len() {
70            return Err("capacities length must match arcs length".into());
71        }
72        if spec.multipliers.len() != count {
73            return Err("multipliers length must match num_vertices".into());
74        }
75        if spec.source >= count || spec.sink >= count {
76            return Err("source and sink must be valid vertices".into());
77        }
78        if spec.source == spec.sink {
79            return Err("source and sink must be distinct".into());
80        }
81        for (v, &m) in spec.multipliers.iter().enumerate() {
82            if v != spec.source && v != spec.sink && m == 0 {
83                return Err("non-terminal multipliers must be positive".into());
84            }
85        }
86        for &c in &spec.capacities {
87            if usize::try_from(c)
88                .ok()
89                .and_then(|v| v.checked_add(1))
90                .is_none()
91            {
92                return Err("capacity is too large".into());
93            }
94        }
95        Ok(Self {
96            graph: DirectedGraph::new(count, spec.arcs),
97            source: spec.source,
98            sink: spec.sink,
99            multipliers: spec.multipliers,
100            capacities: spec.capacities,
101            requirement: spec.requirement,
102        })
103    }
104}
105
106impl IntegralFlowWithMultipliers {
107    pub fn new(
108        graph: DirectedGraph,
109        source: usize,
110        sink: usize,
111        multipliers: Vec<i64>,
112        capacities: Vec<i64>,
113        requirement: i64,
114    ) -> Self {
115        assert_eq!(
116            capacities.len(),
117            graph.num_arcs(),
118            "capacities length must match graph num_arcs"
119        );
120        assert_eq!(
121            multipliers.len(),
122            graph.num_vertices(),
123            "multipliers length must match graph num_vertices"
124        );
125
126        let num_vertices = graph.num_vertices();
127        assert!(
128            source < num_vertices,
129            "source ({source}) must be less than num_vertices ({num_vertices})"
130        );
131        assert!(
132            sink < num_vertices,
133            "sink ({sink}) must be less than num_vertices ({num_vertices})"
134        );
135        assert_ne!(source, sink, "source and sink must be distinct");
136
137        for (vertex, &multiplier) in multipliers.iter().enumerate() {
138            if vertex != source && vertex != sink {
139                assert!(multiplier > 0, "non-terminal multipliers must be positive");
140            }
141        }
142
143        for &capacity in &capacities {
144            let domain = usize::try_from(capacity)
145                .ok()
146                .and_then(|value| value.checked_add(1));
147            assert!(
148                domain.is_some(),
149                "arc capacities must fit into usize for dims()"
150            );
151        }
152
153        Self {
154            graph,
155            source,
156            sink,
157            multipliers,
158            capacities,
159            requirement,
160        }
161    }
162
163    pub fn graph(&self) -> &DirectedGraph {
164        &self.graph
165    }
166
167    pub fn source(&self) -> usize {
168        self.source
169    }
170
171    pub fn sink(&self) -> usize {
172        self.sink
173    }
174
175    pub fn multipliers(&self) -> &[i64] {
176        &self.multipliers
177    }
178
179    pub fn capacities(&self) -> &[i64] {
180        &self.capacities
181    }
182
183    pub fn requirement(&self) -> i64 {
184        self.requirement
185    }
186
187    pub fn num_vertices(&self) -> usize {
188        self.graph.num_vertices()
189    }
190
191    pub fn num_arcs(&self) -> usize {
192        self.graph.num_arcs()
193    }
194
195    pub fn max_capacity(&self) -> i64 {
196        self.capacities.iter().copied().max().unwrap_or(0)
197    }
198
199    fn domain_size(capacity: i64) -> usize {
200        usize::try_from(capacity)
201            .ok()
202            .and_then(|value| value.checked_add(1))
203            .expect("capacity already validated to fit into usize")
204    }
205
206    pub fn is_feasible(&self, config: &[usize]) -> Result<bool, crate::traits::EvaluationError> {
207        if config.len() != self.num_arcs() {
208            return Ok(false);
209        }
210
211        let num_vertices = self.num_vertices();
212        let mut inflow = vec![0_i64; num_vertices];
213        let mut outflow = vec![0_i64; num_vertices];
214
215        for (arc_index, ((u, v), &capacity)) in self
216            .graph
217            .arcs()
218            .into_iter()
219            .zip(self.capacities.iter())
220            .enumerate()
221        {
222            let Some(flow_usize) = config.get(arc_index).copied() else {
223                return Ok(false);
224            };
225            let Ok(flow_u64) = i64::try_from(flow_usize) else {
226                return Ok(false);
227            };
228            if flow_u64 > capacity {
229                return Ok(false);
230            }
231            outflow[u] = outflow[u].checked_add(flow_u64).ok_or_else(|| {
232                crate::traits::EvaluationError::IntegerOverflow(
233                    "summing outgoing multiplied flow".into(),
234                )
235            })?;
236            inflow[v] = inflow[v].checked_add(flow_u64).ok_or_else(|| {
237                crate::traits::EvaluationError::IntegerOverflow(
238                    "summing incoming multiplied flow".into(),
239                )
240            })?;
241        }
242
243        for vertex in 0..num_vertices {
244            if vertex == self.source || vertex == self.sink {
245                continue;
246            }
247            let expected_outflow = inflow[vertex]
248                .checked_mul(self.multipliers[vertex])
249                .ok_or_else(|| {
250                    crate::traits::EvaluationError::IntegerOverflow(
251                        "multiplying incoming flow by vertex multiplier".into(),
252                    )
253                })?;
254            if expected_outflow != outflow[vertex] {
255                return Ok(false);
256            }
257        }
258
259        let sink_net_flow = inflow[self.sink]
260            .checked_sub(outflow[self.sink])
261            .ok_or_else(|| {
262                crate::traits::EvaluationError::IntegerOverflow(
263                    "computing net flow into sink".into(),
264                )
265            })?;
266        Ok(sink_net_flow >= self.requirement)
267    }
268}
269
270impl Problem for IntegralFlowWithMultipliers {
271    const NAME: &'static str = "IntegralFlowWithMultipliers";
272    type Solution = Vec<usize>;
273    type Value = crate::types::Or;
274
275    crate::problem_parameters![
276        ("max_capacity", max_capacity),
277        ("num_arcs", num_arcs),
278        ("num_vertices", num_vertices),
279        ("requirement", requirement),
280    ];
281
282    fn evaluate(
283        &self,
284        config: &Self::Solution,
285    ) -> Result<crate::types::Or, crate::traits::EvaluationError> {
286        if config.len() != self.num_arcs() {
287            return Err(crate::traits::EvaluationError::InvalidConfiguration(
288                "flow vector length does not match the graph arcs".into(),
289            ));
290        }
291        Ok(crate::types::Or(self.is_feasible(config)?))
292    }
293
294    fn variant() -> Vec<(&'static str, &'static str)> {
295        crate::variant_params![]
296    }
297}
298
299impl crate::solvers::BruteForceProblem for IntegralFlowWithMultipliers {
300    fn dimensions(&self) -> Vec<usize> {
301        self.capacities
302            .iter()
303            .map(|&capacity| Self::domain_size(capacity))
304            .collect()
305    }
306}
307
308crate::declare_variants! {
309    default IntegralFlowWithMultipliers => "(max_capacity + 1)^num_arcs" create IntegralFlowWithMultipliersCreateSpec,
310}
311
312crate::register_brute_force! {
313    IntegralFlowWithMultipliers,
314}
315
316#[cfg(feature = "example-db")]
317pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
318    vec![crate::example_db::specs::ModelExampleSpec {
319        id: "integral_flow_with_multipliers",
320        instance: Box::new(IntegralFlowWithMultipliers::new(
321            DirectedGraph::new(
322                8,
323                vec![
324                    (0, 1),
325                    (0, 2),
326                    (0, 3),
327                    (0, 4),
328                    (0, 5),
329                    (0, 6),
330                    (1, 7),
331                    (2, 7),
332                    (3, 7),
333                    (4, 7),
334                    (5, 7),
335                    (6, 7),
336                ],
337            ),
338            0,
339            7,
340            vec![1, 2, 3, 4, 5, 6, 4, 1],
341            vec![1, 1, 1, 1, 1, 1, 2, 3, 4, 5, 6, 4],
342            12,
343        )),
344        optimal_config: serde_json::json!(vec![1, 0, 1, 0, 1, 0, 2, 0, 4, 0, 6, 0]),
345        optimal_value: serde_json::json!(true),
346    }]
347}
348
349#[cfg(test)]
350#[path = "../../unit_tests/models/graph/integral_flow_with_multipliers.rs"]
351mod tests;