Skip to main content

problemreductions/models/graph/
path_constrained_network_flow.rs

1//! Path-Constrained Network Flow problem implementation.
2//!
3//! Given a directed graph with arc capacities, a designated source and sink,
4//! and a prescribed collection of directed s-t paths, determine whether there
5//! exists an integral amount of flow for each prescribed path such that arc
6//! capacities are respected and the total delivered flow reaches the required
7//! threshold.
8
9use crate::registry::{CreateSpec, ProblemSchemaEntry};
10use crate::topology::DirectedGraph;
11use crate::traits::Problem;
12use serde::{Deserialize, Serialize};
13use std::collections::HashSet;
14
15inventory::submit! {
16    ProblemSchemaEntry {
17        name: "PathConstrainedNetworkFlow",
18        display_name: "Path-Constrained Network Flow",
19        aliases: &[],
20        dimensions: &[],
21        category: crate::registry::ProblemCategory::Graph,
22        module_path: module_path!(),
23        description: "Integral flow feasibility on a prescribed collection of directed s-t paths",
24        fields: PathConstrainedNetworkFlowCreateSpec::FIELDS,
25    }
26}
27
28/// Path-Constrained Network Flow.
29///
30/// A configuration contains one integer variable per prescribed path. If
31/// `config[i] = x`, then `x` units of flow are routed along the i-th prescribed
32/// path. A configuration is feasible when:
33/// - each path variable stays within its bottleneck capacity
34/// - the induced arc loads do not exceed the arc capacities
35/// - the total delivered flow reaches the requirement
36#[derive(Debug, Clone, Serialize, Deserialize)]
37#[serde(try_from = "PathConstrainedNetworkFlowData")]
38pub struct PathConstrainedNetworkFlow {
39    graph: DirectedGraph,
40    capacities: Vec<i64>,
41    source: usize,
42    sink: usize,
43    paths: Vec<Vec<usize>>,
44    requirement: i64,
45}
46
47#[derive(Deserialize)]
48struct PathConstrainedNetworkFlowData {
49    graph: DirectedGraph,
50    capacities: Vec<i64>,
51    source: usize,
52    sink: usize,
53    paths: Vec<Vec<usize>>,
54    requirement: i64,
55}
56
57impl TryFrom<PathConstrainedNetworkFlowData> for PathConstrainedNetworkFlow {
58    type Error = crate::registry::ConstructionError;
59    fn try_from(data: PathConstrainedNetworkFlowData) -> Result<Self, Self::Error> {
60        Self::try_new(
61            data.graph,
62            data.capacities,
63            data.source,
64            data.sink,
65            data.paths,
66            data.requirement,
67        )
68    }
69}
70
71#[derive(Debug, Deserialize, crate::CreateSpec)]
72struct PathConstrainedNetworkFlowCreateSpec {
73    /// Directed graph arcs.
74    #[create(codec = "arc-list")]
75    arcs: Vec<(usize, usize)>,
76    /// Vertex count, needed to preserve isolated vertices.
77    num_vertices: Option<usize>,
78    /// Arc capacities; defaults to one per arc.
79    #[create(codec = "comma-separated")]
80    capacities: Option<Vec<i64>>,
81    /// Source vertex.
82    source: usize,
83    /// Sink vertex.
84    sink: usize,
85    /// Prescribed paths as arc-index sequences.
86    #[create(codec = "semicolon-separated")]
87    paths: Vec<Vec<usize>>,
88    /// Required total flow.
89    requirement: i64,
90}
91
92impl TryFrom<PathConstrainedNetworkFlowCreateSpec> for PathConstrainedNetworkFlow {
93    type Error = crate::registry::ConstructionError;
94
95    fn try_from(spec: PathConstrainedNetworkFlowCreateSpec) -> Result<Self, Self::Error> {
96        if spec.arcs.is_empty() {
97            return Err("arcs must be non-empty".to_string().into());
98        }
99        if spec.paths.is_empty() {
100            return Err("paths must be non-empty".to_string().into());
101        }
102        let inferred = spec
103            .arcs
104            .iter()
105            .flat_map(|&(u, v)| [u, v])
106            .max()
107            .map(|vertex| vertex.checked_add(1).ok_or("vertex count overflows usize"))
108            .transpose()?
109            .unwrap_or(0);
110        let num_vertices = spec.num_vertices.unwrap_or(inferred);
111        if num_vertices < inferred {
112            return Err(format!(
113                "num_vertices {num_vertices} is too small for arc endpoints; need at least {inferred}"
114            ).into());
115        }
116        let capacities = spec.capacities.unwrap_or_else(|| vec![1; spec.arcs.len()]);
117        let graph = DirectedGraph::new(num_vertices, spec.arcs);
118        Self::try_new(
119            graph,
120            capacities,
121            spec.source,
122            spec.sink,
123            spec.paths,
124            spec.requirement,
125        )
126    }
127}
128
129impl PathConstrainedNetworkFlow {
130    /// Create a new Path-Constrained Network Flow instance.
131    ///
132    /// # Panics
133    ///
134    /// Panics if:
135    /// - `capacities.len() != graph.num_arcs()` or a capacity is negative
136    /// - `source` or `sink` are out of range or identical
137    /// - any prescribed path is not a valid directed simple s-t path
138    pub fn new(
139        graph: DirectedGraph,
140        capacities: Vec<i64>,
141        source: usize,
142        sink: usize,
143        paths: Vec<Vec<usize>>,
144        requirement: i64,
145    ) -> Self {
146        Self::try_new(graph, capacities, source, sink, paths, requirement)
147            .unwrap_or_else(|message| panic!("{message}"))
148    }
149
150    /// Create an instance, returning validation errors instead of panicking.
151    pub fn try_new(
152        graph: DirectedGraph,
153        capacities: Vec<i64>,
154        source: usize,
155        sink: usize,
156        paths: Vec<Vec<usize>>,
157        requirement: i64,
158    ) -> Result<Self, crate::registry::ConstructionError> {
159        let num_vertices = graph.num_vertices();
160        if capacities.len() != graph.num_arcs() {
161            return Err(crate::registry::ConstructionError::length_mismatch(
162                "capacities",
163                capacities.len(),
164                graph.num_arcs(),
165            ));
166        }
167        if capacities.iter().any(|&capacity| capacity < 0) {
168            return Err("capacities must be nonnegative".into());
169        }
170        if source >= num_vertices {
171            return Err(format!("source ({source}) >= num_vertices ({num_vertices})").into());
172        }
173        if sink >= num_vertices {
174            return Err(format!("sink ({sink}) >= num_vertices ({num_vertices})").into());
175        }
176        if source == sink {
177            return Err("source and sink must be distinct".to_string().into());
178        }
179
180        for (index, path) in paths.iter().enumerate() {
181            Self::validate_path(&graph, path, source, sink)
182                .map_err(|message| format!("path {index}: {message}"))?;
183        }
184
185        Ok(Self {
186            graph,
187            capacities,
188            source,
189            sink,
190            paths,
191            requirement,
192        })
193    }
194
195    fn validate_path(
196        graph: &DirectedGraph,
197        path: &[usize],
198        source: usize,
199        sink: usize,
200    ) -> Result<(), crate::registry::ConstructionError> {
201        if path.is_empty() {
202            return Err("prescribed paths must be non-empty".to_string().into());
203        }
204
205        let arcs = graph.arcs();
206        let mut visited_vertices = HashSet::from([source]);
207        let mut current = source;
208
209        for &arc_idx in path {
210            let &(tail, head) = arcs
211                .get(arc_idx)
212                .ok_or_else(|| format!("arc index {arc_idx} out of bounds"))?;
213            if tail != current {
214                return Err(format!(
215                    "not contiguous: expected arc leaving vertex {current}, got {tail}->{head}"
216                )
217                .into());
218            }
219            if !visited_vertices.insert(head) {
220                return Err(format!("repeats vertex {head}, so it is not a simple path").into());
221            }
222            current = head;
223        }
224        if current != sink {
225            return Err(format!("must end at sink {sink}, ended at {current}").into());
226        }
227        Ok(())
228    }
229
230    pub(crate) fn path_bottleneck(&self, path: &[usize]) -> i64 {
231        path.iter()
232            .map(|&arc_idx| self.capacities[arc_idx])
233            .min()
234            .unwrap_or(0)
235    }
236
237    /// Get a reference to the underlying graph.
238    pub fn graph(&self) -> &DirectedGraph {
239        &self.graph
240    }
241
242    /// Get the arc capacities.
243    pub fn capacities(&self) -> &[i64] {
244        &self.capacities
245    }
246
247    /// Get the prescribed path collection.
248    pub fn paths(&self) -> &[Vec<usize>] {
249        &self.paths
250    }
251
252    /// Get the source vertex.
253    pub fn source(&self) -> usize {
254        self.source
255    }
256
257    /// Get the sink vertex.
258    pub fn sink(&self) -> usize {
259        self.sink
260    }
261
262    /// Get the required total flow.
263    pub fn requirement(&self) -> i64 {
264        self.requirement
265    }
266
267    /// Update the required total flow.
268    pub fn set_requirement(&mut self, requirement: i64) {
269        self.requirement = requirement;
270    }
271
272    /// Get the number of vertices.
273    pub fn num_vertices(&self) -> usize {
274        self.graph.num_vertices()
275    }
276
277    /// Get the number of arcs.
278    pub fn num_arcs(&self) -> usize {
279        self.graph.num_arcs()
280    }
281
282    /// Get the number of prescribed paths.
283    pub fn num_paths(&self) -> usize {
284        self.paths.len()
285    }
286
287    /// Get the maximum arc capacity.
288    pub fn max_capacity(&self) -> i64 {
289        self.capacities.iter().copied().max().unwrap_or(0)
290    }
291
292    /// Check whether a path-flow assignment is feasible.
293    pub fn is_feasible(&self, config: &[usize]) -> Result<bool, crate::traits::EvaluationError> {
294        if config.len() != self.paths.len() {
295            return Ok(false);
296        }
297
298        let mut arc_loads = vec![0_i64; self.capacities.len()];
299        let mut total_flow = 0_i64;
300
301        for (flow_value, path) in config.iter().copied().zip(&self.paths) {
302            let path_flow = i64::try_from(flow_value).map_err(|_| {
303                crate::traits::EvaluationError::IntegerOverflow(
304                    "converting path flow to i64".into(),
305                )
306            })?;
307            if path_flow > self.path_bottleneck(path) {
308                return Ok(false);
309            }
310
311            total_flow = total_flow.checked_add(path_flow).ok_or_else(|| {
312                crate::traits::EvaluationError::IntegerOverflow("summing total path flow".into())
313            })?;
314            for &arc_idx in path {
315                arc_loads[arc_idx] =
316                    arc_loads[arc_idx].checked_add(path_flow).ok_or_else(|| {
317                        crate::traits::EvaluationError::IntegerOverflow(
318                            "summing path flow on an arc".into(),
319                        )
320                    })?;
321                if arc_loads[arc_idx] > self.capacities[arc_idx] {
322                    return Ok(false);
323                }
324            }
325        }
326
327        Ok(total_flow >= self.requirement)
328    }
329}
330
331impl Problem for PathConstrainedNetworkFlow {
332    const NAME: &'static str = "PathConstrainedNetworkFlow";
333    type Solution = Vec<usize>;
334    type Value = crate::types::Or;
335
336    crate::problem_parameters![
337        ("max_capacity", max_capacity),
338        ("num_arcs", num_arcs),
339        ("num_paths", num_paths),
340    ];
341
342    fn evaluate(
343        &self,
344        config: &Self::Solution,
345    ) -> Result<crate::types::Or, crate::traits::EvaluationError> {
346        if config.len() != self.paths.len() {
347            return Err(crate::traits::EvaluationError::InvalidConfiguration(
348                "path-flow vector length does not match the candidate paths".into(),
349            ));
350        }
351        Ok(crate::types::Or(self.is_feasible(config)?))
352    }
353
354    fn variant() -> Vec<(&'static str, &'static str)> {
355        crate::variant_params![]
356    }
357}
358
359impl crate::solvers::BruteForceProblem for PathConstrainedNetworkFlow {
360    fn dimensions(&self) -> Vec<usize> {
361        self.paths
362            .iter()
363            .map(|path| (self.path_bottleneck(path) as usize) + 1)
364            .collect()
365    }
366}
367
368crate::declare_variants! {
369    default PathConstrainedNetworkFlow => "(max_capacity + 1)^num_paths" create PathConstrainedNetworkFlowCreateSpec,
370}
371
372crate::register_brute_force! {
373    PathConstrainedNetworkFlow,
374}
375
376#[cfg(feature = "example-db")]
377pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
378    vec![crate::example_db::specs::ModelExampleSpec {
379        id: "path_constrained_network_flow",
380        instance: Box::new(PathConstrainedNetworkFlow::new(
381            DirectedGraph::new(
382                8,
383                vec![
384                    (0, 1),
385                    (0, 2),
386                    (1, 3),
387                    (1, 4),
388                    (2, 4),
389                    (3, 5),
390                    (4, 5),
391                    (4, 6),
392                    (5, 7),
393                    (6, 7),
394                ],
395            ),
396            vec![2, 1, 1, 1, 1, 1, 1, 1, 2, 1],
397            0,
398            7,
399            vec![
400                vec![0, 2, 5, 8],
401                vec![0, 3, 6, 8],
402                vec![0, 3, 7, 9],
403                vec![1, 4, 6, 8],
404                vec![1, 4, 7, 9],
405            ],
406            3,
407        )),
408        optimal_config: serde_json::json!(vec![1, 1, 0, 0, 1]),
409        optimal_value: serde_json::json!(true),
410    }]
411}
412
413#[cfg(test)]
414#[path = "../../unit_tests/models/graph/path_constrained_network_flow.rs"]
415mod tests;