From 81baf8ea3b7ca7d1759e1ae53f136c9b53f68f9f Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Mon, 16 Mar 2026 15:31:48 +0800 Subject: [PATCH 1/8] Add plan for #296: [Model] UndirectedTwoCommodityIntegralFlow --- ...-undirected-two-commodity-integral-flow.md | 368 ++++++++++++++++++ 1 file changed, 368 insertions(+) create mode 100644 docs/plans/2026-03-16-undirected-two-commodity-integral-flow.md diff --git a/docs/plans/2026-03-16-undirected-two-commodity-integral-flow.md b/docs/plans/2026-03-16-undirected-two-commodity-integral-flow.md new file mode 100644 index 00000000..e285879f --- /dev/null +++ b/docs/plans/2026-03-16-undirected-two-commodity-integral-flow.md @@ -0,0 +1,368 @@ +# Undirected Two-Commodity Integral Flow Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Add the `UndirectedTwoCommodityIntegralFlow` satisfaction model, register it across the crate/CLI/example-db/paper, and verify the issue #296 YES/NO instances end to end. + +**Architecture:** Represent each undirected edge `{u, v}` by four integer variables in a fixed edge order from `graph.edges()`: `f1(u,v)`, `f1(v,u)`, `f2(u,v)`, `f2(v,u)`. `dims()` should expose the exact finite search space by repeating `(capacity + 1)` four times per edge, while `evaluate()` directly enforces antisymmetry, shared edge capacity, per-commodity flow conservation on `V \ {s_1, s_2, t_1, t_2}`, and the sink-demand constraints from the issue body. Use the issue's small even-capacity YES instance as the canonical example because it is brute-force tractable; keep the complexity string `5^num_edges` to match the current issue text/comments. + +**Tech Stack:** Rust workspace, `SimpleGraph`, `Problem`/`SatisfactionProblem`, registry `inventory` + `declare_variants!`, `pred` CLI, example-db fixtures, Typst paper, `make test`, `make clippy`, `make regenerate-fixtures`, `make paper`. + +--- + +## Issue Notes To Carry Into Implementation + +- Issue: `#296 [Model] UndirectedTwoCommodityIntegralFlow` +- Reference: Even, Itai, and Shamir, "On the Complexity of Timetable and Multicommodity Flow Problems," *SIAM J. Comput.* 5(4), 1976. +- Maintainer comments already settled two implementation details: + - keep `graph: SimpleGraph` in the schema instead of raw edge lists + - keep the declared complexity string `5^num_edges` +- The directed companion issue `#295` uses the same conservation interpretation (`V - {s_1, s_2, t_1, t_2}`), so do not silently "correct" that to per-commodity terminals in this PR. +- There is a planned incoming rule (`DirectedTwoCommodityIntegralFlow -> UndirectedTwoCommodityIntegralFlow`), so the model is not orphaned even though the rule is not on `main` yet. + +## Batching + +- **Batch 1 (add-model Steps 1-5.5):** model implementation, registration, CLI support, canonical example, tests, trait consistency, fixture regeneration. +- **Batch 2 (add-model Step 6):** Typst paper entry + bibliography + paper build. +- Keep Batch 2 separate. It depends on Batch 1 because the paper loads checked-in example fixtures and exported schemas. + +## Concrete Design Decisions + +- Category: `src/models/graph/` +- Canonical problem name: `UndirectedTwoCommodityIntegralFlow` +- Problem type: satisfaction (`Metric = bool`) +- Struct fields: + - `graph: SimpleGraph` + - `capacities: Vec` + - `source_1: usize` + - `sink_1: usize` + - `source_2: usize` + - `sink_2: usize` + - `requirement_1: u64` + - `requirement_2: u64` +- Size getters: + - `num_vertices()` + - `num_edges()` +- Variable layout per edge `{u,v}` in `graph.edges()` order: + - offset `4 * edge_index + 0`: commodity 1, `u -> v` + - offset `4 * edge_index + 1`: commodity 1, `v -> u` + - offset `4 * edge_index + 2`: commodity 2, `u -> v` + - offset `4 * edge_index + 3`: commodity 2, `v -> u` +- Canonical paper/example-db instance: + - use issue Instance 3: graph `0-2,1-2,2-3`, capacities `[1, 1, 2]`, `source_1=0`, `sink_1=3`, `source_2=1`, `sink_2=3`, requirements `(1,1)` + - this keeps brute-force enumeration feasible while still illustrating the even-capacity phenomenon mentioned in the issue + +### Task 1: Add the Red Tests for the New Model Surface + +**Files:** +- Create: `src/unit_tests/models/graph/undirected_two_commodity_integral_flow.rs` +- Modify: `src/unit_tests/trait_consistency.rs` +- Modify: `problemreductions-cli/tests/cli_tests.rs` + +**Step 1: Write the failing tests** + +Add model tests that cover: + +- `test_undirected_two_commodity_integral_flow_creation` + - construct the 3-edge even-capacity instance + - assert getters and `dims() == vec![2,2,2,2, 2,2,2,2, 3,3,3,3]` +- `test_undirected_two_commodity_integral_flow_evaluation_yes` + - encode the issue Instance 3 satisfying flow: + - edge `(0,2)`: commodity 1 forward = 1 + - edge `(1,2)`: commodity 2 forward = 1 + - edge `(2,3)`: commodity 1 forward = 1, commodity 2 forward = 1 + - assert `evaluate(...) == true` +- `test_undirected_two_commodity_integral_flow_evaluation_no_shared_bottleneck` + - use issue Instance 2 with capacities `[1,1,1]` + - assert the obvious "both commodities use the bottleneck edge" configuration is rejected + - assert `BruteForce::new().find_satisfying(&problem).is_none()` +- `test_undirected_two_commodity_integral_flow_serialization` +- `test_undirected_two_commodity_integral_flow_paper_example` + - reuse the canonical example instance + - verify the issue's displayed satisfying configuration is accepted + - compute all satisfying solutions and assert the count matches the paper's stated count + +Add a `trait_consistency` entry with a tiny valid instance. + +Add CLI smoke tests in `problemreductions-cli/tests/cli_tests.rs`: + +- `test_list_includes_undirected_two_commodity_integral_flow` +- `test_show_undirected_two_commodity_integral_flow` +- `test_create_undirected_two_commodity_integral_flow` + +For the create test, call: + +```bash +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 +``` + +and assert the JSON contains the new type plus the expected field values. + +**Step 2: Run the targeted tests to verify RED** + +Run: + +```bash +cargo test undirected_two_commodity_integral_flow --lib +cargo test -p problemreductions-cli undirected_two_commodity_integral_flow +``` + +Expected: +- compile failure or failing assertions because the model is not implemented/registered yet +- no unrelated test failures + +**Step 3: Do not fix anything yet beyond test hygiene** + +Only adjust typos or obviously broken test code. Do not add production code in this task beyond what is needed to make the test targets compile once the model file exists in Task 2. + +**Step 4: Checkpoint** + +Do not create a standalone commit here; `issue-to-pr --execute` owns the final implementation commit flow. + +### Task 2: Implement the Core Model and Make the Model Tests Green + +**Files:** +- Create: `src/models/graph/undirected_two_commodity_integral_flow.rs` +- Modify: `src/models/graph/mod.rs` +- Modify: `src/models/mod.rs` +- Modify: `src/lib.rs` + +**Step 1: Implement the minimal model to satisfy the tests** + +Add `src/models/graph/undirected_two_commodity_integral_flow.rs` with: + +- `inventory::submit!` metadata +- `#[derive(Debug, Clone, Serialize, Deserialize)]` +- constructor validation: + - `capacities.len() == graph.num_edges()` + - all terminal indices are `< graph.num_vertices()` + - each capacity fits into `usize` for `dims()` +- getters for graph/capacities/terminals/requirements +- `num_vertices()` / `num_edges()` +- helper methods: + - `edge_variables(config, edge_index)` or similar + - `net_flow_for_commodity(config, commodity, vertex)` + - `is_valid_solution(config)` +- `Problem` impl: + - `NAME = "UndirectedTwoCommodityIntegralFlow"` + - `Metric = bool` + - `variant() = crate::variant_params![]` + - `dims()` repeats `(capacity + 1)` four times per edge + - `evaluate()` returns `false` unless all constraints are satisfied +- `SatisfactionProblem` impl +- `declare_variants!` entry: + +```rust +crate::declare_variants! { + default sat UndirectedTwoCommodityIntegralFlow => "5^num_edges", +} +``` + +Link the new unit test file at the bottom with: + +```rust +#[cfg(test)] +#[path = "../../unit_tests/models/graph/undirected_two_commodity_integral_flow.rs"] +mod tests; +``` + +Register the model in: + +- `src/models/graph/mod.rs` +- `src/models/mod.rs` +- `src/lib.rs` prelude / re-exports + +Also extend `src/models/graph/mod.rs::canonical_model_example_specs()` once the file exposes its example builder in Task 3. + +**Step 2: Run the targeted model tests** + +Run: + +```bash +cargo test undirected_two_commodity_integral_flow --lib +``` + +Expected: +- the new model tests and the new `trait_consistency` entry pass +- failures, if any, should now be about missing CLI/example-db integration rather than core evaluation logic + +**Step 3: Refactor only after green** + +If needed, extract small helpers for: + +- edge-variable indexing +- commodity balance computation +- terminal-set membership + +Keep the representation simple and local to the model file. + +**Step 4: Checkpoint** + +No standalone commit; keep moving to registry/example integration. + +### Task 3: Register the Canonical Example, CLI Create Support, and Remaining Batch-1 Wiring + +**Files:** +- Modify: `src/models/graph/undirected_two_commodity_integral_flow.rs` +- Modify: `src/models/graph/mod.rs` +- Modify: `problemreductions-cli/src/cli.rs` +- Modify: `problemreductions-cli/src/commands/create.rs` +- Modify: `problemreductions-cli/src/problem_name.rs` (only if a lowercase/manual alias hook is still needed after registry registration) + +**Step 1: Add the canonical model example** + +Inside `src/models/graph/undirected_two_commodity_integral_flow.rs`, add: + +- `#[cfg(feature = "example-db")]` +- `canonical_model_example_specs()` +- a single example spec using issue Instance 3 +- `crate::example_db::specs::satisfaction_example(...)` + +Register the example by extending `src/models/graph/mod.rs::canonical_model_example_specs()`. + +**Step 2: Add CLI flags and create support** + +Extend `CreateArgs` in `problemreductions-cli/src/cli.rs` with: + +- `capacities: Option` +- `source_1: Option` +- `sink_1: Option` +- `source_2: Option` +- `sink_2: Option` +- `requirement_1: Option` +- `requirement_2: Option` + +Update: + +- `all_data_flags_empty()` +- the `Flags by problem type` help block +- the `pred create ` examples/help text + +Add `create.rs` parsing helpers for capacities and the four terminal/requirement fields, then wire a new match arm for `"UndirectedTwoCommodityIntegralFlow"`. + +Use this exact usage string in error paths: + +```bash +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 +``` + +**Step 3: Run the targeted tests to verify GREEN** + +Run: + +```bash +cargo test undirected_two_commodity_integral_flow --lib +cargo test -p problemreductions-cli undirected_two_commodity_integral_flow +``` + +Expected: +- model tests pass +- CLI smoke tests pass +- `pred list` / `pred show` now include the new problem + +**Step 4: Regenerate fixtures required by Batch 2** + +Run: + +```bash +make regenerate-fixtures +``` + +Expected: +- `src/example_db/fixtures/examples.json` updates to include the new canonical model example + +If fixture generation surfaces a mismatch in the canonical example count, fix the example or paper wording before moving on. + +**Step 5: Checkpoint** + +Still no standalone commit. Batch 1 is complete once the fixture regeneration is clean. + +### Task 4: Batch 2 — Write the Paper Entry and Bibliography + +**Files:** +- Modify: `docs/paper/reductions.typ` +- Modify: `docs/paper/references.bib` + +**Step 1: Add the bibliography entry** + +Add an `@article` entry for the Even-Itai-Shamir 1976 paper if it is not already present. Reuse the existing `@garey1979` citation for the Garey & Johnson catalog pointer instead of duplicating the textbook. + +**Step 2: Add the display-name and `problem-def` block** + +Update `docs/paper/reductions.typ`: + +- add `"UndirectedTwoCommodityIntegralFlow": [Undirected Two-Commodity Integral Flow],` to `display-name` +- add a `problem-def("UndirectedTwoCommodityIntegralFlow")` section that: + - states the formal decision problem with undirected edges, capacities, and two commodities + - explains the four-variables-per-edge encoding used in the code + - mentions the parity dichotomy from the issue/reference + - uses the canonical issue Instance 3 example and names one satisfying configuration explicitly + - states the exact satisfying-solution count found in Task 3 so the paper and test agree + +Prefer a compact explanatory figure/table over a large graph drawing; this instance is small enough that a textual edge/capacity table is acceptable if a full CeTZ graph adds little value. + +**Step 3: Build the paper** + +Run: + +```bash +make paper +``` + +Expected: +- `docs/paper/reductions.typ` compiles cleanly +- exported schema files update if the new model changed them + +**Step 4: Fix any paper/example drift immediately** + +If `make paper` or the paper-example test disagrees about the satisfying count or displayed configuration, fix the code/example/paper now before moving to final verification. + +### Task 5: Final Verification for issue-to-pr Handoff + +**Files:** +- Review all files changed above + +**Step 1: Run the repo checks required before claiming success** + +Run: + +```bash +make test +make clippy +``` + +If `make test` is too broad to diagnose quickly, first run the targeted tests above, then rerun the full command. + +**Step 2: Run the repo-local completeness review** + +After Batch 2 is green, invoke the repo-local skill: + +```text +/review-implementation model UndirectedTwoCommodityIntegralFlow +``` + +Auto-fix any concrete findings before handing control back to `issue-to-pr`. + +**Step 3: Capture the implementation summary for the PR comment** + +Prepare a short summary covering: + +- new model file + configuration encoding +- CLI flags/support added +- canonical example/fixture updates +- paper entry and bibliography additions +- any deliberate deviations from the issue (expected: none, unless the paper example uses Instance 3 instead of Instance 1 for tractability) + +**Step 4: Leave the tree ready for issue-to-pr** + +Do not remove the plan file manually here; `issue-to-pr` handles: + +- the implementation commit +- review-fix commit(s) +- plan-file removal commit +- push and PR comment From 1805d85ba1b04283b81cca3661326bb0df236a60 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Mon, 16 Mar 2026 15:45:53 +0800 Subject: [PATCH 2/8] Implement #296: [Model] UndirectedTwoCommodityIntegralFlow --- docs/paper/reductions.typ | 52 +++ docs/paper/references.bib | 11 + docs/src/reductions/problem_schemas.json | 46 +++ docs/src/reductions/reduction_graph.json | 7 + problemreductions-cli/src/cli.rs | 23 ++ problemreductions-cli/src/commands/create.rs | 73 ++++ problemreductions-cli/src/problem_name.rs | 8 + problemreductions-cli/tests/cli_tests.rs | 73 ++++ src/example_db/fixtures/examples.json | 3 +- src/example_db/model_builders.rs | 2 + src/lib.rs | 1 + src/models/graph/mod.rs | 4 + .../undirected_two_commodity_integral_flow.rs | 311 ++++++++++++++++++ src/models/mod.rs | 2 +- src/unit_tests/example_db.rs | 19 +- src/unit_tests/export.rs | 3 +- .../undirected_two_commodity_integral_flow.rs | 96 ++++++ src/unit_tests/trait_consistency.rs | 13 + 18 files changed, 735 insertions(+), 12 deletions(-) create mode 100644 src/models/graph/undirected_two_commodity_integral_flow.rs create mode 100644 src/unit_tests/models/graph/undirected_two_commodity_integral_flow.rs diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index b3a286b6..1852cbed 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -67,6 +67,7 @@ "MaxCut": [Max-Cut], "GraphPartitioning": [Graph Partitioning], "HamiltonianPath": [Hamiltonian Path], + "UndirectedTwoCommodityIntegralFlow": [Undirected Two-Commodity Integral Flow], "IsomorphicSpanningTree": [Isomorphic Spanning Tree], "KColoring": [$k$-Coloring], "MinimumDominatingSet": [Minimum Dominating Set], @@ -573,6 +574,57 @@ Graph Partitioning is a core NP-hard problem arising in VLSI design, parallel co ] ] } +#{ + let x = load-model-example("UndirectedTwoCommodityIntegralFlow") + let sample = x.samples.at(0) + let satisfying_count = x.optimal.len() + let source1 = x.instance.source_1 + let source2 = x.instance.source_2 + let sink1 = x.instance.sink_1 + [ + #problem-def("UndirectedTwoCommodityIntegralFlow")[ + 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}$. + ][ + 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. + + 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|$. + + *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. + + #figure( + canvas(length: 1cm, { + import draw: * + let blue = graph-colors.at(0) + let teal = rgb("#76b7b2") + let gray = luma(190) + let verts = ((0, 1.2), (0, -1.2), (2.0, 0), (4.0, 0)) + let labels = ( + [$s_1 = v_0$], + [$s_2 = v_1$], + [$v_2$], + [$t_1 = t_2 = v_3$], + ) + let edges = ((0, 2), (1, 2), (2, 3)) + for (u, v) in edges { + g-edge(verts.at(u), verts.at(v), stroke: 1pt + gray) + } + g-edge(verts.at(0), verts.at(2), stroke: 1.8pt + blue) + g-edge(verts.at(1), verts.at(2), stroke: (paint: teal, thickness: 1.8pt, dash: "dashed")) + g-edge(verts.at(2), verts.at(3), stroke: 1.8pt + blue) + g-edge(verts.at(2), verts.at(3), stroke: (paint: teal, thickness: 1.8pt, dash: "dashed")) + for (i, pos) in verts.enumerate() { + let fill = if i == 0 { blue } else if i == 1 { teal } else if i == 3 { rgb("#e15759") } else { white } + g-node(pos, name: "utcif-" + str(i), fill: fill, label: if i == 2 { labels.at(i) } else { text(fill: white)[#labels.at(i)] }) + } + content((1.0, 0.95), text(8pt, fill: gray)[$c = 1$]) + content((1.0, -0.95), text(8pt, fill: gray)[$c = 1$]) + content((3.0, 0.35), text(8pt, fill: gray)[$c = 2$]) + }), + 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.], + ) + ] + ] +} #{ let x = load-model-example("IsomorphicSpanningTree") let g-edges = x.instance.graph.inner.edges.map(e => (e.at(0), e.at(1))) diff --git a/docs/paper/references.bib b/docs/paper/references.bib index 5fc78c6e..59b77499 100644 --- a/docs/paper/references.bib +++ b/docs/paper/references.bib @@ -74,6 +74,17 @@ @article{gareyJohnsonStockmeyer1976 year = {1976} } +@article{evenItaiShamir1976, + author = {Shimon Even and Alon Itai and Adi Shamir}, + title = {On the Complexity of Timetable and Multicommodity Flow Problems}, + journal = {SIAM Journal on Computing}, + volume = {5}, + number = {4}, + pages = {691--703}, + year = {1976}, + doi = {10.1137/0205048} +} + @article{glover2019, author = {Fred Glover and Gary Kochenberger and Yu Du}, title = {Quantum Bridge Analytics {I}: a tutorial on formulating and using {QUBO} models}, diff --git a/docs/src/reductions/problem_schemas.json b/docs/src/reductions/problem_schemas.json index 3a6e9df8..3a84599a 100644 --- a/docs/src/reductions/problem_schemas.json +++ b/docs/src/reductions/problem_schemas.json @@ -683,5 +683,51 @@ "description": "Edge weights w: E -> R" } ] + }, + { + "name": "UndirectedTwoCommodityIntegralFlow", + "description": "Determine whether two integral commodities can satisfy sink demands in an undirected capacitated graph", + "fields": [ + { + "name": "graph", + "type_name": "SimpleGraph", + "description": "Undirected graph G=(V,E)" + }, + { + "name": "capacities", + "type_name": "Vec", + "description": "Edge capacities c(e) in graph edge order" + }, + { + "name": "source_1", + "type_name": "usize", + "description": "Source vertex s_1 for commodity 1" + }, + { + "name": "sink_1", + "type_name": "usize", + "description": "Sink vertex t_1 for commodity 1" + }, + { + "name": "source_2", + "type_name": "usize", + "description": "Source vertex s_2 for commodity 2" + }, + { + "name": "sink_2", + "type_name": "usize", + "description": "Sink vertex t_2 for commodity 2" + }, + { + "name": "requirement_1", + "type_name": "u64", + "description": "Required net inflow R_1 at sink t_1" + }, + { + "name": "requirement_2", + "type_name": "u64", + "description": "Required net inflow R_2 at sink t_2" + } + ] } ] \ No newline at end of file diff --git a/docs/src/reductions/reduction_graph.json b/docs/src/reductions/reduction_graph.json index c1334cfb..b62a2818 100644 --- a/docs/src/reductions/reduction_graph.json +++ b/docs/src/reductions/reduction_graph.json @@ -514,6 +514,13 @@ "category": "graph", "doc_path": "models/graph/struct.TravelingSalesman.html", "complexity": "2^num_vertices" + }, + { + "name": "UndirectedTwoCommodityIntegralFlow", + "variant": {}, + "category": "graph", + "doc_path": "models/graph/struct.UndirectedTwoCommodityIntegralFlow.html", + "complexity": "5^num_edges" } ], "edges": [ diff --git a/problemreductions-cli/src/cli.rs b/problemreductions-cli/src/cli.rs index c42f69c2..6d32ffa2 100644 --- a/problemreductions-cli/src/cli.rs +++ b/problemreductions-cli/src/cli.rs @@ -223,6 +223,7 @@ Flags by problem type: KColoring --graph, --k PartitionIntoTriangles --graph GraphPartitioning --graph + UndirectedTwoCommodityIntegralFlow --graph, --capacities, --source-1, --sink-1, --source-2, --sink-2, --requirement-1, --requirement-2 IsomorphicSpanningTree --graph, --tree Factoring --target, --m, --n BinPacking --sizes, --capacity @@ -263,6 +264,7 @@ Examples: pred create MIS/UnitDiskGraph --positions \"0,0;1,0;0.5,0.8\" --radius 1.5 pred create MIS --random --num-vertices 10 --edge-prob 0.3 pred create FVS --arcs \"0>1,1>2,2>0\" --weights 1,1,1 + 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 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\"")] pub struct CreateArgs { /// Problem type (e.g., MIS, QUBO, SAT). Omit when using --example. @@ -286,6 +288,9 @@ pub struct CreateArgs { /// Edge weights (e.g., 2,3,1) [default: all 1s] #[arg(long)] pub edge_weights: Option, + /// Edge capacities for multicommodity flow problems (e.g., 1,1,2) + #[arg(long)] + pub capacities: Option, /// Pairwise couplings J_ij for SpinGlass (e.g., 1,-1,1) [default: all 1s] #[arg(long)] pub couplings: Option, @@ -331,6 +336,24 @@ pub struct CreateArgs { /// Radius for UnitDiskGraph [default: 1.0] #[arg(long)] pub radius: Option, + /// Source vertex s_1 for commodity 1 + #[arg(long)] + pub source_1: Option, + /// Sink vertex t_1 for commodity 1 + #[arg(long)] + pub sink_1: Option, + /// Source vertex s_2 for commodity 2 + #[arg(long)] + pub source_2: Option, + /// Sink vertex t_2 for commodity 2 + #[arg(long)] + pub sink_2: Option, + /// Required flow R_1 for commodity 1 + #[arg(long)] + pub requirement_1: Option, + /// Required flow R_2 for commodity 2 + #[arg(long)] + pub requirement_2: Option, /// Item sizes for BinPacking (comma-separated, e.g., "3,3,2,2") #[arg(long)] pub sizes: Option, diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index a1444224..88f5dde0 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -25,6 +25,7 @@ fn all_data_flags_empty(args: &CreateArgs) -> bool { args.graph.is_none() && args.weights.is_none() && args.edge_weights.is_none() + && args.capacities.is_none() && args.couplings.is_none() && args.fields.is_none() && args.clauses.is_none() @@ -39,6 +40,12 @@ fn all_data_flags_empty(args: &CreateArgs) -> bool { && args.seed.is_none() && args.positions.is_none() && args.radius.is_none() + && args.source_1.is_none() + && args.sink_1.is_none() + && args.source_2.is_none() + && args.sink_2.is_none() + && args.requirement_1.is_none() + && args.requirement_2.is_none() && args.sizes.is_none() && args.capacity.is_none() && args.sequence.is_none() @@ -193,11 +200,13 @@ fn create_from_example(args: &CreateArgs, out: &OutputConfig) -> Result<()> { fn type_format_hint(type_name: &str, graph_type: Option<&str>) -> &'static str { match type_name { + "SimpleGraph" => "edge list: 0-1,1-2,2-3", "G" => match graph_type { Some("KingsSubgraph" | "TriangularSubgraph") => "integer positions: \"0,0;1,0;1,1\"", Some("UnitDiskGraph") => "float positions: \"0.0,0.0;1.0,0.0\"", _ => "edge list: 0-1,1-2,2-3", }, + "Vec" => "comma-separated integers: 1,1,2", "Vec" => "comma-separated: 1,2,3", "Vec" => "semicolon-separated clauses: \"1,2;-1,3\"", "Vec>" => "semicolon-separated rows: \"1,0.5;0.5,2\"", @@ -225,6 +234,9 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str { }, "GraphPartitioning" => "--graph 0-1,1-2,2-3,0-2,1-3,0-3", "HamiltonianPath" => "--graph 0-1,1-2,2-3", + "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" + } "IsomorphicSpanningTree" => "--graph 0-1,1-2,0-2 --tree 0-1,1-2", "MaxCut" | "MaximumMatching" | "TravelingSalesman" => { "--graph 0-1,1-2,2-3 --edge-weights 1,1,1" @@ -381,6 +393,48 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { (ser(HamiltonianPath::new(graph))?, resolved_variant.clone()) } + // UndirectedTwoCommodityIntegralFlow (graph + capacities + terminals + requirements) + "UndirectedTwoCommodityIntegralFlow" => { + 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"; + let (graph, _) = parse_graph(args).map_err(|e| anyhow::anyhow!("{e}\n\n{usage}"))?; + let capacities = parse_capacities(args, graph.num_edges())?; + let source_1 = args.source_1.ok_or_else(|| { + anyhow::anyhow!("UndirectedTwoCommodityIntegralFlow requires --source-1\n\n{usage}") + })?; + let sink_1 = args.sink_1.ok_or_else(|| { + anyhow::anyhow!("UndirectedTwoCommodityIntegralFlow requires --sink-1\n\n{usage}") + })?; + let source_2 = args.source_2.ok_or_else(|| { + anyhow::anyhow!("UndirectedTwoCommodityIntegralFlow requires --source-2\n\n{usage}") + })?; + let sink_2 = args.sink_2.ok_or_else(|| { + anyhow::anyhow!("UndirectedTwoCommodityIntegralFlow requires --sink-2\n\n{usage}") + })?; + let requirement_1 = args.requirement_1.ok_or_else(|| { + anyhow::anyhow!( + "UndirectedTwoCommodityIntegralFlow requires --requirement-1\n\n{usage}" + ) + })?; + let requirement_2 = args.requirement_2.ok_or_else(|| { + anyhow::anyhow!( + "UndirectedTwoCommodityIntegralFlow requires --requirement-2\n\n{usage}" + ) + })?; + ( + ser(UndirectedTwoCommodityIntegralFlow::new( + graph, + capacities, + source_1, + sink_1, + source_2, + sink_2, + requirement_1, + requirement_2, + ))?, + resolved_variant.clone(), + ) + } + // IsomorphicSpanningTree (graph + tree) "IsomorphicSpanningTree" => { let (graph, _) = parse_graph(args).map_err(|e| { @@ -1319,6 +1373,25 @@ fn parse_edge_weights(args: &CreateArgs, num_edges: usize) -> Result> { } } +/// Parse `--capacities` as edge capacities (u64). +fn parse_capacities(args: &CreateArgs, num_edges: usize) -> Result> { + let capacities = args.capacities.as_deref().ok_or_else(|| { + anyhow::anyhow!("UndirectedTwoCommodityIntegralFlow requires --capacities") + })?; + let capacities: Vec = capacities + .split(',') + .map(|s| s.trim().parse::()) + .collect::, _>>()?; + if capacities.len() != num_edges { + bail!( + "Expected {} capacities but got {}", + num_edges, + capacities.len() + ); + } + Ok(capacities) +} + /// Parse `--couplings` as SpinGlass pairwise couplings (i32), defaulting to all 1s. fn parse_couplings(args: &CreateArgs, num_edges: usize) -> Result> { match &args.couplings { diff --git a/problemreductions-cli/src/problem_name.rs b/problemreductions-cli/src/problem_name.rs index 2f7ee789..a141398e 100644 --- a/problemreductions-cli/src/problem_name.rs +++ b/problemreductions-cli/src/problem_name.rs @@ -307,6 +307,14 @@ mod tests { assert_eq!(spec.variant_values, vec!["SimpleGraph", "f64"]); } + #[test] + fn test_resolve_alias_pass_through_undirected_two_commodity_integral_flow() { + assert_eq!( + resolve_alias("UndirectedTwoCommodityIntegralFlow"), + "UndirectedTwoCommodityIntegralFlow" + ); + } + #[test] fn test_parse_problem_spec_ksat_alias() { let spec = parse_problem_spec("KSAT").unwrap(); diff --git a/problemreductions-cli/tests/cli_tests.rs b/problemreductions-cli/tests/cli_tests.rs index f011bad1..d2e9903a 100644 --- a/problemreductions-cli/tests/cli_tests.rs +++ b/problemreductions-cli/tests/cli_tests.rs @@ -21,6 +21,18 @@ fn test_list() { assert!(stdout.contains("QUBO")); } +#[test] +fn test_list_includes_undirected_two_commodity_integral_flow() { + let output = pred().args(["list"]).output().unwrap(); + assert!( + output.status.success(), + "stderr: {}", + String::from_utf8_lossy(&output.stderr) + ); + let stdout = String::from_utf8(output.stdout).unwrap(); + assert!(stdout.contains("UndirectedTwoCommodityIntegralFlow")); +} + #[test] fn test_list_rules() { let output = pred().args(["list", "--rules"]).output().unwrap(); @@ -64,6 +76,24 @@ fn test_show() { assert!(stdout.contains("Outgoing reductions")); } +#[test] +fn test_show_undirected_two_commodity_integral_flow() { + let output = pred() + .args(["show", "UndirectedTwoCommodityIntegralFlow"]) + .output() + .unwrap(); + assert!( + output.status.success(), + "stderr: {}", + String::from_utf8_lossy(&output.stderr) + ); + let stdout = String::from_utf8(output.stdout).unwrap(); + assert!(stdout.contains("UndirectedTwoCommodityIntegralFlow")); + assert!(stdout.contains("capacities")); + assert!(stdout.contains("source_1")); + assert!(stdout.contains("requirement_2")); +} + #[test] fn test_show_variant_info() { let output = pred().args(["show", "MIS"]).output().unwrap(); @@ -280,6 +310,49 @@ fn test_evaluate_sat() { std::fs::remove_file(&tmp).ok(); } +#[test] +fn test_create_undirected_two_commodity_integral_flow() { + let output = pred() + .args([ + "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", + ]) + .output() + .unwrap(); + assert!( + output.status.success(), + "stderr: {}", + String::from_utf8_lossy(&output.stderr) + ); + let stdout = String::from_utf8(output.stdout).unwrap(); + let json: serde_json::Value = serde_json::from_str(&stdout).unwrap(); + assert_eq!(json["type"], "UndirectedTwoCommodityIntegralFlow"); + assert_eq!(json["variant"], serde_json::json!({})); + assert_eq!(json["data"]["capacities"], serde_json::json!([1, 1, 2])); + assert_eq!(json["data"]["source_1"], 0); + assert_eq!(json["data"]["sink_1"], 3); + assert_eq!(json["data"]["source_2"], 1); + assert_eq!(json["data"]["sink_2"], 3); + assert_eq!(json["data"]["requirement_1"], 1); + assert_eq!(json["data"]["requirement_2"], 1); +} + #[test] fn test_reduce() { let problem_json = r#"{ diff --git a/src/example_db/fixtures/examples.json b/src/example_db/fixtures/examples.json index e48eef82..987dbb71 100644 --- a/src/example_db/fixtures/examples.json +++ b/src/example_db/fixtures/examples.json @@ -30,7 +30,8 @@ {"problem":"Satisfiability","variant":{},"instance":{"clauses":[{"literals":[1,2]},{"literals":[-1,3]},{"literals":[-2,-3]}],"num_vars":3},"samples":[{"config":[1,0,1],"metric":true}],"optimal":[{"config":[0,1,0],"metric":true},{"config":[1,0,1],"metric":true}]}, {"problem":"ShortestCommonSupersequence","variant":{},"instance":{"alphabet_size":3,"bound":4,"strings":[[0,1,2],[1,0,2]]},"samples":[{"config":[1,0,1,2],"metric":true}],"optimal":[{"config":[0,1,0,2],"metric":true},{"config":[1,0,1,2],"metric":true}]}, {"problem":"SpinGlass","variant":{"graph":"SimpleGraph","weight":"i32"},"instance":{"couplings":[1,1,1,1,1,1,1],"fields":[0,0,0,0,0],"graph":{"inner":{"edge_property":"undirected","edges":[[0,1,null],[1,2,null],[3,4,null],[0,3,null],[1,3,null],[1,4,null],[2,4,null]],"node_holes":[],"nodes":[null,null,null,null,null]}}},"samples":[{"config":[1,0,1,1,0],"metric":{"Valid":-3}}],"optimal":[{"config":[0,0,1,1,0],"metric":{"Valid":-3}},{"config":[0,1,0,0,1],"metric":{"Valid":-3}},{"config":[0,1,0,1,0],"metric":{"Valid":-3}},{"config":[0,1,1,1,0],"metric":{"Valid":-3}},{"config":[1,0,0,0,1],"metric":{"Valid":-3}},{"config":[1,0,1,0,1],"metric":{"Valid":-3}},{"config":[1,0,1,1,0],"metric":{"Valid":-3}},{"config":[1,1,0,0,1],"metric":{"Valid":-3}}]}, - {"problem":"TravelingSalesman","variant":{"graph":"SimpleGraph","weight":"i32"},"instance":{"edge_weights":[1,3,2,2,3,1],"graph":{"inner":{"edge_property":"undirected","edges":[[0,1,null],[0,2,null],[0,3,null],[1,2,null],[1,3,null],[2,3,null]],"node_holes":[],"nodes":[null,null,null,null]}}},"samples":[{"config":[1,0,1,1,0,1],"metric":{"Valid":6}}],"optimal":[{"config":[1,0,1,1,0,1],"metric":{"Valid":6}}]} + {"problem":"TravelingSalesman","variant":{"graph":"SimpleGraph","weight":"i32"},"instance":{"edge_weights":[1,3,2,2,3,1],"graph":{"inner":{"edge_property":"undirected","edges":[[0,1,null],[0,2,null],[0,3,null],[1,2,null],[1,3,null],[2,3,null]],"node_holes":[],"nodes":[null,null,null,null]}}},"samples":[{"config":[1,0,1,1,0,1],"metric":{"Valid":6}}],"optimal":[{"config":[1,0,1,1,0,1],"metric":{"Valid":6}}]}, + {"problem":"UndirectedTwoCommodityIntegralFlow","variant":{},"instance":{"capacities":[1,1,2],"graph":{"inner":{"edge_property":"undirected","edges":[[0,2,null],[1,2,null],[2,3,null]],"node_holes":[],"nodes":[null,null,null,null]}},"requirement_1":1,"requirement_2":1,"sink_1":3,"sink_2":3,"source_1":0,"source_2":1},"samples":[{"config":[1,0,0,0,0,0,1,0,1,0,1,0],"metric":true}],"optimal":[{"config":[0,0,1,0,1,0,0,0,1,0,1,0],"metric":true},{"config":[1,0,0,0,0,0,1,0,1,0,1,0],"metric":true}]} ], "rules": [ {"source":{"problem":"BinPacking","variant":{"weight":"i32"},"instance":{"capacity":10,"sizes":[6,5,5,4,3]}},"target":{"problem":"ILP","variant":{"variable":"bool"},"instance":{"constraints":[{"cmp":"Eq","rhs":1.0,"terms":[[0,1.0],[1,1.0],[2,1.0],[3,1.0],[4,1.0]]},{"cmp":"Eq","rhs":1.0,"terms":[[5,1.0],[6,1.0],[7,1.0],[8,1.0],[9,1.0]]},{"cmp":"Eq","rhs":1.0,"terms":[[10,1.0],[11,1.0],[12,1.0],[13,1.0],[14,1.0]]},{"cmp":"Eq","rhs":1.0,"terms":[[15,1.0],[16,1.0],[17,1.0],[18,1.0],[19,1.0]]},{"cmp":"Eq","rhs":1.0,"terms":[[20,1.0],[21,1.0],[22,1.0],[23,1.0],[24,1.0]]},{"cmp":"Le","rhs":0.0,"terms":[[0,6.0],[5,5.0],[10,5.0],[15,4.0],[20,3.0],[25,-10.0]]},{"cmp":"Le","rhs":0.0,"terms":[[1,6.0],[6,5.0],[11,5.0],[16,4.0],[21,3.0],[26,-10.0]]},{"cmp":"Le","rhs":0.0,"terms":[[2,6.0],[7,5.0],[12,5.0],[17,4.0],[22,3.0],[27,-10.0]]},{"cmp":"Le","rhs":0.0,"terms":[[3,6.0],[8,5.0],[13,5.0],[18,4.0],[23,3.0],[28,-10.0]]},{"cmp":"Le","rhs":0.0,"terms":[[4,6.0],[9,5.0],[14,5.0],[19,4.0],[24,3.0],[29,-10.0]]}],"num_vars":30,"objective":[[25,1.0],[26,1.0],[27,1.0],[28,1.0],[29,1.0]],"sense":"Minimize"}},"solutions":[{"source_config":[2,1,0,0,2],"target_config":[0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,1,0,0,0,0,0,0,1,0,0,1,1,1,0,0]}]}, diff --git a/src/example_db/model_builders.rs b/src/example_db/model_builders.rs index d6c79a47..a866a953 100644 --- a/src/example_db/model_builders.rs +++ b/src/example_db/model_builders.rs @@ -1,6 +1,8 @@ use crate::export::ModelExample; pub fn build_model_examples() -> Vec { + // Graph model examples, including UndirectedTwoCommodityIntegralFlow, are + // gathered from the graph module's canonical example registry. crate::models::graph::canonical_model_example_specs() .into_iter() .chain(crate::models::formula::canonical_model_example_specs()) diff --git a/src/lib.rs b/src/lib.rs index bceccfe2..027abe64 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -53,6 +53,7 @@ pub mod prelude { MinimumDominatingSet, MinimumFeedbackArcSet, MinimumFeedbackVertexSet, MinimumSumMulticenter, MinimumVertexCover, OptimalLinearArrangement, PartitionIntoTriangles, RuralPostman, TravelingSalesman, + UndirectedTwoCommodityIntegralFlow, }; pub use crate::models::misc::{ BinPacking, Factoring, FlowShopScheduling, Knapsack, LongestCommonSubsequence, diff --git a/src/models/graph/mod.rs b/src/models/graph/mod.rs index 7d76de65..89c160fe 100644 --- a/src/models/graph/mod.rs +++ b/src/models/graph/mod.rs @@ -22,6 +22,7 @@ //! - [`MinimumSumMulticenter`]: Min-sum multicenter (p-median) //! - [`RuralPostman`]: Rural Postman (circuit covering required edges) //! - [`SubgraphIsomorphism`]: Subgraph isomorphism (decision problem) +//! - [`UndirectedTwoCommodityIntegralFlow`]: Two-commodity integral flow on undirected graphs pub(crate) mod biclique_cover; pub(crate) mod graph_partitioning; @@ -44,6 +45,7 @@ pub(crate) mod rural_postman; pub(crate) mod spin_glass; pub(crate) mod subgraph_isomorphism; pub(crate) mod traveling_salesman; +pub(crate) mod undirected_two_commodity_integral_flow; pub use biclique_cover::BicliqueCover; pub use graph_partitioning::GraphPartitioning; @@ -66,6 +68,7 @@ pub use rural_postman::RuralPostman; pub use spin_glass::SpinGlass; pub use subgraph_isomorphism::SubgraphIsomorphism; pub use traveling_salesman::TravelingSalesman; +pub use undirected_two_commodity_integral_flow::UndirectedTwoCommodityIntegralFlow; #[cfg(feature = "example-db")] pub(crate) fn canonical_model_example_specs() -> Vec { @@ -86,5 +89,6 @@ pub(crate) fn canonical_model_example_specs() -> Vec", description: "Edge capacities c(e) in graph edge order" }, + FieldInfo { name: "source_1", type_name: "usize", description: "Source vertex s_1 for commodity 1" }, + FieldInfo { name: "sink_1", type_name: "usize", description: "Sink vertex t_1 for commodity 1" }, + FieldInfo { name: "source_2", type_name: "usize", description: "Source vertex s_2 for commodity 2" }, + FieldInfo { name: "sink_2", type_name: "usize", description: "Sink vertex t_2 for commodity 2" }, + FieldInfo { name: "requirement_1", type_name: "u64", description: "Required net inflow R_1 at sink t_1" }, + FieldInfo { name: "requirement_2", type_name: "u64", description: "Required net inflow R_2 at sink t_2" }, + ], + } +} + +/// Undirected two-commodity integral flow on a capacitated graph. +/// +/// For each undirected edge `{u, v}`, a configuration stores four variables in +/// the graph's edge order: +/// - `f1(u, v)` +/// - `f1(v, u)` +/// - `f2(u, v)` +/// - `f2(v, u)` +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct UndirectedTwoCommodityIntegralFlow { + graph: SimpleGraph, + capacities: Vec, + source_1: usize, + sink_1: usize, + source_2: usize, + sink_2: usize, + requirement_1: u64, + requirement_2: u64, +} + +impl UndirectedTwoCommodityIntegralFlow { + #[allow(clippy::too_many_arguments)] + pub fn new( + graph: SimpleGraph, + capacities: Vec, + source_1: usize, + sink_1: usize, + source_2: usize, + sink_2: usize, + requirement_1: u64, + requirement_2: u64, + ) -> Self { + assert_eq!( + capacities.len(), + graph.num_edges(), + "capacities length must match graph num_edges" + ); + + let num_vertices = graph.num_vertices(); + for (label, vertex) in [ + ("source_1", source_1), + ("sink_1", sink_1), + ("source_2", source_2), + ("sink_2", sink_2), + ] { + assert!( + vertex < num_vertices, + "{label} must be less than num_vertices ({num_vertices})" + ); + } + + for &capacity in &capacities { + let domain = usize::try_from(capacity) + .ok() + .and_then(|value| value.checked_add(1)); + assert!( + domain.is_some(), + "edge capacities must fit into usize for dims()" + ); + } + + Self { + graph, + capacities, + source_1, + sink_1, + source_2, + sink_2, + requirement_1, + requirement_2, + } + } + + pub fn graph(&self) -> &SimpleGraph { + &self.graph + } + + pub fn capacities(&self) -> &[u64] { + &self.capacities + } + + pub fn source_1(&self) -> usize { + self.source_1 + } + + pub fn sink_1(&self) -> usize { + self.sink_1 + } + + pub fn source_2(&self) -> usize { + self.source_2 + } + + pub fn sink_2(&self) -> usize { + self.sink_2 + } + + pub fn requirement_1(&self) -> u64 { + self.requirement_1 + } + + pub fn requirement_2(&self) -> u64 { + self.requirement_2 + } + + pub fn num_vertices(&self) -> usize { + self.graph.num_vertices() + } + + pub fn num_edges(&self) -> usize { + self.graph.num_edges() + } + + pub fn is_valid_solution(&self, config: &[usize]) -> bool { + self.evaluate(config) + } + + fn config_len(&self) -> usize { + self.num_edges() * 4 + } + + fn domain_size(capacity: u64) -> usize { + usize::try_from(capacity) + .ok() + .and_then(|value| value.checked_add(1)) + .expect("capacity already validated to fit into usize") + } + + fn edge_flows(&self, config: &[usize], edge_index: usize) -> Option<[usize; 4]> { + let start = edge_index.checked_mul(4)?; + Some([ + *config.get(start)?, + *config.get(start + 1)?, + *config.get(start + 2)?, + *config.get(start + 3)?, + ]) + } + + fn is_terminal(&self, vertex: usize) -> bool { + [self.source_1, self.sink_1, self.source_2, self.sink_2].contains(&vertex) + } + + fn flow_pair_for_commodity(flows: [usize; 4], commodity: usize) -> (usize, usize) { + match commodity { + 1 => (flows[0], flows[1]), + 2 => (flows[2], flows[3]), + _ => unreachable!("commodity must be 1 or 2"), + } + } + + fn commodity_balance(&self, config: &[usize], commodity: usize, vertex: usize) -> Option { + let mut balance = 0i64; + for (edge_index, (u, v)) in self.graph.edges().into_iter().enumerate() { + let flows = self.edge_flows(config, edge_index)?; + let (uv, vu) = Self::flow_pair_for_commodity(flows, commodity); + let uv = i64::try_from(uv).ok()?; + let vu = i64::try_from(vu).ok()?; + + if vertex == u { + balance -= uv; + balance += vu; + } else if vertex == v { + balance += uv; + balance -= vu; + } + } + Some(balance) + } + + fn net_flow_into_sink(&self, config: &[usize], commodity: usize) -> Option { + let sink = match commodity { + 1 => self.sink_1, + 2 => self.sink_2, + _ => unreachable!("commodity must be 1 or 2"), + }; + let balance = self.commodity_balance(config, commodity, sink)?; + u64::try_from(balance).ok() + } +} + +impl Problem for UndirectedTwoCommodityIntegralFlow { + const NAME: &'static str = "UndirectedTwoCommodityIntegralFlow"; + type Metric = bool; + + fn variant() -> Vec<(&'static str, &'static str)> { + crate::variant_params![] + } + + fn dims(&self) -> Vec { + self.capacities + .iter() + .flat_map(|&capacity| { + let domain = Self::domain_size(capacity); + std::iter::repeat_n(domain, 4) + }) + .collect() + } + + fn evaluate(&self, config: &[usize]) -> bool { + if config.len() != self.config_len() { + return false; + } + + for (edge_index, &capacity) in self.capacities.iter().enumerate() { + let Some(flows) = self.edge_flows(config, edge_index) else { + return false; + }; + + if flows + .iter() + .any(|&value| u64::try_from(value).map_or(true, |value| value > capacity)) + { + return false; + } + + if flows[0] > 0 && flows[1] > 0 { + return false; + } + if flows[2] > 0 && flows[3] > 0 { + return false; + } + + let shared = std::cmp::max(flows[0], flows[1]) + std::cmp::max(flows[2], flows[3]); + if u64::try_from(shared).map_or(true, |value| value > capacity) { + return false; + } + } + + for vertex in 0..self.num_vertices() { + if self.is_terminal(vertex) { + continue; + } + + if self.commodity_balance(config, 1, vertex) != Some(0) + || self.commodity_balance(config, 2, vertex) != Some(0) + { + return false; + } + } + + self.net_flow_into_sink(config, 1) + .is_some_and(|flow| flow >= self.requirement_1) + && self + .net_flow_into_sink(config, 2) + .is_some_and(|flow| flow >= self.requirement_2) + } +} + +impl SatisfactionProblem for UndirectedTwoCommodityIntegralFlow {} + +crate::declare_variants! { + default sat UndirectedTwoCommodityIntegralFlow => "5^num_edges", +} + +#[cfg(feature = "example-db")] +pub(crate) fn canonical_model_example_specs() -> Vec { + vec![crate::example_db::specs::ModelExampleSpec { + id: "undirected_two_commodity_integral_flow", + build: || { + let problem = UndirectedTwoCommodityIntegralFlow::new( + SimpleGraph::new(4, vec![(0, 2), (1, 2), (2, 3)]), + vec![1, 1, 2], + 0, + 3, + 1, + 3, + 1, + 1, + ); + crate::example_db::specs::satisfaction_example( + problem, + vec![vec![1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0]], + ) + }, + }] +} + +#[cfg(test)] +#[path = "../../unit_tests/models/graph/undirected_two_commodity_integral_flow.rs"] +mod tests; diff --git a/src/models/mod.rs b/src/models/mod.rs index 8a9da4db..274c4ecd 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -16,7 +16,7 @@ pub use graph::{ MaximalIS, MaximumClique, MaximumIndependentSet, MaximumMatching, MinimumDominatingSet, MinimumFeedbackArcSet, MinimumFeedbackVertexSet, MinimumSumMulticenter, MinimumVertexCover, OptimalLinearArrangement, PartitionIntoTriangles, RuralPostman, SpinGlass, SubgraphIsomorphism, - TravelingSalesman, + TravelingSalesman, UndirectedTwoCommodityIntegralFlow, }; pub use misc::{ BinPacking, Factoring, FlowShopScheduling, Knapsack, LongestCommonSubsequence, diff --git a/src/unit_tests/example_db.rs b/src/unit_tests/example_db.rs index 1152ffbd..66f62c5d 100644 --- a/src/unit_tests/example_db.rs +++ b/src/unit_tests/example_db.rs @@ -705,12 +705,17 @@ fn verify_rule_fixtures_match_computed() { loaded_rule.solutions.len(), computed_rule.solutions.len(), "solution count mismatch for {} -> {} — regenerate fixtures", + loaded_rule.source.problem, + loaded_rule.target.problem + ); + let label = format!( + "{} -> {}", loaded_rule.source.problem, loaded_rule.target.problem ); - let label = - format!("{} -> {}", loaded_rule.source.problem, loaded_rule.target.problem); - for (loaded_pair, computed_pair) in - loaded_rule.solutions.iter().zip(computed_rule.solutions.iter()) + for (loaded_pair, computed_pair) in loaded_rule + .solutions + .iter() + .zip(computed_rule.solutions.iter()) { let loaded_target_problem = load_dyn( &loaded_rule.target.problem, @@ -718,10 +723,8 @@ fn verify_rule_fixtures_match_computed() { loaded_rule.target.instance.clone(), ) .unwrap_or_else(|e| panic!("{label}: load target: {e}")); - let loaded_energy = - loaded_target_problem.evaluate_dyn(&loaded_pair.target_config); - let computed_energy = - loaded_target_problem.evaluate_dyn(&computed_pair.target_config); + let loaded_energy = loaded_target_problem.evaluate_dyn(&loaded_pair.target_config); + let computed_energy = loaded_target_problem.evaluate_dyn(&computed_pair.target_config); assert_eq!( loaded_energy, computed_energy, "{label}: target energy mismatch — regenerate fixtures" diff --git a/src/unit_tests/export.rs b/src/unit_tests/export.rs index d6c4dc39..8b02dc62 100644 --- a/src/unit_tests/export.rs +++ b/src/unit_tests/export.rs @@ -210,8 +210,7 @@ fn test_write_example_db_uses_one_line_per_example_entry() { "model entry should be serialized as one compact JSON object line" ); assert!( - rule_line.trim().starts_with('{') - && rule_line.trim().trim_end_matches(',').ends_with('}'), + rule_line.trim().starts_with('{') && rule_line.trim().trim_end_matches(',').ends_with('}'), "rule entry should be serialized as one compact JSON object line" ); diff --git a/src/unit_tests/models/graph/undirected_two_commodity_integral_flow.rs b/src/unit_tests/models/graph/undirected_two_commodity_integral_flow.rs new file mode 100644 index 00000000..4f10cbc4 --- /dev/null +++ b/src/unit_tests/models/graph/undirected_two_commodity_integral_flow.rs @@ -0,0 +1,96 @@ +use super::*; +use crate::solvers::{BruteForce, Solver}; +use crate::topology::{Graph, SimpleGraph}; +use crate::traits::Problem; + +fn even_capacity_instance() -> UndirectedTwoCommodityIntegralFlow { + UndirectedTwoCommodityIntegralFlow::new( + SimpleGraph::new(4, vec![(0, 2), (1, 2), (2, 3)]), + vec![1, 1, 2], + 0, + 3, + 1, + 3, + 1, + 1, + ) +} + +fn shared_bottleneck_instance() -> UndirectedTwoCommodityIntegralFlow { + UndirectedTwoCommodityIntegralFlow::new( + SimpleGraph::new(4, vec![(0, 2), (1, 2), (2, 3)]), + vec![1, 1, 1], + 0, + 3, + 1, + 3, + 1, + 1, + ) +} + +fn example_config() -> Vec { + // Edge order matches insertion order: + // (0,2): commodity 1 sends 1 from 0 -> 2 + // (1,2): commodity 2 sends 1 from 1 -> 2 + // (2,3): both commodities send 1 from 2 -> 3 + vec![1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0] +} + +#[test] +fn test_undirected_two_commodity_integral_flow_creation() { + let problem = even_capacity_instance(); + assert_eq!(problem.graph().num_vertices(), 4); + assert_eq!(problem.graph().num_edges(), 3); + assert_eq!(problem.capacities(), &[1, 1, 2]); + assert_eq!(problem.source_1(), 0); + assert_eq!(problem.sink_1(), 3); + assert_eq!(problem.source_2(), 1); + assert_eq!(problem.sink_2(), 3); + assert_eq!(problem.requirement_1(), 1); + assert_eq!(problem.requirement_2(), 1); + assert_eq!(problem.num_vertices(), 4); + assert_eq!(problem.num_edges(), 3); + assert_eq!(problem.dims(), vec![2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3]); +} + +#[test] +fn test_undirected_two_commodity_integral_flow_evaluation_yes() { + let problem = even_capacity_instance(); + assert!(problem.evaluate(&example_config())); + assert!(problem.is_valid_solution(&example_config())); +} + +#[test] +fn test_undirected_two_commodity_integral_flow_evaluation_no_shared_bottleneck() { + let problem = shared_bottleneck_instance(); + assert!(!problem.evaluate(&example_config())); + assert!(!problem.is_valid_solution(&example_config())); + assert!(BruteForce::new().find_satisfying(&problem).is_none()); +} + +#[test] +fn test_undirected_two_commodity_integral_flow_serialization() { + let problem = even_capacity_instance(); + let value = serde_json::to_value(&problem).unwrap(); + let deserialized: UndirectedTwoCommodityIntegralFlow = serde_json::from_value(value).unwrap(); + assert_eq!(deserialized.graph(), problem.graph()); + assert_eq!(deserialized.capacities(), problem.capacities()); + assert_eq!(deserialized.source_1(), problem.source_1()); + assert_eq!(deserialized.sink_1(), problem.sink_1()); + assert_eq!(deserialized.source_2(), problem.source_2()); + assert_eq!(deserialized.sink_2(), problem.sink_2()); + assert_eq!(deserialized.requirement_1(), problem.requirement_1()); + assert_eq!(deserialized.requirement_2(), problem.requirement_2()); +} + +#[test] +fn test_undirected_two_commodity_integral_flow_paper_example() { + let problem = even_capacity_instance(); + let config = example_config(); + assert!(problem.evaluate(&config)); + + let all = BruteForce::new().find_all_satisfying(&problem); + assert_eq!(all.len(), 2); + assert!(all.contains(&config)); +} diff --git a/src/unit_tests/trait_consistency.rs b/src/unit_tests/trait_consistency.rs index eee5dc64..99fe0aa3 100644 --- a/src/unit_tests/trait_consistency.rs +++ b/src/unit_tests/trait_consistency.rs @@ -107,6 +107,19 @@ fn test_all_problems_implement_trait_correctly() { &HamiltonianPath::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)])), "HamiltonianPath", ); + check_problem_trait( + &UndirectedTwoCommodityIntegralFlow::new( + SimpleGraph::new(4, vec![(0, 2), (1, 2), (2, 3)]), + vec![1, 1, 2], + 0, + 3, + 1, + 3, + 1, + 1, + ), + "UndirectedTwoCommodityIntegralFlow", + ); check_problem_trait( &OptimalLinearArrangement::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), 3), "OptimalLinearArrangement", From b24acd54162b2bb1b1dc54f68b6ce6c51fe3d435 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Mon, 16 Mar 2026 16:00:37 +0800 Subject: [PATCH 3/8] Fix review issues for #296 --- problemreductions-cli/src/commands/create.rs | 56 ++++++++++++++--- problemreductions-cli/tests/cli_tests.rs | 61 +++++++++++++++++++ .../undirected_two_commodity_integral_flow.rs | 23 +++++-- .../undirected_two_commodity_integral_flow.rs | 38 ++++++++++++ 4 files changed, 165 insertions(+), 13 deletions(-) diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index 88f5dde0..8aa75861 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -397,7 +397,8 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { "UndirectedTwoCommodityIntegralFlow" => { 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"; let (graph, _) = parse_graph(args).map_err(|e| anyhow::anyhow!("{e}\n\n{usage}"))?; - let capacities = parse_capacities(args, graph.num_edges())?; + let capacities = parse_capacities(args, graph.num_edges(), usage)?; + let num_vertices = graph.num_vertices(); let source_1 = args.source_1.ok_or_else(|| { anyhow::anyhow!("UndirectedTwoCommodityIntegralFlow requires --source-1\n\n{usage}") })?; @@ -420,6 +421,14 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { "UndirectedTwoCommodityIntegralFlow requires --requirement-2\n\n{usage}" ) })?; + for (label, vertex) in [ + ("source-1", source_1), + ("sink-1", sink_1), + ("source-2", source_2), + ("sink-2", sink_2), + ] { + validate_vertex_index(label, vertex, num_vertices, usage)?; + } ( ser(UndirectedTwoCommodityIntegralFlow::new( graph, @@ -1373,22 +1382,55 @@ fn parse_edge_weights(args: &CreateArgs, num_edges: usize) -> Result> { } } +fn validate_vertex_index( + label: &str, + vertex: usize, + num_vertices: usize, + usage: &str, +) -> Result<()> { + if vertex < num_vertices { + return Ok(()); + } + + bail!("{label} must be less than num_vertices ({num_vertices})\n\n{usage}"); +} + /// Parse `--capacities` as edge capacities (u64). -fn parse_capacities(args: &CreateArgs, num_edges: usize) -> Result> { +fn parse_capacities(args: &CreateArgs, num_edges: usize, usage: &str) -> Result> { let capacities = args.capacities.as_deref().ok_or_else(|| { - anyhow::anyhow!("UndirectedTwoCommodityIntegralFlow requires --capacities") + anyhow::anyhow!("UndirectedTwoCommodityIntegralFlow requires --capacities\n\n{usage}") })?; let capacities: Vec = capacities .split(',') - .map(|s| s.trim().parse::()) - .collect::, _>>()?; + .map(|s| { + let trimmed = s.trim(); + trimmed + .parse::() + .with_context(|| format!("Invalid capacity `{trimmed}`\n\n{usage}")) + }) + .collect::>>()?; if capacities.len() != num_edges { bail!( - "Expected {} capacities but got {}", + "Expected {} capacities but got {}\n\n{}", num_edges, - capacities.len() + capacities.len(), + usage ); } + for (edge_index, &capacity) in capacities.iter().enumerate() { + let fits = usize::try_from(capacity) + .ok() + .and_then(|value| value.checked_add(1)) + .is_some(); + if !fits { + bail!( + "capacity {} at edge index {} is too large for this platform\n\n{}", + capacity, + edge_index, + usage + ); + } + } Ok(capacities) } diff --git a/problemreductions-cli/tests/cli_tests.rs b/problemreductions-cli/tests/cli_tests.rs index d2e9903a..8142f84f 100644 --- a/problemreductions-cli/tests/cli_tests.rs +++ b/problemreductions-cli/tests/cli_tests.rs @@ -353,6 +353,67 @@ fn test_create_undirected_two_commodity_integral_flow() { assert_eq!(json["data"]["requirement_2"], 1); } +#[test] +fn test_create_undirected_two_commodity_integral_flow_missing_capacities_shows_usage() { + let output = pred() + .args([ + "create", + "UndirectedTwoCommodityIntegralFlow", + "--graph", + "0-2,1-2,2-3", + "--source-1", + "0", + "--sink-1", + "3", + "--source-2", + "1", + "--sink-2", + "3", + "--requirement-1", + "1", + "--requirement-2", + "1", + ]) + .output() + .unwrap(); + assert!(!output.status.success()); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("requires --capacities")); + assert!(stderr.contains("Usage: pred create UndirectedTwoCommodityIntegralFlow")); +} + +#[test] +fn test_create_undirected_two_commodity_integral_flow_rejects_out_of_range_terminal() { + let output = pred() + .args([ + "create", + "UndirectedTwoCommodityIntegralFlow", + "--graph", + "0-2,1-2,2-3", + "--capacities", + "1,1,2", + "--source-1", + "99", + "--sink-1", + "3", + "--source-2", + "1", + "--sink-2", + "3", + "--requirement-1", + "1", + "--requirement-2", + "1", + ]) + .output() + .unwrap(); + assert!(!output.status.success()); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("source-1 must be less than num_vertices (4)")); + assert!(stderr.contains("Usage: pred create UndirectedTwoCommodityIntegralFlow")); + assert!(!stderr.contains("panicked at"), "stderr: {stderr}"); +} + #[test] fn test_reduce() { let problem_json = r#"{ diff --git a/src/models/graph/undirected_two_commodity_integral_flow.rs b/src/models/graph/undirected_two_commodity_integral_flow.rs index 1df5dd92..abe7322c 100644 --- a/src/models/graph/undirected_two_commodity_integral_flow.rs +++ b/src/models/graph/undirected_two_commodity_integral_flow.rs @@ -179,13 +179,18 @@ impl UndirectedTwoCommodityIntegralFlow { } } - fn commodity_balance(&self, config: &[usize], commodity: usize, vertex: usize) -> Option { - let mut balance = 0i64; + fn commodity_balance( + &self, + config: &[usize], + commodity: usize, + vertex: usize, + ) -> Option { + let mut balance = 0i128; for (edge_index, (u, v)) in self.graph.edges().into_iter().enumerate() { let flows = self.edge_flows(config, edge_index)?; let (uv, vu) = Self::flow_pair_for_commodity(flows, commodity); - let uv = i64::try_from(uv).ok()?; - let vu = i64::try_from(vu).ok()?; + let uv = i128::from(u64::try_from(uv).ok()?); + let vu = i128::from(u64::try_from(vu).ok()?); if vertex == u { balance -= uv; @@ -251,8 +256,14 @@ impl Problem for UndirectedTwoCommodityIntegralFlow { return false; } - let shared = std::cmp::max(flows[0], flows[1]) + std::cmp::max(flows[2], flows[3]); - if u64::try_from(shared).map_or(true, |value| value > capacity) { + let commodity_1 = u64::try_from(std::cmp::max(flows[0], flows[1])) + .expect("flow values already validated against u64 capacities"); + let commodity_2 = u64::try_from(std::cmp::max(flows[2], flows[3])) + .expect("flow values already validated against u64 capacities"); + let Some(shared) = commodity_1.checked_add(commodity_2) else { + return false; + }; + if shared > capacity { return false; } } diff --git a/src/unit_tests/models/graph/undirected_two_commodity_integral_flow.rs b/src/unit_tests/models/graph/undirected_two_commodity_integral_flow.rs index 4f10cbc4..c574420f 100644 --- a/src/unit_tests/models/graph/undirected_two_commodity_integral_flow.rs +++ b/src/unit_tests/models/graph/undirected_two_commodity_integral_flow.rs @@ -94,3 +94,41 @@ fn test_undirected_two_commodity_integral_flow_paper_example() { assert_eq!(all.len(), 2); assert!(all.contains(&config)); } + +#[test] +fn test_undirected_two_commodity_integral_flow_large_capacity_sink_balance() { + let Ok(large) = usize::try_from(i64::MAX as u64 + 1) else { + return; + }; + let problem = UndirectedTwoCommodityIntegralFlow::new( + SimpleGraph::new(2, vec![(0, 1)]), + vec![large as u64], + 0, + 1, + 0, + 1, + large as u64, + 0, + ); + + assert!(problem.evaluate(&[large, 0, 0, 0])); +} + +#[test] +fn test_undirected_two_commodity_integral_flow_large_capacity_shared_overflow_is_invalid() { + let Ok(large) = usize::try_from(u64::MAX / 2 + 1) else { + return; + }; + let problem = UndirectedTwoCommodityIntegralFlow::new( + SimpleGraph::new(2, vec![(0, 1)]), + vec![large as u64], + 0, + 1, + 0, + 1, + 0, + 0, + ); + + assert!(!problem.evaluate(&[large, 0, large, 0])); +} From 26749219564faae59e9bce8aa1e14901e8a061d0 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Mon, 16 Mar 2026 16:00:53 +0800 Subject: [PATCH 4/8] chore: remove plan file after implementation --- ...-undirected-two-commodity-integral-flow.md | 368 ------------------ 1 file changed, 368 deletions(-) delete mode 100644 docs/plans/2026-03-16-undirected-two-commodity-integral-flow.md diff --git a/docs/plans/2026-03-16-undirected-two-commodity-integral-flow.md b/docs/plans/2026-03-16-undirected-two-commodity-integral-flow.md deleted file mode 100644 index e285879f..00000000 --- a/docs/plans/2026-03-16-undirected-two-commodity-integral-flow.md +++ /dev/null @@ -1,368 +0,0 @@ -# Undirected Two-Commodity Integral Flow Implementation Plan - -> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. - -**Goal:** Add the `UndirectedTwoCommodityIntegralFlow` satisfaction model, register it across the crate/CLI/example-db/paper, and verify the issue #296 YES/NO instances end to end. - -**Architecture:** Represent each undirected edge `{u, v}` by four integer variables in a fixed edge order from `graph.edges()`: `f1(u,v)`, `f1(v,u)`, `f2(u,v)`, `f2(v,u)`. `dims()` should expose the exact finite search space by repeating `(capacity + 1)` four times per edge, while `evaluate()` directly enforces antisymmetry, shared edge capacity, per-commodity flow conservation on `V \ {s_1, s_2, t_1, t_2}`, and the sink-demand constraints from the issue body. Use the issue's small even-capacity YES instance as the canonical example because it is brute-force tractable; keep the complexity string `5^num_edges` to match the current issue text/comments. - -**Tech Stack:** Rust workspace, `SimpleGraph`, `Problem`/`SatisfactionProblem`, registry `inventory` + `declare_variants!`, `pred` CLI, example-db fixtures, Typst paper, `make test`, `make clippy`, `make regenerate-fixtures`, `make paper`. - ---- - -## Issue Notes To Carry Into Implementation - -- Issue: `#296 [Model] UndirectedTwoCommodityIntegralFlow` -- Reference: Even, Itai, and Shamir, "On the Complexity of Timetable and Multicommodity Flow Problems," *SIAM J. Comput.* 5(4), 1976. -- Maintainer comments already settled two implementation details: - - keep `graph: SimpleGraph` in the schema instead of raw edge lists - - keep the declared complexity string `5^num_edges` -- The directed companion issue `#295` uses the same conservation interpretation (`V - {s_1, s_2, t_1, t_2}`), so do not silently "correct" that to per-commodity terminals in this PR. -- There is a planned incoming rule (`DirectedTwoCommodityIntegralFlow -> UndirectedTwoCommodityIntegralFlow`), so the model is not orphaned even though the rule is not on `main` yet. - -## Batching - -- **Batch 1 (add-model Steps 1-5.5):** model implementation, registration, CLI support, canonical example, tests, trait consistency, fixture regeneration. -- **Batch 2 (add-model Step 6):** Typst paper entry + bibliography + paper build. -- Keep Batch 2 separate. It depends on Batch 1 because the paper loads checked-in example fixtures and exported schemas. - -## Concrete Design Decisions - -- Category: `src/models/graph/` -- Canonical problem name: `UndirectedTwoCommodityIntegralFlow` -- Problem type: satisfaction (`Metric = bool`) -- Struct fields: - - `graph: SimpleGraph` - - `capacities: Vec` - - `source_1: usize` - - `sink_1: usize` - - `source_2: usize` - - `sink_2: usize` - - `requirement_1: u64` - - `requirement_2: u64` -- Size getters: - - `num_vertices()` - - `num_edges()` -- Variable layout per edge `{u,v}` in `graph.edges()` order: - - offset `4 * edge_index + 0`: commodity 1, `u -> v` - - offset `4 * edge_index + 1`: commodity 1, `v -> u` - - offset `4 * edge_index + 2`: commodity 2, `u -> v` - - offset `4 * edge_index + 3`: commodity 2, `v -> u` -- Canonical paper/example-db instance: - - use issue Instance 3: graph `0-2,1-2,2-3`, capacities `[1, 1, 2]`, `source_1=0`, `sink_1=3`, `source_2=1`, `sink_2=3`, requirements `(1,1)` - - this keeps brute-force enumeration feasible while still illustrating the even-capacity phenomenon mentioned in the issue - -### Task 1: Add the Red Tests for the New Model Surface - -**Files:** -- Create: `src/unit_tests/models/graph/undirected_two_commodity_integral_flow.rs` -- Modify: `src/unit_tests/trait_consistency.rs` -- Modify: `problemreductions-cli/tests/cli_tests.rs` - -**Step 1: Write the failing tests** - -Add model tests that cover: - -- `test_undirected_two_commodity_integral_flow_creation` - - construct the 3-edge even-capacity instance - - assert getters and `dims() == vec![2,2,2,2, 2,2,2,2, 3,3,3,3]` -- `test_undirected_two_commodity_integral_flow_evaluation_yes` - - encode the issue Instance 3 satisfying flow: - - edge `(0,2)`: commodity 1 forward = 1 - - edge `(1,2)`: commodity 2 forward = 1 - - edge `(2,3)`: commodity 1 forward = 1, commodity 2 forward = 1 - - assert `evaluate(...) == true` -- `test_undirected_two_commodity_integral_flow_evaluation_no_shared_bottleneck` - - use issue Instance 2 with capacities `[1,1,1]` - - assert the obvious "both commodities use the bottleneck edge" configuration is rejected - - assert `BruteForce::new().find_satisfying(&problem).is_none()` -- `test_undirected_two_commodity_integral_flow_serialization` -- `test_undirected_two_commodity_integral_flow_paper_example` - - reuse the canonical example instance - - verify the issue's displayed satisfying configuration is accepted - - compute all satisfying solutions and assert the count matches the paper's stated count - -Add a `trait_consistency` entry with a tiny valid instance. - -Add CLI smoke tests in `problemreductions-cli/tests/cli_tests.rs`: - -- `test_list_includes_undirected_two_commodity_integral_flow` -- `test_show_undirected_two_commodity_integral_flow` -- `test_create_undirected_two_commodity_integral_flow` - -For the create test, call: - -```bash -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 -``` - -and assert the JSON contains the new type plus the expected field values. - -**Step 2: Run the targeted tests to verify RED** - -Run: - -```bash -cargo test undirected_two_commodity_integral_flow --lib -cargo test -p problemreductions-cli undirected_two_commodity_integral_flow -``` - -Expected: -- compile failure or failing assertions because the model is not implemented/registered yet -- no unrelated test failures - -**Step 3: Do not fix anything yet beyond test hygiene** - -Only adjust typos or obviously broken test code. Do not add production code in this task beyond what is needed to make the test targets compile once the model file exists in Task 2. - -**Step 4: Checkpoint** - -Do not create a standalone commit here; `issue-to-pr --execute` owns the final implementation commit flow. - -### Task 2: Implement the Core Model and Make the Model Tests Green - -**Files:** -- Create: `src/models/graph/undirected_two_commodity_integral_flow.rs` -- Modify: `src/models/graph/mod.rs` -- Modify: `src/models/mod.rs` -- Modify: `src/lib.rs` - -**Step 1: Implement the minimal model to satisfy the tests** - -Add `src/models/graph/undirected_two_commodity_integral_flow.rs` with: - -- `inventory::submit!` metadata -- `#[derive(Debug, Clone, Serialize, Deserialize)]` -- constructor validation: - - `capacities.len() == graph.num_edges()` - - all terminal indices are `< graph.num_vertices()` - - each capacity fits into `usize` for `dims()` -- getters for graph/capacities/terminals/requirements -- `num_vertices()` / `num_edges()` -- helper methods: - - `edge_variables(config, edge_index)` or similar - - `net_flow_for_commodity(config, commodity, vertex)` - - `is_valid_solution(config)` -- `Problem` impl: - - `NAME = "UndirectedTwoCommodityIntegralFlow"` - - `Metric = bool` - - `variant() = crate::variant_params![]` - - `dims()` repeats `(capacity + 1)` four times per edge - - `evaluate()` returns `false` unless all constraints are satisfied -- `SatisfactionProblem` impl -- `declare_variants!` entry: - -```rust -crate::declare_variants! { - default sat UndirectedTwoCommodityIntegralFlow => "5^num_edges", -} -``` - -Link the new unit test file at the bottom with: - -```rust -#[cfg(test)] -#[path = "../../unit_tests/models/graph/undirected_two_commodity_integral_flow.rs"] -mod tests; -``` - -Register the model in: - -- `src/models/graph/mod.rs` -- `src/models/mod.rs` -- `src/lib.rs` prelude / re-exports - -Also extend `src/models/graph/mod.rs::canonical_model_example_specs()` once the file exposes its example builder in Task 3. - -**Step 2: Run the targeted model tests** - -Run: - -```bash -cargo test undirected_two_commodity_integral_flow --lib -``` - -Expected: -- the new model tests and the new `trait_consistency` entry pass -- failures, if any, should now be about missing CLI/example-db integration rather than core evaluation logic - -**Step 3: Refactor only after green** - -If needed, extract small helpers for: - -- edge-variable indexing -- commodity balance computation -- terminal-set membership - -Keep the representation simple and local to the model file. - -**Step 4: Checkpoint** - -No standalone commit; keep moving to registry/example integration. - -### Task 3: Register the Canonical Example, CLI Create Support, and Remaining Batch-1 Wiring - -**Files:** -- Modify: `src/models/graph/undirected_two_commodity_integral_flow.rs` -- Modify: `src/models/graph/mod.rs` -- Modify: `problemreductions-cli/src/cli.rs` -- Modify: `problemreductions-cli/src/commands/create.rs` -- Modify: `problemreductions-cli/src/problem_name.rs` (only if a lowercase/manual alias hook is still needed after registry registration) - -**Step 1: Add the canonical model example** - -Inside `src/models/graph/undirected_two_commodity_integral_flow.rs`, add: - -- `#[cfg(feature = "example-db")]` -- `canonical_model_example_specs()` -- a single example spec using issue Instance 3 -- `crate::example_db::specs::satisfaction_example(...)` - -Register the example by extending `src/models/graph/mod.rs::canonical_model_example_specs()`. - -**Step 2: Add CLI flags and create support** - -Extend `CreateArgs` in `problemreductions-cli/src/cli.rs` with: - -- `capacities: Option` -- `source_1: Option` -- `sink_1: Option` -- `source_2: Option` -- `sink_2: Option` -- `requirement_1: Option` -- `requirement_2: Option` - -Update: - -- `all_data_flags_empty()` -- the `Flags by problem type` help block -- the `pred create ` examples/help text - -Add `create.rs` parsing helpers for capacities and the four terminal/requirement fields, then wire a new match arm for `"UndirectedTwoCommodityIntegralFlow"`. - -Use this exact usage string in error paths: - -```bash -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 -``` - -**Step 3: Run the targeted tests to verify GREEN** - -Run: - -```bash -cargo test undirected_two_commodity_integral_flow --lib -cargo test -p problemreductions-cli undirected_two_commodity_integral_flow -``` - -Expected: -- model tests pass -- CLI smoke tests pass -- `pred list` / `pred show` now include the new problem - -**Step 4: Regenerate fixtures required by Batch 2** - -Run: - -```bash -make regenerate-fixtures -``` - -Expected: -- `src/example_db/fixtures/examples.json` updates to include the new canonical model example - -If fixture generation surfaces a mismatch in the canonical example count, fix the example or paper wording before moving on. - -**Step 5: Checkpoint** - -Still no standalone commit. Batch 1 is complete once the fixture regeneration is clean. - -### Task 4: Batch 2 — Write the Paper Entry and Bibliography - -**Files:** -- Modify: `docs/paper/reductions.typ` -- Modify: `docs/paper/references.bib` - -**Step 1: Add the bibliography entry** - -Add an `@article` entry for the Even-Itai-Shamir 1976 paper if it is not already present. Reuse the existing `@garey1979` citation for the Garey & Johnson catalog pointer instead of duplicating the textbook. - -**Step 2: Add the display-name and `problem-def` block** - -Update `docs/paper/reductions.typ`: - -- add `"UndirectedTwoCommodityIntegralFlow": [Undirected Two-Commodity Integral Flow],` to `display-name` -- add a `problem-def("UndirectedTwoCommodityIntegralFlow")` section that: - - states the formal decision problem with undirected edges, capacities, and two commodities - - explains the four-variables-per-edge encoding used in the code - - mentions the parity dichotomy from the issue/reference - - uses the canonical issue Instance 3 example and names one satisfying configuration explicitly - - states the exact satisfying-solution count found in Task 3 so the paper and test agree - -Prefer a compact explanatory figure/table over a large graph drawing; this instance is small enough that a textual edge/capacity table is acceptable if a full CeTZ graph adds little value. - -**Step 3: Build the paper** - -Run: - -```bash -make paper -``` - -Expected: -- `docs/paper/reductions.typ` compiles cleanly -- exported schema files update if the new model changed them - -**Step 4: Fix any paper/example drift immediately** - -If `make paper` or the paper-example test disagrees about the satisfying count or displayed configuration, fix the code/example/paper now before moving to final verification. - -### Task 5: Final Verification for issue-to-pr Handoff - -**Files:** -- Review all files changed above - -**Step 1: Run the repo checks required before claiming success** - -Run: - -```bash -make test -make clippy -``` - -If `make test` is too broad to diagnose quickly, first run the targeted tests above, then rerun the full command. - -**Step 2: Run the repo-local completeness review** - -After Batch 2 is green, invoke the repo-local skill: - -```text -/review-implementation model UndirectedTwoCommodityIntegralFlow -``` - -Auto-fix any concrete findings before handing control back to `issue-to-pr`. - -**Step 3: Capture the implementation summary for the PR comment** - -Prepare a short summary covering: - -- new model file + configuration encoding -- CLI flags/support added -- canonical example/fixture updates -- paper entry and bibliography additions -- any deliberate deviations from the issue (expected: none, unless the paper example uses Instance 3 instead of Instance 1 for tractability) - -**Step 4: Leave the tree ready for issue-to-pr** - -Do not remove the plan file manually here; `issue-to-pr` handles: - -- the implementation commit -- review-fix commit(s) -- plan-file removal commit -- push and PR comment From 6cc5ade6e69f103950dc99e036e202468c31c498 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Mon, 16 Mar 2026 17:46:01 +0800 Subject: [PATCH 5/8] fix: address PR #658 review follow-ups --- README.md | 2 +- docs/src/cli.md | 3 +- .../test-feature-20260316-174335.md | 47 ++++++ problemreductions-cli/src/commands/create.rs | 2 +- problemreductions-cli/tests/cli_tests.rs | 155 ++++++++++++++++++ src/example_db/fixtures/examples.json | 2 - src/models/graph/mod.rs | 2 +- .../undirected_two_commodity_integral_flow.rs | 16 +- src/models/mod.rs | 5 +- src/registry/mod.rs | 3 +- src/registry/schema.rs | 21 +++ src/rules/graph.rs | 5 +- .../undirected_two_commodity_integral_flow.rs | 38 ++++- 13 files changed, 278 insertions(+), 23 deletions(-) create mode 100644 docs/test-reports/test-feature-20260316-174335.md diff --git a/README.md b/README.md index 94d4cb1f..e50235ce 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ Or build from source: ```bash git clone https://github.com/CodingThrust/problem-reductions cd problem-reductions -make cli # builds target/release/pred +make cli # installs ~/.cargo/bin/pred from the local checkout ``` See the [Getting Started](https://codingthrust.github.io/problem-reductions/getting-started.html) guide for usage examples, the reduction workflow, and [CLI usage](https://codingthrust.github.io/problem-reductions/cli.html). diff --git a/docs/src/cli.md b/docs/src/cli.md index 8166cfd6..34e0d0d4 100644 --- a/docs/src/cli.md +++ b/docs/src/cli.md @@ -15,7 +15,7 @@ Or build from source: ```bash git clone https://github.com/CodingThrust/problem-reductions cd problem-reductions -make cli # builds target/release/pred +make cli # installs ~/.cargo/bin/pred from the local checkout ``` Verify the installation: @@ -276,6 +276,7 @@ pred create KColoring --k 3 --graph 0-1,1-2,2-0 -o kcol.json pred create SpinGlass --graph 0-1,1-2 -o sg.json pred create MaxCut --graph 0-1,1-2,2-0 -o maxcut.json 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 +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 pred create Factoring --target 15 --bits-m 4 --bits-n 4 -o factoring.json pred create Factoring --target 21 --bits-m 3 --bits-n 3 -o factoring2.json 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" -o x3c.json diff --git a/docs/test-reports/test-feature-20260316-174335.md b/docs/test-reports/test-feature-20260316-174335.md new file mode 100644 index 00000000..c291428d --- /dev/null +++ b/docs/test-reports/test-feature-20260316-174335.md @@ -0,0 +1,47 @@ +# Feature Test Report: problem-reductions + +**Date:** 2026-03-16 17:43:35 +0800 +**Project type:** CLI + library +**Features tested:** UndirectedTwoCommodityIntegralFlow via `pred` +**Profile:** ephemeral +**Use Case:** A cautious CLI user wants to find this model from docs/help alone, create the canonical example, inspect its metadata, and solve it with brute force. +**Expected Outcome:** The user can discover the model, understand the required flags, create a valid instance, inspect it including meaningful size fields, and solve it successfully without confusing or inaccurate docs. +**Verdict:** pass +**Critical Issues:** 0 + +## Summary + +| Feature | Discoverable | Setup | Works | Expected Outcome Met | Doc Quality | +|---------|-------------|-------|-------|---------------------|-------------| +| UndirectedTwoCommodityIntegralFlow via `pred` | yes | yes | yes | yes | good | + +## Per-Feature Details + +### UndirectedTwoCommodityIntegralFlow via `pred` +- **Role:** Cautious CLI user relying on repo docs and built-in help. +- **Use Case:** Discover the new model, create the canonical example, inspect it, and solve it locally. +- **What they tried:** Read [README.md](/Users/jinguomini/rcode/problem-reductions/.worktrees/review-pr-658-issue-296-undirected-two-commodity-integral-flow/README.md) and [docs/src/cli.md](/Users/jinguomini/rcode/problem-reductions/.worktrees/review-pr-658-issue-296-undirected-two-commodity-integral-flow/docs/src/cli.md), then used `target/debug/pred list`, `target/debug/pred show UndirectedTwoCommodityIntegralFlow`, `target/debug/pred create --example UndirectedTwoCommodityIntegralFlow`, `target/debug/pred inspect -`, and `target/debug/pred solve - --solver brute-force`. +- **Discoverability:** The model is visible in `pred list`, documented in `pred show`, and the static CLI docs now include a concrete `pred create UndirectedTwoCommodityIntegralFlow ...` example. +- **Setup:** The in-tree binary worked directly. The docs now describe `make cli` accurately as installing the CLI from the local checkout. +- **Functionality:** The canonical example creates successfully, `inspect` reports `num_edges` and `num_vertices`, and brute-force solve returns a satisfying solution. +- **Expected vs Actual Outcome:** Matched. The end-to-end CLI path works without hidden prerequisites or misleading help. +- **Blocked steps:** Did not run `make cli` during the test because it installs under `~/.cargo/bin`, which is outside the workspace-only safety scope. +- **Friction points:** None material after the fixes. +- **Doc suggestions:** None required for this feature path. + +## Expected vs Actual Outcome + +The expected outcome was achieved. The final branch supports a clean downstream path from discovery to creation, inspection, and solving for `UndirectedTwoCommodityIntegralFlow`. + +## Issues Found + +None in the final tested branch. An earlier pass found: +- inaccurate `make cli` wording in user docs +- missing model-specific `pred create` example in static CLI docs +- missing `size_fields` in `pred inspect` for this orphan model + +Those issues were fixed in the current worktree, and the final verification flow passed. + +## Suggestions + +- Keep adding explicit size-field registrations for future problem types that may exist before any reduction edges are added. diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index 80c9a0fc..06b63aac 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -237,7 +237,7 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str { "HamiltonianPath" => "--graph 0-1,1-2,2-3", "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" - } + }, "IsomorphicSpanningTree" => "--graph 0-1,1-2,0-2 --tree 0-1,1-2", "MaxCut" | "MaximumMatching" | "TravelingSalesman" => { "--graph 0-1,1-2,2-3 --edge-weights 1,1,1" diff --git a/problemreductions-cli/tests/cli_tests.rs b/problemreductions-cli/tests/cli_tests.rs index 5d6361c9..ea484b8a 100644 --- a/problemreductions-cli/tests/cli_tests.rs +++ b/problemreductions-cli/tests/cli_tests.rs @@ -382,6 +382,102 @@ fn test_create_undirected_two_commodity_integral_flow_missing_capacities_shows_u assert!(stderr.contains("Usage: pred create UndirectedTwoCommodityIntegralFlow")); } +#[test] +fn test_create_undirected_two_commodity_integral_flow_rejects_invalid_capacity_token() { + let output = pred() + .args([ + "create", + "UndirectedTwoCommodityIntegralFlow", + "--graph", + "0-2,1-2,2-3", + "--capacities", + "1,x,2", + "--source-1", + "0", + "--sink-1", + "3", + "--source-2", + "1", + "--sink-2", + "3", + "--requirement-1", + "1", + "--requirement-2", + "1", + ]) + .output() + .unwrap(); + assert!(!output.status.success()); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("Invalid capacity `x`")); + assert!(stderr.contains("Usage: pred create UndirectedTwoCommodityIntegralFlow")); +} + +#[test] +fn test_create_undirected_two_commodity_integral_flow_rejects_wrong_capacity_count() { + let output = pred() + .args([ + "create", + "UndirectedTwoCommodityIntegralFlow", + "--graph", + "0-2,1-2,2-3", + "--capacities", + "1,2", + "--source-1", + "0", + "--sink-1", + "3", + "--source-2", + "1", + "--sink-2", + "3", + "--requirement-1", + "1", + "--requirement-2", + "1", + ]) + .output() + .unwrap(); + assert!(!output.status.success()); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("Expected 3 capacities but got 2")); + assert!(stderr.contains("Usage: pred create UndirectedTwoCommodityIntegralFlow")); +} + +#[test] +fn test_create_undirected_two_commodity_integral_flow_rejects_oversized_capacity() { + let oversized = ((usize::MAX as u128) + 1).to_string(); + let capacities = format!("1,1,{oversized}"); + let output = pred() + .args([ + "create", + "UndirectedTwoCommodityIntegralFlow", + "--graph", + "0-2,1-2,2-3", + "--capacities", + capacities.as_str(), + "--source-1", + "0", + "--sink-1", + "3", + "--source-2", + "1", + "--sink-2", + "3", + "--requirement-1", + "1", + "--requirement-2", + "1", + ]) + .output() + .unwrap(); + assert!(!output.status.success()); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("is too large for this platform")); + assert!(stderr.contains("edge index 2")); + assert!(stderr.contains("Usage: pred create UndirectedTwoCommodityIntegralFlow")); +} + #[test] fn test_create_undirected_two_commodity_integral_flow_rejects_out_of_range_terminal() { let output = pred() @@ -2762,6 +2858,65 @@ fn test_inspect_json_output() { std::fs::remove_file(&result_file).ok(); } +#[test] +fn test_inspect_undirected_two_commodity_integral_flow_reports_size_fields() { + let problem_file = std::env::temp_dir().join("pred_test_utcif_inspect_in.json"); + let result_file = std::env::temp_dir().join("pred_test_utcif_inspect_out.json"); + let create_out = pred() + .args([ + "-o", + problem_file.to_str().unwrap(), + "create", + "--example", + "UndirectedTwoCommodityIntegralFlow", + ]) + .output() + .unwrap(); + assert!( + create_out.status.success(), + "stderr: {}", + String::from_utf8_lossy(&create_out.stderr) + ); + + let output = pred() + .args([ + "-o", + result_file.to_str().unwrap(), + "inspect", + problem_file.to_str().unwrap(), + ]) + .output() + .unwrap(); + assert!( + output.status.success(), + "stderr: {}", + String::from_utf8_lossy(&output.stderr) + ); + assert!(result_file.exists()); + + let content = std::fs::read_to_string(&result_file).unwrap(); + let json: serde_json::Value = serde_json::from_str(&content).unwrap(); + let size_fields: Vec<&str> = json["size_fields"] + .as_array() + .expect("size_fields should be an array") + .iter() + .map(|v| v.as_str().unwrap()) + .collect(); + assert!( + size_fields.contains(&"num_vertices"), + "UndirectedTwoCommodityIntegralFlow size_fields should contain num_vertices, got: {:?}", + size_fields + ); + assert!( + size_fields.contains(&"num_edges"), + "UndirectedTwoCommodityIntegralFlow size_fields should contain num_edges, got: {:?}", + size_fields + ); + + std::fs::remove_file(&problem_file).ok(); + std::fs::remove_file(&result_file).ok(); +} + // ---- Random generation tests ---- #[test] diff --git a/src/example_db/fixtures/examples.json b/src/example_db/fixtures/examples.json index f3e65be5..1076d5a4 100644 --- a/src/example_db/fixtures/examples.json +++ b/src/example_db/fixtures/examples.json @@ -30,8 +30,6 @@ {"problem":"Satisfiability","variant":{},"instance":{"clauses":[{"literals":[1,2]},{"literals":[-1,3]},{"literals":[-2,-3]}],"num_vars":3},"samples":[{"config":[1,0,1],"metric":true}],"optimal":[{"config":[0,1,0],"metric":true},{"config":[1,0,1],"metric":true}]}, {"problem":"ShortestCommonSupersequence","variant":{},"instance":{"alphabet_size":3,"bound":4,"strings":[[0,1,2],[1,0,2]]},"samples":[{"config":[1,0,1,2],"metric":true}],"optimal":[{"config":[0,1,0,2],"metric":true},{"config":[1,0,1,2],"metric":true}]}, {"problem":"SpinGlass","variant":{"graph":"SimpleGraph","weight":"i32"},"instance":{"couplings":[1,1,1,1,1,1,1],"fields":[0,0,0,0,0],"graph":{"inner":{"edge_property":"undirected","edges":[[0,1,null],[1,2,null],[3,4,null],[0,3,null],[1,3,null],[1,4,null],[2,4,null]],"node_holes":[],"nodes":[null,null,null,null,null]}}},"samples":[{"config":[1,0,1,1,0],"metric":{"Valid":-3}}],"optimal":[{"config":[0,0,1,1,0],"metric":{"Valid":-3}},{"config":[0,1,0,0,1],"metric":{"Valid":-3}},{"config":[0,1,0,1,0],"metric":{"Valid":-3}},{"config":[0,1,1,1,0],"metric":{"Valid":-3}},{"config":[1,0,0,0,1],"metric":{"Valid":-3}},{"config":[1,0,1,0,1],"metric":{"Valid":-3}},{"config":[1,0,1,1,0],"metric":{"Valid":-3}},{"config":[1,1,0,0,1],"metric":{"Valid":-3}}]}, - {"problem":"TravelingSalesman","variant":{"graph":"SimpleGraph","weight":"i32"},"instance":{"edge_weights":[1,3,2,2,3,1],"graph":{"inner":{"edge_property":"undirected","edges":[[0,1,null],[0,2,null],[0,3,null],[1,2,null],[1,3,null],[2,3,null]],"node_holes":[],"nodes":[null,null,null,null]}}},"samples":[{"config":[1,0,1,1,0,1],"metric":{"Valid":6}}],"optimal":[{"config":[1,0,1,1,0,1],"metric":{"Valid":6}}]}, - {"problem":"UndirectedTwoCommodityIntegralFlow","variant":{},"instance":{"capacities":[1,1,2],"graph":{"inner":{"edge_property":"undirected","edges":[[0,2,null],[1,2,null],[2,3,null]],"node_holes":[],"nodes":[null,null,null,null]}},"requirement_1":1,"requirement_2":1,"sink_1":3,"sink_2":3,"source_1":0,"source_2":1},"samples":[{"config":[1,0,0,0,0,0,1,0,1,0,1,0],"metric":true}],"optimal":[{"config":[0,0,1,0,1,0,0,0,1,0,1,0],"metric":true},{"config":[1,0,0,0,0,0,1,0,1,0,1,0],"metric":true}]} {"problem":"SteinerTree","variant":{"graph":"SimpleGraph","weight":"i32"},"instance":{"edge_weights":[2,5,2,1,5,6,1],"graph":{"inner":{"edge_property":"undirected","edges":[[0,1,null],[0,3,null],[1,2,null],[1,3,null],[2,3,null],[2,4,null],[3,4,null]],"node_holes":[],"nodes":[null,null,null,null,null]}},"terminals":[0,2,4]},"samples":[{"config":[1,0,1,1,0,0,1],"metric":{"Valid":6}}],"optimal":[{"config":[1,0,1,1,0,0,1],"metric":{"Valid":6}}]}, {"problem":"TravelingSalesman","variant":{"graph":"SimpleGraph","weight":"i32"},"instance":{"edge_weights":[1,3,2,2,3,1],"graph":{"inner":{"edge_property":"undirected","edges":[[0,1,null],[0,2,null],[0,3,null],[1,2,null],[1,3,null],[2,3,null]],"node_holes":[],"nodes":[null,null,null,null]}}},"samples":[{"config":[1,0,1,1,0,1],"metric":{"Valid":6}}],"optimal":[{"config":[1,0,1,1,0,1],"metric":{"Valid":6}}]}, {"problem":"UndirectedTwoCommodityIntegralFlow","variant":{},"instance":{"capacities":[1,1,2],"graph":{"inner":{"edge_property":"undirected","edges":[[0,2,null],[1,2,null],[2,3,null]],"node_holes":[],"nodes":[null,null,null,null]}},"requirement_1":1,"requirement_2":1,"sink_1":3,"sink_2":3,"source_1":0,"source_2":1},"samples":[{"config":[1,0,0,0,0,0,1,0,1,0,1,0],"metric":true}],"optimal":[{"config":[0,0,1,0,1,0,0,0,1,0,1,0],"metric":true},{"config":[1,0,0,0,0,0,1,0,1,0,1,0],"metric":true}]} diff --git a/src/models/graph/mod.rs b/src/models/graph/mod.rs index 7088ff14..64a7e939 100644 --- a/src/models/graph/mod.rs +++ b/src/models/graph/mod.rs @@ -21,6 +21,7 @@ //! - [`MinimumFeedbackArcSet`]: Minimum feedback arc set on directed graphs //! - [`MinimumSumMulticenter`]: Min-sum multicenter (p-median) //! - [`RuralPostman`]: Rural Postman (circuit covering required edges) +//! - [`SteinerTree`]: Minimum-weight tree spanning all required terminals //! - [`SubgraphIsomorphism`]: Subgraph isomorphism (decision problem) //! - [`UndirectedTwoCommodityIntegralFlow`]: Two-commodity integral flow on undirected graphs @@ -91,7 +92,6 @@ pub(crate) fn canonical_model_example_specs() -> Vec Option { + fn commodity_balance(&self, config: &[usize], commodity: usize, vertex: usize) -> Option { let mut balance = 0i128; for (edge_index, (u, v)) in self.graph.edges().into_iter().enumerate() { let flows = self.edge_flows(config, edge_index)?; diff --git a/src/models/mod.rs b/src/models/mod.rs index e45e17b2..89c89238 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -15,9 +15,8 @@ pub use graph::{ BicliqueCover, GraphPartitioning, HamiltonianPath, IsomorphicSpanningTree, KColoring, MaxCut, MaximalIS, MaximumClique, MaximumIndependentSet, MaximumMatching, MinimumDominatingSet, MinimumFeedbackArcSet, MinimumFeedbackVertexSet, MinimumSumMulticenter, MinimumVertexCover, - OptimalLinearArrangement, PartitionIntoTriangles, RuralPostman, SpinGlass, SubgraphIsomorphism, - TravelingSalesman, UndirectedTwoCommodityIntegralFlow, - SteinerTree, SubgraphIsomorphism, TravelingSalesman, UndirectedTwoCommodityIntegralFlow, + OptimalLinearArrangement, PartitionIntoTriangles, RuralPostman, SpinGlass, SteinerTree, + SubgraphIsomorphism, TravelingSalesman, UndirectedTwoCommodityIntegralFlow, }; pub use misc::{ BinPacking, Factoring, FlowShopScheduling, Knapsack, LongestCommonSubsequence, diff --git a/src/registry/mod.rs b/src/registry/mod.rs index 9732f97b..d7260972 100644 --- a/src/registry/mod.rs +++ b/src/registry/mod.rs @@ -56,7 +56,8 @@ pub use info::{ComplexityClass, FieldInfo, ProblemInfo, ProblemMetadata}; pub use problem_ref::{parse_catalog_problem_ref, require_graph_variant, ProblemRef}; pub use problem_type::{find_problem_type, find_problem_type_by_alias, problem_types, ProblemType}; pub use schema::{ - collect_schemas, FieldInfoJson, ProblemSchemaEntry, ProblemSchemaJson, VariantDimension, + collect_schemas, declared_size_fields, FieldInfoJson, ProblemSchemaEntry, ProblemSchemaJson, + ProblemSizeFieldEntry, VariantDimension, }; pub use variant::{find_variant_entry, VariantEntry}; diff --git a/src/registry/schema.rs b/src/registry/schema.rs index d78cebfa..00f20291 100644 --- a/src/registry/schema.rs +++ b/src/registry/schema.rs @@ -52,6 +52,19 @@ pub struct ProblemSchemaEntry { inventory::collect!(ProblemSchemaEntry); +/// Optional static size-field metadata for problem types. +/// +/// This is used when a problem has meaningful size fields even before it +/// participates in any reduction overhead expressions. +pub struct ProblemSizeFieldEntry { + /// Problem name (e.g., "MaximumIndependentSet"). + pub name: &'static str, + /// Size field names (e.g., `&["num_vertices", "num_edges"]`). + pub fields: &'static [&'static str], +} + +inventory::collect!(ProblemSizeFieldEntry); + /// JSON-serializable problem schema. #[derive(Debug, Clone, Serialize)] pub struct ProblemSchemaJson { @@ -96,6 +109,14 @@ pub fn collect_schemas() -> Vec { schemas } +/// Collect explicitly declared size fields for a problem type. +pub fn declared_size_fields(name: &str) -> Vec<&'static str> { + inventory::iter::() + .filter(|entry| entry.name == name) + .flat_map(|entry| entry.fields.iter().copied()) + .collect() +} + #[cfg(test)] #[path = "../unit_tests/registry/schema.rs"] mod tests; diff --git a/src/rules/graph.rs b/src/rules/graph.rs index dc54d8b2..fff45b23 100644 --- a/src/rules/graph.rs +++ b/src/rules/graph.rs @@ -741,7 +741,10 @@ impl ReductionGraph { /// source, its size fields are the input variables referenced in the overhead /// expressions. When it's a target, its size fields are the output field names. pub fn size_field_names(&self, name: &str) -> Vec<&'static str> { - let mut fields = std::collections::HashSet::new(); + let mut fields: std::collections::HashSet<&'static str> = + crate::registry::declared_size_fields(name) + .into_iter() + .collect(); for entry in inventory::iter:: { if entry.source_name == name { // Source's size fields are the input variables of the overhead. diff --git a/src/unit_tests/models/graph/undirected_two_commodity_integral_flow.rs b/src/unit_tests/models/graph/undirected_two_commodity_integral_flow.rs index c574420f..ddba68d8 100644 --- a/src/unit_tests/models/graph/undirected_two_commodity_integral_flow.rs +++ b/src/unit_tests/models/graph/undirected_two_commodity_integral_flow.rs @@ -3,7 +3,7 @@ use crate::solvers::{BruteForce, Solver}; use crate::topology::{Graph, SimpleGraph}; use crate::traits::Problem; -fn even_capacity_instance() -> UndirectedTwoCommodityIntegralFlow { +fn capacity_two_bottleneck_instance() -> UndirectedTwoCommodityIntegralFlow { UndirectedTwoCommodityIntegralFlow::new( SimpleGraph::new(4, vec![(0, 2), (1, 2), (2, 3)]), vec![1, 1, 2], @@ -39,7 +39,7 @@ fn example_config() -> Vec { #[test] fn test_undirected_two_commodity_integral_flow_creation() { - let problem = even_capacity_instance(); + let problem = capacity_two_bottleneck_instance(); assert_eq!(problem.graph().num_vertices(), 4); assert_eq!(problem.graph().num_edges(), 3); assert_eq!(problem.capacities(), &[1, 1, 2]); @@ -56,7 +56,7 @@ fn test_undirected_two_commodity_integral_flow_creation() { #[test] fn test_undirected_two_commodity_integral_flow_evaluation_yes() { - let problem = even_capacity_instance(); + let problem = capacity_two_bottleneck_instance(); assert!(problem.evaluate(&example_config())); assert!(problem.is_valid_solution(&example_config())); } @@ -69,9 +69,37 @@ fn test_undirected_two_commodity_integral_flow_evaluation_no_shared_bottleneck() assert!(BruteForce::new().find_satisfying(&problem).is_none()); } +#[test] +fn test_undirected_two_commodity_integral_flow_rejects_wrong_config_length() { + let problem = capacity_two_bottleneck_instance(); + let mut config = example_config(); + config.pop(); + + assert!(!problem.evaluate(&config)); +} + +#[test] +fn test_undirected_two_commodity_integral_flow_rejects_value_above_capacity_domain() { + let problem = capacity_two_bottleneck_instance(); + let mut config = example_config(); + config[8] = 3; + + assert!(!problem.evaluate(&config)); +} + +#[test] +fn test_undirected_two_commodity_integral_flow_rejects_antisymmetry_violation() { + let problem = capacity_two_bottleneck_instance(); + let mut config = example_config(); + config[0] = 1; + config[1] = 1; + + assert!(!problem.evaluate(&config)); +} + #[test] fn test_undirected_two_commodity_integral_flow_serialization() { - let problem = even_capacity_instance(); + let problem = capacity_two_bottleneck_instance(); let value = serde_json::to_value(&problem).unwrap(); let deserialized: UndirectedTwoCommodityIntegralFlow = serde_json::from_value(value).unwrap(); assert_eq!(deserialized.graph(), problem.graph()); @@ -86,7 +114,7 @@ fn test_undirected_two_commodity_integral_flow_serialization() { #[test] fn test_undirected_two_commodity_integral_flow_paper_example() { - let problem = even_capacity_instance(); + let problem = capacity_two_bottleneck_instance(); let config = example_config(); assert!(problem.evaluate(&config)); From 8b56a2cd330ead90f3fa8ddd8ace3f126505f651 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Mon, 16 Mar 2026 17:47:35 +0800 Subject: [PATCH 6/8] fix: address PR #658 review follow-ups --- .../test-feature-20260316-174335.md | 46 +++++++++---------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/docs/test-reports/test-feature-20260316-174335.md b/docs/test-reports/test-feature-20260316-174335.md index c291428d..9dbc516f 100644 --- a/docs/test-reports/test-feature-20260316-174335.md +++ b/docs/test-reports/test-feature-20260316-174335.md @@ -1,11 +1,11 @@ # Feature Test Report: problem-reductions **Date:** 2026-03-16 17:43:35 +0800 -**Project type:** CLI + library -**Features tested:** UndirectedTwoCommodityIntegralFlow via `pred` +**Project type:** CLI + Rust library +**Features tested:** `UndirectedTwoCommodityIntegralFlow` **Profile:** ephemeral -**Use Case:** A cautious CLI user wants to find this model from docs/help alone, create the canonical example, inspect its metadata, and solve it with brute force. -**Expected Outcome:** The user can discover the model, understand the required flags, create a valid instance, inspect it including meaningful size fields, and solve it successfully without confusing or inaccurate docs. +**Use Case:** A cautious CLI user wants to discover the model, create its canonical/example instance, inspect its metadata, and solve it from the current workspace. +**Expected Outcome:** The user can find `UndirectedTwoCommodityIntegralFlow` from docs/help alone, understand the required flags, create a valid instance, inspect meaningful size fields, and solve it successfully with the documented CLI workflow. **Verdict:** pass **Critical Issues:** 0 @@ -13,35 +13,35 @@ | Feature | Discoverable | Setup | Works | Expected Outcome Met | Doc Quality | |---------|-------------|-------|-------|---------------------|-------------| -| UndirectedTwoCommodityIntegralFlow via `pred` | yes | yes | yes | yes | good | +| `UndirectedTwoCommodityIntegralFlow` | yes | yes | yes | yes | good | ## Per-Feature Details -### UndirectedTwoCommodityIntegralFlow via `pred` -- **Role:** Cautious CLI user relying on repo docs and built-in help. -- **Use Case:** Discover the new model, create the canonical example, inspect it, and solve it locally. -- **What they tried:** Read [README.md](/Users/jinguomini/rcode/problem-reductions/.worktrees/review-pr-658-issue-296-undirected-two-commodity-integral-flow/README.md) and [docs/src/cli.md](/Users/jinguomini/rcode/problem-reductions/.worktrees/review-pr-658-issue-296-undirected-two-commodity-integral-flow/docs/src/cli.md), then used `target/debug/pred list`, `target/debug/pred show UndirectedTwoCommodityIntegralFlow`, `target/debug/pred create --example UndirectedTwoCommodityIntegralFlow`, `target/debug/pred inspect -`, and `target/debug/pred solve - --solver brute-force`. -- **Discoverability:** The model is visible in `pred list`, documented in `pred show`, and the static CLI docs now include a concrete `pred create UndirectedTwoCommodityIntegralFlow ...` example. -- **Setup:** The in-tree binary worked directly. The docs now describe `make cli` accurately as installing the CLI from the local checkout. -- **Functionality:** The canonical example creates successfully, `inspect` reports `num_edges` and `num_vertices`, and brute-force solve returns a satisfying solution. -- **Expected vs Actual Outcome:** Matched. The end-to-end CLI path works without hidden prerequisites or misleading help. -- **Blocked steps:** Did not run `make cli` during the test because it installs under `~/.cargo/bin`, which is outside the workspace-only safety scope. -- **Friction points:** None material after the fixes. -- **Doc suggestions:** None required for this feature path. +### `UndirectedTwoCommodityIntegralFlow` +- **Role:** Cautious CLI user +- **Use Case:** Discover the model, create a valid instance, inspect its metadata, and solve it with brute force. +- **What they tried:** Read `README.md` and `docs/src/cli.md`, used `target/debug/pred list`, `target/debug/pred show UndirectedTwoCommodityIntegralFlow`, `target/debug/pred create --example UndirectedTwoCommodityIntegralFlow | target/debug/pred inspect -`, and `target/debug/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 | target/debug/pred solve - --solver brute-force`. +- **Discoverability:** The model is listed by `pred list`, `pred show` exposes the required fields, and the static CLI docs now include a concrete `pred create UndirectedTwoCommodityIntegralFlow ...` example. +- **Setup:** The documented `make cli` wording now matches the actual Make target behavior: it installs `pred` from the local checkout rather than producing `target/release/pred`. +- **Functionality:** The canonical example path works, the explicit create/solve path works, and `pred inspect` now reports meaningful `size_fields` for this model. +- **Expected vs Actual Outcome:** Outcome matched expectations on the current worktree. +- **Blocked steps:** None in the workspace-only flow. +- **Friction points:** None remained after the review fixes. +- **Doc suggestions:** None beyond the applied fixes. ## Expected vs Actual Outcome -The expected outcome was achieved. The final branch supports a clean downstream path from discovery to creation, inspection, and solving for `UndirectedTwoCommodityIntegralFlow`. +The expected user path is fully supported on the current worktree. The model is discoverable from both static docs and CLI help, example creation works, explicit creation works, `inspect` exposes `num_vertices` and `num_edges`, and brute-force solve succeeds. ## Issues Found -None in the final tested branch. An earlier pass found: -- inaccurate `make cli` wording in user docs -- missing model-specific `pred create` example in static CLI docs -- missing `size_fields` in `pred inspect` for this orphan model +None remaining in the current worktree. -Those issues were fixed in the current worktree, and the final verification flow passed. +The initial agentic feature pass found three user-facing issues, all of which are fixed here: +- inaccurate `make cli` wording in `README.md` and `docs/src/cli.md` +- missing model-specific `pred create UndirectedTwoCommodityIntegralFlow ...` example in `docs/src/cli.md` +- empty `size_fields` in `pred inspect` for this model because it had no reduction-derived size metadata yet ## Suggestions -- Keep adding explicit size-field registrations for future problem types that may exist before any reduction edges are added. +- Reuse the explicit size-field registry path for future orphan models that have meaningful size metrics before any reductions are added. From f7ef9661a6d5766b88689e54fbc82de542427526 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Mon, 16 Mar 2026 17:51:58 +0800 Subject: [PATCH 7/8] fix: finalize PR #658 feature-test follow-ups --- .../test-feature-20260316-174335.md | 47 +++++++++---------- problemreductions-cli/tests/cli_tests.rs | 4 +- 2 files changed, 25 insertions(+), 26 deletions(-) diff --git a/docs/test-reports/test-feature-20260316-174335.md b/docs/test-reports/test-feature-20260316-174335.md index 9dbc516f..33e501a4 100644 --- a/docs/test-reports/test-feature-20260316-174335.md +++ b/docs/test-reports/test-feature-20260316-174335.md @@ -1,11 +1,11 @@ # Feature Test Report: problem-reductions -**Date:** 2026-03-16 17:43:35 +0800 +**Date:** 2026-03-16 17:43:35 **Project type:** CLI + Rust library -**Features tested:** `UndirectedTwoCommodityIntegralFlow` +**Features tested:** UndirectedTwoCommodityIntegralFlow **Profile:** ephemeral -**Use Case:** A cautious CLI user wants to discover the model, create its canonical/example instance, inspect its metadata, and solve it from the current workspace. -**Expected Outcome:** The user can find `UndirectedTwoCommodityIntegralFlow` from docs/help alone, understand the required flags, create a valid instance, inspect meaningful size fields, and solve it successfully with the documented CLI workflow. +**Use Case:** Discover the model from docs/help alone, create a valid instance, inspect it, and solve it with brute force. +**Expected Outcome:** A downstream CLI user can find `UndirectedTwoCommodityIntegralFlow`, understand the required flags, create the canonical example, inspect meaningful size fields, and solve it successfully. **Verdict:** pass **Critical Issues:** 0 @@ -13,35 +13,34 @@ | Feature | Discoverable | Setup | Works | Expected Outcome Met | Doc Quality | |---------|-------------|-------|-------|---------------------|-------------| -| `UndirectedTwoCommodityIntegralFlow` | yes | yes | yes | yes | good | +| UndirectedTwoCommodityIntegralFlow | yes | yes | yes | yes | good | ## Per-Feature Details -### `UndirectedTwoCommodityIntegralFlow` -- **Role:** Cautious CLI user -- **Use Case:** Discover the model, create a valid instance, inspect its metadata, and solve it with brute force. -- **What they tried:** Read `README.md` and `docs/src/cli.md`, used `target/debug/pred list`, `target/debug/pred show UndirectedTwoCommodityIntegralFlow`, `target/debug/pred create --example UndirectedTwoCommodityIntegralFlow | target/debug/pred inspect -`, and `target/debug/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 | target/debug/pred solve - --solver brute-force`. -- **Discoverability:** The model is listed by `pred list`, `pred show` exposes the required fields, and the static CLI docs now include a concrete `pred create UndirectedTwoCommodityIntegralFlow ...` example. -- **Setup:** The documented `make cli` wording now matches the actual Make target behavior: it installs `pred` from the local checkout rather than producing `target/release/pred`. -- **Functionality:** The canonical example path works, the explicit create/solve path works, and `pred inspect` now reports meaningful `size_fields` for this model. -- **Expected vs Actual Outcome:** Outcome matched expectations on the current worktree. -- **Blocked steps:** None in the workspace-only flow. -- **Friction points:** None remained after the review fixes. -- **Doc suggestions:** None beyond the applied fixes. +### UndirectedTwoCommodityIntegralFlow +- **Role:** Cautious CLI user working from repo docs and command help. +- **Use Case:** Find the model, create the canonical example, inspect its metadata, and solve it. +- **What they tried:** Read [README.md](../../README.md) and [docs/src/cli.md](../src/cli.md), then ran `pred list`, `pred show UndirectedTwoCommodityIntegralFlow`, `pred create --example UndirectedTwoCommodityIntegralFlow | pred inspect -`, and the explicit `pred create ... | pred solve - --solver brute-force` flow. +- **Discoverability:** Good. `pred list` shows the model and complexity, `pred show` shows the schema, and the CLI docs now include a concrete `pred create UndirectedTwoCommodityIntegralFlow ...` example. +- **Setup:** Good. The in-tree `target/debug/pred` binary worked, and the docs now correctly describe `make cli` as installing `~/.cargo/bin/pred` from the local checkout. +- **Functionality:** Good. The canonical example path works, the explicit create-and-solve path works, and `pred inspect` now reports `size_fields: ["num_edges", "num_vertices"]`. +- **Expected vs Actual Outcome:** Matched. The full downstream path now works without missing metadata or inaccurate setup guidance. +- **Blocked steps:** No user-facing feature steps were blocked. Two follow-up automated re-test subagents timed out without returning findings, so the exact downstream commands were rerun directly in the PR worktree for post-fix confirmation. +- **Friction points:** None after the fixes. +- **Doc suggestions:** None. ## Expected vs Actual Outcome -The expected user path is fully supported on the current worktree. The model is discoverable from both static docs and CLI help, example creation works, explicit creation works, `inspect` exposes `num_vertices` and `num_edges`, and brute-force solve succeeds. +The expected outcome was achieved. The feature is discoverable through both static docs and CLI help, the create flow is documented with a concrete example, `inspect` exposes meaningful size fields, and brute-force solving succeeds on the canonical instance. ## Issues Found -None remaining in the current worktree. - -The initial agentic feature pass found three user-facing issues, all of which are fixed here: -- inaccurate `make cli` wording in `README.md` and `docs/src/cli.md` -- missing model-specific `pred create UndirectedTwoCommodityIntegralFlow ...` example in `docs/src/cli.md` -- empty `size_fields` in `pred inspect` for this model because it had no reduction-derived size metadata yet +- None remaining. +- The first feature-test pass found three issues, and all were fixed on this branch: + - `fixed`: `make cli` docs incorrectly said it built `target/release/pred`; they now describe the actual local install behavior. + - `fixed`: static CLI docs lacked a model-specific `UndirectedTwoCommodityIntegralFlow` create example. + - `fixed`: `pred inspect` showed empty `size_fields` for this orphan model; it now reports `num_edges` and `num_vertices`. ## Suggestions -- Reuse the explicit size-field registry path for future orphan models that have meaningful size metrics before any reductions are added. +- None. diff --git a/problemreductions-cli/tests/cli_tests.rs b/problemreductions-cli/tests/cli_tests.rs index ea484b8a..b9d88e4f 100644 --- a/problemreductions-cli/tests/cli_tests.rs +++ b/problemreductions-cli/tests/cli_tests.rs @@ -473,8 +473,8 @@ fn test_create_undirected_two_commodity_integral_flow_rejects_oversized_capacity .unwrap(); assert!(!output.status.success()); let stderr = String::from_utf8_lossy(&output.stderr); - assert!(stderr.contains("is too large for this platform")); - assert!(stderr.contains("edge index 2")); + assert!(stderr.contains(format!("Invalid capacity `{oversized}`").as_str())); + assert!(stderr.contains("number too large to fit in target type")); assert!(stderr.contains("Usage: pred create UndirectedTwoCommodityIntegralFlow")); } From 03a37535a6aa6e872f65d8102ddc75fed9ccacec Mon Sep 17 00:00:00 2001 From: zazabap Date: Mon, 16 Mar 2026 13:14:13 +0000 Subject: [PATCH 8/8] 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) --- README.md | 2 +- .../test-feature-20260316-174335.md | 46 ---------- .../undirected_two_commodity_integral_flow.rs | 90 ++++++++++++++----- 3 files changed, 71 insertions(+), 67 deletions(-) delete mode 100644 docs/test-reports/test-feature-20260316-174335.md diff --git a/README.md b/README.md index e50235ce..94d4cb1f 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ Or build from source: ```bash git clone https://github.com/CodingThrust/problem-reductions cd problem-reductions -make cli # installs ~/.cargo/bin/pred from the local checkout +make cli # builds target/release/pred ``` See the [Getting Started](https://codingthrust.github.io/problem-reductions/getting-started.html) guide for usage examples, the reduction workflow, and [CLI usage](https://codingthrust.github.io/problem-reductions/cli.html). diff --git a/docs/test-reports/test-feature-20260316-174335.md b/docs/test-reports/test-feature-20260316-174335.md deleted file mode 100644 index 33e501a4..00000000 --- a/docs/test-reports/test-feature-20260316-174335.md +++ /dev/null @@ -1,46 +0,0 @@ -# Feature Test Report: problem-reductions - -**Date:** 2026-03-16 17:43:35 -**Project type:** CLI + Rust library -**Features tested:** UndirectedTwoCommodityIntegralFlow -**Profile:** ephemeral -**Use Case:** Discover the model from docs/help alone, create a valid instance, inspect it, and solve it with brute force. -**Expected Outcome:** A downstream CLI user can find `UndirectedTwoCommodityIntegralFlow`, understand the required flags, create the canonical example, inspect meaningful size fields, and solve it successfully. -**Verdict:** pass -**Critical Issues:** 0 - -## Summary - -| Feature | Discoverable | Setup | Works | Expected Outcome Met | Doc Quality | -|---------|-------------|-------|-------|---------------------|-------------| -| UndirectedTwoCommodityIntegralFlow | yes | yes | yes | yes | good | - -## Per-Feature Details - -### UndirectedTwoCommodityIntegralFlow -- **Role:** Cautious CLI user working from repo docs and command help. -- **Use Case:** Find the model, create the canonical example, inspect its metadata, and solve it. -- **What they tried:** Read [README.md](../../README.md) and [docs/src/cli.md](../src/cli.md), then ran `pred list`, `pred show UndirectedTwoCommodityIntegralFlow`, `pred create --example UndirectedTwoCommodityIntegralFlow | pred inspect -`, and the explicit `pred create ... | pred solve - --solver brute-force` flow. -- **Discoverability:** Good. `pred list` shows the model and complexity, `pred show` shows the schema, and the CLI docs now include a concrete `pred create UndirectedTwoCommodityIntegralFlow ...` example. -- **Setup:** Good. The in-tree `target/debug/pred` binary worked, and the docs now correctly describe `make cli` as installing `~/.cargo/bin/pred` from the local checkout. -- **Functionality:** Good. The canonical example path works, the explicit create-and-solve path works, and `pred inspect` now reports `size_fields: ["num_edges", "num_vertices"]`. -- **Expected vs Actual Outcome:** Matched. The full downstream path now works without missing metadata or inaccurate setup guidance. -- **Blocked steps:** No user-facing feature steps were blocked. Two follow-up automated re-test subagents timed out without returning findings, so the exact downstream commands were rerun directly in the PR worktree for post-fix confirmation. -- **Friction points:** None after the fixes. -- **Doc suggestions:** None. - -## Expected vs Actual Outcome - -The expected outcome was achieved. The feature is discoverable through both static docs and CLI help, the create flow is documented with a concrete example, `inspect` exposes meaningful size fields, and brute-force solving succeeds on the canonical instance. - -## Issues Found - -- None remaining. -- The first feature-test pass found three issues, and all were fixed on this branch: - - `fixed`: `make cli` docs incorrectly said it built `target/release/pred`; they now describe the actual local install behavior. - - `fixed`: static CLI docs lacked a model-specific `UndirectedTwoCommodityIntegralFlow` create example. - - `fixed`: `pred inspect` showed empty `size_fields` for this orphan model; it now reports `num_edges` and `num_vertices`. - -## Suggestions - -- None. diff --git a/src/unit_tests/models/graph/undirected_two_commodity_integral_flow.rs b/src/unit_tests/models/graph/undirected_two_commodity_integral_flow.rs index ddba68d8..e34f7d92 100644 --- a/src/unit_tests/models/graph/undirected_two_commodity_integral_flow.rs +++ b/src/unit_tests/models/graph/undirected_two_commodity_integral_flow.rs @@ -3,7 +3,7 @@ use crate::solvers::{BruteForce, Solver}; use crate::topology::{Graph, SimpleGraph}; use crate::traits::Problem; -fn capacity_two_bottleneck_instance() -> UndirectedTwoCommodityIntegralFlow { +fn canonical_instance() -> UndirectedTwoCommodityIntegralFlow { UndirectedTwoCommodityIntegralFlow::new( SimpleGraph::new(4, vec![(0, 2), (1, 2), (2, 3)]), vec![1, 1, 2], @@ -39,7 +39,7 @@ fn example_config() -> Vec { #[test] fn test_undirected_two_commodity_integral_flow_creation() { - let problem = capacity_two_bottleneck_instance(); + let problem = canonical_instance(); assert_eq!(problem.graph().num_vertices(), 4); assert_eq!(problem.graph().num_edges(), 3); assert_eq!(problem.capacities(), &[1, 1, 2]); @@ -56,7 +56,7 @@ fn test_undirected_two_commodity_integral_flow_creation() { #[test] fn test_undirected_two_commodity_integral_flow_evaluation_yes() { - let problem = capacity_two_bottleneck_instance(); + let problem = canonical_instance(); assert!(problem.evaluate(&example_config())); assert!(problem.is_valid_solution(&example_config())); } @@ -71,7 +71,7 @@ fn test_undirected_two_commodity_integral_flow_evaluation_no_shared_bottleneck() #[test] fn test_undirected_two_commodity_integral_flow_rejects_wrong_config_length() { - let problem = capacity_two_bottleneck_instance(); + let problem = canonical_instance(); let mut config = example_config(); config.pop(); @@ -80,7 +80,7 @@ fn test_undirected_two_commodity_integral_flow_rejects_wrong_config_length() { #[test] fn test_undirected_two_commodity_integral_flow_rejects_value_above_capacity_domain() { - let problem = capacity_two_bottleneck_instance(); + let problem = canonical_instance(); let mut config = example_config(); config[8] = 3; @@ -89,7 +89,7 @@ fn test_undirected_two_commodity_integral_flow_rejects_value_above_capacity_doma #[test] fn test_undirected_two_commodity_integral_flow_rejects_antisymmetry_violation() { - let problem = capacity_two_bottleneck_instance(); + let problem = canonical_instance(); let mut config = example_config(); config[0] = 1; config[1] = 1; @@ -99,7 +99,7 @@ fn test_undirected_two_commodity_integral_flow_rejects_antisymmetry_violation() #[test] fn test_undirected_two_commodity_integral_flow_serialization() { - let problem = capacity_two_bottleneck_instance(); + let problem = canonical_instance(); let value = serde_json::to_value(&problem).unwrap(); let deserialized: UndirectedTwoCommodityIntegralFlow = serde_json::from_value(value).unwrap(); assert_eq!(deserialized.graph(), problem.graph()); @@ -114,7 +114,7 @@ fn test_undirected_two_commodity_integral_flow_serialization() { #[test] fn test_undirected_two_commodity_integral_flow_paper_example() { - let problem = capacity_two_bottleneck_instance(); + let problem = canonical_instance(); let config = example_config(); assert!(problem.evaluate(&config)); @@ -125,38 +125,88 @@ fn test_undirected_two_commodity_integral_flow_paper_example() { #[test] fn test_undirected_two_commodity_integral_flow_large_capacity_sink_balance() { - let Ok(large) = usize::try_from(i64::MAX as u64 + 1) else { - return; - }; + // Use a moderately large capacity that fits in usize on all platforms. + let large: u64 = 1_000_000; + let large_usize = large as usize; let problem = UndirectedTwoCommodityIntegralFlow::new( SimpleGraph::new(2, vec![(0, 1)]), - vec![large as u64], + vec![large], 0, 1, 0, 1, - large as u64, + large, 0, ); - assert!(problem.evaluate(&[large, 0, 0, 0])); + assert!(problem.evaluate(&[large_usize, 0, 0, 0])); } #[test] -fn test_undirected_two_commodity_integral_flow_large_capacity_shared_overflow_is_invalid() { - let Ok(large) = usize::try_from(u64::MAX / 2 + 1) else { - return; - }; +fn test_undirected_two_commodity_integral_flow_shared_capacity_exceeded() { + // Two commodities each sending 2 units on an edge with capacity 3. let problem = UndirectedTwoCommodityIntegralFlow::new( SimpleGraph::new(2, vec![(0, 1)]), - vec![large as u64], + vec![3], 0, 1, 0, 1, + 2, + 2, + ); + + // f1(0->1)=2, f1(1->0)=0, f2(0->1)=2, f2(1->0)=0 => shared = 4 > 3 + assert!(!problem.evaluate(&[2, 0, 2, 0])); +} + +#[test] +#[should_panic(expected = "capacities length must match")] +fn test_undirected_two_commodity_integral_flow_panics_wrong_capacity_count() { + UndirectedTwoCommodityIntegralFlow::new( + SimpleGraph::new(3, vec![(0, 1), (1, 2)]), + vec![1], // 1 capacity but 2 edges + 0, + 2, + 0, + 2, + 1, + 1, + ); +} + +#[test] +#[should_panic(expected = "must be less than num_vertices")] +fn test_undirected_two_commodity_integral_flow_panics_vertex_out_of_bounds() { + UndirectedTwoCommodityIntegralFlow::new( + SimpleGraph::new(3, vec![(0, 1), (1, 2)]), + vec![1, 1], 0, + 5, // out of bounds 0, + 2, + 1, + 1, + ); +} + +#[test] +fn test_undirected_two_commodity_integral_flow_flow_conservation_violated() { + // 0 -- 1 -- 2, commodity 1: s=0 t=2, commodity 2: s=0 t=2 + let problem = UndirectedTwoCommodityIntegralFlow::new( + SimpleGraph::new(3, vec![(0, 1), (1, 2)]), + vec![2, 2], + 0, + 2, + 0, + 2, + 1, + 1, ); - assert!(!problem.evaluate(&[large, 0, large, 0])); + // Flow conservation violated at vertex 1: commodity 1 enters but doesn't leave. + // Edge (0,1): f1(0->1)=1, f1(1->0)=0, f2=0,0 + // Edge (1,2): f1(1->2)=0, f1(2->1)=0, f2=0,0 + // Vertex 1 gets +1 for commodity 1 from edge (0,1) but no outflow on edge (1,2) + assert!(!problem.evaluate(&[1, 0, 0, 0, 0, 0, 0, 0])); }