From a2471374f632e363adfad57741c34a2fdefea0f7 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Mon, 16 Mar 2026 06:46:39 +0800 Subject: [PATCH 1/5] Add plan for #249: [Model] KthBestSpanningTree --- .../2026-03-16-kth-best-spanning-tree.md | 219 ++++++++++++++++++ 1 file changed, 219 insertions(+) create mode 100644 docs/plans/2026-03-16-kth-best-spanning-tree.md diff --git a/docs/plans/2026-03-16-kth-best-spanning-tree.md b/docs/plans/2026-03-16-kth-best-spanning-tree.md new file mode 100644 index 000000000..e367e375c --- /dev/null +++ b/docs/plans/2026-03-16-kth-best-spanning-tree.md @@ -0,0 +1,219 @@ +# KthBestSpanningTree Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Add the `KthBestSpanningTree` satisfaction model, wire it into the registry/CLI/example database/paper, and implement the issue #249 example semantics. + +**Architecture:** Model `KthBestSpanningTree` as a `SimpleGraph` plus per-edge weights, a target multiplicity `k`, and a weight bound `B`. Each configuration is `k` consecutive binary edge-selection blocks of length `num_edges`; `evaluate()` accepts exactly those assignments where every block is a distinct spanning tree whose total weight is at most `B`. + +**Tech Stack:** Rust, serde, petgraph-backed `SimpleGraph`, registry macros (`inventory::submit!`, `declare_variants!`), CLI `pred create`, Typst paper. + +**Notes:** The issue comment thread already settled two design points that this plan follows: `dims()` uses `k * num_edges` binary variables, and the registry complexity uses the general-case bound `2^(num_edges * k)`. There is no open associated rule issue whose title mentions `KthBestSpanningTree`; carry that orphan-model warning into the PR summary instead of blocking execution. + +--- + +## Batch 1: Model, Registry, CLI, Tests + +### Task 1: Add failing model tests for the issue semantics + +**Files:** +- Create: `src/unit_tests/models/graph/kth_best_spanning_tree.rs` + +**Step 1: Write the failing test** + +Add focused tests that reference the new type before it exists: +- `test_kthbestspanningtree_creation` +- `test_kthbestspanningtree_evaluation_yes_instance` +- `test_kthbestspanningtree_evaluation_rejects_duplicate_trees` +- `test_kthbestspanningtree_evaluation_rejects_overweight_tree` +- `test_kthbestspanningtree_solver_yes_and_no_examples` +- `test_kthbestspanningtree_serialization` +- `test_kthbestspanningtree_paper_example` + +Use the issue’s two worked instances as the source of truth: +- YES instance: 5-vertex, 8-edge graph with `k = 3`, `bound = 12` +- NO instance: 4-vertex path with `k = 2`, `bound = 3` + +**Step 2: Run test to verify it fails** + +Run: `cargo test kthbestspanningtree --lib` +Expected: FAIL because `KthBestSpanningTree` does not exist yet. + +**Step 3: Write minimal implementation** + +Create `src/models/graph/kth_best_spanning_tree.rs` with: +- `ProblemSchemaEntry` for `graph = SimpleGraph`, `weight = i32` +- `#[derive(Debug, Clone, Serialize, Deserialize)] pub struct KthBestSpanningTree` +- Constructor `new(graph, weights, k, bound)` validating edge-weight count and positive `k` +- Getters `graph()`, `weights()`, `k()`, `bound()`, `num_vertices()`, `num_edges()` +- Helper predicates for one block: + - length/range check for binary edge choices + - exact `n - 1` selected-edge count + - selected-edge subgraph connectivity + - total selected weight `<= bound` +- Pairwise distinctness check across the `k` blocks +- `Problem` implementation with `dims() = vec![2; self.k * self.graph.num_edges()]` +- `variant() = crate::variant_params![W]` +- `impl SatisfactionProblem` +- `declare_variants! { default sat KthBestSpanningTree => "2^(num_edges * k)", }` +- `#[cfg(feature = "example-db")] canonical_model_example_specs()` using the issue YES instance +- test link at the bottom + +Prefer the `RuralPostman` pattern for `bound: W::Sum` rather than `bound: W`, and only register the `i32` variant because the issue requires integer edge weights. + +**Step 4: Run test to verify it passes** + +Run: `cargo test kthbestspanningtree --lib` +Expected: PASS for the new targeted model tests. + +**Step 5: Commit** + +Run: +```bash +git add src/models/graph/kth_best_spanning_tree.rs src/unit_tests/models/graph/kth_best_spanning_tree.rs +git commit -m "Add KthBestSpanningTree model" +``` + +### Task 2: Register the model across the crate and example-db + +**Files:** +- Modify: `src/models/graph/mod.rs` +- Modify: `src/models/mod.rs` +- Modify: `src/lib.rs` +- Modify: `src/unit_tests/trait_consistency.rs` + +**Step 1: Write the failing test** + +Add a trait-consistency entry for a tiny positive instance so the new model is checked by the shared trait suite. + +**Step 2: Run test to verify it fails** + +Run: `cargo test trait_consistency --lib` +Expected: FAIL before the new type is exported and referenced consistently. + +**Step 3: Write minimal implementation** + +Wire the type through the graph module, top-level model exports, and prelude exports. In `trait_consistency.rs`, add one `check_problem_trait(...)` call using a 3-vertex triangle with `k = 1`, plus any minimal imports needed. + +Also add the new example-db spec to `src/models/graph/mod.rs` so `build_model_examples()` includes the canonical example. + +**Step 4: Run test to verify it passes** + +Run: `cargo test trait_consistency --lib` +Expected: PASS with the new model included. + +**Step 5: Commit** + +Run: +```bash +git add src/models/graph/mod.rs src/models/mod.rs src/lib.rs src/unit_tests/trait_consistency.rs +git commit -m "Register KthBestSpanningTree across the crate" +``` + +### Task 3: Add CLI creation support for the new model + +**Files:** +- Modify: `problemreductions-cli/src/commands/create.rs` +- Modify: `problemreductions-cli/src/cli.rs` + +**Step 1: Write the failing test** + +Use a direct CLI smoke command as the behavior check: +- `pred create KthBestSpanningTree --graph 0-1,1-2,2-0 --edge-weights 1,2,3 --k 1 --bound 3` + +No dedicated Rust test file is required if the smoke command is the verification target for this task. + +**Step 2: Run test to verify it fails** + +Run: `cargo run -p problemreductions-cli -- create KthBestSpanningTree --graph 0-1,1-2,2-0 --edge-weights 1,2,3 --k 1 --bound 3` +Expected: FAIL with unknown problem or unsupported create-path errors before the CLI wiring exists. + +**Step 3: Write minimal implementation** + +Update CLI creation support by: +- Adding `KthBestSpanningTree` to the help text table in `problemreductions-cli/src/cli.rs` +- Adding an `example_for()` entry in `problemreductions-cli/src/commands/create.rs` +- Adding a `match` arm that parses: + - `--graph` + - `--edge-weights` + - `--k` + - `--bound` +- Constructing `KthBestSpanningTree::new(graph, edge_weights, k, bound)` + +Use the existing `RuralPostman` and weighted-graph parsing helpers as the template. + +**Step 4: Run test to verify it passes** + +Run: `cargo run -p problemreductions-cli -- create KthBestSpanningTree --graph 0-1,1-2,2-0 --edge-weights 1,2,3 --k 1 --bound 3` +Expected: PASS and emit valid problem JSON. + +**Step 5: Commit** + +Run: +```bash +git add problemreductions-cli/src/commands/create.rs problemreductions-cli/src/cli.rs +git commit -m "Add CLI support for KthBestSpanningTree" +``` + +## Batch 2: Paper Entry and Paper-Aligned Verification + +### Task 4: Add the paper entry and align the paper example test + +**Files:** +- Modify: `docs/paper/reductions.typ` +- Modify: `src/unit_tests/models/graph/kth_best_spanning_tree.rs` + +**Step 1: Write the failing test** + +Ensure `test_kthbestspanningtree_paper_example` uses the exact same instance and witness that the paper will show. If the current test uses the issue example already, tighten it so it also checks the claimed count of satisfying witnesses from brute force. + +**Step 2: Run test to verify it fails** + +Run: `cargo test test_kthbestspanningtree_paper_example --lib` +Expected: FAIL until the test and paper example agree on one concrete instance. + +**Step 3: Write minimal implementation** + +In `docs/paper/reductions.typ`: +- Add `"KthBestSpanningTree": [Kth Best Spanning Tree],` to `display-name` +- Add a `problem-def("KthBestSpanningTree")[...][...]` entry near the other graph problems +- Load the canonical example with `load-model-example("KthBestSpanningTree")` +- Use the issue’s YES instance as the worked example, including: + - the graph and edge weights + - `k = 3`, `B = 12` + - three distinct spanning trees under the bound + - the explanation that the witness is accepted because all three blocks are valid, distinct, and under the bound +- Cite Garey & Johnson and mention the comment-thread correction about Eppstein’s bounds in prose instead of restating the incorrect issue draft + +Then update `test_kthbestspanningtree_paper_example` so it matches the exact paper instance and asserted satisfying-witness count. + +**Step 4: Run test to verify it passes** + +Run: +- `cargo test test_kthbestspanningtree_paper_example --lib` +- `make paper` + +Expected: both PASS. + +**Step 5: Commit** + +Run: +```bash +git add docs/paper/reductions.typ src/unit_tests/models/graph/kth_best_spanning_tree.rs +git commit -m "Document KthBestSpanningTree in the paper" +``` + +## Final Verification + +After all tasks: + +1. Run targeted verification first: + - `cargo test kthbestspanningtree --lib` + - `cargo test trait_consistency --lib` + - `cargo test example_db --lib` + - `cargo run -p problemreductions-cli -- create KthBestSpanningTree --graph 0-1,1-2,2-0 --edge-weights 1,2,3 --k 1 --bound 3` +2. Run repo verification: + - `make test` + - `make clippy` + - `make paper` +3. Run `review-implementation` after code lands to catch structural omissions before the final implementation commit. From c91825b0223f5fc90d6a76b668b4c91ec62904d5 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Mon, 16 Mar 2026 07:11:07 +0800 Subject: [PATCH 2/5] Implement #249: [Model] KthBestSpanningTree --- docs/paper/reductions.typ | 36 +++ docs/paper/references.bib | 22 ++ docs/src/reductions/problem_schemas.json | 26 ++ docs/src/reductions/reduction_graph.json | 171 ++++++----- problemreductions-cli/src/cli.rs | 1 + problemreductions-cli/src/commands/create.rs | 28 ++ src/example_db/fixtures/examples.json | 1 + src/lib.rs | 4 +- src/models/graph/kth_best_spanning_tree.rs | 277 ++++++++++++++++++ src/models/graph/mod.rs | 4 + src/models/mod.rs | 10 +- src/unit_tests/example_db.rs | 19 +- src/unit_tests/export.rs | 3 +- .../models/graph/kth_best_spanning_tree.rs | 127 ++++++++ src/unit_tests/trait_consistency.rs | 9 + 15 files changed, 640 insertions(+), 98 deletions(-) create mode 100644 src/models/graph/kth_best_spanning_tree.rs create mode 100644 src/unit_tests/models/graph/kth_best_spanning_tree.rs diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index b3a286b66..d03713d05 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -68,6 +68,7 @@ "GraphPartitioning": [Graph Partitioning], "HamiltonianPath": [Hamiltonian Path], "IsomorphicSpanningTree": [Isomorphic Spanning Tree], + "KthBestSpanningTree": [Kth Best Spanning Tree], "KColoring": [$k$-Coloring], "MinimumDominatingSet": [Minimum Dominating Set], "MaximumMatching": [Maximum Matching], @@ -631,6 +632,41 @@ Graph Partitioning is a core NP-hard problem arising in VLSI design, parallel co ] ] } +#{ + let x = load-model-example("KthBestSpanningTree") + let edges = x.instance.graph.inner.edges.map(e => (e.at(0), e.at(1))) + let m = edges.len() + let sol = x.optimal.at(0).config + let tree1 = sol.enumerate().filter(((i, v)) => i < m and v == 1).map(((i, _)) => edges.at(i)) + let blue = graph-colors.at(0) + let gray = luma(190) + [ + #problem-def("KthBestSpanningTree")[ + Given an undirected graph $G = (V, E)$ with edge weights $w: E -> ZZ_(gt.eq 0)$, a positive integer $k$, and a bound $B in ZZ_(gt.eq 0)$, determine whether there exist $k$ distinct spanning trees $T_1, dots, T_k subset.eq E$ such that $sum_(e in T_i) w(e) lt.eq B$ for every $i$. + ][ + Kth Best Spanning Tree is catalogued as ND9 in Garey and Johnson @garey1979 and is marked there with an asterisk because the general problem is NP-hard but not known to lie in NP. For any fixed value of $k$, Lawler's $k$-best enumeration framework gives a polynomial-time algorithm when combined with minimum-spanning-tree subroutines @lawler1972. For output-sensitive enumeration, Eppstein gave an algorithm that lists the $k$ smallest spanning trees of a weighted graph in $O(m log beta(m, n) + k^2)$ time @eppstein1992. + + Variables: $k |E|$ binary values grouped into $k$ consecutive edge-selection blocks. Entry $x_(i, e) = 1$ means edge $e$ belongs to the $i$-th candidate tree. A configuration is satisfying exactly when each block selects a spanning tree, every selected tree has total weight at most $B$, and the $k$ blocks encode pairwise distinct edge sets. + + *Example.* Consider the 5-vertex graph with weighted edges ${(0,1): 2, (0,2): 3, (1,2): 1, (1,3): 4, (2,3): 2, (2,4): 5, (3,4): 3, (0,4): 6}$. With $k = 3$ and $B = 12$, the spanning trees $T_1 = {(0,1), (1,2), (2,3), (3,4)}$, $T_2 = {(0,1), (1,2), (1,3), (3,4)}$, and $T_3 = {(0,2), (1,2), (2,3), (3,4)}$ have weights $8$, $10$, and $9$, respectively. They are all distinct and all satisfy the bound, so this instance is a YES-instance. + + #figure({ + canvas(length: 1cm, { + let pos = ((0.0, 1.2), (1.1, 2.0), (2.3, 1.2), (1.8, 0.0), (0.3, 0.0)) + for (u, v) in edges { + let in-tree1 = tree1.any(e => (e.at(0) == u and e.at(1) == v) or (e.at(0) == v and e.at(1) == u)) + g-edge(pos.at(u), pos.at(v), stroke: if in-tree1 { 2pt + blue } else { 1pt + gray }) + } + for (idx, p) in pos.enumerate() { + g-node(p, name: "v" + str(idx), fill: white, label: $v_#idx$) + } + }) + }, + caption: [Kth Best Spanning Tree example graph. Blue edges show $T_1 = {(0,1), (1,2), (2,3), (3,4)}$, one of the three bounded spanning trees used to certify the YES-instance for $k = 3$ and $B = 12$.], + ) + ] + ] +} #{ let x = load-model-example("KColoring") let nv = graph-num-vertices(x.instance) diff --git a/docs/paper/references.bib b/docs/paper/references.bib index 5fc78c6e0..9dcacb4a6 100644 --- a/docs/paper/references.bib +++ b/docs/paper/references.bib @@ -650,3 +650,25 @@ @article{papadimitriou1982 year = {1982}, doi = {10.1145/322307.322309} } + +@article{lawler1972, + author = {Eugene L. Lawler}, + title = {A Procedure for Computing the $K$ Best Solutions to Discrete Optimization Problems and Its Application to the Shortest Path Problem}, + journal = {Management Science}, + volume = {18}, + number = {7}, + pages = {401--405}, + year = {1972}, + doi = {10.1287/mnsc.18.7.401} +} + +@article{eppstein1992, + author = {David Eppstein}, + title = {Finding the $k$ Smallest Spanning Trees}, + journal = {BIT}, + volume = {32}, + number = {2}, + pages = {237--248}, + year = {1992}, + doi = {10.1007/BF01994880} +} diff --git a/docs/src/reductions/problem_schemas.json b/docs/src/reductions/problem_schemas.json index 3a6e9df87..a6defe3a3 100644 --- a/docs/src/reductions/problem_schemas.json +++ b/docs/src/reductions/problem_schemas.json @@ -259,6 +259,32 @@ } ] }, + { + "name": "KthBestSpanningTree", + "description": "Do there exist k distinct spanning trees with total weight at most B?", + "fields": [ + { + "name": "graph", + "type_name": "SimpleGraph", + "description": "The underlying graph G=(V,E)" + }, + { + "name": "weights", + "type_name": "Vec", + "description": "Edge weights w(e) for each edge in E" + }, + { + "name": "k", + "type_name": "usize", + "description": "Number of distinct spanning trees required" + }, + { + "name": "bound", + "type_name": "W::Sum", + "description": "Upper bound B on each spanning tree weight" + } + ] + }, { "name": "LongestCommonSubsequence", "description": "Find the longest string that is a subsequence of every input string", diff --git a/docs/src/reductions/reduction_graph.json b/docs/src/reductions/reduction_graph.json index c1334cfb2..6f24ff271 100644 --- a/docs/src/reductions/reduction_graph.json +++ b/docs/src/reductions/reduction_graph.json @@ -205,6 +205,15 @@ "doc_path": "models/misc/struct.Knapsack.html", "complexity": "2^(num_items / 2)" }, + { + "name": "KthBestSpanningTree", + "variant": { + "weight": "i32" + }, + "category": "graph", + "doc_path": "models/graph/struct.KthBestSpanningTree.html", + "complexity": "2^(num_edges * k)" + }, { "name": "LongestCommonSubsequence", "variant": {}, @@ -549,7 +558,7 @@ }, { "source": 4, - "target": 54, + "target": 55, "overhead": [ { "field": "num_spins", @@ -609,7 +618,7 @@ }, { "source": 12, - "target": 49, + "target": 50, "overhead": [ { "field": "num_vars", @@ -650,7 +659,7 @@ }, { "source": 19, - "target": 49, + "target": 50, "overhead": [ { "field": "num_vars", @@ -676,7 +685,7 @@ }, { "source": 20, - "target": 49, + "target": 50, "overhead": [ { "field": "num_vars", @@ -702,7 +711,7 @@ }, { "source": 21, - "target": 49, + "target": 50, "overhead": [ { "field": "num_vars", @@ -713,7 +722,7 @@ }, { "source": 21, - "target": 56, + "target": 57, "overhead": [ { "field": "num_elements", @@ -724,7 +733,7 @@ }, { "source": 22, - "target": 51, + "target": 52, "overhead": [ { "field": "num_clauses", @@ -743,7 +752,7 @@ }, { "source": 23, - "target": 49, + "target": 50, "overhead": [ { "field": "num_vars", @@ -753,7 +762,7 @@ "doc_path": "rules/knapsack_qubo/index.html" }, { - "source": 24, + "source": 25, "target": 12, "overhead": [ { @@ -768,8 +777,8 @@ "doc_path": "rules/longestcommonsubsequence_ilp/index.html" }, { - "source": 25, - "target": 54, + "source": 26, + "target": 55, "overhead": [ { "field": "num_spins", @@ -783,7 +792,7 @@ "doc_path": "rules/spinglass_maxcut/index.html" }, { - "source": 27, + "source": 28, "target": 12, "overhead": [ { @@ -798,8 +807,8 @@ "doc_path": "rules/maximumclique_ilp/index.html" }, { - "source": 27, - "target": 31, + "source": 28, + "target": 32, "overhead": [ { "field": "num_vertices", @@ -813,8 +822,8 @@ "doc_path": "rules/maximumclique_maximumindependentset/index.html" }, { - "source": 28, - "target": 29, + "source": 29, + "target": 30, "overhead": [ { "field": "num_vertices", @@ -828,8 +837,8 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 28, - "target": 33, + "source": 29, + "target": 34, "overhead": [ { "field": "num_vertices", @@ -843,8 +852,8 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 29, - "target": 34, + "source": 30, + "target": 35, "overhead": [ { "field": "num_vertices", @@ -858,8 +867,8 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 30, - "target": 28, + "source": 31, + "target": 29, "overhead": [ { "field": "num_vertices", @@ -873,8 +882,8 @@ "doc_path": "rules/maximumindependentset_gridgraph/index.html" }, { - "source": 30, - "target": 31, + "source": 31, + "target": 32, "overhead": [ { "field": "num_vertices", @@ -888,8 +897,8 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 30, - "target": 32, + "source": 31, + "target": 33, "overhead": [ { "field": "num_vertices", @@ -903,8 +912,8 @@ "doc_path": "rules/maximumindependentset_triangular/index.html" }, { - "source": 30, - "target": 36, + "source": 31, + "target": 37, "overhead": [ { "field": "num_sets", @@ -918,8 +927,8 @@ "doc_path": "rules/maximumindependentset_maximumsetpacking/index.html" }, { - "source": 31, - "target": 27, + "source": 32, + "target": 28, "overhead": [ { "field": "num_vertices", @@ -933,8 +942,8 @@ "doc_path": "rules/maximumindependentset_maximumclique/index.html" }, { - "source": 31, - "target": 38, + "source": 32, + "target": 39, "overhead": [ { "field": "num_sets", @@ -948,8 +957,8 @@ "doc_path": "rules/maximumindependentset_maximumsetpacking/index.html" }, { - "source": 31, - "target": 45, + "source": 32, + "target": 46, "overhead": [ { "field": "num_vertices", @@ -963,8 +972,8 @@ "doc_path": "rules/minimumvertexcover_maximumindependentset/index.html" }, { - "source": 32, - "target": 34, + "source": 33, + "target": 35, "overhead": [ { "field": "num_vertices", @@ -978,8 +987,8 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 33, - "target": 30, + "source": 34, + "target": 31, "overhead": [ { "field": "num_vertices", @@ -993,8 +1002,8 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 33, - "target": 34, + "source": 34, + "target": 35, "overhead": [ { "field": "num_vertices", @@ -1008,8 +1017,8 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 34, - "target": 31, + "source": 35, + "target": 32, "overhead": [ { "field": "num_vertices", @@ -1023,7 +1032,7 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 35, + "source": 36, "target": 12, "overhead": [ { @@ -1038,8 +1047,8 @@ "doc_path": "rules/maximummatching_ilp/index.html" }, { - "source": 35, - "target": 38, + "source": 36, + "target": 39, "overhead": [ { "field": "num_sets", @@ -1053,8 +1062,8 @@ "doc_path": "rules/maximummatching_maximumsetpacking/index.html" }, { - "source": 36, - "target": 30, + "source": 37, + "target": 31, "overhead": [ { "field": "num_vertices", @@ -1068,8 +1077,8 @@ "doc_path": "rules/maximumindependentset_maximumsetpacking/index.html" }, { - "source": 36, - "target": 38, + "source": 37, + "target": 39, "overhead": [ { "field": "num_sets", @@ -1083,8 +1092,8 @@ "doc_path": "rules/maximumsetpacking_casts/index.html" }, { - "source": 37, - "target": 49, + "source": 38, + "target": 50, "overhead": [ { "field": "num_vars", @@ -1094,7 +1103,7 @@ "doc_path": "rules/maximumsetpacking_qubo/index.html" }, { - "source": 38, + "source": 39, "target": 12, "overhead": [ { @@ -1109,8 +1118,8 @@ "doc_path": "rules/maximumsetpacking_ilp/index.html" }, { - "source": 38, - "target": 31, + "source": 39, + "target": 32, "overhead": [ { "field": "num_vertices", @@ -1124,8 +1133,8 @@ "doc_path": "rules/maximumindependentset_maximumsetpacking/index.html" }, { - "source": 38, - "target": 37, + "source": 39, + "target": 38, "overhead": [ { "field": "num_sets", @@ -1139,7 +1148,7 @@ "doc_path": "rules/maximumsetpacking_casts/index.html" }, { - "source": 39, + "source": 40, "target": 12, "overhead": [ { @@ -1154,7 +1163,7 @@ "doc_path": "rules/minimumdominatingset_ilp/index.html" }, { - "source": 42, + "source": 43, "target": 12, "overhead": [ { @@ -1169,8 +1178,8 @@ "doc_path": "rules/minimumsetcovering_ilp/index.html" }, { - "source": 45, - "target": 31, + "source": 46, + "target": 32, "overhead": [ { "field": "num_vertices", @@ -1184,8 +1193,8 @@ "doc_path": "rules/minimumvertexcover_maximumindependentset/index.html" }, { - "source": 45, - "target": 42, + "source": 46, + "target": 43, "overhead": [ { "field": "num_sets", @@ -1199,7 +1208,7 @@ "doc_path": "rules/minimumvertexcover_minimumsetcovering/index.html" }, { - "source": 49, + "source": 50, "target": 12, "overhead": [ { @@ -1214,8 +1223,8 @@ "doc_path": "rules/qubo_ilp/index.html" }, { - "source": 49, - "target": 53, + "source": 50, + "target": 54, "overhead": [ { "field": "num_spins", @@ -1225,7 +1234,7 @@ "doc_path": "rules/spinglass_qubo/index.html" }, { - "source": 51, + "source": 52, "target": 4, "overhead": [ { @@ -1240,7 +1249,7 @@ "doc_path": "rules/sat_circuitsat/index.html" }, { - "source": 51, + "source": 52, "target": 16, "overhead": [ { @@ -1255,7 +1264,7 @@ "doc_path": "rules/sat_coloring/index.html" }, { - "source": 51, + "source": 52, "target": 21, "overhead": [ { @@ -1270,8 +1279,8 @@ "doc_path": "rules/sat_ksat/index.html" }, { - "source": 51, - "target": 30, + "source": 52, + "target": 31, "overhead": [ { "field": "num_vertices", @@ -1285,8 +1294,8 @@ "doc_path": "rules/sat_maximumindependentset/index.html" }, { - "source": 51, - "target": 39, + "source": 52, + "target": 40, "overhead": [ { "field": "num_vertices", @@ -1300,8 +1309,8 @@ "doc_path": "rules/sat_minimumdominatingset/index.html" }, { - "source": 53, - "target": 49, + "source": 54, + "target": 50, "overhead": [ { "field": "num_vars", @@ -1311,8 +1320,8 @@ "doc_path": "rules/spinglass_qubo/index.html" }, { - "source": 54, - "target": 25, + "source": 55, + "target": 26, "overhead": [ { "field": "num_vertices", @@ -1326,8 +1335,8 @@ "doc_path": "rules/spinglass_maxcut/index.html" }, { - "source": 54, - "target": 53, + "source": 55, + "target": 54, "overhead": [ { "field": "num_spins", @@ -1341,7 +1350,7 @@ "doc_path": "rules/spinglass_casts/index.html" }, { - "source": 57, + "source": 58, "target": 12, "overhead": [ { @@ -1356,8 +1365,8 @@ "doc_path": "rules/travelingsalesman_ilp/index.html" }, { - "source": 57, - "target": 49, + "source": 58, + "target": 50, "overhead": [ { "field": "num_vars", diff --git a/problemreductions-cli/src/cli.rs b/problemreductions-cli/src/cli.rs index c42f69c24..d8ed844f0 100644 --- a/problemreductions-cli/src/cli.rs +++ b/problemreductions-cli/src/cli.rs @@ -224,6 +224,7 @@ Flags by problem type: PartitionIntoTriangles --graph GraphPartitioning --graph IsomorphicSpanningTree --graph, --tree + KthBestSpanningTree --graph, --edge-weights, --k, --bound Factoring --target, --m, --n BinPacking --sizes, --capacity SubsetSum --sizes, --target diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index a14442240..053a4bb0f 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -226,6 +226,7 @@ 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", "IsomorphicSpanningTree" => "--graph 0-1,1-2,0-2 --tree 0-1,1-2", + "KthBestSpanningTree" => "--graph 0-1,0-2,1-2 --edge-weights 2,3,1 --k 1 --bound 3", "MaxCut" | "MaximumMatching" | "TravelingSalesman" => { "--graph 0-1,1-2,2-3 --edge-weights 1,1,1" } @@ -420,6 +421,33 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { ) } + // KthBestSpanningTree (weighted graph + k + bound) + "KthBestSpanningTree" => { + let (graph, _) = parse_graph(args).map_err(|e| { + anyhow::anyhow!( + "{e}\n\nUsage: pred create KthBestSpanningTree --graph 0-1,0-2,1-2 --edge-weights 2,3,1 --k 1 --bound 3" + ) + })?; + let edge_weights = parse_edge_weights(args, graph.num_edges())?; + let (k, _variant) = + util::validate_k_param(&resolved_variant, args.k, None, "KthBestSpanningTree")?; + let bound = args.bound.ok_or_else(|| { + anyhow::anyhow!( + "KthBestSpanningTree requires --bound\n\n\ + Usage: pred create KthBestSpanningTree --graph 0-1,0-2,1-2 --edge-weights 2,3,1 --k 1 --bound 3" + ) + })? as i32; + ( + ser(problemreductions::models::graph::KthBestSpanningTree::new( + graph, + edge_weights, + k, + bound, + ))?, + resolved_variant.clone(), + ) + } + // Graph problems with edge weights "MaxCut" | "MaximumMatching" | "TravelingSalesman" => { let (graph, _) = parse_graph(args).map_err(|e| { diff --git a/src/example_db/fixtures/examples.json b/src/example_db/fixtures/examples.json index e48eef824..75f5e8abc 100644 --- a/src/example_db/fixtures/examples.json +++ b/src/example_db/fixtures/examples.json @@ -11,6 +11,7 @@ {"problem":"IsomorphicSpanningTree","variant":{},"instance":{"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]}},"tree":{"inner":{"edge_property":"undirected","edges":[[0,1,null],[0,2,null],[0,3,null]],"node_holes":[],"nodes":[null,null,null,null]}}},"samples":[{"config":[0,1,2,3],"metric":true}],"optimal":[{"config":[0,1,2,3],"metric":true},{"config":[0,1,3,2],"metric":true},{"config":[0,2,1,3],"metric":true},{"config":[0,2,3,1],"metric":true},{"config":[0,3,1,2],"metric":true},{"config":[0,3,2,1],"metric":true},{"config":[1,0,2,3],"metric":true},{"config":[1,0,3,2],"metric":true},{"config":[1,2,0,3],"metric":true},{"config":[1,2,3,0],"metric":true},{"config":[1,3,0,2],"metric":true},{"config":[1,3,2,0],"metric":true},{"config":[2,0,1,3],"metric":true},{"config":[2,0,3,1],"metric":true},{"config":[2,1,0,3],"metric":true},{"config":[2,1,3,0],"metric":true},{"config":[2,3,0,1],"metric":true},{"config":[2,3,1,0],"metric":true},{"config":[3,0,1,2],"metric":true},{"config":[3,0,2,1],"metric":true},{"config":[3,1,0,2],"metric":true},{"config":[3,1,2,0],"metric":true},{"config":[3,2,0,1],"metric":true},{"config":[3,2,1,0],"metric":true}]}, {"problem":"KColoring","variant":{"graph":"SimpleGraph","k":"K3"},"instance":{"graph":{"inner":{"edge_property":"undirected","edges":[[0,1,null],[0,2,null],[1,3,null],[2,3,null],[2,4,null],[3,4,null]],"node_holes":[],"nodes":[null,null,null,null,null]}},"num_colors":3},"samples":[{"config":[0,1,1,0,2],"metric":true}],"optimal":[{"config":[0,1,1,0,2],"metric":true},{"config":[0,1,1,2,0],"metric":true},{"config":[0,1,2,0,1],"metric":true},{"config":[0,2,1,0,2],"metric":true},{"config":[0,2,2,0,1],"metric":true},{"config":[0,2,2,1,0],"metric":true},{"config":[1,0,0,1,2],"metric":true},{"config":[1,0,0,2,1],"metric":true},{"config":[1,0,2,1,0],"metric":true},{"config":[1,2,0,1,2],"metric":true},{"config":[1,2,2,0,1],"metric":true},{"config":[1,2,2,1,0],"metric":true},{"config":[2,0,0,1,2],"metric":true},{"config":[2,0,0,2,1],"metric":true},{"config":[2,0,1,2,0],"metric":true},{"config":[2,1,0,2,1],"metric":true},{"config":[2,1,1,0,2],"metric":true},{"config":[2,1,1,2,0],"metric":true}]}, {"problem":"KSatisfiability","variant":{"k":"K3"},"instance":{"clauses":[{"literals":[1,2,3]},{"literals":[-1,-2,3]},{"literals":[1,-2,-3]}],"num_vars":3},"samples":[{"config":[1,0,1],"metric":true}],"optimal":[{"config":[0,0,1],"metric":true},{"config":[0,1,0],"metric":true},{"config":[1,0,0],"metric":true},{"config":[1,0,1],"metric":true},{"config":[1,1,1],"metric":true}]}, + {"problem":"KthBestSpanningTree","variant":{"weight":"i32"},"instance":{"bound":12,"graph":{"inner":{"edge_property":"undirected","edges":[[0,1,null],[0,2,null],[1,2,null],[1,3,null],[2,3,null],[2,4,null],[3,4,null],[0,4,null]],"node_holes":[],"nodes":[null,null,null,null,null]}},"k":3,"weights":[2,3,1,4,2,5,3,6]},"samples":[{"config":[1,0,1,0,1,0,1,0,1,0,1,1,0,0,1,0,0,1,1,0,1,0,1,0],"metric":true}],"optimal":[{"config":[1,0,1,0,1,0,1,0,1,0,1,1,0,0,1,0,0,1,1,0,1,0,1,0],"metric":true}]}, {"problem":"MaxCut","variant":{"graph":"SimpleGraph","weight":"i32"},"instance":{"edge_weights":[1,1,1,1,1,1],"graph":{"inner":{"edge_property":"undirected","edges":[[0,1,null],[0,2,null],[1,3,null],[2,3,null],[2,4,null],[3,4,null]],"node_holes":[],"nodes":[null,null,null,null,null]}}},"samples":[{"config":[1,0,0,1,0],"metric":{"Valid":5}}],"optimal":[{"config":[0,1,1,0,0],"metric":{"Valid":5}},{"config":[0,1,1,0,1],"metric":{"Valid":5}},{"config":[1,0,0,1,0],"metric":{"Valid":5}},{"config":[1,0,0,1,1],"metric":{"Valid":5}}]}, {"problem":"MaximalIS","variant":{"graph":"SimpleGraph","weight":"i32"},"instance":{"graph":{"inner":{"edge_property":"undirected","edges":[[0,1,null],[1,2,null],[2,3,null],[3,4,null]],"node_holes":[],"nodes":[null,null,null,null,null]}},"weights":[1,1,1,1,1]},"samples":[{"config":[0,1,0,1,0],"metric":{"Valid":2}},{"config":[1,0,1,0,1],"metric":{"Valid":3}}],"optimal":[{"config":[1,0,1,0,1],"metric":{"Valid":3}}]}, {"problem":"MaximumClique","variant":{"graph":"SimpleGraph","weight":"i32"},"instance":{"graph":{"inner":{"edge_property":"undirected","edges":[[0,1,null],[0,2,null],[1,3,null],[2,3,null],[2,4,null],[3,4,null]],"node_holes":[],"nodes":[null,null,null,null,null]}},"weights":[1,1,1,1,1]},"samples":[{"config":[0,0,1,1,1],"metric":{"Valid":3}}],"optimal":[{"config":[0,0,1,1,1],"metric":{"Valid":3}}]}, diff --git a/src/lib.rs b/src/lib.rs index bceccfe25..9fed7dc5c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -45,8 +45,8 @@ pub mod prelude { pub use crate::models::algebraic::{BMF, QUBO}; pub use crate::models::formula::{CNFClause, CircuitSAT, KSatisfiability, Satisfiability}; pub use crate::models::graph::{ - BicliqueCover, GraphPartitioning, HamiltonianPath, IsomorphicSpanningTree, SpinGlass, - SubgraphIsomorphism, + BicliqueCover, GraphPartitioning, HamiltonianPath, IsomorphicSpanningTree, + KthBestSpanningTree, SpinGlass, SubgraphIsomorphism, }; pub use crate::models::graph::{ KColoring, MaxCut, MaximalIS, MaximumClique, MaximumIndependentSet, MaximumMatching, diff --git a/src/models/graph/kth_best_spanning_tree.rs b/src/models/graph/kth_best_spanning_tree.rs new file mode 100644 index 000000000..80e001bf6 --- /dev/null +++ b/src/models/graph/kth_best_spanning_tree.rs @@ -0,0 +1,277 @@ +//! Kth Best Spanning Tree problem implementation. +//! +//! Given a weighted graph, determine whether it contains `k` distinct spanning +//! trees whose total weights are all at most a prescribed bound. + +use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}; +use crate::topology::{Graph, SimpleGraph}; +use crate::traits::{Problem, SatisfactionProblem}; +use crate::types::WeightElement; +use num_traits::Zero; +use serde::{Deserialize, Serialize}; +use std::collections::VecDeque; + +inventory::submit! { + ProblemSchemaEntry { + name: "KthBestSpanningTree", + display_name: "Kth Best Spanning Tree", + aliases: &[], + dimensions: &[VariantDimension::new("weight", "i32", &["i32"])], + module_path: module_path!(), + description: "Do there exist k distinct spanning trees with total weight at most B?", + fields: &[ + FieldInfo { name: "graph", type_name: "SimpleGraph", description: "The underlying graph G=(V,E)" }, + FieldInfo { name: "weights", type_name: "Vec", description: "Edge weights w(e) for each edge in E" }, + FieldInfo { name: "k", type_name: "usize", description: "Number of distinct spanning trees required" }, + FieldInfo { name: "bound", type_name: "W::Sum", description: "Upper bound B on each spanning tree weight" }, + ], + } +} + +/// Kth Best Spanning Tree. +/// +/// Given an undirected graph `G = (V, E)`, non-negative edge weights `w(e)`, +/// a positive integer `k`, and a bound `B`, determine whether there are `k` +/// distinct spanning trees of `G` whose total weights are all at most `B`. +/// +/// # Representation +/// +/// A configuration is `k` consecutive binary blocks of length `|E|`. +/// Each block selects the edges of one candidate spanning tree. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct KthBestSpanningTree { + graph: SimpleGraph, + weights: Vec, + k: usize, + bound: W::Sum, +} + +impl KthBestSpanningTree { + /// Create a new KthBestSpanningTree instance. + /// + /// # Panics + /// + /// Panics if the number of weights does not match the number of edges, or + /// if `k` is zero. + pub fn new(graph: SimpleGraph, weights: Vec, k: usize, bound: W::Sum) -> Self { + assert_eq!( + weights.len(), + graph.num_edges(), + "weights length must match graph num_edges" + ); + assert!(k > 0, "k must be positive"); + + Self { + graph, + weights, + k, + bound, + } + } + + /// Get the underlying graph. + pub fn graph(&self) -> &SimpleGraph { + &self.graph + } + + /// Get the edge weights. + pub fn weights(&self) -> &[W] { + &self.weights + } + + /// Get the requested number of trees. + pub fn k(&self) -> usize { + self.k + } + + /// Get the weight bound. + pub fn bound(&self) -> &W::Sum { + &self.bound + } + + /// Get the number of vertices. + pub fn num_vertices(&self) -> usize { + self.graph.num_vertices() + } + + /// Get the number of edges. + pub fn num_edges(&self) -> usize { + self.graph.num_edges() + } + + /// Check whether the problem uses a non-unit weight type. + pub fn is_weighted(&self) -> bool { + !W::IS_UNIT + } + + /// Check whether a configuration satisfies the problem. + pub fn is_valid_solution(&self, config: &[usize]) -> bool { + self.evaluate_config(config) + } + + fn block_is_valid_tree(&self, block: &[usize], edges: &[(usize, usize)]) -> bool { + if block.len() != edges.len() || block.iter().any(|&value| value > 1) { + return false; + } + + let num_vertices = self.graph.num_vertices(); + let selected_count = block.iter().filter(|&&value| value == 1).count(); + if selected_count != num_vertices.saturating_sub(1) { + return false; + } + + let mut total_weight = W::Sum::zero(); + let mut adjacency = vec![Vec::new(); num_vertices]; + let mut start = None; + + for (idx, &selected) in block.iter().enumerate() { + if selected == 0 { + continue; + } + total_weight += self.weights[idx].to_sum(); + let (u, v) = edges[idx]; + adjacency[u].push(v); + adjacency[v].push(u); + if start.is_none() { + start = Some(u); + } + } + + if total_weight > self.bound { + return false; + } + + if num_vertices <= 1 { + return true; + } + + let start = match start { + Some(vertex) => vertex, + None => return false, + }; + + let mut visited = vec![false; num_vertices]; + let mut queue = VecDeque::new(); + visited[start] = true; + queue.push_back(start); + + while let Some(vertex) = queue.pop_front() { + for &neighbor in &adjacency[vertex] { + if !visited[neighbor] { + visited[neighbor] = true; + queue.push_back(neighbor); + } + } + } + + visited.into_iter().all(|seen| seen) + } + + fn blocks_are_pairwise_distinct(&self, config: &[usize], block_size: usize) -> bool { + if block_size == 0 { + return self.k == 1; + } + + let blocks: Vec<&[usize]> = config.chunks_exact(block_size).collect(); + for left in 0..blocks.len() { + for right in (left + 1)..blocks.len() { + if blocks[left] == blocks[right] { + return false; + } + } + } + true + } + + fn evaluate_config(&self, config: &[usize]) -> bool { + let block_size = self.graph.num_edges(); + let expected_len = self.k * block_size; + if config.len() != expected_len { + return false; + } + + if block_size == 0 { + return self.k == 1 && self.block_is_valid_tree(config, &[]); + } + + let edges = self.graph.edges(); + let blocks = config.chunks_exact(block_size); + if !blocks.remainder().is_empty() { + return false; + } + + if !self.blocks_are_pairwise_distinct(config, block_size) { + return false; + } + + config + .chunks_exact(block_size) + .all(|block| self.block_is_valid_tree(block, &edges)) + } +} + +impl Problem for KthBestSpanningTree +where + W: WeightElement + crate::variant::VariantParam, +{ + const NAME: &'static str = "KthBestSpanningTree"; + type Metric = bool; + + fn variant() -> Vec<(&'static str, &'static str)> { + crate::variant_params![W] + } + + fn dims(&self) -> Vec { + vec![2; self.k * self.graph.num_edges()] + } + + fn evaluate(&self, config: &[usize]) -> bool { + self.evaluate_config(config) + } +} + +impl SatisfactionProblem for KthBestSpanningTree where + W: WeightElement + crate::variant::VariantParam +{ +} + +#[cfg(feature = "example-db")] +pub(crate) fn canonical_model_example_specs() -> Vec { + vec![crate::example_db::specs::ModelExampleSpec { + id: "kth_best_spanning_tree_i32", + build: || { + let graph = SimpleGraph::new( + 5, + vec![ + (0, 1), + (0, 2), + (1, 2), + (1, 3), + (2, 3), + (2, 4), + (3, 4), + (0, 4), + ], + ); + let problem = KthBestSpanningTree::new(graph, vec![2, 3, 1, 4, 2, 5, 3, 6], 3, 12); + let witness = vec![ + 1, 0, 1, 0, 1, 0, 1, 0, // + 1, 0, 1, 1, 0, 0, 1, 0, // + 0, 1, 1, 0, 1, 0, 1, 0, + ]; + crate::example_db::specs::explicit_example( + problem, + vec![witness.clone()], + vec![witness], + ) + }, + }] +} + +crate::declare_variants! { + default sat KthBestSpanningTree => "2^(num_edges * k)", +} + +#[cfg(test)] +#[path = "../../unit_tests/models/graph/kth_best_spanning_tree.rs"] +mod tests; diff --git a/src/models/graph/mod.rs b/src/models/graph/mod.rs index 7d76de656..76684432c 100644 --- a/src/models/graph/mod.rs +++ b/src/models/graph/mod.rs @@ -10,6 +10,7 @@ //! - [`MaxCut`]: Maximum cut on weighted graphs //! - [`GraphPartitioning`]: Minimum bisection (balanced graph partitioning) //! - [`IsomorphicSpanningTree`]: Isomorphic spanning tree (satisfaction) +//! - [`KthBestSpanningTree`]: K distinct bounded spanning trees (satisfaction) //! - [`KColoring`]: K-vertex coloring //! - [`PartitionIntoTriangles`]: Partition vertices into triangles //! - [`MaximumMatching`]: Maximum weight matching @@ -28,6 +29,7 @@ pub(crate) mod graph_partitioning; pub(crate) mod hamiltonian_path; pub(crate) mod isomorphic_spanning_tree; pub(crate) mod kcoloring; +pub(crate) mod kth_best_spanning_tree; pub(crate) mod max_cut; pub(crate) mod maximal_is; pub(crate) mod maximum_clique; @@ -50,6 +52,7 @@ pub use graph_partitioning::GraphPartitioning; pub use hamiltonian_path::HamiltonianPath; pub use isomorphic_spanning_tree::IsomorphicSpanningTree; pub use kcoloring::KColoring; +pub use kth_best_spanning_tree::KthBestSpanningTree; pub use max_cut::MaxCut; pub use maximal_is::MaximalIS; pub use maximum_clique::MaximumClique; @@ -76,6 +79,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec {} — 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 d6c4dc395..8b02dc623 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/kth_best_spanning_tree.rs b/src/unit_tests/models/graph/kth_best_spanning_tree.rs new file mode 100644 index 000000000..c50845f9e --- /dev/null +++ b/src/unit_tests/models/graph/kth_best_spanning_tree.rs @@ -0,0 +1,127 @@ +use super::*; +use crate::solvers::{BruteForce, Solver}; +use crate::topology::SimpleGraph; +use crate::traits::Problem; + +fn yes_instance() -> KthBestSpanningTree { + let graph = SimpleGraph::new( + 5, + vec![ + (0, 1), + (0, 2), + (1, 2), + (1, 3), + (2, 3), + (2, 4), + (3, 4), + (0, 4), + ], + ); + let weights = vec![2, 3, 1, 4, 2, 5, 3, 6]; + KthBestSpanningTree::new(graph, weights, 3, 12) +} + +fn no_instance() -> KthBestSpanningTree { + let graph = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]); + let weights = vec![1, 1, 1]; + KthBestSpanningTree::new(graph, weights, 2, 3) +} + +fn yes_witness_config() -> Vec { + vec![ + 1, 0, 1, 0, 1, 0, 1, 0, // {0,1}, {1,2}, {2,3}, {3,4} + 1, 0, 1, 1, 0, 0, 1, 0, // {0,1}, {1,2}, {1,3}, {3,4} + 0, 1, 1, 0, 1, 0, 1, 0, // {0,2}, {1,2}, {2,3}, {3,4} + ] +} + +fn duplicate_tree_config() -> Vec { + vec![ + 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 1, 1, 0, 1, 0, 1, 0, + ] +} + +fn overweight_tree_config() -> Vec { + vec![ + 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0, + 0, // {0,1}, {0,2}, {1,3}, {2,4} => 14 + ] +} + +#[test] +fn test_kthbestspanningtree_creation() { + let problem = yes_instance(); + + assert_eq!(problem.dims(), vec![2; 24]); + assert_eq!(problem.num_vertices(), 5); + assert_eq!(problem.num_edges(), 8); + assert_eq!(problem.k(), 3); + assert_eq!(problem.weights(), &[2, 3, 1, 4, 2, 5, 3, 6]); + assert_eq!(*problem.bound(), 12); + assert!(problem.is_weighted()); + assert_eq!(KthBestSpanningTree::::NAME, "KthBestSpanningTree"); +} + +#[test] +fn test_kthbestspanningtree_evaluation_yes_instance() { + let problem = yes_instance(); + assert!(problem.evaluate(&yes_witness_config())); +} + +#[test] +fn test_kthbestspanningtree_evaluation_rejects_duplicate_trees() { + let problem = yes_instance(); + assert!(!problem.evaluate(&duplicate_tree_config())); +} + +#[test] +fn test_kthbestspanningtree_evaluation_rejects_overweight_tree() { + let problem = yes_instance(); + assert!(!problem.evaluate(&overweight_tree_config())); +} + +#[test] +fn test_kthbestspanningtree_solver_yes_instance() { + let problem = yes_instance(); + let solver = BruteForce::new(); + + let solution = solver.find_satisfying(&problem); + assert!(solution.is_some()); + assert!(problem.evaluate(&solution.unwrap())); +} + +#[test] +fn test_kthbestspanningtree_solver_no_instance() { + let problem = no_instance(); + let solver = BruteForce::new(); + + assert!(solver.find_satisfying(&problem).is_none()); + assert!(solver.find_all_satisfying(&problem).is_empty()); +} + +#[test] +fn test_kthbestspanningtree_serialization() { + let problem = yes_instance(); + let json = serde_json::to_string(&problem).unwrap(); + let restored: KthBestSpanningTree = serde_json::from_str(&json).unwrap(); + + assert_eq!(restored.num_vertices(), problem.num_vertices()); + assert_eq!(restored.num_edges(), problem.num_edges()); + assert_eq!(restored.k(), problem.k()); + assert_eq!(restored.weights(), problem.weights()); + assert_eq!(restored.bound(), problem.bound()); + assert!(restored.evaluate(&yes_witness_config())); +} + +#[test] +fn test_kthbestspanningtree_paper_example() { + let problem = yes_instance(); + let witness = yes_witness_config(); + + assert!(problem.evaluate(&witness)); + + let solver = BruteForce::new(); + let all = solver.find_all_satisfying(&problem); + assert_eq!(all.len(), 4_896); + assert!(all.iter().any(|config| config == &witness)); +} diff --git a/src/unit_tests/trait_consistency.rs b/src/unit_tests/trait_consistency.rs index eee5dc642..23dc118b6 100644 --- a/src/unit_tests/trait_consistency.rs +++ b/src/unit_tests/trait_consistency.rs @@ -118,6 +118,15 @@ fn test_all_problems_implement_trait_correctly() { ), "IsomorphicSpanningTree", ); + check_problem_trait( + &KthBestSpanningTree::new( + SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]), + vec![1, 1, 1], + 1, + 2, + ), + "KthBestSpanningTree", + ); check_problem_trait( &ShortestCommonSupersequence::new(2, vec![vec![0, 1], vec![1, 0]], 3), "ShortestCommonSupersequence", From e8d54ed81dfa4d3ab2ab91300d9e086106935c33 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Mon, 16 Mar 2026 07:11:23 +0800 Subject: [PATCH 3/5] chore: remove plan file after implementation --- .../2026-03-16-kth-best-spanning-tree.md | 219 ------------------ 1 file changed, 219 deletions(-) delete mode 100644 docs/plans/2026-03-16-kth-best-spanning-tree.md diff --git a/docs/plans/2026-03-16-kth-best-spanning-tree.md b/docs/plans/2026-03-16-kth-best-spanning-tree.md deleted file mode 100644 index e367e375c..000000000 --- a/docs/plans/2026-03-16-kth-best-spanning-tree.md +++ /dev/null @@ -1,219 +0,0 @@ -# KthBestSpanningTree Implementation Plan - -> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. - -**Goal:** Add the `KthBestSpanningTree` satisfaction model, wire it into the registry/CLI/example database/paper, and implement the issue #249 example semantics. - -**Architecture:** Model `KthBestSpanningTree` as a `SimpleGraph` plus per-edge weights, a target multiplicity `k`, and a weight bound `B`. Each configuration is `k` consecutive binary edge-selection blocks of length `num_edges`; `evaluate()` accepts exactly those assignments where every block is a distinct spanning tree whose total weight is at most `B`. - -**Tech Stack:** Rust, serde, petgraph-backed `SimpleGraph`, registry macros (`inventory::submit!`, `declare_variants!`), CLI `pred create`, Typst paper. - -**Notes:** The issue comment thread already settled two design points that this plan follows: `dims()` uses `k * num_edges` binary variables, and the registry complexity uses the general-case bound `2^(num_edges * k)`. There is no open associated rule issue whose title mentions `KthBestSpanningTree`; carry that orphan-model warning into the PR summary instead of blocking execution. - ---- - -## Batch 1: Model, Registry, CLI, Tests - -### Task 1: Add failing model tests for the issue semantics - -**Files:** -- Create: `src/unit_tests/models/graph/kth_best_spanning_tree.rs` - -**Step 1: Write the failing test** - -Add focused tests that reference the new type before it exists: -- `test_kthbestspanningtree_creation` -- `test_kthbestspanningtree_evaluation_yes_instance` -- `test_kthbestspanningtree_evaluation_rejects_duplicate_trees` -- `test_kthbestspanningtree_evaluation_rejects_overweight_tree` -- `test_kthbestspanningtree_solver_yes_and_no_examples` -- `test_kthbestspanningtree_serialization` -- `test_kthbestspanningtree_paper_example` - -Use the issue’s two worked instances as the source of truth: -- YES instance: 5-vertex, 8-edge graph with `k = 3`, `bound = 12` -- NO instance: 4-vertex path with `k = 2`, `bound = 3` - -**Step 2: Run test to verify it fails** - -Run: `cargo test kthbestspanningtree --lib` -Expected: FAIL because `KthBestSpanningTree` does not exist yet. - -**Step 3: Write minimal implementation** - -Create `src/models/graph/kth_best_spanning_tree.rs` with: -- `ProblemSchemaEntry` for `graph = SimpleGraph`, `weight = i32` -- `#[derive(Debug, Clone, Serialize, Deserialize)] pub struct KthBestSpanningTree` -- Constructor `new(graph, weights, k, bound)` validating edge-weight count and positive `k` -- Getters `graph()`, `weights()`, `k()`, `bound()`, `num_vertices()`, `num_edges()` -- Helper predicates for one block: - - length/range check for binary edge choices - - exact `n - 1` selected-edge count - - selected-edge subgraph connectivity - - total selected weight `<= bound` -- Pairwise distinctness check across the `k` blocks -- `Problem` implementation with `dims() = vec![2; self.k * self.graph.num_edges()]` -- `variant() = crate::variant_params![W]` -- `impl SatisfactionProblem` -- `declare_variants! { default sat KthBestSpanningTree => "2^(num_edges * k)", }` -- `#[cfg(feature = "example-db")] canonical_model_example_specs()` using the issue YES instance -- test link at the bottom - -Prefer the `RuralPostman` pattern for `bound: W::Sum` rather than `bound: W`, and only register the `i32` variant because the issue requires integer edge weights. - -**Step 4: Run test to verify it passes** - -Run: `cargo test kthbestspanningtree --lib` -Expected: PASS for the new targeted model tests. - -**Step 5: Commit** - -Run: -```bash -git add src/models/graph/kth_best_spanning_tree.rs src/unit_tests/models/graph/kth_best_spanning_tree.rs -git commit -m "Add KthBestSpanningTree model" -``` - -### Task 2: Register the model across the crate and example-db - -**Files:** -- Modify: `src/models/graph/mod.rs` -- Modify: `src/models/mod.rs` -- Modify: `src/lib.rs` -- Modify: `src/unit_tests/trait_consistency.rs` - -**Step 1: Write the failing test** - -Add a trait-consistency entry for a tiny positive instance so the new model is checked by the shared trait suite. - -**Step 2: Run test to verify it fails** - -Run: `cargo test trait_consistency --lib` -Expected: FAIL before the new type is exported and referenced consistently. - -**Step 3: Write minimal implementation** - -Wire the type through the graph module, top-level model exports, and prelude exports. In `trait_consistency.rs`, add one `check_problem_trait(...)` call using a 3-vertex triangle with `k = 1`, plus any minimal imports needed. - -Also add the new example-db spec to `src/models/graph/mod.rs` so `build_model_examples()` includes the canonical example. - -**Step 4: Run test to verify it passes** - -Run: `cargo test trait_consistency --lib` -Expected: PASS with the new model included. - -**Step 5: Commit** - -Run: -```bash -git add src/models/graph/mod.rs src/models/mod.rs src/lib.rs src/unit_tests/trait_consistency.rs -git commit -m "Register KthBestSpanningTree across the crate" -``` - -### Task 3: Add CLI creation support for the new model - -**Files:** -- Modify: `problemreductions-cli/src/commands/create.rs` -- Modify: `problemreductions-cli/src/cli.rs` - -**Step 1: Write the failing test** - -Use a direct CLI smoke command as the behavior check: -- `pred create KthBestSpanningTree --graph 0-1,1-2,2-0 --edge-weights 1,2,3 --k 1 --bound 3` - -No dedicated Rust test file is required if the smoke command is the verification target for this task. - -**Step 2: Run test to verify it fails** - -Run: `cargo run -p problemreductions-cli -- create KthBestSpanningTree --graph 0-1,1-2,2-0 --edge-weights 1,2,3 --k 1 --bound 3` -Expected: FAIL with unknown problem or unsupported create-path errors before the CLI wiring exists. - -**Step 3: Write minimal implementation** - -Update CLI creation support by: -- Adding `KthBestSpanningTree` to the help text table in `problemreductions-cli/src/cli.rs` -- Adding an `example_for()` entry in `problemreductions-cli/src/commands/create.rs` -- Adding a `match` arm that parses: - - `--graph` - - `--edge-weights` - - `--k` - - `--bound` -- Constructing `KthBestSpanningTree::new(graph, edge_weights, k, bound)` - -Use the existing `RuralPostman` and weighted-graph parsing helpers as the template. - -**Step 4: Run test to verify it passes** - -Run: `cargo run -p problemreductions-cli -- create KthBestSpanningTree --graph 0-1,1-2,2-0 --edge-weights 1,2,3 --k 1 --bound 3` -Expected: PASS and emit valid problem JSON. - -**Step 5: Commit** - -Run: -```bash -git add problemreductions-cli/src/commands/create.rs problemreductions-cli/src/cli.rs -git commit -m "Add CLI support for KthBestSpanningTree" -``` - -## Batch 2: Paper Entry and Paper-Aligned Verification - -### Task 4: Add the paper entry and align the paper example test - -**Files:** -- Modify: `docs/paper/reductions.typ` -- Modify: `src/unit_tests/models/graph/kth_best_spanning_tree.rs` - -**Step 1: Write the failing test** - -Ensure `test_kthbestspanningtree_paper_example` uses the exact same instance and witness that the paper will show. If the current test uses the issue example already, tighten it so it also checks the claimed count of satisfying witnesses from brute force. - -**Step 2: Run test to verify it fails** - -Run: `cargo test test_kthbestspanningtree_paper_example --lib` -Expected: FAIL until the test and paper example agree on one concrete instance. - -**Step 3: Write minimal implementation** - -In `docs/paper/reductions.typ`: -- Add `"KthBestSpanningTree": [Kth Best Spanning Tree],` to `display-name` -- Add a `problem-def("KthBestSpanningTree")[...][...]` entry near the other graph problems -- Load the canonical example with `load-model-example("KthBestSpanningTree")` -- Use the issue’s YES instance as the worked example, including: - - the graph and edge weights - - `k = 3`, `B = 12` - - three distinct spanning trees under the bound - - the explanation that the witness is accepted because all three blocks are valid, distinct, and under the bound -- Cite Garey & Johnson and mention the comment-thread correction about Eppstein’s bounds in prose instead of restating the incorrect issue draft - -Then update `test_kthbestspanningtree_paper_example` so it matches the exact paper instance and asserted satisfying-witness count. - -**Step 4: Run test to verify it passes** - -Run: -- `cargo test test_kthbestspanningtree_paper_example --lib` -- `make paper` - -Expected: both PASS. - -**Step 5: Commit** - -Run: -```bash -git add docs/paper/reductions.typ src/unit_tests/models/graph/kth_best_spanning_tree.rs -git commit -m "Document KthBestSpanningTree in the paper" -``` - -## Final Verification - -After all tasks: - -1. Run targeted verification first: - - `cargo test kthbestspanningtree --lib` - - `cargo test trait_consistency --lib` - - `cargo test example_db --lib` - - `cargo run -p problemreductions-cli -- create KthBestSpanningTree --graph 0-1,1-2,2-0 --edge-weights 1,2,3 --k 1 --bound 3` -2. Run repo verification: - - `make test` - - `make clippy` - - `make paper` -3. Run `review-implementation` after code lands to catch structural omissions before the final implementation commit. From 3363873511dd36fe7e99f0db41826e4c91ba41ac Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Mon, 16 Mar 2026 12:49:57 +0800 Subject: [PATCH 4/5] fix: address KthBestSpanningTree review feedback --- docs/src/cli.md | 1 + problemreductions-cli/src/util.rs | 20 ++++++ problemreductions-cli/tests/cli_tests.rs | 25 ++++++++ .../models/graph/kth_best_spanning_tree.rs | 64 ++++++++++++++++++- 4 files changed, 107 insertions(+), 3 deletions(-) diff --git a/docs/src/cli.md b/docs/src/cli.md index 6507c61f5..93f2abf81 100644 --- a/docs/src/cli.md +++ b/docs/src/cli.md @@ -270,6 +270,7 @@ pred create MIS --graph 0-1,1-2,2-3 --weights 2,1,3,1 -o problem.json pred create SAT --num-vars 3 --clauses "1,2;-1,3" -o sat.json pred create QUBO --matrix "1,0.5;0.5,2" -o qubo.json pred create KColoring --k 3 --graph 0-1,1-2,2-0 -o kcol.json +pred create KthBestSpanningTree --graph 0-1,0-2,1-2 --edge-weights 2,3,1 --k 1 --bound 3 -o kth.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 Factoring --target 15 --bits-m 4 --bits-n 4 -o factoring.json diff --git a/problemreductions-cli/src/util.rs b/problemreductions-cli/src/util.rs index bc63585c0..70ac24b52 100644 --- a/problemreductions-cli/src/util.rs +++ b/problemreductions-cli/src/util.rs @@ -65,6 +65,10 @@ pub fn validate_k_param( }, }; + if effective_k == 0 { + bail!("{problem_name}: --k must be positive"); + } + // Build the variant map with the effective k let mut variant = resolved_variant.clone(); variant.insert("k".to_string(), k_variant_str(effective_k).to_string()); @@ -268,3 +272,19 @@ pub fn parse_edge_pairs(s: &str) -> Result> { }) .collect() } + +#[cfg(test)] +mod tests { + use super::validate_k_param; + use std::collections::BTreeMap; + + #[test] + fn test_validate_k_param_rejects_zero() { + let err = validate_k_param(&BTreeMap::new(), Some(0), None, "KthBestSpanningTree") + .expect_err("k=0 should be rejected before problem construction"); + assert!( + err.to_string().contains("positive"), + "unexpected error message: {err}" + ); + } +} diff --git a/problemreductions-cli/tests/cli_tests.rs b/problemreductions-cli/tests/cli_tests.rs index f011bad11..fa0fc4ba0 100644 --- a/problemreductions-cli/tests/cli_tests.rs +++ b/problemreductions-cli/tests/cli_tests.rs @@ -1424,6 +1424,31 @@ fn test_create_kcoloring_missing_k() { assert!(stderr.contains("--k")); } +#[test] +fn test_create_kth_best_spanning_tree_rejects_zero_k() { + let output = pred() + .args([ + "create", + "KthBestSpanningTree", + "--graph", + "0-1,1-2,0-2", + "--edge-weights", + "2,3,1", + "--k", + "0", + "--bound", + "3", + ]) + .output() + .unwrap(); + assert!(!output.status.success()); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + stderr.contains("must be positive"), + "expected positive-k validation error, got: {stderr}" + ); +} + #[test] fn test_evaluate_wrong_config_length() { let problem_file = std::env::temp_dir().join("pred_test_eval_wrong_len.json"); diff --git a/src/unit_tests/models/graph/kth_best_spanning_tree.rs b/src/unit_tests/models/graph/kth_best_spanning_tree.rs index c50845f9e..a146397cb 100644 --- a/src/unit_tests/models/graph/kth_best_spanning_tree.rs +++ b/src/unit_tests/models/graph/kth_best_spanning_tree.rs @@ -27,6 +27,12 @@ fn no_instance() -> KthBestSpanningTree { KthBestSpanningTree::new(graph, weights, 2, 3) } +fn small_yes_instance() -> KthBestSpanningTree { + let graph = SimpleGraph::new(3, vec![(0, 1), (0, 2), (1, 2)]); + let weights = vec![1, 1, 1]; + KthBestSpanningTree::new(graph, weights, 2, 2) +} + fn yes_witness_config() -> Vec { vec![ 1, 0, 1, 0, 1, 0, 1, 0, // {0,1}, {1,2}, {2,3}, {3,4} @@ -53,6 +59,8 @@ fn test_kthbestspanningtree_creation() { let problem = yes_instance(); assert_eq!(problem.dims(), vec![2; 24]); + assert_eq!(problem.graph().num_vertices(), 5); + assert_eq!(problem.graph().num_edges(), 8); assert_eq!(problem.num_vertices(), 5); assert_eq!(problem.num_edges(), 8); assert_eq!(problem.k(), 3); @@ -66,6 +74,7 @@ fn test_kthbestspanningtree_creation() { fn test_kthbestspanningtree_evaluation_yes_instance() { let problem = yes_instance(); assert!(problem.evaluate(&yes_witness_config())); + assert!(problem.is_valid_solution(&yes_witness_config())); } #[test] @@ -80,6 +89,20 @@ fn test_kthbestspanningtree_evaluation_rejects_overweight_tree() { assert!(!problem.evaluate(&overweight_tree_config())); } +#[test] +fn test_kthbestspanningtree_evaluation_rejects_wrong_length_config() { + let problem = yes_instance(); + assert!(!problem.evaluate(&yes_witness_config()[..23])); +} + +#[test] +fn test_kthbestspanningtree_evaluation_rejects_nonbinary_value() { + let problem = yes_instance(); + let mut config = yes_witness_config(); + config[0] = 2; + assert!(!problem.evaluate(&config)); +} + #[test] fn test_kthbestspanningtree_solver_yes_instance() { let problem = yes_instance(); @@ -99,6 +122,16 @@ fn test_kthbestspanningtree_solver_no_instance() { assert!(solver.find_all_satisfying(&problem).is_empty()); } +#[test] +fn test_kthbestspanningtree_small_exhaustive_search() { + let problem = small_yes_instance(); + let solver = BruteForce::new(); + + let all = solver.find_all_satisfying(&problem); + assert_eq!(all.len(), 6); + assert!(all.iter().all(|config| problem.evaluate(config))); +} + #[test] fn test_kthbestspanningtree_serialization() { let problem = yes_instance(); @@ -121,7 +154,32 @@ fn test_kthbestspanningtree_paper_example() { assert!(problem.evaluate(&witness)); let solver = BruteForce::new(); - let all = solver.find_all_satisfying(&problem); - assert_eq!(all.len(), 4_896); - assert!(all.iter().any(|config| config == &witness)); + let solution = solver.find_satisfying(&problem).unwrap(); + assert!(problem.evaluate(&solution)); +} + +#[test] +fn test_kthbestspanningtree_single_vertex_accepts_single_empty_tree() { + let problem = KthBestSpanningTree::::new(SimpleGraph::new(1, vec![]), vec![], 1, 0); + assert!(problem.evaluate(&[])); + assert!(problem.is_valid_solution(&[])); +} + +#[test] +fn test_kthbestspanningtree_single_vertex_rejects_multiple_empty_trees() { + let problem = KthBestSpanningTree::::new(SimpleGraph::new(1, vec![]), vec![], 2, 0); + assert!(!problem.evaluate(&[])); +} + +#[test] +#[should_panic(expected = "weights length must match graph num_edges")] +fn test_kthbestspanningtree_creation_rejects_weight_length_mismatch() { + let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); + let _ = KthBestSpanningTree::new(graph, vec![1], 1, 2); +} + +#[test] +#[should_panic(expected = "k must be positive")] +fn test_kthbestspanningtree_creation_rejects_zero_k() { + let _ = KthBestSpanningTree::::new(SimpleGraph::new(1, vec![]), vec![], 0, 0); } From 669bb0a5d3c68f0a98b869bee7fe5998b3ca041a Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Mon, 16 Mar 2026 13:00:55 +0800 Subject: [PATCH 5/5] fix: clarify edge-weight create help --- problemreductions-cli/src/commands/create.rs | 43 +++++++++++++++++++- problemreductions-cli/tests/cli_tests.rs | 43 ++++++++++++++++++++ 2 files changed, 85 insertions(+), 1 deletion(-) diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index 053a4bb0f..213ee8fbe 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -252,6 +252,44 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str { } } +fn uses_edge_weights_flag(canonical: &str) -> bool { + matches!( + canonical, + "KthBestSpanningTree" + | "MaxCut" + | "MaximumMatching" + | "TravelingSalesman" + | "RuralPostman" + ) +} + +fn help_flag_name(canonical: &str, field_name: &str) -> String { + if field_name == "weights" && uses_edge_weights_flag(canonical) { + "edge-weights".to_string() + } else { + field_name.replace('_', "-") + } +} + +fn reject_vertex_weights_for_edge_weight_problem( + args: &CreateArgs, + canonical: &str, + graph_type: Option<&str>, +) -> Result<()> { + if args.weights.is_some() && uses_edge_weights_flag(canonical) { + bail!( + "{canonical} uses --edge-weights, not --weights.\n\n\ + Usage: pred create {} {}", + match graph_type { + Some(g) => format!("{canonical}/{g}"), + None => canonical.to_string(), + }, + example_for(canonical, graph_type) + ); + } + Ok(()) +} + fn print_problem_help(canonical: &str, graph_type: Option<&str>) -> Result<()> { let is_geometry = matches!( graph_type, @@ -279,7 +317,7 @@ fn print_problem_help(canonical: &str, graph_type: Option<&str>) -> Result<()> { let hint = type_format_hint(&field.type_name, graph_type); eprintln!( " --{:<16} {} ({})", - field.name.replace('_', "-"), + help_flag_name(canonical, &field.name), field.description, hint ); @@ -423,6 +461,7 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { // KthBestSpanningTree (weighted graph + k + bound) "KthBestSpanningTree" => { + reject_vertex_weights_for_edge_weight_problem(args, canonical, None)?; let (graph, _) = parse_graph(args).map_err(|e| { anyhow::anyhow!( "{e}\n\nUsage: pred create KthBestSpanningTree --graph 0-1,0-2,1-2 --edge-weights 2,3,1 --k 1 --bound 3" @@ -450,6 +489,7 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { // Graph problems with edge weights "MaxCut" | "MaximumMatching" | "TravelingSalesman" => { + reject_vertex_weights_for_edge_weight_problem(args, canonical, None)?; let (graph, _) = parse_graph(args).map_err(|e| { anyhow::anyhow!( "{e}\n\nUsage: pred create {} --graph 0-1,1-2,2-3 [--edge-weights 1,1,1]", @@ -468,6 +508,7 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { // RuralPostman "RuralPostman" => { + reject_vertex_weights_for_edge_weight_problem(args, canonical, None)?; let (graph, _) = parse_graph(args).map_err(|e| { anyhow::anyhow!( "{e}\n\nUsage: pred create RuralPostman --graph 0-1,1-2,2-3 --edge-weights 1,1,1 --required-edges 0,2 --bound 6" diff --git a/problemreductions-cli/tests/cli_tests.rs b/problemreductions-cli/tests/cli_tests.rs index fa0fc4ba0..0e15e3071 100644 --- a/problemreductions-cli/tests/cli_tests.rs +++ b/problemreductions-cli/tests/cli_tests.rs @@ -1449,6 +1449,49 @@ fn test_create_kth_best_spanning_tree_rejects_zero_k() { ); } +#[test] +fn test_create_kth_best_spanning_tree_help_uses_edge_weights() { + let output = pred() + .args(["create", "KthBestSpanningTree"]) + .output() + .unwrap(); + assert!(!output.status.success()); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + stderr.contains("--edge-weights"), + "expected edge-weight help, got: {stderr}" + ); + assert!( + !stderr.contains("\n --weights"), + "vertex-weight flag should not be suggested, got: {stderr}" + ); +} + +#[test] +fn test_create_kth_best_spanning_tree_rejects_vertex_weights_flag() { + let output = pred() + .args([ + "create", + "KthBestSpanningTree", + "--graph", + "0-1,0-2,1-2", + "--weights", + "9,9,9", + "--k", + "1", + "--bound", + "3", + ]) + .output() + .unwrap(); + assert!(!output.status.success()); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + stderr.contains("--edge-weights"), + "expected guidance toward edge weights, got: {stderr}" + ); +} + #[test] fn test_evaluate_wrong_config_length() { let problem_file = std::env::temp_dir().join("pred_test_eval_wrong_len.json");