1use crate::registry::{CreateSpec, ProblemSchemaEntry};
7use crate::topology::{Graph, SimpleGraph};
8use crate::traits::Problem;
9use serde::{Deserialize, Serialize};
10
11inventory::submit! {
12 ProblemSchemaEntry {
13 name: "UndirectedTwoCommodityIntegralFlow",
14 display_name: "Undirected Two-Commodity Integral Flow",
15 aliases: &[],
16 dimensions: &[],
17 category: crate::registry::ProblemCategory::Graph,
18 module_path: module_path!(),
19 description: "Determine whether two integral commodities can satisfy sink demands in an undirected capacitated graph",
20 fields: UndirectedTwoCommodityIntegralFlowCreateSpec::FIELDS,
21 }
22}
23
24#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct UndirectedTwoCommodityIntegralFlow {
34 graph: SimpleGraph,
35 capacities: Vec<i64>,
36 source_1: usize,
37 sink_1: usize,
38 source_2: usize,
39 sink_2: usize,
40 requirement_1: i64,
41 requirement_2: i64,
42}
43
44#[derive(Debug, Deserialize, crate::CreateSpec)]
45struct UndirectedTwoCommodityIntegralFlowCreateSpec {
46 #[create(codec = "edge-list")]
48 graph: Vec<(usize, usize)>,
49 num_vertices: Option<usize>,
51 #[create(codec = "comma-separated")]
53 capacities: Vec<i64>,
54 source_1: usize,
55 sink_1: usize,
56 source_2: usize,
57 sink_2: usize,
58 requirement_1: i64,
59 requirement_2: i64,
60}
61
62impl TryFrom<UndirectedTwoCommodityIntegralFlowCreateSpec> for UndirectedTwoCommodityIntegralFlow {
63 type Error = crate::registry::ConstructionError;
64 fn try_from(spec: UndirectedTwoCommodityIntegralFlowCreateSpec) -> Result<Self, Self::Error> {
65 if spec.graph.is_empty() && spec.num_vertices.is_none() {
66 return Err("num_vertices is required for an empty graph".into());
67 }
68 for &(u, v) in &spec.graph {
69 if u == v {
70 return Err(format!("self-loop {u}-{v} is not allowed").into());
71 }
72 }
73 let inferred = spec
74 .graph
75 .iter()
76 .flat_map(|&(u, v)| [u, v])
77 .max()
78 .map(|v| v.checked_add(1).ok_or("vertex count overflows usize"))
79 .transpose()?
80 .unwrap_or(0);
81 let count = spec.num_vertices.unwrap_or(inferred);
82 if count < inferred {
83 return Err("num_vertices is too small for graph endpoints".into());
84 }
85 if spec.capacities.len() != spec.graph.len() {
86 return Err("capacities length must match graph edge count".into());
87 }
88 for &capacity in &spec.capacities {
89 if usize::try_from(capacity)
90 .ok()
91 .and_then(|v| v.checked_add(1))
92 .is_none()
93 {
94 return Err("capacity is too large for this platform".into());
95 }
96 }
97 for (label, vertex) in [
98 ("source_1", spec.source_1),
99 ("sink_1", spec.sink_1),
100 ("source_2", spec.source_2),
101 ("sink_2", spec.sink_2),
102 ] {
103 if vertex >= count {
104 return Err(format!("{label} must be less than num_vertices").into());
105 }
106 }
107 Ok(Self {
108 graph: SimpleGraph::new(count, spec.graph),
109 capacities: spec.capacities,
110 source_1: spec.source_1,
111 sink_1: spec.sink_1,
112 source_2: spec.source_2,
113 sink_2: spec.sink_2,
114 requirement_1: spec.requirement_1,
115 requirement_2: spec.requirement_2,
116 })
117 }
118}
119
120impl UndirectedTwoCommodityIntegralFlow {
121 #[allow(clippy::too_many_arguments)]
122 pub fn new(
123 graph: SimpleGraph,
124 capacities: Vec<i64>,
125 source_1: usize,
126 sink_1: usize,
127 source_2: usize,
128 sink_2: usize,
129 requirement_1: i64,
130 requirement_2: i64,
131 ) -> Self {
132 assert_eq!(
133 capacities.len(),
134 graph.num_edges(),
135 "capacities length must match graph num_edges"
136 );
137
138 let num_vertices = graph.num_vertices();
139 for (label, vertex) in [
140 ("source_1", source_1),
141 ("sink_1", sink_1),
142 ("source_2", source_2),
143 ("sink_2", sink_2),
144 ] {
145 assert!(
146 vertex < num_vertices,
147 "{label} must be less than num_vertices ({num_vertices})"
148 );
149 }
150
151 for &capacity in &capacities {
152 let domain = usize::try_from(capacity)
153 .ok()
154 .and_then(|value| value.checked_add(1));
155 assert!(
156 domain.is_some(),
157 "edge capacities must fit into usize for dims()"
158 );
159 }
160
161 Self {
162 graph,
163 capacities,
164 source_1,
165 sink_1,
166 source_2,
167 sink_2,
168 requirement_1,
169 requirement_2,
170 }
171 }
172
173 pub fn graph(&self) -> &SimpleGraph {
174 &self.graph
175 }
176
177 pub fn capacities(&self) -> &[i64] {
178 &self.capacities
179 }
180
181 pub fn source_1(&self) -> usize {
182 self.source_1
183 }
184
185 pub fn sink_1(&self) -> usize {
186 self.sink_1
187 }
188
189 pub fn source_2(&self) -> usize {
190 self.source_2
191 }
192
193 pub fn sink_2(&self) -> usize {
194 self.sink_2
195 }
196
197 pub fn requirement_1(&self) -> i64 {
198 self.requirement_1
199 }
200
201 pub fn requirement_2(&self) -> i64 {
202 self.requirement_2
203 }
204
205 pub fn num_vertices(&self) -> usize {
206 self.graph.num_vertices()
207 }
208
209 pub fn num_edges(&self) -> usize {
210 self.graph.num_edges()
211 }
212
213 pub fn num_conservation_constraints(&self) -> usize {
214 [(self.source_1, self.sink_1), (self.source_2, self.sink_2)]
215 .into_iter()
216 .map(|(source, sink)| self.num_vertices() - if source == sink { 1 } else { 2 })
217 .sum()
218 }
219
220 pub fn is_valid_solution(
221 &self,
222 config: &[usize],
223 ) -> Result<bool, crate::traits::EvaluationError> {
224 Ok(self.evaluate_solution(config)?.0)
225 }
226
227 fn config_len(&self) -> usize {
228 self.num_edges() * 4
229 }
230
231 fn domain_size(capacity: i64) -> usize {
232 usize::try_from(capacity)
233 .ok()
234 .and_then(|value| value.checked_add(1))
235 .expect("capacity already validated to fit into usize")
236 }
237
238 fn edge_flows(&self, config: &[usize], edge_index: usize) -> Option<[usize; 4]> {
239 let start = edge_index.checked_mul(4)?;
240 Some([
241 *config.get(start)?,
242 *config.get(start + 1)?,
243 *config.get(start + 2)?,
244 *config.get(start + 3)?,
245 ])
246 }
247
248 fn flow_pair_for_commodity(flows: [usize; 4], commodity: usize) -> (usize, usize) {
249 match commodity {
250 1 => (flows[0], flows[1]),
251 2 => (flows[2], flows[3]),
252 _ => unreachable!("commodity must be 1 or 2"),
253 }
254 }
255
256 fn commodity_balance(
257 &self,
258 config: &[usize],
259 commodity: usize,
260 vertex: usize,
261 ) -> Result<Option<i64>, crate::traits::EvaluationError> {
262 let mut balance = 0_i64;
263 for (edge_index, (u, v)) in self.graph.edges().into_iter().enumerate() {
264 let Some(flows) = self.edge_flows(config, edge_index) else {
265 return Ok(None);
266 };
267 let (uv, vu) = Self::flow_pair_for_commodity(flows, commodity);
268 let uv = i64::try_from(uv).map_err(|_| {
269 crate::traits::EvaluationError::IntegerOverflow(
270 "converting forward commodity flow to i64".into(),
271 )
272 })?;
273 let vu = i64::try_from(vu).map_err(|_| {
274 crate::traits::EvaluationError::IntegerOverflow(
275 "converting reverse commodity flow to i64".into(),
276 )
277 })?;
278
279 if vertex == u {
280 balance = balance.checked_sub(uv).ok_or_else(|| {
281 crate::traits::EvaluationError::IntegerOverflow(
282 "subtracting forward commodity flow".into(),
283 )
284 })?;
285 balance = balance.checked_add(vu).ok_or_else(|| {
286 crate::traits::EvaluationError::IntegerOverflow(
287 "adding reverse commodity flow".into(),
288 )
289 })?;
290 } else if vertex == v {
291 balance = balance.checked_add(uv).ok_or_else(|| {
292 crate::traits::EvaluationError::IntegerOverflow(
293 "adding forward commodity flow".into(),
294 )
295 })?;
296 balance = balance.checked_sub(vu).ok_or_else(|| {
297 crate::traits::EvaluationError::IntegerOverflow(
298 "subtracting reverse commodity flow".into(),
299 )
300 })?;
301 }
302 }
303 Ok(Some(balance))
304 }
305
306 fn net_flow_into_sink(
307 &self,
308 config: &[usize],
309 commodity: usize,
310 ) -> Result<Option<i64>, crate::traits::EvaluationError> {
311 let sink = match commodity {
312 1 => self.sink_1,
313 2 => self.sink_2,
314 _ => unreachable!("commodity must be 1 or 2"),
315 };
316 self.commodity_balance(config, commodity, sink)
317 }
318
319 fn evaluate_solution(
320 &self,
321 config: &[usize],
322 ) -> Result<crate::types::Or, crate::traits::EvaluationError> {
323 if config.len() != self.config_len() {
324 return Err(crate::traits::EvaluationError::InvalidConfiguration(
325 "flow representation length does not match the graph".into(),
326 ));
327 }
328
329 for (edge_index, &capacity) in self.capacities.iter().enumerate() {
330 let Some(flows) = self.edge_flows(config, edge_index) else {
331 return Ok(crate::types::Or(false));
332 };
333
334 if flows
335 .iter()
336 .any(|&value| i64::try_from(value).map_or(true, |value| value > capacity))
337 {
338 return Ok(crate::types::Or(false));
339 }
340 if flows[0] > 0 && flows[1] > 0 || flows[2] > 0 && flows[3] > 0 {
341 return Ok(crate::types::Or(false));
342 }
343
344 let commodity_1 = i64::try_from(std::cmp::max(flows[0], flows[1]))
345 .expect("flow values already validated against i64 capacities");
346 let commodity_2 = i64::try_from(std::cmp::max(flows[2], flows[3]))
347 .expect("flow values already validated against i64 capacities");
348 let shared = commodity_1.checked_add(commodity_2).ok_or_else(|| {
349 crate::traits::EvaluationError::IntegerOverflow(
350 "summing two commodities on an undirected edge".into(),
351 )
352 })?;
353 if shared > capacity {
354 return Ok(crate::types::Or(false));
355 }
356 }
357
358 for (commodity, source, sink) in [
359 (1, self.source_1, self.sink_1),
360 (2, self.source_2, self.sink_2),
361 ] {
362 for vertex in 0..self.num_vertices() {
363 if vertex != source
364 && vertex != sink
365 && self.commodity_balance(config, commodity, vertex)? != Some(0)
366 {
367 return Ok(crate::types::Or(false));
368 }
369 }
370 }
371
372 Ok(crate::types::Or(
373 self.net_flow_into_sink(config, 1)?
374 .is_some_and(|flow| flow >= self.requirement_1)
375 && self
376 .net_flow_into_sink(config, 2)?
377 .is_some_and(|flow| flow >= self.requirement_2),
378 ))
379 }
380}
381
382impl Problem for UndirectedTwoCommodityIntegralFlow {
383 const NAME: &'static str = "UndirectedTwoCommodityIntegralFlow";
384 type Solution = Vec<usize>;
385 type Value = crate::types::Or;
386
387 crate::problem_parameters![
388 ("num_edges", num_edges),
389 ("num_conservation_constraints", num_conservation_constraints),
390 ("num_vertices", num_vertices),
391 ];
392
393 fn variant() -> Vec<(&'static str, &'static str)> {
394 crate::variant_params![]
395 }
396
397 fn evaluate(
398 &self,
399 config: &Self::Solution,
400 ) -> Result<crate::types::Or, crate::traits::EvaluationError> {
401 self.evaluate_solution(config)
402 }
403}
404
405impl crate::solvers::BruteForceProblem for UndirectedTwoCommodityIntegralFlow {
406 fn dimensions(&self) -> Vec<usize> {
407 self.capacities
408 .iter()
409 .flat_map(|&capacity| {
410 let domain = Self::domain_size(capacity);
411 std::iter::repeat_n(domain, 4)
412 })
413 .collect()
414 }
415}
416
417crate::declare_variants! {
418 default UndirectedTwoCommodityIntegralFlow => "5^num_edges" create UndirectedTwoCommodityIntegralFlowCreateSpec,
419}
420
421crate::register_brute_force! {
422 UndirectedTwoCommodityIntegralFlow,
423}
424
425#[cfg(feature = "example-db")]
426pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
427 vec![crate::example_db::specs::ModelExampleSpec {
428 id: "undirected_two_commodity_integral_flow",
429 instance: Box::new(UndirectedTwoCommodityIntegralFlow::new(
430 SimpleGraph::new(4, vec![(0, 2), (1, 2), (2, 3)]),
431 vec![1, 1, 2],
432 0,
433 3,
434 1,
435 3,
436 1,
437 1,
438 )),
439 optimal_config: serde_json::json!(vec![1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0]),
440 optimal_value: serde_json::json!(true),
441 }]
442}
443
444#[cfg(test)]
445#[path = "../../unit_tests/models/graph/undirected_two_commodity_integral_flow.rs"]
446mod tests;