1use 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)]
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 #[create(codec = "arc-list")]
75 arcs: Vec<(usize, usize)>,
76 num_vertices: Option<usize>,
78 #[create(codec = "comma-separated")]
80 capacities: Option<Vec<i64>>,
81 source: usize,
83 sink: usize,
85 #[create(codec = "semicolon-separated")]
87 paths: Vec<Vec<usize>>,
88 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 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 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 pub fn graph(&self) -> &DirectedGraph {
239 &self.graph
240 }
241
242 pub fn capacities(&self) -> &[i64] {
244 &self.capacities
245 }
246
247 pub fn paths(&self) -> &[Vec<usize>] {
249 &self.paths
250 }
251
252 pub fn source(&self) -> usize {
254 self.source
255 }
256
257 pub fn sink(&self) -> usize {
259 self.sink
260 }
261
262 pub fn requirement(&self) -> i64 {
264 self.requirement
265 }
266
267 pub fn set_requirement(&mut self, requirement: i64) {
269 self.requirement = requirement;
270 }
271
272 pub fn num_vertices(&self) -> usize {
274 self.graph.num_vertices()
275 }
276
277 pub fn num_arcs(&self) -> usize {
279 self.graph.num_arcs()
280 }
281
282 pub fn num_paths(&self) -> usize {
284 self.paths.len()
285 }
286
287 pub fn max_capacity(&self) -> i64 {
289 self.capacities.iter().copied().max().unwrap_or(0)
290 }
291
292 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;