From d8cfc5b4a4a332277a5b7b48af9bd1319f492f58 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Sat, 21 Mar 2026 20:58:38 +0800 Subject: [PATCH 1/4] Add plan for #245: [Model] StackerCrane --- docs/plans/2026-03-21-stacker-crane.md | 416 +++++++++++++++++++++++++ 1 file changed, 416 insertions(+) create mode 100644 docs/plans/2026-03-21-stacker-crane.md diff --git a/docs/plans/2026-03-21-stacker-crane.md b/docs/plans/2026-03-21-stacker-crane.md new file mode 100644 index 000000000..3d178069a --- /dev/null +++ b/docs/plans/2026-03-21-stacker-crane.md @@ -0,0 +1,416 @@ +# StackerCrane Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Add the `[Model] StackerCrane` decision problem as a new `misc` model with canonical example-db support, `pred create` support for mixed-graph inputs, and a paper `problem-def` entry based on issue #245's corrected hourglass example. + +**Architecture:** Implement `StackerCrane` as a no-variant satisfaction problem whose configuration is a permutation of the required directed arcs. `evaluate()` should reject non-permutations, then compute the closed-walk length by traversing the required arcs in the chosen order and inserting shortest-path connectors through a mixed graph formed from directed arcs plus bidirectional undirected edges. Keep the model self-contained in `src/models/misc/stacker_crane.rs`; do not introduce a new topology type for this issue. + +**Tech Stack:** Rust workspace (`problemreductions`, `problemreductions-cli`), serde/inventory registry metadata, existing brute-force solver, Typst paper in `docs/paper/reductions.typ`. + +--- + +## Constraints And Notes + +- Treat the issue comments as the approved design basis. The latest `fix-issue` changelog supersedes the original vague encoding. +- This issue currently has no open companion rule issue mentioning `StackerCrane`. The implementation should proceed, but the PR body must carry an orphan-model warning. +- Keep `StackerCrane` as a **decision** problem with `Metric = bool`. Do not convert it to an optimization model in this PR. +- Use the issue's hourglass instance and satisfying permutation `[0, 2, 1, 4, 3]` as the canonical example and paper example. +- Do not add a manual alias in `problemreductions-cli/src/problem_name.rs`; alias resolution is registry-backed in this checkout. If an alias is added at all, it must come from `ProblemSchemaEntry.aliases`. Prefer no short alias here because `SCP` is ambiguous. +- Keep the paper batch separate from implementation so it runs after the example-db/model wiring is finished. + +## Batch Layout + +- **Batch 1:** Add-model Steps 1-5.5 + - Model implementation, registry/module wiring, canonical example, CLI creation, tests, verification. +- **Batch 2:** Add-model Step 6 + - Paper citations and `problem-def`, then `make paper`. + +### Task 1: Write The Failing Model Tests First + +**Files:** +- Create: `src/unit_tests/models/misc/stacker_crane.rs` +- Modify: `src/models/misc/mod.rs` +- Modify: `src/models/mod.rs` +- Modify: `src/lib.rs` +- Test: `src/unit_tests/models/misc/stacker_crane.rs` + +**Step 1: Write failing tests for the intended public API** + +Add a new test file with these initial tests: + +- `test_stacker_crane_creation_and_metadata` + - Construct the issue's hourglass instance. + - Assert `num_vertices() == 6`, `num_arcs() == 5`, `num_edges() == 7`, `bound() == 20`. + - Assert `dims() == vec![5; 5]`. + - Assert `Problem::NAME == "StackerCrane"` and `Problem::variant().is_empty()`. +- `test_stacker_crane_rejects_non_permutations_and_wrong_lengths` + - Reject duplicate arc indices, out-of-range indices, and wrong config length. +- `test_stacker_crane_issue_witness_and_tighter_bound` + - Assert `[0, 2, 1, 4, 3]` evaluates to `true` for `B = 20`. + - Assert the same instance with `B = 19` evaluates to `false`. +- `test_stacker_crane_small_solver_instance` + - Use a tiny 2-arc instance with search space small enough for brute force and assert `BruteForce::find_satisfying()` returns a valid permutation. +- `test_stacker_crane_serialization_round_trip` + - Round-trip serde JSON and re-check the witness. +- `test_stacker_crane_is_available_in_prelude` + - Use `crate::prelude::StackerCrane` to ensure the export is actually wired. + +In the same task, add the module/re-export placeholders in: +- `src/models/misc/mod.rs` +- `src/models/mod.rs` +- `src/lib.rs` + +The test should compile far enough to fail because `StackerCrane` does not exist yet. + +**Step 2: Run the targeted test and verify RED** + +Run: + +```bash +cargo test stacker_crane --lib +``` + +Expected: +- FAIL at compile time with unresolved `StackerCrane` symbols or missing module errors. + +**Step 3: Commit the failing-test scaffold** + +```bash +git add src/unit_tests/models/misc/stacker_crane.rs src/models/misc/mod.rs src/models/mod.rs src/lib.rs +git commit -m "test: add failing StackerCrane model tests" +``` + +### Task 2: Implement The Core `StackerCrane` Model + +**Files:** +- Create: `src/models/misc/stacker_crane.rs` +- Modify: `src/models/misc/mod.rs` +- Modify: `src/models/mod.rs` +- Modify: `src/lib.rs` +- Test: `src/unit_tests/models/misc/stacker_crane.rs` + +**Step 1: Implement the minimal model to satisfy Task 1** + +Create `src/models/misc/stacker_crane.rs` with: + +- `inventory::submit!` `ProblemSchemaEntry`: + - `name = "StackerCrane"` + - `display_name = "Stacker Crane"` + - `aliases = &[]` + - `dimensions = &[]` + - constructor-facing fields: + - `num_vertices: usize` + - `arcs: Vec<(usize, usize)>` + - `edges: Vec<(usize, usize)>` + - `arc_lengths: Vec` + - `edge_lengths: Vec` + - `bound: i32` +- `#[derive(Debug, Clone, Serialize, Deserialize)] pub struct StackerCrane` +- `new(...)` plus `try_new(...) -> Result`: + - lengths must match arc/edge counts + - all endpoints must be `< num_vertices` + - all lengths must be non-negative + - `bound` must be non-negative +- accessors: + - `num_vertices()`, `num_arcs()`, `num_edges()` + - `arcs()`, `edges()`, `arc_lengths()`, `edge_lengths()`, `bound()` +- helper(s): + - permutation validation for configs + - shortest-path routine on the mixed graph with non-negative integer weights + - optional `closed_walk_length(config) -> Option` helper for test readability +- `Problem` impl: + - `type Metric = bool` + - `variant() -> crate::variant_params![]` + - `dims() -> vec![num_arcs; num_arcs]` + - `evaluate()` returns `false` on invalid config or unreachable connector, otherwise checks total length `<= bound` +- `SatisfactionProblem` impl +- `declare_variants! { default sat StackerCrane => "num_vertices * 2^num_arcs", }` +- `canonical_model_example_specs()` behind `#[cfg(feature = "example-db")]` using the issue's hourglass instance and optimal config `[0, 2, 1, 4, 3]` +- test link: + +```rust +#[cfg(test)] +#[path = "../../unit_tests/models/misc/stacker_crane.rs"] +mod tests; +``` + +Wire the new module through: +- `src/models/misc/mod.rs` +- `src/models/mod.rs` +- `src/lib.rs` + +**Step 2: Run the targeted model tests and verify GREEN** + +Run: + +```bash +cargo test stacker_crane --lib +``` + +Expected: +- PASS for the new `StackerCrane` tests. + +**Step 3: Refactor only if needed** + +Allowed cleanups: +- Extract adjacency construction or shortest-path helpers inside `stacker_crane.rs` +- Normalize constructor validation messages + +Do not add new behavior beyond the tests. + +**Step 4: Commit the model implementation** + +```bash +git add src/models/misc/stacker_crane.rs src/models/misc/mod.rs src/models/mod.rs src/lib.rs src/unit_tests/models/misc/stacker_crane.rs +git commit -m "feat: add the StackerCrane model" +``` + +### Task 3: Verify Example-DB Integration For The Canonical Instance + +**Files:** +- Modify: `src/models/misc/stacker_crane.rs` +- Modify: `src/models/misc/mod.rs` +- Test: `src/unit_tests/example_db.rs` + +**Step 1: Add a failing example-db assertion if coverage is missing** + +Add or extend tests so the new example is exercised via the shared example DB. Prefer one targeted assertion in `src/unit_tests/example_db.rs`: + +- `test_find_model_example_stacker_crane` + - Look up `ProblemRef { name: "StackerCrane", variant: BTreeMap::new() }` + - Assert the example exists and stores the expected optimal config `[0, 2, 1, 4, 3]` + +If the generic example-db self-consistency tests already cover everything after registration, keep the new test minimal and focused on discoverability. + +**Step 2: Run the targeted example-db test and verify RED/GREEN** + +Run first after adding the test: + +```bash +cargo test test_find_model_example_stacker_crane --features example-db +``` + +Expected: +- FAIL until the example registration is correct. + +After fixing any missing registration, rerun the same command and expect PASS. + +Then run the generic example-db consistency checks: + +```bash +cargo test model_specs_are_self_consistent --features example-db +``` + +Expected: +- PASS, including the new `StackerCrane` spec. + +**Step 3: Commit the example-db wiring** + +```bash +git add src/models/misc/stacker_crane.rs src/models/misc/mod.rs src/unit_tests/example_db.rs +git commit -m "test: register StackerCrane canonical example" +``` + +### Task 4: Add `pred create` Support For Mixed-Graph Input + +**Files:** +- Modify: `problemreductions-cli/src/commands/create.rs` +- Modify: `problemreductions-cli/src/cli.rs` +- Test: `problemreductions-cli/src/commands/create.rs` +- Test: `problemreductions-cli/src/cli.rs` + +**Step 1: Write failing CLI tests** + +Add CLI tests in `problemreductions-cli/src/commands/create.rs`: + +- `test_create_stacker_crane_json` + - Use: + +```text +pred create StackerCrane --arcs "0>4,2>5,5>1,3>0,4>3" --graph "0-1,1-2,2-3,3-5,4-5,0-3,1-5" --arc-costs 3,4,2,5,3 --edge-lengths 2,1,3,2,1,4,3 --bound 20 --num-vertices 6 +``` + + - Assert the serialized problem type is `StackerCrane` and key fields match the issue instance. +- `test_create_stacker_crane_rejects_mismatched_arc_lengths` + - Expect an error when `--arc-costs` length does not match the arc count. +- `test_create_stacker_crane_rejects_out_of_range_vertices` + - Expect an error when `--num-vertices` is smaller than the largest referenced endpoint. + +Add a small help test in `problemreductions-cli/src/cli.rs` that checks the create help mentions: + +- `StackerCrane --arcs, --graph, --arc-costs, --edge-lengths, --bound [--num-vertices]` + +**Step 2: Run the targeted CLI tests and verify RED** + +Run: + +```bash +cargo test -p problemreductions-cli stacker_crane +``` + +Expected: +- FAIL because the create arm/help text do not exist yet. + +**Step 3: Implement the CLI support** + +Update `problemreductions-cli/src/commands/create.rs`: + +- add a `StackerCrane` match arm in `create()` +- reuse existing parsers where possible: + - `parse_directed_graph(args.arcs, args.num_vertices)` + - `parse_graph(args)` for undirected edges + - `parse_arc_costs(args, num_arcs)` + - `parse_i32_edge_values(args.edge_lengths.as_ref(), num_edges, "edge length")` or a small wrapper +- parse `--bound` as a non-negative integer and convert to `i32` with a range check +- construct `StackerCrane::try_new(...)` and bubble validation errors instead of panicking + +Update `problemreductions-cli/src/cli.rs`: +- add the StackerCrane line to the "Flags by problem type" help block +- add one example command to the create examples list if the file already keeps that section current + +**Step 4: Run the targeted CLI tests and verify GREEN** + +Run: + +```bash +cargo test -p problemreductions-cli stacker_crane +``` + +Expected: +- PASS for the new create/help tests. + +**Step 5: Commit the CLI support** + +```bash +git add problemreductions-cli/src/commands/create.rs problemreductions-cli/src/cli.rs +git commit -m "feat: add pred create support for StackerCrane" +``` + +### Task 5: Batch-1 Verification Before Paper Work + +**Files:** +- Modify: none unless verification exposes gaps + +**Step 1: Run the batch-1 verification suite** + +Run: + +```bash +cargo test stacker_crane --lib +cargo test test_find_model_example_stacker_crane --features example-db +cargo test model_specs_are_self_consistent --features example-db +cargo test -p problemreductions-cli stacker_crane +``` + +Expected: +- All commands PASS. + +**Step 2: Fix anything that fails, then rerun the same commands** + +Do not move to the paper batch until these commands are green. + +### Task 6: Add Citations And The `problem-def` Entry (Separate Batch) + +**Files:** +- Modify: `docs/paper/references.bib` +- Modify: `docs/paper/reductions.typ` +- Test: `src/unit_tests/models/misc/stacker_crane.rs` + +**Step 1: Add a failing paper-example assertion if missing** + +If Task 1's `test_stacker_crane_issue_witness_and_tighter_bound` is not already acting as the canonical paper example check, extend `src/unit_tests/models/misc/stacker_crane.rs` with: + +- `test_stacker_crane_paper_example` + - Build the exact hourglass instance shown in the paper. + - Assert `[0, 2, 1, 4, 3]` is satisfying. + - For this small instance, use `BruteForce::find_all_satisfying()` and assert the known witness is among the satisfying configs. + +Run: + +```bash +cargo test test_stacker_crane_paper_example --lib +``` + +Expected: +- PASS before touching the paper text, so the paper is anchored to a verified example. + +**Step 2: Update citations** + +Add BibTeX entries to `docs/paper/references.bib` for: +- Frederickson, Hecht, Kim (1978), SIAM J. Comput. 7(2):178-193, DOI `10.1137/0207017` +- Frederickson and Guan (1993), *Nonpreemptive Ensemble Motion Planning on a Tree*, J. Algorithms 15(1):29-60 + +Keep the existing `frederickson1979` entry for the broader postman/routing context. + +**Step 3: Add the display name and `problem-def` entry** + +Update `docs/paper/reductions.typ`: + +- add `"StackerCrane": [Stacker-Crane],` to the `display-name` dictionary +- add `#problem-def("StackerCrane")[ ... ][ ... ]` in the appropriate section near other routing/scheduling/misc problems + +The body must include: +- background linking it to mixed-graph arc routing and the Hamiltonian Circuit reduction +- best-known exact algorithm prose consistent with the declared complexity (`O(|V| 2^{|A|})` style wording) and cited +- the hourglass example from the canonical example spec, not a separate invented instance +- a `pred-commands()` block using `pred create --example StackerCrane -o stacker-crane.json` +- a short verifier walkthrough explaining why `[0, 2, 1, 4, 3]` yields total length 20 + +If a full CeTZ mixed-graph figure is too time-consuming, prefer a compact text/table example over inventing a half-baked graphic, but keep the example concrete and reproducible. + +**Step 4: Build the paper** + +Run: + +```bash +make paper +``` + +Expected: +- PASS. This auto-runs the graph/schema exports before Typst compilation. + +**Step 5: Commit the paper batch** + +```bash +git add docs/paper/references.bib docs/paper/reductions.typ src/unit_tests/models/misc/stacker_crane.rs +git commit -m "docs: add the StackerCrane paper entry" +``` + +### Task 7: Final Verification And PR-Ready Cleanup + +**Files:** +- Modify: any file only if verification exposes a real defect + +**Step 1: Run the full verification needed before completion claims** + +Run: + +```bash +make fmt +make check +cargo test --features example-db test_find_model_example_stacker_crane +cargo test --features example-db model_specs_are_self_consistent +cargo test -p problemreductions-cli stacker_crane +make paper +``` + +Expected: +- Every command exits 0. + +**Step 2: Review against the issue and this plan** + +Confirm all of the following before closing the task: +- `StackerCrane` exists in `src/models/misc/stacker_crane.rs` +- the hourglass example is canonical in example-db +- `pred create StackerCrane ...` works with mixed-graph input +- the paper has both display name and `problem-def` +- the implementation still uses the corrected decision encoding from the issue comments +- no new alias was invented + +**Step 3: Final commit only if verification required follow-up fixes** + +```bash +git add -A +git commit -m "fix: finish StackerCrane verification follow-ups" +``` From 6ad7cf1df30f32f84dcb1bd20be9d31f54fa1007 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Sat, 21 Mar 2026 21:23:27 +0800 Subject: [PATCH 2/4] Fix #245: add StackerCrane model --- docs/paper/reductions.typ | 61 ++++ docs/paper/references.bib | 21 ++ problemreductions-cli/src/cli.rs | 19 ++ problemreductions-cli/src/commands/create.rs | 140 +++++++- problemreductions-cli/tests/cli_tests.rs | 28 +- src/lib.rs | 15 +- src/models/misc/mod.rs | 4 + src/models/misc/stacker_crane.rs | 327 +++++++++++++++++++ src/models/mod.rs | 2 +- src/unit_tests/example_db.rs | 15 + src/unit_tests/models/misc/stacker_crane.rs | 120 +++++++ 11 files changed, 734 insertions(+), 18 deletions(-) create mode 100644 src/models/misc/stacker_crane.rs create mode 100644 src/unit_tests/models/misc/stacker_crane.rs diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index 34d0f83b3..2b2d4693d 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -117,6 +117,7 @@ "MinimumMultiwayCut": [Minimum Multiway Cut], "OptimalLinearArrangement": [Optimal Linear Arrangement], "RuralPostman": [Rural Postman], + "StackerCrane": [Stacker Crane], "LongestCommonSubsequence": [Longest Common Subsequence], "ExactCoverBy3Sets": [Exact Cover by 3-Sets], "SubsetSum": [Subset Sum], @@ -3570,6 +3571,66 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76], ] } +#{ + let x = load-model-example("StackerCrane") + let arcs = x.instance.arcs.map(a => (a.at(0), a.at(1))) + let edges = x.instance.edges.map(e => (e.at(0), e.at(1))) + let B = x.instance.bound + let config = x.optimal_config + let positions = ( + (-2.0, 0.9), + (-2.0, -0.9), + (0.0, -1.5), + (2.0, -0.9), + (0.0, 1.5), + (2.0, 0.9), + ) + [ + #problem-def("StackerCrane")[ + Given a mixed graph $G = (V, A, E)$ with directed arcs $A$, undirected edges $E$, nonnegative lengths $l: A union E -> ZZ_(gt.eq 0)$, and a bound $B in ZZ^+$, determine whether there exists a closed walk in $G$ that traverses every arc in $A$ in its prescribed direction and has total length at most $B$. + ][ + Stacker Crane is the mixed-graph arc-routing problem listed as ND26 in Garey and Johnson @garey1979. Frederickson, Hecht, and Kim prove the problem NP-complete via reduction from Hamiltonian Circuit and give the classical $9 slash 5$-approximation for the metric case @frederickson1978routing. The problem stays difficult even on trees @fredericksonguan1993. The registry records the standard subset-dynamic-programming state space $O(|V| dot 2^|A|)$#footnote[Included as a straightforward exact dynamic-programming baseline over subsets of required arcs; no sharper exact bound was independently verified while preparing this entry.]. + + A configuration is a permutation of the required arcs, interpreted as the order in which those arcs are forced into the tour. The verifier traverses each chosen arc, then inserts the shortest available connector path from that arc's head to the tail of the next arc, wrapping around at the end to close the walk. + + *Example.* The canonical instance has 6 vertices, 5 required arcs, 7 undirected edges, and bound $B = #B$. The witness configuration $[#config.map(str).join(", ")]$ orders the required arcs as $a_0, a_2, a_1, a_4, a_3$. Traversing those arcs contributes 17 units of required-arc length, and the shortest connector paths contribute $1 + 1 + 1 + 0 + 0 = 3$, so the resulting closed walk has total length $20 = B$. Reducing the bound to 19 makes the same instance unsatisfiable. + + #pred-commands( + "pred create --example " + problem-spec(x) + " -o stacker-crane.json", + "pred solve stacker-crane.json --solver brute-force", + "pred evaluate stacker-crane.json --config " + x.optimal_config.map(str).join(","), + ) + + #figure( + canvas(length: 1cm, { + import draw: * + let blue = graph-colors.at(0) + let gray = luma(200) + + for (u, v) in edges { + line(positions.at(u), positions.at(v), stroke: (paint: gray, thickness: 0.7pt)) + } + + for (i, (u, v)) in arcs.enumerate() { + line(positions.at(u), positions.at(v), stroke: (paint: blue, thickness: 1.7pt)) + let mid = ( + (positions.at(u).at(0) + positions.at(v).at(0)) / 2, + (positions.at(u).at(1) + positions.at(v).at(1)) / 2, + ) + content(mid, text(6pt, fill: blue)[$a_#i$], fill: white, frame: "rect", padding: 0.05, stroke: none) + } + + for (i, pos) in positions.enumerate() { + circle(pos, radius: 0.18, fill: white, stroke: 0.6pt + black) + content(pos, text(7pt)[$v_#i$]) + } + }), + caption: [Stacker Crane hourglass instance. Required directed arcs are shown in blue and labeled $a_0$ through $a_4$; undirected connector edges are gray. The satisfying order $a_0, a_2, a_1, a_4, a_3$ yields total length 20.], + ) + ] + ] +} + #{ let x = load-model-example("SubgraphIsomorphism") let nv-host = x.instance.host_graph.num_vertices diff --git a/docs/paper/references.bib b/docs/paper/references.bib index fd8106120..01d413d77 100644 --- a/docs/paper/references.bib +++ b/docs/paper/references.bib @@ -1068,6 +1068,27 @@ @article{frederickson1979 doi = {10.1145/322139.322150} } +@article{frederickson1978routing, + author = {Greg N. Frederickson and Matthew S. Hecht and Chul E. Kim}, + title = {Approximation Algorithms for Some Routing Problems}, + journal = {SIAM Journal on Computing}, + volume = {7}, + number = {2}, + pages = {178--193}, + year = {1978}, + doi = {10.1137/0207017} +} + +@article{fredericksonguan1993, + author = {Greg N. Frederickson and Da-Wei Guan}, + title = {Nonpreemptive Ensemble Motion Planning on a Tree}, + journal = {Journal of Algorithms}, + volume = {15}, + number = {1}, + pages = {29--60}, + year = {1993} +} + @article{gottlob2002, author = {Georg Gottlob and Nicola Leone and Francesco Scarcello}, title = {Hypertree Decompositions and Tractable Queries}, diff --git a/problemreductions-cli/src/cli.rs b/problemreductions-cli/src/cli.rs index 5a26d50c5..328a173fd 100644 --- a/problemreductions-cli/src/cli.rs +++ b/problemreductions-cli/src/cli.rs @@ -266,6 +266,7 @@ Flags by problem type: OptimalLinearArrangement --graph, --bound MinMaxMulticenter (pCenter) --graph, --weights, --edge-weights, --k, --bound RuralPostman (RPP) --graph, --edge-weights, --required-edges, --bound + StackerCrane --arcs, --graph, --arc-costs, --edge-lengths, --bound [--num-vertices] MultipleChoiceBranching --arcs [--weights] --partition --bound [--num-vertices] AdditionalKey --num-attributes, --dependencies, --relation-attrs [--known-keys] ConsistencyOfDatabaseFrequencyTables --num-objects, --attribute-domains, --frequency-tables [--known-values] @@ -849,4 +850,22 @@ mod tests { assert!(help.contains("--potential-edges")); assert!(help.contains("--budget")); } + + #[test] + fn test_create_help_mentions_stacker_crane_flags() { + let cmd = Cli::command(); + let create = cmd.find_subcommand("create").expect("create subcommand"); + let help = create + .get_after_help() + .expect("create after_help") + .to_string(); + + assert!(help.contains("StackerCrane")); + assert!(help.contains("--arcs")); + assert!(help.contains("--graph")); + assert!(help.contains("--arc-costs")); + assert!(help.contains("--edge-lengths")); + assert!(help.contains("--bound")); + assert!(help.contains("--num-vertices")); + } } diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index 73e3f6b52..076eaeef1 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -587,6 +587,9 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str { "RuralPostman" => { "--graph 0-1,1-2,2-3,3-0 --edge-weights 1,1,1,1 --required-edges 0,2 --bound 4" } + "StackerCrane" => { + "--arcs \"0>4,2>5,5>1,3>0,4>3\" --graph \"0-1,1-2,2-3,3-5,4-5,0-3,1-5\" --arc-costs 3,4,2,5,3 --edge-lengths 2,1,3,2,1,4,3 --bound 20 --num-vertices 6" + } "MultipleChoiceBranching" => { "--arcs \"0>1,0>2,1>3,2>3,1>4,3>5,4>5,2>4\" --weights 3,2,4,1,2,3,1,3 --partition \"0,1;2,3;4,7;5,6\" --bound 10" } @@ -662,6 +665,9 @@ fn help_flag_name(canonical: &str, field_name: &str) -> String { ("PrimeAttributeName", "dependencies") => return "deps".to_string(), ("PrimeAttributeName", "query_attribute") => return "query".to_string(), ("ConsecutiveOnesSubmatrix", "bound") => return "bound".to_string(), + ("StackerCrane", "edges") => return "graph".to_string(), + ("StackerCrane", "arc_lengths") => return "arc-costs".to_string(), + ("StackerCrane", "edge_lengths") => return "edge-lengths".to_string(), ("StaffScheduling", "shifts_per_schedule") => return "k".to_string(), ("TimetableDesign", "num_tasks") => return "num-tasks".to_string(), _ => {} @@ -1497,6 +1503,51 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { ) } + // StackerCrane + "StackerCrane" => { + let usage = "Usage: pred create StackerCrane --arcs \"0>4,2>5,5>1,3>0,4>3\" --graph \"0-1,1-2,2-3,3-5,4-5,0-3,1-5\" --arc-costs 3,4,2,5,3 --edge-lengths 2,1,3,2,1,4,3 --bound 20 --num-vertices 6"; + let arcs_str = args + .arcs + .as_deref() + .ok_or_else(|| anyhow::anyhow!("StackerCrane requires --arcs\n\n{usage}"))?; + let (arcs_graph, num_arcs) = parse_directed_graph(arcs_str, args.num_vertices)?; + let (edges_graph, num_vertices) = + parse_graph(args).map_err(|e| anyhow::anyhow!("{e}\n\n{usage}"))?; + anyhow::ensure!( + edges_graph.num_vertices() == num_vertices, + "internal error: inconsistent graph vertex count" + ); + anyhow::ensure!( + num_vertices == arcs_graph.num_vertices(), + "StackerCrane requires the directed and undirected inputs to agree on --num-vertices\n\n{usage}" + ); + let arc_lengths = parse_arc_costs(args, num_arcs)?; + let edge_lengths = parse_i32_edge_values( + args.edge_lengths.as_ref(), + edges_graph.num_edges(), + "edge length", + )?; + let bound_raw = args + .bound + .ok_or_else(|| anyhow::anyhow!("StackerCrane requires --bound\n\n{usage}"))?; + let bound = parse_nonnegative_usize_bound(bound_raw, "StackerCrane", usage)?; + let bound = i32::try_from(bound).map_err(|_| { + anyhow::anyhow!("StackerCrane --bound must fit in i32 (got {bound_raw})\n\n{usage}") + })?; + ( + ser(StackerCrane::try_new( + num_vertices, + arcs_graph.arcs(), + edges_graph.edges(), + arc_lengths, + edge_lengths, + bound, + ) + .map_err(|e| anyhow::anyhow!(e))?)?, + resolved_variant.clone(), + ) + } + // MultipleChoiceBranching "MultipleChoiceBranching" => { let usage = "Usage: pred create MultipleChoiceBranching/i32 --arcs \"0>1,0>2,1>3,2>3,1>4,3>5,4>5,2>4\" --weights 3,2,4,1,2,3,1,3 --partition \"0,1;2,3;4,7;5,6\" --bound 10"; @@ -3003,9 +3054,10 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { // AcyclicPartition "AcyclicPartition" => { let usage = "Usage: pred create AcyclicPartition/i32 --arcs \"0>1,0>2,1>3,1>4,2>4,2>5,3>5,4>5\" --weights 2,3,2,1,3,1 --arc-costs 1,1,1,1,1,1,1,1 --weight-bound 5 --cost-bound 5"; - let arcs_str = args.arcs.as_deref().ok_or_else(|| { - anyhow::anyhow!("AcyclicPartition requires --arcs\n\n{usage}") - })?; + let arcs_str = args + .arcs + .as_deref() + .ok_or_else(|| anyhow::anyhow!("AcyclicPartition requires --arcs\n\n{usage}"))?; let (graph, num_arcs) = parse_directed_graph(arcs_str, args.num_vertices)?; let vertex_weights = parse_vertex_weights(args, graph.num_vertices())?; let arc_costs = parse_arc_costs(args, num_arcs)?; @@ -4789,11 +4841,7 @@ fn parse_arc_costs(args: &CreateArgs, num_arcs: usize) -> Result> { .map(|s| s.trim().parse::()) .collect::, _>>()?; if parsed.len() != num_arcs { - bail!( - "Expected {} arc costs but got {}", - num_arcs, - parsed.len() - ); + bail!("Expected {} arc costs but got {}", num_arcs, parsed.len()); } Ok(parsed) } @@ -5942,6 +5990,82 @@ mod tests { std::fs::remove_file(output_path).ok(); } + #[test] + fn test_create_stacker_crane_json() { + let mut args = empty_args(); + args.problem = Some("StackerCrane".to_string()); + args.num_vertices = Some(6); + args.arcs = Some("0>4,2>5,5>1,3>0,4>3".to_string()); + args.graph = Some("0-1,1-2,2-3,3-5,4-5,0-3,1-5".to_string()); + args.arc_costs = Some("3,4,2,5,3".to_string()); + args.edge_lengths = Some("2,1,3,2,1,4,3".to_string()); + args.bound = Some(20); + + let output_path = std::env::temp_dir().join("pred_test_create_stacker_crane.json"); + let out = OutputConfig { + output: Some(output_path.clone()), + quiet: true, + json: false, + auto_json: false, + }; + + create(&args, &out).unwrap(); + + let content = std::fs::read_to_string(&output_path).unwrap(); + let json: serde_json::Value = serde_json::from_str(&content).unwrap(); + assert_eq!(json["type"], "StackerCrane"); + assert_eq!(json["data"]["num_vertices"], 6); + assert_eq!(json["data"]["bound"], 20); + assert_eq!(json["data"]["arcs"][0], serde_json::json!([0, 4])); + assert_eq!(json["data"]["edge_lengths"][6], 3); + + std::fs::remove_file(output_path).ok(); + } + + #[test] + fn test_create_stacker_crane_rejects_mismatched_arc_lengths() { + let mut args = empty_args(); + args.problem = Some("StackerCrane".to_string()); + args.num_vertices = Some(6); + args.arcs = Some("0>4,2>5,5>1,3>0,4>3".to_string()); + args.graph = Some("0-1,1-2,2-3,3-5,4-5,0-3,1-5".to_string()); + args.arc_costs = Some("3,4,2,5".to_string()); + args.edge_lengths = Some("2,1,3,2,1,4,3".to_string()); + args.bound = Some(20); + + let out = OutputConfig { + output: None, + quiet: true, + json: false, + auto_json: false, + }; + + let err = create(&args, &out).unwrap_err().to_string(); + assert!(err.contains("Expected 5 arc costs but got 4")); + } + + #[test] + fn test_create_stacker_crane_rejects_out_of_range_vertices() { + let mut args = empty_args(); + args.problem = Some("StackerCrane".to_string()); + args.num_vertices = Some(5); + args.arcs = Some("0>4,2>5,5>1,3>0,4>3".to_string()); + args.graph = Some("0-1,1-2,2-3,3-5,4-5,0-3,1-5".to_string()); + args.arc_costs = Some("3,4,2,5,3".to_string()); + args.edge_lengths = Some("2,1,3,2,1,4,3".to_string()); + args.bound = Some(20); + + let out = OutputConfig { + output: None, + quiet: true, + json: false, + auto_json: false, + }; + + let err = create(&args, &out).unwrap_err().to_string(); + assert!(err.contains("--num-vertices (5) is too small for the arcs")); + } + #[test] fn test_create_balanced_complete_bipartite_subgraph() { use crate::dispatch::ProblemJsonOutput; diff --git a/problemreductions-cli/tests/cli_tests.rs b/problemreductions-cli/tests/cli_tests.rs index a5e4abe85..58c0a7062 100644 --- a/problemreductions-cli/tests/cli_tests.rs +++ b/problemreductions-cli/tests/cli_tests.rs @@ -132,6 +132,24 @@ fn test_show_balanced_complete_bipartite_subgraph_complexity() { ); } +#[test] +fn test_create_stacker_crane_schema_help_uses_documented_flags() { + let output = pred().args(["create", "StackerCrane"]).output().unwrap(); + assert!(!output.status.success()); + + let stderr = String::from_utf8(output.stderr).unwrap(); + assert!(stderr.contains("StackerCrane"), "stderr: {stderr}"); + assert!(stderr.contains("--arcs"), "stderr: {stderr}"); + assert!(stderr.contains("--graph"), "stderr: {stderr}"); + assert!(stderr.contains("--arc-costs"), "stderr: {stderr}"); + assert!(stderr.contains("--edge-lengths"), "stderr: {stderr}"); + assert!(stderr.contains("--bound"), "stderr: {stderr}"); + assert!(stderr.contains("--num-vertices"), "stderr: {stderr}"); + assert!(!stderr.contains("--biedges"), "stderr: {stderr}"); + assert!(!stderr.contains("--arc-lengths"), "stderr: {stderr}"); + assert!(!stderr.contains("--edge-weights"), "stderr: {stderr}"); +} + #[test] fn test_solve_balanced_complete_bipartite_subgraph_suggests_bruteforce() { let tmp = std::env::temp_dir().join("pred_test_bcbs_problem.json"); @@ -1980,8 +1998,14 @@ fn test_create_acyclic_partition() { let json: serde_json::Value = serde_json::from_str(&stdout).unwrap(); assert_eq!(json["type"], "AcyclicPartition"); assert_eq!(json["variant"]["weight"], "i32"); - assert_eq!(json["data"]["vertex_weights"], serde_json::json!([2, 3, 2, 1, 3, 1])); - assert_eq!(json["data"]["arc_costs"], serde_json::json!([1, 1, 1, 1, 1, 1, 1, 1])); + assert_eq!( + json["data"]["vertex_weights"], + serde_json::json!([2, 3, 2, 1, 3, 1]) + ); + assert_eq!( + json["data"]["arc_costs"], + serde_json::json!([1, 1, 1, 1, 1, 1, 1, 1]) + ); assert_eq!(json["data"]["weight_bound"], 5); assert_eq!(json["data"]["cost_bound"], 5); } diff --git a/src/lib.rs b/src/lib.rs index 9c8bd8664..46b845aae 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -48,11 +48,12 @@ pub mod prelude { Satisfiability, }; pub use crate::models::graph::{ - AcyclicPartition, BalancedCompleteBipartiteSubgraph, BicliqueCover, BiconnectivityAugmentation, - BoundedComponentSpanningForest, DirectedTwoCommodityIntegralFlow, GeneralizedHex, - GraphPartitioning, HamiltonianCircuit, HamiltonianPath, IsomorphicSpanningTree, KClique, - KthBestSpanningTree, LengthBoundedDisjointPaths, SpinGlass, SteinerTree, - StrongConnectivityAugmentation, SubgraphIsomorphism, + AcyclicPartition, BalancedCompleteBipartiteSubgraph, BicliqueCover, + BiconnectivityAugmentation, 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, @@ -72,8 +73,8 @@ pub mod prelude { SchedulingWithIndividualDeadlines, SequencingToMinimizeMaximumCumulativeCost, SequencingToMinimizeWeightedCompletionTime, SequencingToMinimizeWeightedTardiness, SequencingWithReleaseTimesAndDeadlines, SequencingWithinIntervals, - ShortestCommonSupersequence, StaffScheduling, StringToStringCorrection, SubsetSum, - SumOfSquaresPartition, Term, TimetableDesign, + ShortestCommonSupersequence, StackerCrane, StaffScheduling, StringToStringCorrection, + SubsetSum, SumOfSquaresPartition, Term, TimetableDesign, }; pub use crate::models::set::{ ComparativeContainment, ConsecutiveSets, ExactCoverBy3Sets, MaximumSetPacking, diff --git a/src/models/misc/mod.rs b/src/models/misc/mod.rs index 71267a59d..e3b86f58b 100644 --- a/src/models/misc/mod.rs +++ b/src/models/misc/mod.rs @@ -20,6 +20,7 @@ //! - [`RectilinearPictureCompression`]: Cover 1-entries with bounded rectangles //! - [`ResourceConstrainedScheduling`]: Schedule unit-length tasks on processors with resource constraints //! - [`SchedulingWithIndividualDeadlines`]: Meet per-task deadlines on parallel processors +//! - [`StackerCrane`]: Route a crane through required arcs within a length bound //! - [`SequencingToMinimizeMaximumCumulativeCost`]: Keep every cumulative schedule cost prefix under a bound //! - [`SequencingToMinimizeWeightedCompletionTime`]: Minimize total weighted completion time //! - [`SequencingToMinimizeWeightedTardiness`]: Decide whether a schedule meets a weighted tardiness bound @@ -57,6 +58,7 @@ mod sequencing_to_minimize_weighted_tardiness; mod sequencing_with_release_times_and_deadlines; mod sequencing_within_intervals; pub(crate) mod shortest_common_supersequence; +mod stacker_crane; mod staff_scheduling; pub(crate) mod string_to_string_correction; mod subset_sum; @@ -91,6 +93,7 @@ pub use sequencing_to_minimize_weighted_tardiness::SequencingToMinimizeWeightedT pub use sequencing_with_release_times_and_deadlines::SequencingWithReleaseTimesAndDeadlines; pub use sequencing_within_intervals::SequencingWithinIntervals; pub use shortest_common_supersequence::ShortestCommonSupersequence; +pub use stacker_crane::StackerCrane; pub use staff_scheduling::StaffScheduling; pub use string_to_string_correction::StringToStringCorrection; pub use subset_sum::SubsetSum; @@ -114,6 +117,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec", description: "Required directed arcs that must be traversed" }, + FieldInfo { name: "edges", type_name: "Vec<(usize, usize)>", description: "Undirected edges available for connector paths" }, + FieldInfo { name: "arc_lengths", type_name: "Vec", description: "Nonnegative lengths of the required directed arcs" }, + FieldInfo { name: "edge_lengths", type_name: "Vec", description: "Nonnegative lengths of the undirected connector edges" }, + FieldInfo { name: "bound", type_name: "i32", description: "Upper bound on the total closed-walk length" }, + ], + } +} + +/// The Stacker Crane problem. +/// +/// A configuration is a permutation of the required arc indices. The walk +/// traverses those arcs in the chosen order, connecting the head of each arc +/// to the tail of the next arc by a shortest path in the mixed graph induced +/// by the required directed arcs together with the undirected edges. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "StackerCraneDef")] +pub struct StackerCrane { + num_vertices: usize, + arcs: Vec<(usize, usize)>, + edges: Vec<(usize, usize)>, + arc_lengths: Vec, + edge_lengths: Vec, + bound: i32, +} + +impl StackerCrane { + /// Create a new Stacker Crane instance. + /// + /// # Panics + /// + /// Panics if the instance data are inconsistent or contain negative + /// lengths or a negative bound. + pub fn new( + num_vertices: usize, + arcs: Vec<(usize, usize)>, + edges: Vec<(usize, usize)>, + arc_lengths: Vec, + edge_lengths: Vec, + bound: i32, + ) -> Self { + Self::try_new(num_vertices, arcs, edges, arc_lengths, edge_lengths, bound) + .unwrap_or_else(|message| panic!("{message}")) + } + + /// Create a new Stacker Crane instance, returning validation errors. + pub fn try_new( + num_vertices: usize, + arcs: Vec<(usize, usize)>, + edges: Vec<(usize, usize)>, + arc_lengths: Vec, + edge_lengths: Vec, + bound: i32, + ) -> Result { + if arc_lengths.len() != arcs.len() { + return Err("arc_lengths length must match arcs length".to_string()); + } + if edge_lengths.len() != edges.len() { + return Err("edge_lengths length must match edges length".to_string()); + } + if bound < 0 { + return Err("bound must be nonnegative".to_string()); + } + for (arc_index, &(tail, head)) in arcs.iter().enumerate() { + if tail >= num_vertices || head >= num_vertices { + return Err(format!( + "arc {arc_index} endpoint out of range for {num_vertices} vertices" + )); + } + } + for (edge_index, &(u, v)) in edges.iter().enumerate() { + if u >= num_vertices || v >= num_vertices { + return Err(format!( + "edge {edge_index} endpoint out of range for {num_vertices} vertices" + )); + } + } + for (arc_index, &length) in arc_lengths.iter().enumerate() { + if length < 0 { + return Err(format!("arc length {arc_index} must be nonnegative")); + } + } + for (edge_index, &length) in edge_lengths.iter().enumerate() { + if length < 0 { + return Err(format!("edge length {edge_index} must be nonnegative")); + } + } + + Ok(Self { + num_vertices, + arcs, + edges, + arc_lengths, + edge_lengths, + bound, + }) + } + + /// Get the number of vertices in the mixed graph. + pub fn num_vertices(&self) -> usize { + self.num_vertices + } + + /// Get the required directed arcs. + pub fn arcs(&self) -> &[(usize, usize)] { + &self.arcs + } + + /// Get the available undirected edges. + pub fn edges(&self) -> &[(usize, usize)] { + &self.edges + } + + /// Get the required arc lengths. + pub fn arc_lengths(&self) -> &[i32] { + &self.arc_lengths + } + + /// Get the undirected edge lengths. + pub fn edge_lengths(&self) -> &[i32] { + &self.edge_lengths + } + + /// Get the upper bound on total walk length. + pub fn bound(&self) -> i32 { + self.bound + } + + /// Get the number of required arcs. + pub fn num_arcs(&self) -> usize { + self.arcs.len() + } + + /// Get the number of undirected edges. + pub fn num_edges(&self) -> usize { + self.edges.len() + } + + fn is_arc_permutation(&self, config: &[usize]) -> bool { + if config.len() != self.num_arcs() { + return false; + } + + let mut seen = vec![false; self.num_arcs()]; + for &arc_index in config { + if arc_index >= self.num_arcs() || seen[arc_index] { + return false; + } + seen[arc_index] = true; + } + + true + } + + fn mixed_graph_adjacency(&self) -> Vec> { + let mut adjacency = vec![Vec::new(); self.num_vertices]; + + for (&(tail, head), &length) in self.arcs.iter().zip(&self.arc_lengths) { + adjacency[tail].push((head, length)); + } + + for (&(u, v), &length) in self.edges.iter().zip(&self.edge_lengths) { + adjacency[u].push((v, length)); + adjacency[v].push((u, length)); + } + + adjacency + } + + fn shortest_path_length( + &self, + adjacency: &[Vec<(usize, i32)>], + source: usize, + target: usize, + ) -> Option { + if source == target { + return Some(0); + } + + let mut dist = vec![i64::MAX; self.num_vertices]; + let mut heap = BinaryHeap::new(); + dist[source] = 0; + heap.push((Reverse(0i64), source)); + + while let Some((Reverse(cost), node)) = heap.pop() { + if cost > dist[node] { + continue; + } + if node == target { + return Some(cost); + } + + for &(next, length) in &adjacency[node] { + let next_cost = cost.checked_add(i64::from(length))?; + if next_cost < dist[next] { + dist[next] = next_cost; + heap.push((Reverse(next_cost), next)); + } + } + } + + None + } + + /// Compute the total closed-walk length induced by a configuration. + /// + /// Returns `None` for invalid permutations, unreachable connector paths, + /// or arithmetic overflow. + pub fn closed_walk_length(&self, config: &[usize]) -> Option { + if !self.is_arc_permutation(config) { + return None; + } + if config.is_empty() { + return Some(0); + } + + let adjacency = self.mixed_graph_adjacency(); + let mut total = 0i64; + + for position in 0..config.len() { + let arc_index = config[position]; + let next_arc_index = config[(position + 1) % config.len()]; + let (_, arc_head) = self.arcs[arc_index]; + let (next_arc_tail, _) = self.arcs[next_arc_index]; + + total = total.checked_add(i64::from(self.arc_lengths[arc_index]))?; + total = total.checked_add(self.shortest_path_length( + &adjacency, + arc_head, + next_arc_tail, + )?)?; + } + + i32::try_from(total).ok() + } +} + +impl Problem for StackerCrane { + const NAME: &'static str = "StackerCrane"; + type Metric = bool; + + fn variant() -> Vec<(&'static str, &'static str)> { + crate::variant_params![] + } + + fn dims(&self) -> Vec { + vec![self.num_arcs(); self.num_arcs()] + } + + fn evaluate(&self, config: &[usize]) -> bool { + matches!(self.closed_walk_length(config), Some(total) if total <= self.bound) + } +} + +impl SatisfactionProblem for StackerCrane {} + +crate::declare_variants! { + default sat StackerCrane => "num_vertices * 2^num_arcs", +} + +#[derive(Debug, Clone, Deserialize)] +struct StackerCraneDef { + num_vertices: usize, + arcs: Vec<(usize, usize)>, + edges: Vec<(usize, usize)>, + arc_lengths: Vec, + edge_lengths: Vec, + bound: i32, +} + +impl TryFrom for StackerCrane { + type Error = String; + + fn try_from(value: StackerCraneDef) -> Result { + Self::try_new( + value.num_vertices, + value.arcs, + value.edges, + value.arc_lengths, + value.edge_lengths, + value.bound, + ) + } +} + +#[cfg(feature = "example-db")] +pub(crate) fn canonical_model_example_specs() -> Vec { + vec![crate::example_db::specs::ModelExampleSpec { + id: "stacker_crane", + instance: Box::new(StackerCrane::new( + 6, + vec![(0, 4), (2, 5), (5, 1), (3, 0), (4, 3)], + vec![(0, 1), (1, 2), (2, 3), (3, 5), (4, 5), (0, 3), (1, 5)], + vec![3, 4, 2, 5, 3], + vec![2, 1, 3, 2, 1, 4, 3], + 20, + )), + optimal_config: vec![0, 2, 1, 4, 3], + optimal_value: serde_json::json!(true), + }] +} + +#[cfg(test)] +#[path = "../../unit_tests/models/misc/stacker_crane.rs"] +mod tests; diff --git a/src/models/mod.rs b/src/models/mod.rs index e364f8254..cd0aa4741 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -39,7 +39,7 @@ pub use misc::{ ResourceConstrainedScheduling, SchedulingWithIndividualDeadlines, SequencingToMinimizeMaximumCumulativeCost, SequencingToMinimizeWeightedCompletionTime, SequencingToMinimizeWeightedTardiness, SequencingWithReleaseTimesAndDeadlines, - SequencingWithinIntervals, ShortestCommonSupersequence, StaffScheduling, + SequencingWithinIntervals, ShortestCommonSupersequence, StackerCrane, StaffScheduling, StringToStringCorrection, SubsetSum, SumOfSquaresPartition, Term, TimetableDesign, }; pub use set::{ diff --git a/src/unit_tests/example_db.rs b/src/unit_tests/example_db.rs index c42f22bac..5350da092 100644 --- a/src/unit_tests/example_db.rs +++ b/src/unit_tests/example_db.rs @@ -80,6 +80,21 @@ fn test_find_model_example_staff_scheduling() { ); } +#[test] +fn test_find_model_example_stacker_crane() { + let problem = ProblemRef { + name: "StackerCrane".to_string(), + variant: BTreeMap::new(), + }; + + let example = find_model_example(&problem).expect("StackerCrane example should exist"); + assert_eq!(example.problem, "StackerCrane"); + assert_eq!(example.variant, problem.variant); + assert_eq!(example.optimal_config, vec![0, 2, 1, 4, 3]); + assert_eq!(example.instance["num_vertices"], 6); + assert_eq!(example.instance["bound"], 20); +} + #[test] fn test_find_model_example_multiprocessor_scheduling() { let problem = ProblemRef { diff --git a/src/unit_tests/models/misc/stacker_crane.rs b/src/unit_tests/models/misc/stacker_crane.rs new file mode 100644 index 000000000..f73bf9ec3 --- /dev/null +++ b/src/unit_tests/models/misc/stacker_crane.rs @@ -0,0 +1,120 @@ +use super::*; +use crate::solvers::{BruteForce, Solver}; +use crate::traits::Problem; + +fn issue_problem(bound: i32) -> StackerCrane { + StackerCrane::new( + 6, + vec![(0, 4), (2, 5), (5, 1), (3, 0), (4, 3)], + vec![(0, 1), (1, 2), (2, 3), (3, 5), (4, 5), (0, 3), (1, 5)], + vec![3, 4, 2, 5, 3], + vec![2, 1, 3, 2, 1, 4, 3], + bound, + ) +} + +fn small_problem() -> StackerCrane { + StackerCrane::new( + 3, + vec![(0, 1), (1, 2)], + vec![(0, 2)], + vec![1, 1], + vec![1], + 3, + ) +} + +#[test] +fn test_stacker_crane_creation_and_metadata() { + let problem = issue_problem(20); + + assert_eq!(problem.num_vertices(), 6); + assert_eq!(problem.num_arcs(), 5); + assert_eq!(problem.num_edges(), 7); + assert_eq!(problem.bound(), 20); + assert_eq!(problem.dims(), vec![5; 5]); + assert_eq!(::NAME, "StackerCrane"); + assert!(::variant().is_empty()); +} + +#[test] +fn test_stacker_crane_rejects_non_permutations_and_wrong_lengths() { + let problem = issue_problem(20); + + assert!(!problem.evaluate(&[0, 2, 1, 4, 4])); + assert!(!problem.evaluate(&[0, 2, 1, 4, 5])); + assert!(!problem.evaluate(&[0, 2, 1, 4])); + assert!(!problem.evaluate(&[0, 2, 1, 4, 3, 0])); +} + +#[test] +fn test_stacker_crane_issue_witness_and_tighter_bound() { + assert!(issue_problem(20).evaluate(&[0, 2, 1, 4, 3])); + assert!(!issue_problem(19).evaluate(&[0, 2, 1, 4, 3])); +} + +#[test] +fn test_stacker_crane_issue_instance_is_unsatisfiable_at_bound_19() { + let problem = issue_problem(19); + let solver = BruteForce::new(); + + assert!(solver.find_all_satisfying(&problem).is_empty()); +} + +#[test] +fn test_stacker_crane_paper_example() { + let problem = issue_problem(20); + let witness = vec![0, 2, 1, 4, 3]; + + assert_eq!(problem.closed_walk_length(&witness), Some(20)); + assert!(problem.evaluate(&witness)); + + let solver = BruteForce::new(); + let satisfying = solver.find_all_satisfying(&problem); + assert!(!satisfying.is_empty()); + assert!(satisfying.contains(&witness)); + for config in &satisfying { + assert!(problem.evaluate(config)); + } +} + +#[test] +fn test_stacker_crane_small_solver_instance() { + let problem = small_problem(); + let solver = BruteForce::new(); + + let satisfying = solver + .find_satisfying(&problem) + .expect("small instance should be satisfiable"); + let mut sorted = satisfying.clone(); + sorted.sort_unstable(); + assert_eq!(sorted, vec![0, 1]); + assert!(problem.evaluate(&satisfying)); +} + +#[test] +fn test_stacker_crane_serialization_round_trip() { + let problem = issue_problem(20); + let json = serde_json::to_string(&problem).unwrap(); + let round_trip: StackerCrane = serde_json::from_str(&json).unwrap(); + + assert_eq!(round_trip.num_vertices(), 6); + assert_eq!(round_trip.num_arcs(), 5); + assert_eq!(round_trip.num_edges(), 7); + assert_eq!(round_trip.bound(), 20); + assert!(round_trip.evaluate(&[0, 2, 1, 4, 3])); +} + +#[test] +fn test_stacker_crane_is_available_in_prelude() { + let problem = crate::prelude::StackerCrane::new( + 3, + vec![(0, 1), (1, 2)], + vec![(0, 2)], + vec![1, 1], + vec![1], + 3, + ); + + assert_eq!(problem.num_arcs(), 2); +} From 254561172e2cd32d386275a5da37fcdd0a0df02f Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Sat, 21 Mar 2026 21:23:40 +0800 Subject: [PATCH 3/4] chore: remove plan file after implementation --- docs/plans/2026-03-21-stacker-crane.md | 416 ------------------------- 1 file changed, 416 deletions(-) delete mode 100644 docs/plans/2026-03-21-stacker-crane.md diff --git a/docs/plans/2026-03-21-stacker-crane.md b/docs/plans/2026-03-21-stacker-crane.md deleted file mode 100644 index 3d178069a..000000000 --- a/docs/plans/2026-03-21-stacker-crane.md +++ /dev/null @@ -1,416 +0,0 @@ -# StackerCrane Implementation Plan - -> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. - -**Goal:** Add the `[Model] StackerCrane` decision problem as a new `misc` model with canonical example-db support, `pred create` support for mixed-graph inputs, and a paper `problem-def` entry based on issue #245's corrected hourglass example. - -**Architecture:** Implement `StackerCrane` as a no-variant satisfaction problem whose configuration is a permutation of the required directed arcs. `evaluate()` should reject non-permutations, then compute the closed-walk length by traversing the required arcs in the chosen order and inserting shortest-path connectors through a mixed graph formed from directed arcs plus bidirectional undirected edges. Keep the model self-contained in `src/models/misc/stacker_crane.rs`; do not introduce a new topology type for this issue. - -**Tech Stack:** Rust workspace (`problemreductions`, `problemreductions-cli`), serde/inventory registry metadata, existing brute-force solver, Typst paper in `docs/paper/reductions.typ`. - ---- - -## Constraints And Notes - -- Treat the issue comments as the approved design basis. The latest `fix-issue` changelog supersedes the original vague encoding. -- This issue currently has no open companion rule issue mentioning `StackerCrane`. The implementation should proceed, but the PR body must carry an orphan-model warning. -- Keep `StackerCrane` as a **decision** problem with `Metric = bool`. Do not convert it to an optimization model in this PR. -- Use the issue's hourglass instance and satisfying permutation `[0, 2, 1, 4, 3]` as the canonical example and paper example. -- Do not add a manual alias in `problemreductions-cli/src/problem_name.rs`; alias resolution is registry-backed in this checkout. If an alias is added at all, it must come from `ProblemSchemaEntry.aliases`. Prefer no short alias here because `SCP` is ambiguous. -- Keep the paper batch separate from implementation so it runs after the example-db/model wiring is finished. - -## Batch Layout - -- **Batch 1:** Add-model Steps 1-5.5 - - Model implementation, registry/module wiring, canonical example, CLI creation, tests, verification. -- **Batch 2:** Add-model Step 6 - - Paper citations and `problem-def`, then `make paper`. - -### Task 1: Write The Failing Model Tests First - -**Files:** -- Create: `src/unit_tests/models/misc/stacker_crane.rs` -- Modify: `src/models/misc/mod.rs` -- Modify: `src/models/mod.rs` -- Modify: `src/lib.rs` -- Test: `src/unit_tests/models/misc/stacker_crane.rs` - -**Step 1: Write failing tests for the intended public API** - -Add a new test file with these initial tests: - -- `test_stacker_crane_creation_and_metadata` - - Construct the issue's hourglass instance. - - Assert `num_vertices() == 6`, `num_arcs() == 5`, `num_edges() == 7`, `bound() == 20`. - - Assert `dims() == vec![5; 5]`. - - Assert `Problem::NAME == "StackerCrane"` and `Problem::variant().is_empty()`. -- `test_stacker_crane_rejects_non_permutations_and_wrong_lengths` - - Reject duplicate arc indices, out-of-range indices, and wrong config length. -- `test_stacker_crane_issue_witness_and_tighter_bound` - - Assert `[0, 2, 1, 4, 3]` evaluates to `true` for `B = 20`. - - Assert the same instance with `B = 19` evaluates to `false`. -- `test_stacker_crane_small_solver_instance` - - Use a tiny 2-arc instance with search space small enough for brute force and assert `BruteForce::find_satisfying()` returns a valid permutation. -- `test_stacker_crane_serialization_round_trip` - - Round-trip serde JSON and re-check the witness. -- `test_stacker_crane_is_available_in_prelude` - - Use `crate::prelude::StackerCrane` to ensure the export is actually wired. - -In the same task, add the module/re-export placeholders in: -- `src/models/misc/mod.rs` -- `src/models/mod.rs` -- `src/lib.rs` - -The test should compile far enough to fail because `StackerCrane` does not exist yet. - -**Step 2: Run the targeted test and verify RED** - -Run: - -```bash -cargo test stacker_crane --lib -``` - -Expected: -- FAIL at compile time with unresolved `StackerCrane` symbols or missing module errors. - -**Step 3: Commit the failing-test scaffold** - -```bash -git add src/unit_tests/models/misc/stacker_crane.rs src/models/misc/mod.rs src/models/mod.rs src/lib.rs -git commit -m "test: add failing StackerCrane model tests" -``` - -### Task 2: Implement The Core `StackerCrane` Model - -**Files:** -- Create: `src/models/misc/stacker_crane.rs` -- Modify: `src/models/misc/mod.rs` -- Modify: `src/models/mod.rs` -- Modify: `src/lib.rs` -- Test: `src/unit_tests/models/misc/stacker_crane.rs` - -**Step 1: Implement the minimal model to satisfy Task 1** - -Create `src/models/misc/stacker_crane.rs` with: - -- `inventory::submit!` `ProblemSchemaEntry`: - - `name = "StackerCrane"` - - `display_name = "Stacker Crane"` - - `aliases = &[]` - - `dimensions = &[]` - - constructor-facing fields: - - `num_vertices: usize` - - `arcs: Vec<(usize, usize)>` - - `edges: Vec<(usize, usize)>` - - `arc_lengths: Vec` - - `edge_lengths: Vec` - - `bound: i32` -- `#[derive(Debug, Clone, Serialize, Deserialize)] pub struct StackerCrane` -- `new(...)` plus `try_new(...) -> Result`: - - lengths must match arc/edge counts - - all endpoints must be `< num_vertices` - - all lengths must be non-negative - - `bound` must be non-negative -- accessors: - - `num_vertices()`, `num_arcs()`, `num_edges()` - - `arcs()`, `edges()`, `arc_lengths()`, `edge_lengths()`, `bound()` -- helper(s): - - permutation validation for configs - - shortest-path routine on the mixed graph with non-negative integer weights - - optional `closed_walk_length(config) -> Option` helper for test readability -- `Problem` impl: - - `type Metric = bool` - - `variant() -> crate::variant_params![]` - - `dims() -> vec![num_arcs; num_arcs]` - - `evaluate()` returns `false` on invalid config or unreachable connector, otherwise checks total length `<= bound` -- `SatisfactionProblem` impl -- `declare_variants! { default sat StackerCrane => "num_vertices * 2^num_arcs", }` -- `canonical_model_example_specs()` behind `#[cfg(feature = "example-db")]` using the issue's hourglass instance and optimal config `[0, 2, 1, 4, 3]` -- test link: - -```rust -#[cfg(test)] -#[path = "../../unit_tests/models/misc/stacker_crane.rs"] -mod tests; -``` - -Wire the new module through: -- `src/models/misc/mod.rs` -- `src/models/mod.rs` -- `src/lib.rs` - -**Step 2: Run the targeted model tests and verify GREEN** - -Run: - -```bash -cargo test stacker_crane --lib -``` - -Expected: -- PASS for the new `StackerCrane` tests. - -**Step 3: Refactor only if needed** - -Allowed cleanups: -- Extract adjacency construction or shortest-path helpers inside `stacker_crane.rs` -- Normalize constructor validation messages - -Do not add new behavior beyond the tests. - -**Step 4: Commit the model implementation** - -```bash -git add src/models/misc/stacker_crane.rs src/models/misc/mod.rs src/models/mod.rs src/lib.rs src/unit_tests/models/misc/stacker_crane.rs -git commit -m "feat: add the StackerCrane model" -``` - -### Task 3: Verify Example-DB Integration For The Canonical Instance - -**Files:** -- Modify: `src/models/misc/stacker_crane.rs` -- Modify: `src/models/misc/mod.rs` -- Test: `src/unit_tests/example_db.rs` - -**Step 1: Add a failing example-db assertion if coverage is missing** - -Add or extend tests so the new example is exercised via the shared example DB. Prefer one targeted assertion in `src/unit_tests/example_db.rs`: - -- `test_find_model_example_stacker_crane` - - Look up `ProblemRef { name: "StackerCrane", variant: BTreeMap::new() }` - - Assert the example exists and stores the expected optimal config `[0, 2, 1, 4, 3]` - -If the generic example-db self-consistency tests already cover everything after registration, keep the new test minimal and focused on discoverability. - -**Step 2: Run the targeted example-db test and verify RED/GREEN** - -Run first after adding the test: - -```bash -cargo test test_find_model_example_stacker_crane --features example-db -``` - -Expected: -- FAIL until the example registration is correct. - -After fixing any missing registration, rerun the same command and expect PASS. - -Then run the generic example-db consistency checks: - -```bash -cargo test model_specs_are_self_consistent --features example-db -``` - -Expected: -- PASS, including the new `StackerCrane` spec. - -**Step 3: Commit the example-db wiring** - -```bash -git add src/models/misc/stacker_crane.rs src/models/misc/mod.rs src/unit_tests/example_db.rs -git commit -m "test: register StackerCrane canonical example" -``` - -### Task 4: Add `pred create` Support For Mixed-Graph Input - -**Files:** -- Modify: `problemreductions-cli/src/commands/create.rs` -- Modify: `problemreductions-cli/src/cli.rs` -- Test: `problemreductions-cli/src/commands/create.rs` -- Test: `problemreductions-cli/src/cli.rs` - -**Step 1: Write failing CLI tests** - -Add CLI tests in `problemreductions-cli/src/commands/create.rs`: - -- `test_create_stacker_crane_json` - - Use: - -```text -pred create StackerCrane --arcs "0>4,2>5,5>1,3>0,4>3" --graph "0-1,1-2,2-3,3-5,4-5,0-3,1-5" --arc-costs 3,4,2,5,3 --edge-lengths 2,1,3,2,1,4,3 --bound 20 --num-vertices 6 -``` - - - Assert the serialized problem type is `StackerCrane` and key fields match the issue instance. -- `test_create_stacker_crane_rejects_mismatched_arc_lengths` - - Expect an error when `--arc-costs` length does not match the arc count. -- `test_create_stacker_crane_rejects_out_of_range_vertices` - - Expect an error when `--num-vertices` is smaller than the largest referenced endpoint. - -Add a small help test in `problemreductions-cli/src/cli.rs` that checks the create help mentions: - -- `StackerCrane --arcs, --graph, --arc-costs, --edge-lengths, --bound [--num-vertices]` - -**Step 2: Run the targeted CLI tests and verify RED** - -Run: - -```bash -cargo test -p problemreductions-cli stacker_crane -``` - -Expected: -- FAIL because the create arm/help text do not exist yet. - -**Step 3: Implement the CLI support** - -Update `problemreductions-cli/src/commands/create.rs`: - -- add a `StackerCrane` match arm in `create()` -- reuse existing parsers where possible: - - `parse_directed_graph(args.arcs, args.num_vertices)` - - `parse_graph(args)` for undirected edges - - `parse_arc_costs(args, num_arcs)` - - `parse_i32_edge_values(args.edge_lengths.as_ref(), num_edges, "edge length")` or a small wrapper -- parse `--bound` as a non-negative integer and convert to `i32` with a range check -- construct `StackerCrane::try_new(...)` and bubble validation errors instead of panicking - -Update `problemreductions-cli/src/cli.rs`: -- add the StackerCrane line to the "Flags by problem type" help block -- add one example command to the create examples list if the file already keeps that section current - -**Step 4: Run the targeted CLI tests and verify GREEN** - -Run: - -```bash -cargo test -p problemreductions-cli stacker_crane -``` - -Expected: -- PASS for the new create/help tests. - -**Step 5: Commit the CLI support** - -```bash -git add problemreductions-cli/src/commands/create.rs problemreductions-cli/src/cli.rs -git commit -m "feat: add pred create support for StackerCrane" -``` - -### Task 5: Batch-1 Verification Before Paper Work - -**Files:** -- Modify: none unless verification exposes gaps - -**Step 1: Run the batch-1 verification suite** - -Run: - -```bash -cargo test stacker_crane --lib -cargo test test_find_model_example_stacker_crane --features example-db -cargo test model_specs_are_self_consistent --features example-db -cargo test -p problemreductions-cli stacker_crane -``` - -Expected: -- All commands PASS. - -**Step 2: Fix anything that fails, then rerun the same commands** - -Do not move to the paper batch until these commands are green. - -### Task 6: Add Citations And The `problem-def` Entry (Separate Batch) - -**Files:** -- Modify: `docs/paper/references.bib` -- Modify: `docs/paper/reductions.typ` -- Test: `src/unit_tests/models/misc/stacker_crane.rs` - -**Step 1: Add a failing paper-example assertion if missing** - -If Task 1's `test_stacker_crane_issue_witness_and_tighter_bound` is not already acting as the canonical paper example check, extend `src/unit_tests/models/misc/stacker_crane.rs` with: - -- `test_stacker_crane_paper_example` - - Build the exact hourglass instance shown in the paper. - - Assert `[0, 2, 1, 4, 3]` is satisfying. - - For this small instance, use `BruteForce::find_all_satisfying()` and assert the known witness is among the satisfying configs. - -Run: - -```bash -cargo test test_stacker_crane_paper_example --lib -``` - -Expected: -- PASS before touching the paper text, so the paper is anchored to a verified example. - -**Step 2: Update citations** - -Add BibTeX entries to `docs/paper/references.bib` for: -- Frederickson, Hecht, Kim (1978), SIAM J. Comput. 7(2):178-193, DOI `10.1137/0207017` -- Frederickson and Guan (1993), *Nonpreemptive Ensemble Motion Planning on a Tree*, J. Algorithms 15(1):29-60 - -Keep the existing `frederickson1979` entry for the broader postman/routing context. - -**Step 3: Add the display name and `problem-def` entry** - -Update `docs/paper/reductions.typ`: - -- add `"StackerCrane": [Stacker-Crane],` to the `display-name` dictionary -- add `#problem-def("StackerCrane")[ ... ][ ... ]` in the appropriate section near other routing/scheduling/misc problems - -The body must include: -- background linking it to mixed-graph arc routing and the Hamiltonian Circuit reduction -- best-known exact algorithm prose consistent with the declared complexity (`O(|V| 2^{|A|})` style wording) and cited -- the hourglass example from the canonical example spec, not a separate invented instance -- a `pred-commands()` block using `pred create --example StackerCrane -o stacker-crane.json` -- a short verifier walkthrough explaining why `[0, 2, 1, 4, 3]` yields total length 20 - -If a full CeTZ mixed-graph figure is too time-consuming, prefer a compact text/table example over inventing a half-baked graphic, but keep the example concrete and reproducible. - -**Step 4: Build the paper** - -Run: - -```bash -make paper -``` - -Expected: -- PASS. This auto-runs the graph/schema exports before Typst compilation. - -**Step 5: Commit the paper batch** - -```bash -git add docs/paper/references.bib docs/paper/reductions.typ src/unit_tests/models/misc/stacker_crane.rs -git commit -m "docs: add the StackerCrane paper entry" -``` - -### Task 7: Final Verification And PR-Ready Cleanup - -**Files:** -- Modify: any file only if verification exposes a real defect - -**Step 1: Run the full verification needed before completion claims** - -Run: - -```bash -make fmt -make check -cargo test --features example-db test_find_model_example_stacker_crane -cargo test --features example-db model_specs_are_self_consistent -cargo test -p problemreductions-cli stacker_crane -make paper -``` - -Expected: -- Every command exits 0. - -**Step 2: Review against the issue and this plan** - -Confirm all of the following before closing the task: -- `StackerCrane` exists in `src/models/misc/stacker_crane.rs` -- the hourglass example is canonical in example-db -- `pred create StackerCrane ...` works with mixed-graph input -- the paper has both display name and `problem-def` -- the implementation still uses the corrected decision encoding from the issue comments -- no new alias was invented - -**Step 3: Final commit only if verification required follow-up fixes** - -```bash -git add -A -git commit -m "fix: finish StackerCrane verification follow-ups" -``` From e938f2d264153627875416a4ee1d57959d78304e Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Sat, 21 Mar 2026 22:49:29 +0800 Subject: [PATCH 4/4] Fix StackerCrane complexity string and add coverage tests - Change complexity from num_vertices * 2^num_arcs to num_vertices^2 * 2^num_arcs (total DP time, not just state space) - Update paper text to say "total time" instead of "state space" - Add tests for try_new() validation errors, unreachable connectors, and invalid deserialization to bring coverage above 95% Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/paper/reductions.typ | 2 +- src/models/misc/stacker_crane.rs | 2 +- src/unit_tests/models/misc/stacker_crane.rs | 42 +++++++++++++++++++++ 3 files changed, 44 insertions(+), 2 deletions(-) diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index 2b7ac96a6..83c5c5f7f 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -3681,7 +3681,7 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76], #problem-def("StackerCrane")[ Given a mixed graph $G = (V, A, E)$ with directed arcs $A$, undirected edges $E$, nonnegative lengths $l: A union E -> ZZ_(gt.eq 0)$, and a bound $B in ZZ^+$, determine whether there exists a closed walk in $G$ that traverses every arc in $A$ in its prescribed direction and has total length at most $B$. ][ - Stacker Crane is the mixed-graph arc-routing problem listed as ND26 in Garey and Johnson @garey1979. Frederickson, Hecht, and Kim prove the problem NP-complete via reduction from Hamiltonian Circuit and give the classical $9 slash 5$-approximation for the metric case @frederickson1978routing. The problem stays difficult even on trees @fredericksonguan1993. The registry records the standard subset-dynamic-programming state space $O(|V| dot 2^|A|)$#footnote[Included as a straightforward exact dynamic-programming baseline over subsets of required arcs; no sharper exact bound was independently verified while preparing this entry.]. + Stacker Crane is the mixed-graph arc-routing problem listed as ND26 in Garey and Johnson @garey1979. Frederickson, Hecht, and Kim prove the problem NP-complete via reduction from Hamiltonian Circuit and give the classical $9 slash 5$-approximation for the metric case @frederickson1978routing. The problem stays difficult even on trees @fredericksonguan1993. The standard Held-Karp-style dynamic program over (current vertex, covered-arc subset) runs in $O(|V|^2 dot 2^|A|)$ time#footnote[Included as a straightforward exact dynamic-programming baseline over subsets of required arcs; no sharper exact bound was independently verified while preparing this entry.]. A configuration is a permutation of the required arcs, interpreted as the order in which those arcs are forced into the tour. The verifier traverses each chosen arc, then inserts the shortest available connector path from that arc's head to the tail of the next arc, wrapping around at the end to close the walk. diff --git a/src/models/misc/stacker_crane.rs b/src/models/misc/stacker_crane.rs index 84224b37c..b8786a4f1 100644 --- a/src/models/misc/stacker_crane.rs +++ b/src/models/misc/stacker_crane.rs @@ -277,7 +277,7 @@ impl Problem for StackerCrane { impl SatisfactionProblem for StackerCrane {} crate::declare_variants! { - default sat StackerCrane => "num_vertices * 2^num_arcs", + default sat StackerCrane => "num_vertices^2 * 2^num_arcs", } #[derive(Debug, Clone, Deserialize)] diff --git a/src/unit_tests/models/misc/stacker_crane.rs b/src/unit_tests/models/misc/stacker_crane.rs index f73bf9ec3..258d45fec 100644 --- a/src/unit_tests/models/misc/stacker_crane.rs +++ b/src/unit_tests/models/misc/stacker_crane.rs @@ -105,6 +105,48 @@ fn test_stacker_crane_serialization_round_trip() { assert!(round_trip.evaluate(&[0, 2, 1, 4, 3])); } +#[test] +fn test_stacker_crane_try_new_validation_errors() { + // Mismatched arc_lengths length + assert!(StackerCrane::try_new(3, vec![(0, 1)], vec![], vec![1, 2], vec![], 5).is_err()); + + // Mismatched edge_lengths length + assert!(StackerCrane::try_new(3, vec![], vec![(0, 1)], vec![], vec![], 5).is_err()); + + // Negative bound + assert!(StackerCrane::try_new(3, vec![], vec![], vec![], vec![], -1).is_err()); + + // Arc endpoint out of range + assert!(StackerCrane::try_new(2, vec![(0, 5)], vec![], vec![1], vec![], 5).is_err()); + + // Edge endpoint out of range + assert!(StackerCrane::try_new(2, vec![], vec![(0, 5)], vec![], vec![1], 5).is_err()); + + // Negative arc length + assert!(StackerCrane::try_new(3, vec![(0, 1)], vec![], vec![-1], vec![], 5).is_err()); + + // Negative edge length + assert!(StackerCrane::try_new(3, vec![], vec![(0, 1)], vec![], vec![-1], 5).is_err()); +} + +#[test] +fn test_stacker_crane_unreachable_connector() { + // Two disconnected components: arc 0→1 and arc 2→3 with no connecting edges. + let problem = StackerCrane::new(4, vec![(0, 1), (2, 3)], vec![], vec![1, 1], vec![], 100); + + // No permutation can find a connector path from vertex 1 to vertex 2 (or 3 to 0). + assert_eq!(problem.closed_walk_length(&[0, 1]), None); + assert_eq!(problem.closed_walk_length(&[1, 0]), None); + assert!(!problem.evaluate(&[0, 1])); + assert!(!problem.evaluate(&[1, 0])); +} + +#[test] +fn test_stacker_crane_deserialization_rejects_invalid() { + let bad_json = r#"{"num_vertices":2,"arcs":[[0,5]],"edges":[],"arc_lengths":[1],"edge_lengths":[],"bound":5}"#; + assert!(serde_json::from_str::(bad_json).is_err()); +} + #[test] fn test_stacker_crane_is_available_in_prelude() { let problem = crate::prelude::StackerCrane::new(