problemreductions/models/graph/
path_constrained_network_flow.rs1use 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#[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 #[create(codec = "arc-list")]
50 arcs: Vec<(usize, usize)>,
51 num_vertices: Option<usize>,
53 #[create(codec = "comma-separated")]
55 capacities: Option<Vec<i64>>,
56 source: usize,
58 sink: usize,
60 #[create(codec = "semicolon-separated")]
62 paths: Vec<Vec<usize>>,
63 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 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 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 pub fn graph(&self) -> &DirectedGraph {
209 &self.graph
210 }
211
212 pub fn capacities(&self) -> &[i64] {
214 &self.capacities
215 }
216
217 pub fn paths(&self) -> &[Vec<usize>] {
219 &self.paths
220 }
221
222 pub fn source(&self) -> usize {
224 self.source
225 }
226
227 pub fn sink(&self) -> usize {
229 self.sink
230 }
231
232 pub fn requirement(&self) -> i64 {
234 self.requirement
235 }
236
237 pub fn set_requirement(&mut self, requirement: i64) {
239 self.requirement = requirement;
240 }
241
242 pub fn num_vertices(&self) -> usize {
244 self.graph.num_vertices()
245 }
246
247 pub fn num_arcs(&self) -> usize {
249 self.graph.num_arcs()
250 }
251
252 pub fn num_paths(&self) -> usize {
254 self.paths.len()
255 }
256
257 pub fn max_capacity(&self) -> i64 {
259 self.capacities.iter().copied().max().unwrap_or(0)
260 }
261
262 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;