From 74c901e665dd5525eac739143258692ce66c2aff Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Sat, 21 Mar 2026 18:47:48 +0800 Subject: [PATCH 1/3] Add plan for #240: [Model] BottleneckTravelingSalesman --- ...026-03-21-bottleneck-traveling-salesman.md | 321 ++++++++++++++++++ 1 file changed, 321 insertions(+) create mode 100644 docs/plans/2026-03-21-bottleneck-traveling-salesman.md diff --git a/docs/plans/2026-03-21-bottleneck-traveling-salesman.md b/docs/plans/2026-03-21-bottleneck-traveling-salesman.md new file mode 100644 index 000000000..f43e1fc95 --- /dev/null +++ b/docs/plans/2026-03-21-bottleneck-traveling-salesman.md @@ -0,0 +1,321 @@ +# Bottleneck Traveling Salesman Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. +> +> **Repo skill reference:** Follow [.claude/skills/add-model/SKILL.md](../../.claude/skills/add-model/SKILL.md) Steps 1-7. Batch 1 below covers add-model Steps 1-5.5. Batch 2 covers add-model Step 6. + +**Goal:** Add the `BottleneckTravelingSalesman` graph model from issue #240 as a fixed `SimpleGraph + i32` optimization problem, with brute-force support, CLI creation, canonical example data, and paper documentation. + +**Architecture:** Reuse the existing edge-subset representation and Hamiltonian-cycle validation used by `TravelingSalesman`, but change the objective aggregation from sum to max. Keep the new model non-generic as requested by the issue comments (`SimpleGraph` plus `i32` only), register it through the schema/variant inventory, and wire its canonical K5 example through the graph example-spec chain so both the CLI example flow and the paper can load the same source of truth. + +**Tech Stack:** Rust workspace, inventory registry metadata, `pred` CLI, Typst paper, brute-force solver, GitHub issue #240 canonical example. + +--- + +## Batch 1: Model, Registration, CLI, Tests + +### Task 1: Write the model tests first + +**Files:** +- Create: `src/unit_tests/models/graph/bottleneck_traveling_salesman.rs` +- Reference: `src/unit_tests/models/graph/traveling_salesman.rs` +- Reference: `src/models/graph/traveling_salesman.rs` + +**Step 1: Write failing tests for the new model** + +Create `src/unit_tests/models/graph/bottleneck_traveling_salesman.rs` with tests that assume a future `BottleneckTravelingSalesman` type exists and covers the exact issue semantics: + +- `test_bottleneck_traveling_salesman_creation_and_size_getters` + - Construct the issue K5 graph with 10 edges and the issue weights `[5, 4, 4, 5, 4, 1, 2, 1, 5, 4]` + - Assert `graph().num_vertices() == 5` + - Assert `num_vertices() == 5` + - Assert `num_edges() == 10` + - Assert `dims() == vec![2; 10]` +- `test_bottleneck_traveling_salesman_evaluate_valid_and_invalid_configs` + - Valid config for the issue optimum: `[0, 1, 1, 0, 1, 0, 1, 0, 0, 1]` corresponding to edges `(0,2), (0,3), (1,2), (1,4), (3,4)` + - Assert `evaluate(valid) == SolutionSize::Valid(4)` + - Assert a degree-violating config and a disconnected-subtour config both return `SolutionSize::Invalid` +- `test_bottleneck_traveling_salesman_direction` + - Assert `direction() == Direction::Minimize` +- `test_bottleneck_traveling_salesman_solver_unique_optimum` + - Use `BruteForce::find_all_best` + - Assert exactly one best config + - Assert it matches the issue optimum config + - Assert its objective is `SolutionSize::Valid(4)` +- `test_bottleneck_traveling_salesman_serialization` + - Round-trip through `serde_json` + - Assert graph size and weights are preserved +- `test_bottleneck_traveling_salesman_paper_example` + - Use the same K5 instance and assert the worked-example config is valid and optimal + - Assert every best solution found by brute force has bottleneck `4` + +**Step 2: Run the new tests to verify RED** + +Run: + +```bash +cargo test test_bottleneck_traveling_salesman_creation_and_size_getters --lib +``` + +Expected: FAIL at compile time because `BottleneckTravelingSalesman` does not exist yet. + +**Step 3: Do not implement here** + +Stop after the failure. The implementation belongs in Task 2. + +### Task 2: Implement the model and make the tests pass + +**Files:** +- Create: `src/models/graph/bottleneck_traveling_salesman.rs` +- Modify: `src/models/graph/mod.rs` + +**Step 1: Implement the new model file** + +Create `src/models/graph/bottleneck_traveling_salesman.rs` with: + +- `inventory::submit!` schema entry: + - `name: "BottleneckTravelingSalesman"` + - `display_name: "Bottleneck Traveling Salesman"` + - `aliases: &[]` + - `dimensions: &[]` + - `fields` for `graph: SimpleGraph` and `edge_weights: Vec` +- `#[derive(Debug, Clone, Serialize, Deserialize)]` +- struct: + +```rust +pub struct BottleneckTravelingSalesman { + graph: SimpleGraph, + edge_weights: Vec, +} +``` + +- inherent methods: + - `new(graph: SimpleGraph, edge_weights: Vec)` + - `graph(&self) -> &SimpleGraph` + - `weights(&self) -> Vec` + - `set_weights(&mut self, weights: Vec)` + - `edges(&self) -> Vec<(usize, usize, i32)>` + - `num_vertices(&self) -> usize` + - `num_edges(&self) -> usize` + - `is_weighted(&self) -> bool` returning `true` + - `is_valid_solution(&self, config: &[usize]) -> bool` +- `Problem` impl: + - `const NAME = "BottleneckTravelingSalesman"` + - `type Metric = SolutionSize` + - `variant() -> crate::variant_params![]` + - `dims() -> vec![2; self.graph.num_edges()]` + - `evaluate()`: + - reject non-Hamiltonian-cycle configs + - reuse `super::traveling_salesman::is_hamiltonian_cycle(&self.graph, &selected)` + - compute the maximum selected edge weight + - return `SolutionSize::Valid(max_weight)` for feasible configs +- `OptimizationProblem` impl with `Direction::Minimize` +- canonical example spec under `#[cfg(feature = "example-db")]` using the issue K5 instance and optimum config `[0, 1, 1, 0, 1, 0, 1, 0, 0, 1]` +- `crate::declare_variants! { default opt BottleneckTravelingSalesman => "num_vertices^2 * 2^num_vertices", }` +- the unit-test link at the bottom + +**Step 2: Register the module locally** + +Update `src/models/graph/mod.rs` to: + +- add the module doc bullet for `BottleneckTravelingSalesman` +- add `pub(crate) mod bottleneck_traveling_salesman;` +- add `pub use bottleneck_traveling_salesman::BottleneckTravelingSalesman;` +- extend `canonical_model_example_specs()` with `bottleneck_traveling_salesman::canonical_model_example_specs()` + +**Step 3: Run the targeted tests to verify GREEN** + +Run: + +```bash +cargo test bottleneck_traveling_salesman --lib +``` + +Expected: PASS for the new model tests. + +**Step 4: Refactor only if needed** + +Keep the model self-contained unless test failures prove a shared helper needs adjustment. Do not generalize `TravelingSalesman` or add type parameters. + +### Task 3: Register the model across exports and catalog surfaces + +**Files:** +- Modify: `src/models/mod.rs` +- Modify: `src/lib.rs` + +**Step 1: Add crate-level exports** + +Update: + +- `src/models/mod.rs` re-export list to include `BottleneckTravelingSalesman` +- `src/lib.rs` graph-model re-exports and `prelude` re-exports to include `BottleneckTravelingSalesman` + +**Step 2: Run a focused compile check** + +Run: + +```bash +cargo test test_bottleneck_traveling_salesman_direction --lib +``` + +Expected: PASS and no unresolved-export errors. + +### Task 4: Add CLI create support for the new model + +**Files:** +- Modify: `problemreductions-cli/src/commands/create.rs` +- Modify: `problemreductions-cli/src/cli.rs` + +**Step 1: Extend the CLI problem groups** + +Update `problemreductions-cli/src/commands/create.rs` in all existing edge-weight graph groupings so `BottleneckTravelingSalesman` is treated like `TravelingSalesman`: + +- problem example args list near the top-level example matcher +- `all_data_flags_empty()` or equivalent edge-weight validation grouping if the file requires it +- explicit create handler match arm for graph + `--edge-weights` +- random instance generation arm + +The concrete constructor in both explicit and random creation paths should be: + +```rust +BottleneckTravelingSalesman::new(graph, edge_weights) +``` + +Import the new model type if the file’s `use problemreductions::models::graph::{...}` list needs it. + +**Step 2: Update help text** + +In `problemreductions-cli/src/cli.rs`, add `BottleneckTravelingSalesman` to the edge-weight graph help line, for example by expanding: + +```text +MaxCut, MaxMatching, TSP, BottleneckTravelingSalesman --graph, --edge-weights +``` + +Do not invent a short alias such as `BTSP`; the issue and registry metadata do not establish one. + +**Step 3: Verify RED/GREEN with a CLI smoke test** + +Run: + +```bash +cargo run -p problemreductions-cli -- create BottleneckTravelingSalesman --graph 0-1,0-2,0-3,0-4,1-2,1-3,1-4,2-3,2-4,3-4 --edge-weights 5,4,4,5,4,1,2,1,5,4 +``` + +Expected: command succeeds and prints serialized JSON for the new problem without unknown-problem or flag-validation errors. + +### Task 5: Finish verification for Batch 1 + +**Files:** +- Modify only if previous tasks exposed missing imports or example wiring errors + +**Step 1: Run the graph/example-db focused checks** + +Run: + +```bash +cargo test bottleneck_traveling_salesman --lib +cargo test example_db --lib +``` + +Expected: PASS. The example-db test proves the canonical example spec is wired into the graph chain correctly. + +**Step 2: Inspect git diff for Batch 1 scope** + +Run: + +```bash +git diff -- src/models/graph src/models/mod.rs src/lib.rs problemreductions-cli/src/commands/create.rs problemreductions-cli/src/cli.rs +``` + +Expected: only the new model, exports, CLI hooks, and related docs/comments. + +## Batch 2: Paper and Reference Updates + +### Task 6: Add the paper entry and bibliography support + +**Files:** +- Modify: `docs/paper/reductions.typ` +- Modify: `docs/paper/references.bib` + +**Step 1: Add the missing bibliography entry if needed** + +Check whether `Gilmore1964` already exists in `docs/paper/references.bib`. If not, add a BibTeX entry for: + +- P. C. Gilmore and R. E. Gomory +- "Sequencing a one state-variable machine: a solvable case of the traveling salesman problem" +- Operations Research 12 (1964), 655-679 + +Reuse the existing `heldkarp1962` entry already present in the repo. + +**Step 2: Add the display name** + +Add: + +```typst +"BottleneckTravelingSalesman": [Bottleneck Traveling Salesman], +``` + +to the `display-name` dictionary in `docs/paper/reductions.typ`. + +**Step 3: Add the `problem-def` entry** + +Place a new `problem-def("BottleneckTravelingSalesman")` entry near `TravelingSalesman`. The entry should: + +- define the optimization version explicitly +- mention the decision version from Garey and Johnson as the threshold form it subsumes +- cite Held-Karp for the `O(n^2 2^n)` exact algorithm discussion +- cite Gilmore-Gomory only as the polynomial-time special-case remark from the G&J note +- load the canonical example from the example-db path rather than duplicating raw data +- use the issue’s K5 example and explain: + - the unique optimum bottleneck is `4` + - the optimal BTSP tour differs from the minimum-total-weight TSP tour + +**Step 4: Build the paper** + +Run: + +```bash +make paper +``` + +Expected: PASS with no Typst errors or missing-citation failures. + +## Final Verification + +### Task 7: Run the repo-required verification before handoff + +**Files:** +- No intentional edits in this task + +**Step 1: Run the full verification commands** + +Run: + +```bash +make test +make clippy +``` + +Expected: PASS. + +If `make paper` generated ignored exports under `docs/src/reductions/`, confirm they remain unstaged. + +**Step 2: Manual CLI sanity check** + +Run: + +```bash +cargo run -p problemreductions-cli -- create --example BottleneckTravelingSalesman +``` + +Expected: the canonical example loads successfully through the example-db pathway. + +**Step 3: Record implementation notes for the PR summary** + +Capture: + +- files added/modified +- whether any shared helper had to move beyond `traveling_salesman::is_hamiltonian_cycle` +- whether the paper entry deviated from the initial structure because of Typst/example-db constraints + +These notes feed the implementation-summary PR comment required by `issue-to-pr`. From d11653a00483a0d26d41231fcc97f610390e4a57 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Sat, 21 Mar 2026 19:12:10 +0800 Subject: [PATCH 2/3] Implement #240: [Model] BottleneckTravelingSalesman --- docs/paper/reductions.typ | 91 +++++++++ docs/paper/references.bib | 11 ++ problemreductions-cli/src/cli.rs | 2 +- problemreductions-cli/src/commands/create.rs | 25 ++- src/lib.rs | 9 +- .../graph/bottleneck_traveling_salesman.rs | 173 ++++++++++++++++++ src/models/graph/mod.rs | 4 + src/models/mod.rs | 13 +- .../graph/bottleneck_traveling_salesman.rs | 135 ++++++++++++++ 9 files changed, 446 insertions(+), 17 deletions(-) create mode 100644 src/models/graph/bottleneck_traveling_salesman.rs create mode 100644 src/unit_tests/models/graph/bottleneck_traveling_salesman.rs diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index eb6e854d8..100c40906 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -81,6 +81,7 @@ "KClique": [$k$-Clique], "MinimumDominatingSet": [Minimum Dominating Set], "MaximumMatching": [Maximum Matching], + "BottleneckTravelingSalesman": [Bottleneck Traveling Salesman], "TravelingSalesman": [Traveling Salesman], "MaximumClique": [Maximum Clique], "MaximumSetPacking": [Maximum Set Packing], @@ -1175,6 +1176,96 @@ is feasible: each set induces a connected subgraph, the component weights are $2 ] } +#{ + let x = load-model-example("BottleneckTravelingSalesman") + let nv = graph-num-vertices(x.instance) + let edges = x.instance.graph.edges + let ew = x.instance.edge_weights + let sol = (config: x.optimal_config, metric: x.optimal_value) + let tour-edges = sol.config.enumerate().filter(((i, v)) => v == 1).map(((i, _)) => edges.at(i)) + let bottleneck = sol.metric.Valid + let tour-weights = tour-edges.map(((u, v)) => { + let idx = edges.position(e => e == (u, v) or e == (v, u)) + int(ew.at(idx)) + }) + let tour-total = tour-weights.sum() + let tour-order = (0,) + let remaining = tour-edges + for _ in range(nv - 1) { + let curr = tour-order.last() + let next-edge = remaining.find(e => e.at(0) == curr or e.at(1) == curr) + let next-v = if next-edge.at(0) == curr { next-edge.at(1) } else { next-edge.at(0) } + tour-order.push(next-v) + remaining = remaining.filter(e => e != next-edge) + } + let tsp-order = (0, 2, 3, 1, 4) + let tsp-total = 13 + let tsp-bottleneck = 5 + let weight-labels = edges.map(((u, v)) => { + let idx = edges.position(e => e == (u, v)) + (u: u, v: v, w: ew.at(idx)) + }) + [ + #problem-def("BottleneckTravelingSalesman")[ + Given an undirected graph $G=(V,E)$ with edge weights $w: E -> RR$, find an edge set $C subset.eq E$ that forms a cycle visiting every vertex exactly once and minimizes $max_(e in C) w(e)$. + ][ + This min-max variant models routing where the worst leg matters more than the total distance. Garey and Johnson list the threshold decision version as ND24 @garey1979: given a bound $B$, ask whether some Hamiltonian tour has every edge weight at most $B$. The optimization version implemented here subsumes that decision problem. The classical Held--Karp dynamic programming algorithm still yields an exact $O(n^2 dot 2^n)$-time algorithm @heldkarp1962, while Garey and Johnson note the polynomial-time special case of Gilmore and Gomory @gilmore1964. + + *Example.* Consider the complete graph $K_#nv$ with vertices ${#range(nv).map(i => $v_#i$).join(", ")}$ and edge weights #weight-labels.map(l => $w(v_#(l.u), v_#(l.v)) = #(int(l.w))$).join(", "). The unique optimal bottleneck tour is $#tour-order.map(v => $v_#v$).join($arrow$) arrow v_#(tour-order.at(0))$ with edge weights #tour-weights.map(w => str(w)).join(", ") and bottleneck #bottleneck. Its total weight is #tour-total. By contrast, the minimum-total-weight TSP tour $#tsp-order.map(v => $v_#v$).join($arrow$) arrow v_#(tsp-order.at(0))$ has total weight #tsp-total but bottleneck #tsp-bottleneck, because it uses the weight-5 edge $(v_0, v_4)$. Here every other Hamiltonian tour in $K_#nv$ contains a weight-5 edge, so the blue tour is the only one that keeps the maximum edge weight at 4. + + #figure({ + let verts = ((0, 1.8), (1.7, 0.55), (1.05, -1.45), (-1.05, -1.45), (-1.7, 0.55)) + canvas(length: 1cm, { + for (idx, (u, v)) in edges.enumerate() { + let on-tour = tour-edges.any(t => (t.at(0) == u and t.at(1) == v) or (t.at(0) == v and t.at(1) == u)) + let on-tsp-only = (u == 0 and v == 4) or (u == 4 and v == 0) + g-edge( + verts.at(u), + verts.at(v), + stroke: if on-tour { + 2pt + graph-colors.at(0) + } else if on-tsp-only { + 1.5pt + rgb("#c44e38") + } else { + 0.8pt + luma(200) + }, + ) + let mx = (verts.at(u).at(0) + verts.at(v).at(0)) / 2 + let my = (verts.at(u).at(1) + verts.at(v).at(1)) / 2 + let (dx, dy) = if u == 0 and v == 1 { + (0.16, 0.2) + } else if u == 0 and v == 2 { + (0.25, 0.03) + } else if u == 0 and v == 3 { + (-0.25, 0.03) + } else if u == 0 and v == 4 { + (-0.16, 0.2) + } else if u == 1 and v == 2 { + (0.22, -0.05) + } else if u == 1 and v == 3 { + (0.12, -0.18) + } else if u == 1 and v == 4 { + (0, 0.12) + } else if u == 2 and v == 3 { + (0, -0.2) + } else if u == 2 and v == 4 { + (-0.12, -0.18) + } else { + (-0.22, -0.05) + } + draw.content((mx + dx, my + dy), text(7pt, fill: luma(80))[#str(int(ew.at(idx)))]) + } + for (k, pos) in verts.enumerate() { + g-node(pos, name: "v" + str(k), fill: graph-colors.at(0), label: text(fill: white)[$v_#k$]) + } + }) + }, + caption: [The $K_5$ bottleneck-TSP instance. Blue edges form the unique optimal bottleneck tour; the red edge $(v_0, v_4)$ is the weight-5 edge used by the minimum-total-weight TSP tour.], + ) + ] + ] +} + #{ let x = load-model-example("TravelingSalesman") let nv = graph-num-vertices(x.instance) diff --git a/docs/paper/references.bib b/docs/paper/references.bib index fd8106120..cd86cb361 100644 --- a/docs/paper/references.bib +++ b/docs/paper/references.bib @@ -449,6 +449,17 @@ @article{heldkarp1962 doi = {10.1137/0110015} } +@article{gilmore1964, + author = {P. C. Gilmore and R. E. Gomory}, + title = {Sequencing a One State-Variable Machine: A Solvable Case of the Traveling Salesman Problem}, + journal = {Operations Research}, + volume = {12}, + number = {5}, + pages = {655--679}, + year = {1964}, + doi = {10.1287/opre.12.5.655} +} + @article{beigel2005, author = {Richard Beigel and David Eppstein}, title = {3-Coloring in Time {$O(1.3289^n)$}}, diff --git a/problemreductions-cli/src/cli.rs b/problemreductions-cli/src/cli.rs index 5a26d50c5..95be4bea9 100644 --- a/problemreductions-cli/src/cli.rs +++ b/problemreductions-cli/src/cli.rs @@ -216,7 +216,7 @@ TIP: Run `pred create ` (no other flags) to see problem-specific help. Flags by problem type: MIS, MVC, MaxClique, MinDomSet --graph, --weights - MaxCut, MaxMatching, TSP --graph, --edge-weights + MaxCut, MaxMatching, TSP, BottleneckTravelingSalesman --graph, --edge-weights ShortestWeightConstrainedPath --graph, --edge-lengths, --edge-weights, --source-vertex, --target-vertex, --length-bound, --weight-bound MaximalIS --graph, --weights SAT, NAESAT --num-vars, --clauses diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index 5947fb94b..67bdd98ed 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -536,7 +536,7 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str { } "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" => { + "BottleneckTravelingSalesman" | "MaxCut" | "MaximumMatching" | "TravelingSalesman" => { "--graph 0-1,1-2,2-3 --edge-weights 1,1,1" } "ShortestWeightConstrainedPath" => { @@ -652,7 +652,12 @@ 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" + "BottleneckTravelingSalesman" + | "KthBestSpanningTree" + | "MaxCut" + | "MaximumMatching" + | "TravelingSalesman" + | "RuralPostman" ) } @@ -1440,7 +1445,7 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { } // Graph problems with edge weights - "MaxCut" | "MaximumMatching" | "TravelingSalesman" => { + "BottleneckTravelingSalesman" | "MaxCut" | "MaximumMatching" | "TravelingSalesman" => { reject_vertex_weights_for_edge_weight_problem(args, canonical, None)?; let (graph, _) = parse_graph(args).map_err(|e| { anyhow::anyhow!( @@ -1450,6 +1455,7 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { })?; let edge_weights = parse_edge_weights(args, graph.num_edges())?; let data = match canonical { + "BottleneckTravelingSalesman" => ser(BottleneckTravelingSalesman::new(graph, edge_weights))?, "MaxCut" => ser(MaxCut::new(graph, edge_weights))?, "MaximumMatching" => ser(MaximumMatching::new(graph, edge_weights))?, "TravelingSalesman" => ser(TravelingSalesman::new(graph, edge_weights))?, @@ -5070,7 +5076,7 @@ fn create_random( } // Graph problems with edge weights - "MaxCut" | "MaximumMatching" | "TravelingSalesman" => { + "BottleneckTravelingSalesman" | "MaxCut" | "MaximumMatching" | "TravelingSalesman" => { let edge_prob = args.edge_prob.unwrap_or(0.5); if !(0.0..=1.0).contains(&edge_prob) { bail!("--edge-prob must be between 0.0 and 1.0"); @@ -5078,8 +5084,14 @@ fn create_random( let graph = util::create_random_graph(num_vertices, edge_prob, args.seed); let num_edges = graph.num_edges(); let edge_weights = vec![1i32; num_edges]; - let variant = variant_map(&[("graph", "SimpleGraph"), ("weight", "i32")]); + let variant = match canonical { + "BottleneckTravelingSalesman" => variant_map(&[]), + _ => variant_map(&[("graph", "SimpleGraph"), ("weight", "i32")]), + }; let data = match canonical { + "BottleneckTravelingSalesman" => { + ser(BottleneckTravelingSalesman::new(graph, edge_weights))? + } "MaxCut" => ser(MaxCut::new(graph, edge_weights))?, "MaximumMatching" => ser(MaximumMatching::new(graph, edge_weights))?, "TravelingSalesman" => ser(TravelingSalesman::new(graph, edge_weights))?, @@ -5187,7 +5199,8 @@ fn create_random( "Random generation is not supported for {canonical}. \ Supported: graph-based problems (MIS, MVC, MaxCut, MaxClique, \ MaximumMatching, MinimumDominatingSet, SpinGlass, KColoring, KClique, TravelingSalesman, \ - SteinerTreeInGraphs, HamiltonianCircuit, SteinerTree, OptimalLinearArrangement, HamiltonianPath, GeneralizedHex)" + BottleneckTravelingSalesman, SteinerTreeInGraphs, HamiltonianCircuit, SteinerTree, \ + OptimalLinearArrangement, HamiltonianPath, GeneralizedHex)" ), }; diff --git a/src/lib.rs b/src/lib.rs index 9c8bd8664..91c37bc35 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -49,10 +49,11 @@ pub mod prelude { }; pub use crate::models::graph::{ AcyclicPartition, BalancedCompleteBipartiteSubgraph, BicliqueCover, BiconnectivityAugmentation, - BoundedComponentSpanningForest, DirectedTwoCommodityIntegralFlow, GeneralizedHex, - GraphPartitioning, HamiltonianCircuit, HamiltonianPath, IsomorphicSpanningTree, KClique, - KthBestSpanningTree, LengthBoundedDisjointPaths, SpinGlass, SteinerTree, - StrongConnectivityAugmentation, SubgraphIsomorphism, + BottleneckTravelingSalesman, BoundedComponentSpanningForest, + DirectedTwoCommodityIntegralFlow, GeneralizedHex, GraphPartitioning, HamiltonianCircuit, + HamiltonianPath, IsomorphicSpanningTree, KClique, KthBestSpanningTree, + LengthBoundedDisjointPaths, SpinGlass, SteinerTree, StrongConnectivityAugmentation, + SubgraphIsomorphism, }; pub use crate::models::graph::{ KColoring, MaxCut, MaximalIS, MaximumClique, MaximumIndependentSet, MaximumMatching, diff --git a/src/models/graph/bottleneck_traveling_salesman.rs b/src/models/graph/bottleneck_traveling_salesman.rs new file mode 100644 index 000000000..e86b7c95e --- /dev/null +++ b/src/models/graph/bottleneck_traveling_salesman.rs @@ -0,0 +1,173 @@ +//! Bottleneck Traveling Salesman problem implementation. +//! +//! The Bottleneck Traveling Salesman problem asks for a Hamiltonian cycle +//! minimizing the maximum selected edge weight. + +use crate::registry::{FieldInfo, ProblemSchemaEntry}; +use crate::topology::{Graph, SimpleGraph}; +use crate::traits::{OptimizationProblem, Problem}; +use crate::types::{Direction, SolutionSize}; +use serde::{Deserialize, Serialize}; + +inventory::submit! { + ProblemSchemaEntry { + name: "BottleneckTravelingSalesman", + display_name: "Bottleneck Traveling Salesman", + aliases: &[], + dimensions: &[], + module_path: module_path!(), + description: "Find a Hamiltonian cycle minimizing the maximum selected edge weight", + fields: &[ + FieldInfo { name: "graph", type_name: "SimpleGraph", description: "The underlying graph G=(V,E)" }, + FieldInfo { name: "edge_weights", type_name: "Vec", description: "Edge weights w: E -> Z" }, + ], + } +} + +/// The Bottleneck Traveling Salesman problem on a simple weighted graph. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct BottleneckTravelingSalesman { + graph: SimpleGraph, + edge_weights: Vec, +} + +impl BottleneckTravelingSalesman { + /// Create a BottleneckTravelingSalesman problem from a graph with edge weights. + pub fn new(graph: SimpleGraph, edge_weights: Vec) -> Self { + assert_eq!( + edge_weights.len(), + graph.num_edges(), + "edge_weights length must match num_edges" + ); + Self { + graph, + edge_weights, + } + } + + /// Get a reference to the underlying graph. + pub fn graph(&self) -> &SimpleGraph { + &self.graph + } + + /// Get the weights for the problem. + pub fn weights(&self) -> Vec { + self.edge_weights.clone() + } + + /// Set new weights for the problem. + pub fn set_weights(&mut self, weights: Vec) { + assert_eq!(weights.len(), self.graph.num_edges()); + self.edge_weights = weights; + } + + /// Get all edges with their weights. + pub fn edges(&self) -> Vec<(usize, usize, i32)> { + self.graph + .edges() + .into_iter() + .zip(self.edge_weights.iter().copied()) + .map(|((u, v), w)| (u, v, w)) + .collect() + } + + /// Get the number of vertices in the underlying graph. + pub fn num_vertices(&self) -> usize { + self.graph.num_vertices() + } + + /// Get the number of edges in the underlying graph. + pub fn num_edges(&self) -> usize { + self.graph.num_edges() + } + + /// This model is always weighted. + pub fn is_weighted(&self) -> bool { + true + } + + /// Check if a configuration is a valid Hamiltonian cycle. + pub fn is_valid_solution(&self, config: &[usize]) -> bool { + if config.len() != self.graph.num_edges() { + return false; + } + let selected: Vec = config.iter().map(|&s| s == 1).collect(); + super::traveling_salesman::is_hamiltonian_cycle(&self.graph, &selected) + } +} + +impl Problem for BottleneckTravelingSalesman { + const NAME: &'static str = "BottleneckTravelingSalesman"; + type Metric = SolutionSize; + + fn variant() -> Vec<(&'static str, &'static str)> { + crate::variant_params![] + } + + fn dims(&self) -> Vec { + vec![2; self.graph.num_edges()] + } + + fn evaluate(&self, config: &[usize]) -> SolutionSize { + if config.len() != self.graph.num_edges() { + return SolutionSize::Invalid; + } + + let selected: Vec = config.iter().map(|&s| s == 1).collect(); + if !super::traveling_salesman::is_hamiltonian_cycle(&self.graph, &selected) { + return SolutionSize::Invalid; + } + + let bottleneck = config + .iter() + .zip(self.edge_weights.iter()) + .filter_map(|(&selected, &weight)| (selected == 1).then_some(weight)) + .max() + .expect("valid Hamiltonian cycle selects at least one edge"); + + SolutionSize::Valid(bottleneck) + } +} + +impl OptimizationProblem for BottleneckTravelingSalesman { + type Value = i32; + + fn direction(&self) -> Direction { + Direction::Minimize + } +} + +#[cfg(feature = "example-db")] +pub(crate) fn canonical_model_example_specs() -> Vec { + vec![crate::example_db::specs::ModelExampleSpec { + id: "bottleneck_traveling_salesman", + instance: Box::new(BottleneckTravelingSalesman::new( + SimpleGraph::new( + 5, + vec![ + (0, 1), + (0, 2), + (0, 3), + (0, 4), + (1, 2), + (1, 3), + (1, 4), + (2, 3), + (2, 4), + (3, 4), + ], + ), + vec![5, 4, 4, 5, 4, 1, 2, 1, 5, 4], + )), + optimal_config: vec![0, 1, 1, 0, 1, 0, 1, 0, 0, 1], + optimal_value: serde_json::json!({"Valid": 4}), + }] +} + +crate::declare_variants! { + default opt BottleneckTravelingSalesman => "num_vertices^2 * 2^num_vertices", +} + +#[cfg(test)] +#[path = "../../unit_tests/models/graph/bottleneck_traveling_salesman.rs"] +mod tests; diff --git a/src/models/graph/mod.rs b/src/models/graph/mod.rs index c971b7746..fb14589af 100644 --- a/src/models/graph/mod.rs +++ b/src/models/graph/mod.rs @@ -29,6 +29,7 @@ //! - [`BalancedCompleteBipartiteSubgraph`]: Balanced biclique decision problem //! - [`BiconnectivityAugmentation`]: Biconnectivity augmentation with weighted potential edges //! - [`BoundedComponentSpanningForest`]: Partition vertices into bounded-weight connected components +//! - [`BottleneckTravelingSalesman`]: Hamiltonian cycle minimizing the maximum selected edge weight //! - [`MultipleCopyFileAllocation`]: File-copy placement under storage and access costs //! - [`OptimalLinearArrangement`]: Optimal linear arrangement (total edge length at most K) //! - [`MinimumFeedbackArcSet`]: Minimum feedback arc set on directed graphs @@ -47,6 +48,7 @@ pub(crate) mod balanced_complete_bipartite_subgraph; pub(crate) mod acyclic_partition; pub(crate) mod biclique_cover; pub(crate) mod biconnectivity_augmentation; +pub(crate) mod bottleneck_traveling_salesman; pub(crate) mod bounded_component_spanning_forest; pub(crate) mod directed_two_commodity_integral_flow; pub(crate) mod generalized_hex; @@ -90,6 +92,7 @@ pub use acyclic_partition::AcyclicPartition; pub use balanced_complete_bipartite_subgraph::BalancedCompleteBipartiteSubgraph; pub use biclique_cover::BicliqueCover; pub use biconnectivity_augmentation::BiconnectivityAugmentation; +pub use bottleneck_traveling_salesman::BottleneckTravelingSalesman; pub use bounded_component_spanning_forest::BoundedComponentSpanningForest; pub use directed_two_commodity_integral_flow::DirectedTwoCommodityIntegralFlow; pub use generalized_hex::GeneralizedHex; @@ -161,6 +164,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec BottleneckTravelingSalesman { + BottleneckTravelingSalesman::new( + SimpleGraph::new( + 5, + vec![ + (0, 1), + (0, 2), + (0, 3), + (0, 4), + (1, 2), + (1, 3), + (1, 4), + (2, 3), + (2, 4), + (3, 4), + ], + ), + vec![5, 4, 4, 5, 4, 1, 2, 1, 5, 4], + ) +} + +#[test] +fn test_bottleneck_traveling_salesman_creation_and_size_getters() { + let mut problem = k5_btsp(); + + assert_eq!(problem.graph().num_vertices(), 5); + assert_eq!(problem.num_vertices(), 5); + assert_eq!(problem.graph().num_edges(), 10); + assert_eq!(problem.num_edges(), 10); + assert_eq!(problem.dims(), vec![2; 10]); + assert_eq!(problem.num_variables(), 10); + assert_eq!(problem.weights(), vec![5, 4, 4, 5, 4, 1, 2, 1, 5, 4]); + assert_eq!( + problem.edges(), + vec![ + (0, 1, 5), + (0, 2, 4), + (0, 3, 4), + (0, 4, 5), + (1, 2, 4), + (1, 3, 1), + (1, 4, 2), + (2, 3, 1), + (2, 4, 5), + (3, 4, 4), + ] + ); + assert!(problem.is_weighted()); + + problem.set_weights(vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); + assert_eq!(problem.weights(), vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); +} + +#[test] +fn test_bottleneck_traveling_salesman_evaluate_valid_and_invalid() { + let problem = k5_btsp(); + + let valid_cycle = vec![0, 1, 1, 0, 1, 0, 1, 0, 0, 1]; + assert!(problem.is_valid_solution(&valid_cycle)); + assert_eq!(problem.evaluate(&valid_cycle), SolutionSize::Valid(4)); + + let degree_violation = vec![1, 1, 1, 0, 1, 0, 1, 0, 0, 1]; + assert!(!problem.is_valid_solution(°ree_violation)); + assert_eq!(problem.evaluate(°ree_violation), SolutionSize::Invalid); +} + +#[test] +fn test_bottleneck_traveling_salesman_evaluate_disconnected_subtour_invalid() { + let problem = BottleneckTravelingSalesman::new( + SimpleGraph::new( + 6, + vec![(0, 1), (1, 2), (0, 2), (3, 4), (4, 5), (3, 5)], + ), + vec![1, 1, 1, 2, 2, 2], + ); + + let disconnected_subtour = vec![1, 1, 1, 1, 1, 1]; + assert!(!problem.is_valid_solution(&disconnected_subtour)); + assert_eq!(problem.evaluate(&disconnected_subtour), SolutionSize::Invalid); +} + +#[test] +fn test_bottleneck_traveling_salesman_direction() { + let problem = BottleneckTravelingSalesman::new( + SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]), + vec![7, 4, 6], + ); + + assert_eq!(problem.direction(), Direction::Minimize); +} + +#[test] +fn test_bottleneck_traveling_salesman_bruteforce_unique_optimum() { + let problem = k5_btsp(); + let solver = BruteForce::new(); + let best = solver.find_all_best(&problem); + + assert_eq!(best, vec![vec![0, 1, 1, 0, 1, 0, 1, 0, 0, 1]]); + assert_eq!(problem.evaluate(&best[0]), SolutionSize::Valid(4)); +} + +#[test] +fn test_bottleneck_traveling_salesman_serialization() { + let problem = k5_btsp(); + + let json = serde_json::to_string(&problem).unwrap(); + let restored: BottleneckTravelingSalesman = serde_json::from_str(&json).unwrap(); + + assert_eq!(restored.graph(), problem.graph()); + assert_eq!(restored.weights(), problem.weights()); + assert_eq!( + restored.evaluate(&[0, 1, 1, 0, 1, 0, 1, 0, 0, 1]), + SolutionSize::Valid(4) + ); +} + +#[test] +fn test_bottleneck_traveling_salesman_paper_example() { + let problem = k5_btsp(); + let config = vec![0, 1, 1, 0, 1, 0, 1, 0, 0, 1]; + + assert!(problem.is_valid_solution(&config)); + assert_eq!(problem.evaluate(&config), SolutionSize::Valid(4)); + + let solver = BruteForce::new(); + let best = solver.find_all_best(&problem); + assert_eq!(best.len(), 1); + assert_eq!(best[0], config); +} From 01829c77fdff7a0f50bb1ae28873a55f8b4a7a28 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Sat, 21 Mar 2026 19:12:15 +0800 Subject: [PATCH 3/3] chore: remove plan file after implementation --- ...026-03-21-bottleneck-traveling-salesman.md | 321 ------------------ 1 file changed, 321 deletions(-) delete mode 100644 docs/plans/2026-03-21-bottleneck-traveling-salesman.md diff --git a/docs/plans/2026-03-21-bottleneck-traveling-salesman.md b/docs/plans/2026-03-21-bottleneck-traveling-salesman.md deleted file mode 100644 index f43e1fc95..000000000 --- a/docs/plans/2026-03-21-bottleneck-traveling-salesman.md +++ /dev/null @@ -1,321 +0,0 @@ -# Bottleneck Traveling Salesman Implementation Plan - -> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. -> -> **Repo skill reference:** Follow [.claude/skills/add-model/SKILL.md](../../.claude/skills/add-model/SKILL.md) Steps 1-7. Batch 1 below covers add-model Steps 1-5.5. Batch 2 covers add-model Step 6. - -**Goal:** Add the `BottleneckTravelingSalesman` graph model from issue #240 as a fixed `SimpleGraph + i32` optimization problem, with brute-force support, CLI creation, canonical example data, and paper documentation. - -**Architecture:** Reuse the existing edge-subset representation and Hamiltonian-cycle validation used by `TravelingSalesman`, but change the objective aggregation from sum to max. Keep the new model non-generic as requested by the issue comments (`SimpleGraph` plus `i32` only), register it through the schema/variant inventory, and wire its canonical K5 example through the graph example-spec chain so both the CLI example flow and the paper can load the same source of truth. - -**Tech Stack:** Rust workspace, inventory registry metadata, `pred` CLI, Typst paper, brute-force solver, GitHub issue #240 canonical example. - ---- - -## Batch 1: Model, Registration, CLI, Tests - -### Task 1: Write the model tests first - -**Files:** -- Create: `src/unit_tests/models/graph/bottleneck_traveling_salesman.rs` -- Reference: `src/unit_tests/models/graph/traveling_salesman.rs` -- Reference: `src/models/graph/traveling_salesman.rs` - -**Step 1: Write failing tests for the new model** - -Create `src/unit_tests/models/graph/bottleneck_traveling_salesman.rs` with tests that assume a future `BottleneckTravelingSalesman` type exists and covers the exact issue semantics: - -- `test_bottleneck_traveling_salesman_creation_and_size_getters` - - Construct the issue K5 graph with 10 edges and the issue weights `[5, 4, 4, 5, 4, 1, 2, 1, 5, 4]` - - Assert `graph().num_vertices() == 5` - - Assert `num_vertices() == 5` - - Assert `num_edges() == 10` - - Assert `dims() == vec![2; 10]` -- `test_bottleneck_traveling_salesman_evaluate_valid_and_invalid_configs` - - Valid config for the issue optimum: `[0, 1, 1, 0, 1, 0, 1, 0, 0, 1]` corresponding to edges `(0,2), (0,3), (1,2), (1,4), (3,4)` - - Assert `evaluate(valid) == SolutionSize::Valid(4)` - - Assert a degree-violating config and a disconnected-subtour config both return `SolutionSize::Invalid` -- `test_bottleneck_traveling_salesman_direction` - - Assert `direction() == Direction::Minimize` -- `test_bottleneck_traveling_salesman_solver_unique_optimum` - - Use `BruteForce::find_all_best` - - Assert exactly one best config - - Assert it matches the issue optimum config - - Assert its objective is `SolutionSize::Valid(4)` -- `test_bottleneck_traveling_salesman_serialization` - - Round-trip through `serde_json` - - Assert graph size and weights are preserved -- `test_bottleneck_traveling_salesman_paper_example` - - Use the same K5 instance and assert the worked-example config is valid and optimal - - Assert every best solution found by brute force has bottleneck `4` - -**Step 2: Run the new tests to verify RED** - -Run: - -```bash -cargo test test_bottleneck_traveling_salesman_creation_and_size_getters --lib -``` - -Expected: FAIL at compile time because `BottleneckTravelingSalesman` does not exist yet. - -**Step 3: Do not implement here** - -Stop after the failure. The implementation belongs in Task 2. - -### Task 2: Implement the model and make the tests pass - -**Files:** -- Create: `src/models/graph/bottleneck_traveling_salesman.rs` -- Modify: `src/models/graph/mod.rs` - -**Step 1: Implement the new model file** - -Create `src/models/graph/bottleneck_traveling_salesman.rs` with: - -- `inventory::submit!` schema entry: - - `name: "BottleneckTravelingSalesman"` - - `display_name: "Bottleneck Traveling Salesman"` - - `aliases: &[]` - - `dimensions: &[]` - - `fields` for `graph: SimpleGraph` and `edge_weights: Vec` -- `#[derive(Debug, Clone, Serialize, Deserialize)]` -- struct: - -```rust -pub struct BottleneckTravelingSalesman { - graph: SimpleGraph, - edge_weights: Vec, -} -``` - -- inherent methods: - - `new(graph: SimpleGraph, edge_weights: Vec)` - - `graph(&self) -> &SimpleGraph` - - `weights(&self) -> Vec` - - `set_weights(&mut self, weights: Vec)` - - `edges(&self) -> Vec<(usize, usize, i32)>` - - `num_vertices(&self) -> usize` - - `num_edges(&self) -> usize` - - `is_weighted(&self) -> bool` returning `true` - - `is_valid_solution(&self, config: &[usize]) -> bool` -- `Problem` impl: - - `const NAME = "BottleneckTravelingSalesman"` - - `type Metric = SolutionSize` - - `variant() -> crate::variant_params![]` - - `dims() -> vec![2; self.graph.num_edges()]` - - `evaluate()`: - - reject non-Hamiltonian-cycle configs - - reuse `super::traveling_salesman::is_hamiltonian_cycle(&self.graph, &selected)` - - compute the maximum selected edge weight - - return `SolutionSize::Valid(max_weight)` for feasible configs -- `OptimizationProblem` impl with `Direction::Minimize` -- canonical example spec under `#[cfg(feature = "example-db")]` using the issue K5 instance and optimum config `[0, 1, 1, 0, 1, 0, 1, 0, 0, 1]` -- `crate::declare_variants! { default opt BottleneckTravelingSalesman => "num_vertices^2 * 2^num_vertices", }` -- the unit-test link at the bottom - -**Step 2: Register the module locally** - -Update `src/models/graph/mod.rs` to: - -- add the module doc bullet for `BottleneckTravelingSalesman` -- add `pub(crate) mod bottleneck_traveling_salesman;` -- add `pub use bottleneck_traveling_salesman::BottleneckTravelingSalesman;` -- extend `canonical_model_example_specs()` with `bottleneck_traveling_salesman::canonical_model_example_specs()` - -**Step 3: Run the targeted tests to verify GREEN** - -Run: - -```bash -cargo test bottleneck_traveling_salesman --lib -``` - -Expected: PASS for the new model tests. - -**Step 4: Refactor only if needed** - -Keep the model self-contained unless test failures prove a shared helper needs adjustment. Do not generalize `TravelingSalesman` or add type parameters. - -### Task 3: Register the model across exports and catalog surfaces - -**Files:** -- Modify: `src/models/mod.rs` -- Modify: `src/lib.rs` - -**Step 1: Add crate-level exports** - -Update: - -- `src/models/mod.rs` re-export list to include `BottleneckTravelingSalesman` -- `src/lib.rs` graph-model re-exports and `prelude` re-exports to include `BottleneckTravelingSalesman` - -**Step 2: Run a focused compile check** - -Run: - -```bash -cargo test test_bottleneck_traveling_salesman_direction --lib -``` - -Expected: PASS and no unresolved-export errors. - -### Task 4: Add CLI create support for the new model - -**Files:** -- Modify: `problemreductions-cli/src/commands/create.rs` -- Modify: `problemreductions-cli/src/cli.rs` - -**Step 1: Extend the CLI problem groups** - -Update `problemreductions-cli/src/commands/create.rs` in all existing edge-weight graph groupings so `BottleneckTravelingSalesman` is treated like `TravelingSalesman`: - -- problem example args list near the top-level example matcher -- `all_data_flags_empty()` or equivalent edge-weight validation grouping if the file requires it -- explicit create handler match arm for graph + `--edge-weights` -- random instance generation arm - -The concrete constructor in both explicit and random creation paths should be: - -```rust -BottleneckTravelingSalesman::new(graph, edge_weights) -``` - -Import the new model type if the file’s `use problemreductions::models::graph::{...}` list needs it. - -**Step 2: Update help text** - -In `problemreductions-cli/src/cli.rs`, add `BottleneckTravelingSalesman` to the edge-weight graph help line, for example by expanding: - -```text -MaxCut, MaxMatching, TSP, BottleneckTravelingSalesman --graph, --edge-weights -``` - -Do not invent a short alias such as `BTSP`; the issue and registry metadata do not establish one. - -**Step 3: Verify RED/GREEN with a CLI smoke test** - -Run: - -```bash -cargo run -p problemreductions-cli -- create BottleneckTravelingSalesman --graph 0-1,0-2,0-3,0-4,1-2,1-3,1-4,2-3,2-4,3-4 --edge-weights 5,4,4,5,4,1,2,1,5,4 -``` - -Expected: command succeeds and prints serialized JSON for the new problem without unknown-problem or flag-validation errors. - -### Task 5: Finish verification for Batch 1 - -**Files:** -- Modify only if previous tasks exposed missing imports or example wiring errors - -**Step 1: Run the graph/example-db focused checks** - -Run: - -```bash -cargo test bottleneck_traveling_salesman --lib -cargo test example_db --lib -``` - -Expected: PASS. The example-db test proves the canonical example spec is wired into the graph chain correctly. - -**Step 2: Inspect git diff for Batch 1 scope** - -Run: - -```bash -git diff -- src/models/graph src/models/mod.rs src/lib.rs problemreductions-cli/src/commands/create.rs problemreductions-cli/src/cli.rs -``` - -Expected: only the new model, exports, CLI hooks, and related docs/comments. - -## Batch 2: Paper and Reference Updates - -### Task 6: Add the paper entry and bibliography support - -**Files:** -- Modify: `docs/paper/reductions.typ` -- Modify: `docs/paper/references.bib` - -**Step 1: Add the missing bibliography entry if needed** - -Check whether `Gilmore1964` already exists in `docs/paper/references.bib`. If not, add a BibTeX entry for: - -- P. C. Gilmore and R. E. Gomory -- "Sequencing a one state-variable machine: a solvable case of the traveling salesman problem" -- Operations Research 12 (1964), 655-679 - -Reuse the existing `heldkarp1962` entry already present in the repo. - -**Step 2: Add the display name** - -Add: - -```typst -"BottleneckTravelingSalesman": [Bottleneck Traveling Salesman], -``` - -to the `display-name` dictionary in `docs/paper/reductions.typ`. - -**Step 3: Add the `problem-def` entry** - -Place a new `problem-def("BottleneckTravelingSalesman")` entry near `TravelingSalesman`. The entry should: - -- define the optimization version explicitly -- mention the decision version from Garey and Johnson as the threshold form it subsumes -- cite Held-Karp for the `O(n^2 2^n)` exact algorithm discussion -- cite Gilmore-Gomory only as the polynomial-time special-case remark from the G&J note -- load the canonical example from the example-db path rather than duplicating raw data -- use the issue’s K5 example and explain: - - the unique optimum bottleneck is `4` - - the optimal BTSP tour differs from the minimum-total-weight TSP tour - -**Step 4: Build the paper** - -Run: - -```bash -make paper -``` - -Expected: PASS with no Typst errors or missing-citation failures. - -## Final Verification - -### Task 7: Run the repo-required verification before handoff - -**Files:** -- No intentional edits in this task - -**Step 1: Run the full verification commands** - -Run: - -```bash -make test -make clippy -``` - -Expected: PASS. - -If `make paper` generated ignored exports under `docs/src/reductions/`, confirm they remain unstaged. - -**Step 2: Manual CLI sanity check** - -Run: - -```bash -cargo run -p problemreductions-cli -- create --example BottleneckTravelingSalesman -``` - -Expected: the canonical example loads successfully through the example-db pathway. - -**Step 3: Record implementation notes for the PR summary** - -Capture: - -- files added/modified -- whether any shared helper had to move beyond `traveling_salesman::is_hamiltonian_cycle` -- whether the paper entry deviated from the initial structure because of Typst/example-db constraints - -These notes feed the implementation-summary PR comment required by `issue-to-pr`.