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