Skip to content

Commit a4c3516

Browse files
GiggleLiuzazabapclaude
authored
Fix #296: [Model] UndirectedTwoCommodityIntegralFlow (#658)
* Add plan for #296: [Model] UndirectedTwoCommodityIntegralFlow * Implement #296: [Model] UndirectedTwoCommodityIntegralFlow * Fix review issues for #296 * chore: remove plan file after implementation * fix: address PR #658 review follow-ups * fix: address PR #658 review follow-ups * fix: finalize PR #658 feature-test follow-ups * fix: coverage, rename test helper, revert unrelated files - Rename capacity_two_bottleneck_instance to canonical_instance - Add #[should_panic] tests for constructor validation (capacity count mismatch, vertex out of bounds) - Add flow conservation violation test and shared capacity exceeded test - Replace platform-dependent large-capacity tests with portable versions - Revert unrelated README.md change - Remove stale test report file Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: zazabap <sweynan@icloud.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent a977548 commit a4c3516

20 files changed

Lines changed: 1139 additions & 3 deletions

File tree

docs/paper/reductions.typ

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@
6767
"MaxCut": [Max-Cut],
6868
"GraphPartitioning": [Graph Partitioning],
6969
"HamiltonianPath": [Hamiltonian Path],
70+
"UndirectedTwoCommodityIntegralFlow": [Undirected Two-Commodity Integral Flow],
7071
"LengthBoundedDisjointPaths": [Length-Bounded Disjoint Paths],
7172
"IsomorphicSpanningTree": [Isomorphic Spanning Tree],
7273
"KColoring": [$k$-Coloring],
@@ -637,6 +638,57 @@ Graph Partitioning is a core NP-hard problem arising in VLSI design, parallel co
637638
]
638639
]
639640
}
641+
#{
642+
let x = load-model-example("UndirectedTwoCommodityIntegralFlow")
643+
let sample = x.samples.at(0)
644+
let satisfying_count = x.optimal.len()
645+
let source1 = x.instance.source_1
646+
let source2 = x.instance.source_2
647+
let sink1 = x.instance.sink_1
648+
[
649+
#problem-def("UndirectedTwoCommodityIntegralFlow")[
650+
Given an undirected graph $G = (V, E)$, specified terminals $s_1, s_2, t_1, t_2 in V$, edge capacities $c: E -> ZZ^+$, and requirements $R_1, R_2 in ZZ^+$, determine whether there exist two integral flow functions $f_1, f_2$ that orient each used edge for each commodity, respect the shared edge capacities, conserve flow at every vertex in $V backslash {s_1, s_2, t_1, t_2}$, and deliver at least $R_i$ units of net flow into $t_i$ for each commodity $i in {1, 2}$.
651+
][
652+
Undirected Two-Commodity Integral Flow is the undirected counterpart of the classical two-commodity integral flow problem from Garey \& Johnson (ND39) @garey1979. Even, Itai, and Shamir proved that it remains NP-complete even when every capacity is 1, but becomes polynomial-time solvable when all capacities are even, giving a rare parity-driven complexity dichotomy @evenItaiShamir1976.
653+
654+
The implementation uses four variables per undirected edge ${u, v}$: $f_1(u, v)$, $f_1(v, u)$, $f_2(u, v)$, and $f_2(v, u)$. In the unit-capacity regime, each edge has exactly five meaningful local states: unused, commodity 1 in either direction, or commodity 2 in either direction, which matches the catalog bound $O(5^m)$ for $m = |E|$.
655+
656+
*Example.* Consider the graph with edges $(0, 2)$, $(1, 2)$, and $(2, 3)$, capacities $(1, 1, 2)$, sources $s_1 = v_#source1$, $s_2 = v_#source2$, and shared sink $t_1 = t_2 = v_#sink1$. The sample configuration in the fixture database sets $f_1(0, 2) = 1$, $f_2(1, 2) = 1$, and $f_1(2, 3) = f_2(2, 3) = 1$, with all reverse-direction variables zero. The only nonterminal vertex is $v_2$, where each commodity has one unit of inflow and one unit of outflow, so conservation holds. Vertex $v_3$ receives one unit of net inflow from each commodity, and the shared edge $(2,3)$ uses its full capacity 2. The fixture database contains exactly #satisfying_count satisfying configurations for this instance: the one shown below and the symmetric variant that swaps which commodity uses the two left edges.
657+
658+
#figure(
659+
canvas(length: 1cm, {
660+
import draw: *
661+
let blue = graph-colors.at(0)
662+
let teal = rgb("#76b7b2")
663+
let gray = luma(190)
664+
let verts = ((0, 1.2), (0, -1.2), (2.0, 0), (4.0, 0))
665+
let labels = (
666+
[$s_1 = v_0$],
667+
[$s_2 = v_1$],
668+
[$v_2$],
669+
[$t_1 = t_2 = v_3$],
670+
)
671+
let edges = ((0, 2), (1, 2), (2, 3))
672+
for (u, v) in edges {
673+
g-edge(verts.at(u), verts.at(v), stroke: 1pt + gray)
674+
}
675+
g-edge(verts.at(0), verts.at(2), stroke: 1.8pt + blue)
676+
g-edge(verts.at(1), verts.at(2), stroke: (paint: teal, thickness: 1.8pt, dash: "dashed"))
677+
g-edge(verts.at(2), verts.at(3), stroke: 1.8pt + blue)
678+
g-edge(verts.at(2), verts.at(3), stroke: (paint: teal, thickness: 1.8pt, dash: "dashed"))
679+
for (i, pos) in verts.enumerate() {
680+
let fill = if i == 0 { blue } else if i == 1 { teal } else if i == 3 { rgb("#e15759") } else { white }
681+
g-node(pos, name: "utcif-" + str(i), fill: fill, label: if i == 2 { labels.at(i) } else { text(fill: white)[#labels.at(i)] })
682+
}
683+
content((1.0, 0.95), text(8pt, fill: gray)[$c = 1$])
684+
content((1.0, -0.95), text(8pt, fill: gray)[$c = 1$])
685+
content((3.0, 0.35), text(8pt, fill: gray)[$c = 2$])
686+
}),
687+
caption: [Canonical shared-capacity YES instance for Undirected Two-Commodity Integral Flow. Solid blue carries commodity 1 and dashed teal carries commodity 2; both commodities share the edge $(v_2, v_3)$ of capacity 2.],
688+
) <fig:undirected-two-commodity-integral-flow>
689+
]
690+
]
691+
}
640692
#{
641693
let x = load-model-example("IsomorphicSpanningTree")
642694
let g-edges = x.instance.graph.inner.edges.map(e => (e.at(0), e.at(1)))

docs/paper/references.bib

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,17 @@ @article{gareyJohnsonStockmeyer1976
7474
year = {1976}
7575
}
7676

77+
@article{evenItaiShamir1976,
78+
author = {Shimon Even and Alon Itai and Adi Shamir},
79+
title = {On the Complexity of Timetable and Multicommodity Flow Problems},
80+
journal = {SIAM Journal on Computing},
81+
volume = {5},
82+
number = {4},
83+
pages = {691--703},
84+
year = {1976},
85+
doi = {10.1137/0205048}
86+
}
87+
7788
@article{glover2019,
7889
author = {Fred Glover and Gary Kochenberger and Yu Du},
7990
title = {Quantum Bridge Analytics {I}: a tutorial on formulating and using {QUBO} models},

docs/src/cli.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,7 @@ pred create KColoring --k 3 --graph 0-1,1-2,2-0 -o kcol.json
292292
pred create SpinGlass --graph 0-1,1-2 -o sg.json
293293
pred create MaxCut --graph 0-1,1-2,2-0 -o maxcut.json
294294
pred create SteinerTree --graph 0-1,0-3,1-2,1-3,2-3,2-4,3-4 --edge-weights 2,5,2,1,5,6,1 --terminals 0,2,4 -o steiner.json
295+
pred create UndirectedTwoCommodityIntegralFlow --graph 0-2,1-2,2-3 --capacities 1,1,2 --source-1 0 --sink-1 3 --source-2 1 --sink-2 3 --requirement-1 1 --requirement-2 1 -o utcif.json
295296
pred create LengthBoundedDisjointPaths --graph 0-1,1-6,0-2,2-3,3-6,0-4,4-5,5-6 --source 0 --sink 6 --num-paths-required 2 --bound 3 -o lbdp.json
296297
pred create Factoring --target 15 --bits-m 4 --bits-n 4 -o factoring.json
297298
pred create Factoring --target 21 --bits-m 3 --bits-n 3 -o factoring2.json

docs/src/reductions/problem_schemas.json

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -756,5 +756,51 @@
756756
"description": "Edge weights w: E -> R"
757757
}
758758
]
759+
},
760+
{
761+
"name": "UndirectedTwoCommodityIntegralFlow",
762+
"description": "Determine whether two integral commodities can satisfy sink demands in an undirected capacitated graph",
763+
"fields": [
764+
{
765+
"name": "graph",
766+
"type_name": "SimpleGraph",
767+
"description": "Undirected graph G=(V,E)"
768+
},
769+
{
770+
"name": "capacities",
771+
"type_name": "Vec<u64>",
772+
"description": "Edge capacities c(e) in graph edge order"
773+
},
774+
{
775+
"name": "source_1",
776+
"type_name": "usize",
777+
"description": "Source vertex s_1 for commodity 1"
778+
},
779+
{
780+
"name": "sink_1",
781+
"type_name": "usize",
782+
"description": "Sink vertex t_1 for commodity 1"
783+
},
784+
{
785+
"name": "source_2",
786+
"type_name": "usize",
787+
"description": "Source vertex s_2 for commodity 2"
788+
},
789+
{
790+
"name": "sink_2",
791+
"type_name": "usize",
792+
"description": "Sink vertex t_2 for commodity 2"
793+
},
794+
{
795+
"name": "requirement_1",
796+
"type_name": "u64",
797+
"description": "Required net inflow R_1 at sink t_1"
798+
},
799+
{
800+
"name": "requirement_2",
801+
"type_name": "u64",
802+
"description": "Required net inflow R_2 at sink t_2"
803+
}
804+
]
759805
}
760806
]

docs/src/reductions/reduction_graph.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -550,6 +550,13 @@
550550
"category": "graph",
551551
"doc_path": "models/graph/struct.TravelingSalesman.html",
552552
"complexity": "2^num_vertices"
553+
},
554+
{
555+
"name": "UndirectedTwoCommodityIntegralFlow",
556+
"variant": {},
557+
"category": "graph",
558+
"doc_path": "models/graph/struct.UndirectedTwoCommodityIntegralFlow.html",
559+
"complexity": "5^num_edges"
553560
}
554561
],
555562
"edges": [

problemreductions-cli/src/cli.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,7 @@ Flags by problem type:
223223
KColoring --graph, --k
224224
PartitionIntoTriangles --graph
225225
GraphPartitioning --graph
226+
UndirectedTwoCommodityIntegralFlow --graph, --capacities, --source-1, --sink-1, --source-2, --sink-2, --requirement-1, --requirement-2
226227
IsomorphicSpanningTree --graph, --tree
227228
LengthBoundedDisjointPaths --graph, --source, --sink, --num-paths-required, --bound
228229
Factoring --target, --m, --n
@@ -266,6 +267,7 @@ Examples:
266267
pred create MIS/UnitDiskGraph --positions \"0,0;1,0;0.5,0.8\" --radius 1.5
267268
pred create MIS --random --num-vertices 10 --edge-prob 0.3
268269
pred create FVS --arcs \"0>1,1>2,2>0\" --weights 1,1,1
270+
pred create UndirectedTwoCommodityIntegralFlow --graph 0-2,1-2,2-3 --capacities 1,1,2 --source-1 0 --sink-1 3 --source-2 1 --sink-2 3 --requirement-1 1 --requirement-2 1
269271
pred create X3C --universe 9 --sets \"0,1,2;0,2,4;3,4,5;3,5,7;6,7,8;1,4,6;2,5,8\"
270272
pred create SetBasis --universe 4 --sets \"0,1;1,2;0,2;0,1,2\" --k 3")]
271273
pub struct CreateArgs {
@@ -290,6 +292,9 @@ pub struct CreateArgs {
290292
/// Edge weights (e.g., 2,3,1) [default: all 1s]
291293
#[arg(long)]
292294
pub edge_weights: Option<String>,
295+
/// Edge capacities for multicommodity flow problems (e.g., 1,1,2)
296+
#[arg(long)]
297+
pub capacities: Option<String>,
293298
/// Source vertex for path-based graph problems
294299
#[arg(long)]
295300
pub source: Option<usize>,
@@ -344,6 +349,24 @@ pub struct CreateArgs {
344349
/// Radius for UnitDiskGraph [default: 1.0]
345350
#[arg(long)]
346351
pub radius: Option<f64>,
352+
/// Source vertex s_1 for commodity 1
353+
#[arg(long)]
354+
pub source_1: Option<usize>,
355+
/// Sink vertex t_1 for commodity 1
356+
#[arg(long)]
357+
pub sink_1: Option<usize>,
358+
/// Source vertex s_2 for commodity 2
359+
#[arg(long)]
360+
pub source_2: Option<usize>,
361+
/// Sink vertex t_2 for commodity 2
362+
#[arg(long)]
363+
pub sink_2: Option<usize>,
364+
/// Required flow R_1 for commodity 1
365+
#[arg(long)]
366+
pub requirement_1: Option<u64>,
367+
/// Required flow R_2 for commodity 2
368+
#[arg(long)]
369+
pub requirement_2: Option<u64>,
347370
/// Item sizes for BinPacking (comma-separated, e.g., "3,3,2,2")
348371
#[arg(long)]
349372
pub sizes: Option<String>,

problemreductions-cli/src/commands/create.rs

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ fn all_data_flags_empty(args: &CreateArgs) -> bool {
2727
args.graph.is_none()
2828
&& args.weights.is_none()
2929
&& args.edge_weights.is_none()
30+
&& args.capacities.is_none()
3031
&& args.source.is_none()
3132
&& args.sink.is_none()
3233
&& args.num_paths_required.is_none()
@@ -44,6 +45,12 @@ fn all_data_flags_empty(args: &CreateArgs) -> bool {
4445
&& args.seed.is_none()
4546
&& args.positions.is_none()
4647
&& args.radius.is_none()
48+
&& args.source_1.is_none()
49+
&& args.sink_1.is_none()
50+
&& args.source_2.is_none()
51+
&& args.sink_2.is_none()
52+
&& args.requirement_1.is_none()
53+
&& args.requirement_2.is_none()
4754
&& args.sizes.is_none()
4855
&& args.capacity.is_none()
4956
&& args.sequence.is_none()
@@ -199,11 +206,13 @@ fn create_from_example(args: &CreateArgs, out: &OutputConfig) -> Result<()> {
199206

200207
fn type_format_hint(type_name: &str, graph_type: Option<&str>) -> &'static str {
201208
match type_name {
209+
"SimpleGraph" => "edge list: 0-1,1-2,2-3",
202210
"G" => match graph_type {
203211
Some("KingsSubgraph" | "TriangularSubgraph") => "integer positions: \"0,0;1,0;1,1\"",
204212
Some("UnitDiskGraph") => "float positions: \"0.0,0.0;1.0,0.0\"",
205213
_ => "edge list: 0-1,1-2,2-3",
206214
},
215+
"Vec<u64>" => "comma-separated integers: 1,1,2",
207216
"Vec<W>" => "comma-separated: 1,2,3",
208217
"Vec<CNFClause>" => "semicolon-separated clauses: \"1,2;-1,3\"",
209218
"Vec<Vec<W>>" => "semicolon-separated rows: \"1,0.5;0.5,2\"",
@@ -246,6 +255,9 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str {
246255
},
247256
"GraphPartitioning" => "--graph 0-1,1-2,2-3,0-2,1-3,0-3",
248257
"HamiltonianPath" => "--graph 0-1,1-2,2-3",
258+
"UndirectedTwoCommodityIntegralFlow" => {
259+
"--graph 0-2,1-2,2-3 --capacities 1,1,2 --source-1 0 --sink-1 3 --source-2 1 --sink-2 3 --requirement-1 1 --requirement-2 1"
260+
},
249261
"LengthBoundedDisjointPaths" => {
250262
"--graph 0-1,1-6,0-2,2-3,3-6,0-4,4-5,5-6 --source 0 --sink 6 --num-paths-required 2 --bound 3"
251263
}
@@ -485,6 +497,57 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> {
485497
(ser(HamiltonianPath::new(graph))?, resolved_variant.clone())
486498
}
487499

500+
// UndirectedTwoCommodityIntegralFlow (graph + capacities + terminals + requirements)
501+
"UndirectedTwoCommodityIntegralFlow" => {
502+
let usage = "Usage: pred create UndirectedTwoCommodityIntegralFlow --graph 0-2,1-2,2-3 --capacities 1,1,2 --source-1 0 --sink-1 3 --source-2 1 --sink-2 3 --requirement-1 1 --requirement-2 1";
503+
let (graph, _) = parse_graph(args).map_err(|e| anyhow::anyhow!("{e}\n\n{usage}"))?;
504+
let capacities = parse_capacities(args, graph.num_edges(), usage)?;
505+
let num_vertices = graph.num_vertices();
506+
let source_1 = args.source_1.ok_or_else(|| {
507+
anyhow::anyhow!("UndirectedTwoCommodityIntegralFlow requires --source-1\n\n{usage}")
508+
})?;
509+
let sink_1 = args.sink_1.ok_or_else(|| {
510+
anyhow::anyhow!("UndirectedTwoCommodityIntegralFlow requires --sink-1\n\n{usage}")
511+
})?;
512+
let source_2 = args.source_2.ok_or_else(|| {
513+
anyhow::anyhow!("UndirectedTwoCommodityIntegralFlow requires --source-2\n\n{usage}")
514+
})?;
515+
let sink_2 = args.sink_2.ok_or_else(|| {
516+
anyhow::anyhow!("UndirectedTwoCommodityIntegralFlow requires --sink-2\n\n{usage}")
517+
})?;
518+
let requirement_1 = args.requirement_1.ok_or_else(|| {
519+
anyhow::anyhow!(
520+
"UndirectedTwoCommodityIntegralFlow requires --requirement-1\n\n{usage}"
521+
)
522+
})?;
523+
let requirement_2 = args.requirement_2.ok_or_else(|| {
524+
anyhow::anyhow!(
525+
"UndirectedTwoCommodityIntegralFlow requires --requirement-2\n\n{usage}"
526+
)
527+
})?;
528+
for (label, vertex) in [
529+
("source-1", source_1),
530+
("sink-1", sink_1),
531+
("source-2", source_2),
532+
("sink-2", sink_2),
533+
] {
534+
validate_vertex_index(label, vertex, num_vertices, usage)?;
535+
}
536+
(
537+
ser(UndirectedTwoCommodityIntegralFlow::new(
538+
graph,
539+
capacities,
540+
source_1,
541+
sink_1,
542+
source_2,
543+
sink_2,
544+
requirement_1,
545+
requirement_2,
546+
))?,
547+
resolved_variant.clone(),
548+
)
549+
}
550+
488551
// LengthBoundedDisjointPaths (graph + source + sink + path count + bound)
489552
"LengthBoundedDisjointPaths" => {
490553
let usage = "Usage: pred create LengthBoundedDisjointPaths --graph 0-1,1-6,0-2,2-3,3-6,0-4,4-5,5-6 --source 0 --sink 6 --num-paths-required 2 --bound 3";
@@ -1523,6 +1586,58 @@ fn parse_edge_weights(args: &CreateArgs, num_edges: usize) -> Result<Vec<i32>> {
15231586
}
15241587
}
15251588

1589+
fn validate_vertex_index(
1590+
label: &str,
1591+
vertex: usize,
1592+
num_vertices: usize,
1593+
usage: &str,
1594+
) -> Result<()> {
1595+
if vertex < num_vertices {
1596+
return Ok(());
1597+
}
1598+
1599+
bail!("{label} must be less than num_vertices ({num_vertices})\n\n{usage}");
1600+
}
1601+
1602+
/// Parse `--capacities` as edge capacities (u64).
1603+
fn parse_capacities(args: &CreateArgs, num_edges: usize, usage: &str) -> Result<Vec<u64>> {
1604+
let capacities = args.capacities.as_deref().ok_or_else(|| {
1605+
anyhow::anyhow!("UndirectedTwoCommodityIntegralFlow requires --capacities\n\n{usage}")
1606+
})?;
1607+
let capacities: Vec<u64> = capacities
1608+
.split(',')
1609+
.map(|s| {
1610+
let trimmed = s.trim();
1611+
trimmed
1612+
.parse::<u64>()
1613+
.with_context(|| format!("Invalid capacity `{trimmed}`\n\n{usage}"))
1614+
})
1615+
.collect::<Result<Vec<_>>>()?;
1616+
if capacities.len() != num_edges {
1617+
bail!(
1618+
"Expected {} capacities but got {}\n\n{}",
1619+
num_edges,
1620+
capacities.len(),
1621+
usage
1622+
);
1623+
}
1624+
for (edge_index, &capacity) in capacities.iter().enumerate() {
1625+
let fits = usize::try_from(capacity)
1626+
.ok()
1627+
.and_then(|value| value.checked_add(1))
1628+
.is_some();
1629+
if !fits {
1630+
bail!(
1631+
"capacity {} at edge index {} is too large for this platform\n\n{}",
1632+
capacity,
1633+
edge_index,
1634+
usage
1635+
);
1636+
}
1637+
}
1638+
Ok(capacities)
1639+
}
1640+
15261641
/// Parse `--couplings` as SpinGlass pairwise couplings (i32), defaulting to all 1s.
15271642
fn parse_couplings(args: &CreateArgs, num_edges: usize) -> Result<Vec<i32>> {
15281643
match &args.couplings {

problemreductions-cli/src/problem_name.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,14 @@ mod tests {
307307
assert_eq!(spec.variant_values, vec!["SimpleGraph", "f64"]);
308308
}
309309

310+
#[test]
311+
fn test_resolve_alias_pass_through_undirected_two_commodity_integral_flow() {
312+
assert_eq!(
313+
resolve_alias("UndirectedTwoCommodityIntegralFlow"),
314+
"UndirectedTwoCommodityIntegralFlow"
315+
);
316+
}
317+
310318
#[test]
311319
fn test_parse_problem_spec_ksat_alias() {
312320
let spec = parse_problem_spec("KSAT").unwrap();

0 commit comments

Comments
 (0)