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::{FieldInfo, 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        module_path: module_path!(),
22        description: "Integral flow feasibility on a prescribed collection of directed s-t paths",
23        fields: &[
24            FieldInfo { name: "graph", type_name: "DirectedGraph", description: "Directed graph G = (V, A)" },
25            FieldInfo { name: "capacities", type_name: "Vec<u64>", description: "Capacity c(a) for each arc" },
26            FieldInfo { name: "source", type_name: "usize", description: "Source vertex s" },
27            FieldInfo { name: "sink", type_name: "usize", description: "Sink vertex t" },
28            FieldInfo { name: "paths", type_name: "Vec<Vec<usize>>", description: "Prescribed directed s-t paths as arc-index sequences" },
29            FieldInfo { name: "requirement", type_name: "u64", description: "Required total flow R" },
30        ],
31    }
32}
33
34/// Path-Constrained Network Flow.
35///
36/// A configuration contains one integer variable per prescribed path. If
37/// `config[i] = x`, then `x` units of flow are routed along the i-th prescribed
38/// path. A configuration is feasible when:
39/// - each path variable stays within its bottleneck capacity
40/// - the induced arc loads do not exceed the arc capacities
41/// - the total delivered flow reaches the requirement
42#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct PathConstrainedNetworkFlow {
44    graph: DirectedGraph,
45    capacities: Vec<u64>,
46    source: usize,
47    sink: usize,
48    paths: Vec<Vec<usize>>,
49    requirement: u64,
50}
51
52impl PathConstrainedNetworkFlow {
53    /// Create a new Path-Constrained Network Flow instance.
54    ///
55    /// # Panics
56    ///
57    /// Panics if:
58    /// - `capacities.len() != graph.num_arcs()`
59    /// - `source` or `sink` are out of range or identical
60    /// - any prescribed path is not a valid directed simple s-t path
61    pub fn new(
62        graph: DirectedGraph,
63        capacities: Vec<u64>,
64        source: usize,
65        sink: usize,
66        paths: Vec<Vec<usize>>,
67        requirement: u64,
68    ) -> Self {
69        let num_vertices = graph.num_vertices();
70        assert_eq!(
71            capacities.len(),
72            graph.num_arcs(),
73            "capacities length must match graph num_arcs"
74        );
75        assert!(
76            source < num_vertices,
77            "source ({source}) >= num_vertices ({num_vertices})"
78        );
79        assert!(
80            sink < num_vertices,
81            "sink ({sink}) >= num_vertices ({num_vertices})"
82        );
83        assert_ne!(source, sink, "source and sink must be distinct");
84
85        for path in &paths {
86            Self::assert_valid_path(&graph, path, source, sink);
87        }
88
89        Self {
90            graph,
91            capacities,
92            source,
93            sink,
94            paths,
95            requirement,
96        }
97    }
98
99    fn assert_valid_path(graph: &DirectedGraph, path: &[usize], source: usize, sink: usize) {
100        assert!(!path.is_empty(), "prescribed paths must be non-empty");
101
102        let arcs = graph.arcs();
103        let mut visited_vertices = HashSet::from([source]);
104        let mut current = source;
105
106        for &arc_idx in path {
107            let &(tail, head) = arcs
108                .get(arc_idx)
109                .unwrap_or_else(|| panic!("path arc index {arc_idx} out of bounds"));
110            assert_eq!(
111                tail, current,
112                "prescribed path is not contiguous: expected arc leaving vertex {current}, got {tail}->{head}"
113            );
114            assert!(
115                visited_vertices.insert(head),
116                "prescribed path repeats vertex {head}, so it is not a simple path"
117            );
118            current = head;
119        }
120
121        assert_eq!(
122            current, sink,
123            "prescribed path must end at sink {sink}, ended at {current}"
124        );
125    }
126
127    fn path_bottleneck(&self, path: &[usize]) -> u64 {
128        path.iter()
129            .map(|&arc_idx| self.capacities[arc_idx])
130            .min()
131            .unwrap_or(0)
132    }
133
134    /// Get a reference to the underlying graph.
135    pub fn graph(&self) -> &DirectedGraph {
136        &self.graph
137    }
138
139    /// Get the arc capacities.
140    pub fn capacities(&self) -> &[u64] {
141        &self.capacities
142    }
143
144    /// Get the prescribed path collection.
145    pub fn paths(&self) -> &[Vec<usize>] {
146        &self.paths
147    }
148
149    /// Get the source vertex.
150    pub fn source(&self) -> usize {
151        self.source
152    }
153
154    /// Get the sink vertex.
155    pub fn sink(&self) -> usize {
156        self.sink
157    }
158
159    /// Get the required total flow.
160    pub fn requirement(&self) -> u64 {
161        self.requirement
162    }
163
164    /// Update the required total flow.
165    pub fn set_requirement(&mut self, requirement: u64) {
166        self.requirement = requirement;
167    }
168
169    /// Get the number of vertices.
170    pub fn num_vertices(&self) -> usize {
171        self.graph.num_vertices()
172    }
173
174    /// Get the number of arcs.
175    pub fn num_arcs(&self) -> usize {
176        self.graph.num_arcs()
177    }
178
179    /// Get the number of prescribed paths.
180    pub fn num_paths(&self) -> usize {
181        self.paths.len()
182    }
183
184    /// Get the maximum arc capacity.
185    pub fn max_capacity(&self) -> u64 {
186        self.capacities.iter().copied().max().unwrap_or(0)
187    }
188
189    /// Check whether a path-flow assignment is feasible.
190    pub fn is_feasible(&self, config: &[usize]) -> bool {
191        if config.len() != self.paths.len() {
192            return false;
193        }
194
195        let mut arc_loads = vec![0_u64; self.capacities.len()];
196        let mut total_flow = 0_u64;
197
198        for (flow_value, path) in config.iter().copied().zip(&self.paths) {
199            let path_flow = flow_value as u64;
200            if path_flow > self.path_bottleneck(path) {
201                return false;
202            }
203
204            total_flow += path_flow;
205            for &arc_idx in path {
206                arc_loads[arc_idx] += path_flow;
207                if arc_loads[arc_idx] > self.capacities[arc_idx] {
208                    return false;
209                }
210            }
211        }
212
213        total_flow >= self.requirement
214    }
215}
216
217impl Problem for PathConstrainedNetworkFlow {
218    const NAME: &'static str = "PathConstrainedNetworkFlow";
219    type Value = crate::types::Or;
220
221    fn dims(&self) -> Vec<usize> {
222        self.paths
223            .iter()
224            .map(|path| (self.path_bottleneck(path) as usize) + 1)
225            .collect()
226    }
227
228    fn evaluate(&self, config: &[usize]) -> crate::types::Or {
229        crate::types::Or(self.is_feasible(config))
230    }
231
232    fn variant() -> Vec<(&'static str, &'static str)> {
233        crate::variant_params![]
234    }
235}
236
237crate::declare_variants! {
238    default PathConstrainedNetworkFlow => "(max_capacity + 1)^num_paths",
239}
240
241#[cfg(feature = "example-db")]
242pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
243    vec![crate::example_db::specs::ModelExampleSpec {
244        id: "path_constrained_network_flow",
245        instance: Box::new(PathConstrainedNetworkFlow::new(
246            DirectedGraph::new(
247                8,
248                vec![
249                    (0, 1),
250                    (0, 2),
251                    (1, 3),
252                    (1, 4),
253                    (2, 4),
254                    (3, 5),
255                    (4, 5),
256                    (4, 6),
257                    (5, 7),
258                    (6, 7),
259                ],
260            ),
261            vec![2, 1, 1, 1, 1, 1, 1, 1, 2, 1],
262            0,
263            7,
264            vec![
265                vec![0, 2, 5, 8],
266                vec![0, 3, 6, 8],
267                vec![0, 3, 7, 9],
268                vec![1, 4, 6, 8],
269                vec![1, 4, 7, 9],
270            ],
271            3,
272        )),
273        optimal_config: vec![1, 1, 0, 0, 1],
274        optimal_value: serde_json::json!(true),
275    }]
276}
277
278#[cfg(test)]
279#[path = "../../unit_tests/models/graph/path_constrained_network_flow.rs"]
280mod tests;