From 241492adf6a2d62b987a4e87e6576d2ade6bdf52 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Mon, 23 Mar 2026 14:43:38 +0800 Subject: [PATCH 1/3] Add plan for #440: [Model] GroupingBySwapping --- docs/plans/2026-03-23-grouping-by-swapping.md | 308 ++++++++++++++++++ 1 file changed, 308 insertions(+) create mode 100644 docs/plans/2026-03-23-grouping-by-swapping.md diff --git a/docs/plans/2026-03-23-grouping-by-swapping.md b/docs/plans/2026-03-23-grouping-by-swapping.md new file mode 100644 index 00000000..06cee0c1 --- /dev/null +++ b/docs/plans/2026-03-23-grouping-by-swapping.md @@ -0,0 +1,308 @@ +# GroupingBySwapping Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Add the `GroupingBySwapping` satisfaction model from issue #440, including registry/CLI/example-db/paper integration, with the issue's corrected `abcabc` example as the canonical fixture. + +**Architecture:** Implement `GroupingBySwapping` as a `misc` satisfaction model with fields `alphabet_size`, `string`, and `budget`. A configuration is a length-`budget` swap program whose entries are adjacent swap positions `0..string_len-2` plus the no-op slot `string_len-1`; `evaluate()` applies the swaps and accepts exactly when every symbol appears in a single contiguous block. Batch 1 covers model code, registration, CLI, tests, and canonical example wiring. Batch 2 covers the paper entry after the implementation is complete and exports are available. + +**Tech Stack:** Rust library crate, `inventory` schema registration, registry-backed CLI creation, serde, Typst paper, `cargo test`, `make paper`, `make test`, `make clippy` + +--- + +## Batch 1: Model, Registry, CLI, Tests, Example DB + +### Task 1: Write the failing model tests + +**Files:** +- Create: `src/unit_tests/models/misc/grouping_by_swapping.rs` +- Modify: `src/models/misc/grouping_by_swapping.rs` + +**Step 1: Write the failing test** + +Add tests that describe the issue-backed behavior before implementing the model: +- `test_grouping_by_swapping_basic` +- `test_grouping_by_swapping_evaluate_issue_yes` +- `test_grouping_by_swapping_rejects_wrong_length_and_out_of_range_swaps` +- `test_grouping_by_swapping_bruteforce_yes_and_no` +- `test_grouping_by_swapping_paper_example` +- `test_grouping_by_swapping_serialization` + +Use these concrete instances: +- YES instance: `alphabet_size = 3`, `string = [0,1,2,0,1,2]`, `budget = 5`, satisfying config `[2,1,3,5,5]` +- NO-short-budget instance: same string with `budget = 2` +- Minimum-3-swaps witness: same string with `budget = 3`, config `[2,1,3]` + +**Step 2: Run test to verify it fails** + +Run: `cargo test grouping_by_swapping --lib` +Expected: compile/test failure because `GroupingBySwapping` is not implemented or not registered yet. + +**Step 3: Write minimal implementation** + +Create `src/models/misc/grouping_by_swapping.rs` with: +- `ProblemSchemaEntry` +- `GroupingBySwapping` struct +- constructor + getters +- helper(s) to apply a swap program and test groupedness +- `Problem` + `SatisfactionProblem` impls +- `declare_variants! { default sat GroupingBySwapping => "string_len ^ budget", }` +- `#[cfg(test)]` link to the test file + +Required issue-specific semantics: +- symbols are encoded as `0..alphabet_size-1` +- each config entry is either an adjacent swap position or the no-op slot `string_len-1` +- `evaluate()` returns `false` on wrong-length configs, out-of-range values, or invalid input symbols +- groupedness means no symbol reappears after its contiguous block ends + +**Step 4: Run test to verify it passes** + +Run: `cargo test grouping_by_swapping --lib` +Expected: the new model tests pass. + +**Step 5: Commit** + +Run: +```bash +git add src/models/misc/grouping_by_swapping.rs src/unit_tests/models/misc/grouping_by_swapping.rs +git commit -m "Add GroupingBySwapping model" +``` + +### Task 2: Register the model in the crate surface + +**Files:** +- Modify: `src/models/misc/mod.rs` +- Modify: `src/models/mod.rs` +- Modify: `src/lib.rs` + +**Step 1: Write the failing test** + +Extend the new unit test file with assertions that: +- `Problem::NAME == "GroupingBySwapping"` +- `Problem::variant() == vec![]` +- the type is reachable through `crate::models::misc`, `crate::models`, and `crate::prelude` + +**Step 2: Run test to verify it fails** + +Run: `cargo test grouping_by_swapping --lib` +Expected: import/re-export assertions fail or do not compile because the type is not fully re-exported yet. + +**Step 3: Write minimal implementation** + +Register the model in: +- `src/models/misc/mod.rs` +- `src/models/mod.rs` +- `src/lib.rs` prelude exports + +Also add the example-db spec hook in `src/models/misc/mod.rs` once Task 4 creates it. + +**Step 4: Run test to verify it passes** + +Run: `cargo test grouping_by_swapping --lib` +Expected: re-export/import assertions pass. + +**Step 5: Commit** + +Run: +```bash +git add src/models/misc/mod.rs src/models/mod.rs src/lib.rs src/unit_tests/models/misc/grouping_by_swapping.rs +git commit -m "Register GroupingBySwapping exports" +``` + +### Task 3: Add CLI discovery and creation support + +**Files:** +- Modify: `problemreductions-cli/src/problem_name.rs` +- Modify: `problemreductions-cli/src/cli.rs` +- Modify: `problemreductions-cli/src/commands/create.rs` +- Modify: `problemreductions-cli/tests/cli_tests.rs` + +**Step 1: Write the failing test** + +Add CLI integration coverage for: +- `pred create GroupingBySwapping --string "0,1,2,0,1,2" --bound 5` +- optional `--alphabet-size 3` +- `pred create --example GroupingBySwapping` +- helpful usage text when `--string` or `--bound` is missing + +Prefer the same assertions used by the `LongestCommonSubsequence` and `StringToStringCorrection` tests: +- JSON `type == "GroupingBySwapping"` +- `data.alphabet_size == 3` +- `data.string == [0,1,2,0,1,2]` +- `data.budget == 5` + +**Step 2: Run test to verify it fails** + +Run: `cargo test -p problemreductions-cli grouping_by_swapping` +Expected: CLI tests fail because the alias/help/create path does not exist yet. + +**Step 3: Write minimal implementation** + +Add the canonical name wiring: +- `resolve_alias()` support for `"groupingbyswapping"` + +Add a dedicated CLI flag: +- `--string` for a comma-separated symbol list + +Update: +- `all_data_flags_empty()` +- `CreateArgs` docs / "Flags by problem type" help +- problem-specific help examples in `create.rs` +- `create()` match arm that parses `--string`, infers `alphabet_size` when omitted, validates `--alphabet-size >= max(symbol)+1`, and constructs `GroupingBySwapping` + +**Step 4: Run test to verify it passes** + +Run: `cargo test -p problemreductions-cli grouping_by_swapping` +Expected: the new CLI tests pass. + +**Step 5: Commit** + +Run: +```bash +git add problemreductions-cli/src/problem_name.rs problemreductions-cli/src/cli.rs problemreductions-cli/src/commands/create.rs problemreductions-cli/tests/cli_tests.rs +git commit -m "Add GroupingBySwapping CLI support" +``` + +### Task 4: Add the canonical example-db fixture and paper-backed test data + +**Files:** +- Modify: `src/models/misc/grouping_by_swapping.rs` +- Modify: `src/models/misc/mod.rs` + +**Step 1: Write the failing test** + +Extend `src/unit_tests/models/misc/grouping_by_swapping.rs` to require: +- `canonical_model_example_specs()` exports the issue's `abcabc`, `K=5`, config `[2,1,3,5,5]` +- the paper/example test verifies the exact issue witness is satisfying +- `BruteForce` confirms the same string is unsatisfiable for `budget = 2` + +**Step 2: Run test to verify it fails** + +Run: `cargo test grouping_by_swapping --features example-db --lib` +Expected: failure because the canonical example spec is missing or incomplete. + +**Step 3: Write minimal implementation** + +Add `canonical_model_example_specs()` to the model file and register it from `src/models/misc/mod.rs`. + +Use the issue-corrected example exactly: +- instance: `GroupingBySwapping::new(3, vec![0,1,2,0,1,2], 5)` +- optimal/satisfying config: `vec![2,1,3,5,5]` +- optimal value: `true` + +**Step 4: Run test to verify it passes** + +Run: `cargo test grouping_by_swapping --features example-db --lib` +Expected: example-db-aware tests pass. + +**Step 5: Commit** + +Run: +```bash +git add src/models/misc/grouping_by_swapping.rs src/models/misc/mod.rs src/unit_tests/models/misc/grouping_by_swapping.rs +git commit -m "Add GroupingBySwapping canonical example" +``` + +### Task 5: Batch 1 verification + +**Files:** +- Modify: none + +**Step 1: Run focused verification** + +Run: +```bash +cargo test grouping_by_swapping --workspace +cargo test -p problemreductions-cli grouping_by_swapping +``` + +Expected: all focused library and CLI tests for `GroupingBySwapping` pass. + +**Step 2: Run broader verification** + +Run: +```bash +make test +make clippy +``` + +Expected: repository tests and clippy pass without introducing regressions. + +**Step 3: Commit** + +If verification required follow-up fixes, commit them coherently before moving to Batch 2. + +## Batch 2: Paper Entry + +### Task 6: Add the Typst paper entry after exports are available + +**Files:** +- Modify: `docs/paper/reductions.typ` +- Test: `src/unit_tests/models/misc/grouping_by_swapping.rs` + +**Step 1: Write the failing test** + +Finalize `test_grouping_by_swapping_paper_example` so it matches the paper/example-db instance exactly: +- construct the canonical `abcabc`, `K=5` instance +- assert `[2,1,3,5,5]` is satisfying +- assert `budget = 2` is unsatisfiable via `BruteForce` + +**Step 2: Run test to verify it fails if needed** + +Run: `cargo test grouping_by_swapping_paper_example --lib` +Expected: if the example/test drifted during Batch 1, fix the test before touching the paper. + +**Step 3: Write minimal implementation** + +In `docs/paper/reductions.typ`: +- add `"GroupingBySwapping": [Grouping by Swapping]` to `display-name` +- add `#problem-def("GroupingBySwapping")[...]` +- use the canonical example data from the checked-in fixture flow +- describe the `abcabc -> aabbcc` witness with the issue's 3 effective swaps and 2 trailing no-ops +- include `pred-commands()` using the canonical example spec +- cite Garey & Johnson SR21 and note the brute-force bound used by the model metadata + +**Step 4: Run paper verification** + +Run: `make paper` +Expected: the Typst paper builds cleanly and the new entry renders without missing references. + +**Step 5: Commit** + +Run: +```bash +git add docs/paper/reductions.typ src/unit_tests/models/misc/grouping_by_swapping.rs +git commit -m "Document GroupingBySwapping in paper" +``` + +## Final Verification + +### Task 7: Full verification before handoff + +**Files:** +- Modify: none + +**Step 1: Run the final required checks** + +Run: +```bash +make test +make clippy +make paper +git status --short +``` + +Expected: +- tests pass +- clippy passes +- paper builds +- the tree is clean except for intentionally ignored/generated files + +**Step 2: Prepare the implementation summary** + +Summarize: +- model file and helper logic +- registration/export/CLI/example-db integration +- paper entry +- any deviations from this plan + From 43f5bf293e6a3418091f3c6da0c583fbb7be98d5 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Mon, 23 Mar 2026 15:01:11 +0800 Subject: [PATCH 2/3] Implement #440: [Model] GroupingBySwapping --- docs/paper/reductions.typ | 62 ++++++ problemreductions-cli/src/cli.rs | 15 +- problemreductions-cli/src/commands/create.rs | 56 +++++- problemreductions-cli/src/problem_name.rs | 3 + problemreductions-cli/tests/cli_tests.rs | 63 +++++++ src/lib.rs | 16 +- src/models/misc/grouping_by_swapping.rs | 178 ++++++++++++++++++ src/models/misc/mod.rs | 4 + src/models/mod.rs | 6 +- .../models/misc/grouping_by_swapping.rs | 108 +++++++++++ 10 files changed, 495 insertions(+), 16 deletions(-) create mode 100644 src/models/misc/grouping_by_swapping.rs create mode 100644 src/unit_tests/models/misc/grouping_by_swapping.rs diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index c60bbcde..657f9e8f 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -144,6 +144,7 @@ "IntegralFlowWithMultipliers": [Integral Flow With Multipliers], "MinMaxMulticenter": [Min-Max Multicenter], "FlowShopScheduling": [Flow Shop Scheduling], + "GroupingBySwapping": [Grouping by Swapping], "MinimumCutIntoBoundedSets": [Minimum Cut Into Bounded Sets], "MinimumDummyActivitiesPert": [Minimum Dummy Activities in PERT Networks], "MinimumSumMulticenter": [Minimum Sum Multicenter], @@ -4380,6 +4381,67 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76], ] } +#{ + let x = load-model-example("GroupingBySwapping") + let source = x.instance.string + let alpha-size = x.instance.alphabet_size + let budget = x.instance.budget + let config = x.optimal_config + let alpha-map = range(alpha-size).map(i => str.from-unicode(97 + i)) + let fmt-str(s) = s.map(c => alpha-map.at(c)).join("") + let source-str = fmt-str(source) + let step1 = (0, 1, 0, 2, 1, 2) + let step2 = (0, 0, 1, 2, 1, 2) + let step3 = (0, 0, 1, 1, 2, 2) + let step3-str = fmt-str(step3) + [ + #problem-def("GroupingBySwapping")[ + Given a finite alphabet $Sigma$, a string $x in Sigma^*$, and a positive integer $K$, determine whether there exists a sequence of at most $K$ adjacent symbol interchanges that transforms $x$ into a string $y in Sigma^*$ in which every symbol $a in Sigma$ appears in a single contiguous block. Equivalently, $y$ contains no subsequence $a b a$ with distinct $a, b in Sigma$. + ][ + Grouping by Swapping is the storage-and-retrieval problem SR21 in Garey and Johnson @garey1979. It asks whether a string can be locally reorganized, using only adjacent transpositions, until equal symbols coalesce into blocks. The implementation in this crate uses a fixed-length swap program with one slot per allowed operation, so the direct brute-force search explores $O(|x|^K)$ configurations.#footnote[This is the exact search bound induced by the fixed-length witness encoding implemented in the codebase; no sharper exact worst-case bound is claimed here.] + + *Example.* Let $Sigma = {#alpha-map.join(", ")}$, $x = #source-str$, and $K = #budget$. The configuration $p = (#config.map(str).join(", "))$ performs adjacent swaps at positions $(2, 3)$, $(1, 2)$, and $(3, 4)$, then uses two trailing no-op slots. The resulting string is $y = #step3-str$, so every symbol now appears in one contiguous block and the verifier returns YES. + + #pred-commands( + "pred create --example " + problem-spec(x) + " -o grouping-by-swapping.json", + "pred solve grouping-by-swapping.json --solver brute-force", + "pred evaluate grouping-by-swapping.json --config " + x.optimal_config.map(str).join(","), + ) + + #figure({ + let blue = graph-colors.at(0) + let cell(ch, highlight: false) = { + let fill = if highlight { blue.transparentize(72%) } else { white } + box(width: 0.55cm, height: 0.55cm, fill: fill, stroke: 0.5pt + luma(120), + align(center + horizon, text(9pt, weight: "bold", ch))) + } + align(center, stack(dir: ttb, spacing: 0.45cm, + stack(dir: ltr, spacing: 0pt, + box(width: 2.2cm, height: 0.5cm, align(right + horizon, text(8pt)[$x: quad$])), + ..source.map(c => cell(alpha-map.at(c))), + ), + stack(dir: ltr, spacing: 0pt, + box(width: 2.2cm, height: 0.5cm, align(right + horizon, text(8pt)[swap$(2,3)$: quad])), + ..step1.enumerate().map(((i, c)) => cell(alpha-map.at(c), highlight: i == 2 or i == 3)), + ), + stack(dir: ltr, spacing: 0pt, + box(width: 2.2cm, height: 0.5cm, align(right + horizon, text(8pt)[swap$(1,2)$: quad])), + ..step2.enumerate().map(((i, c)) => cell(alpha-map.at(c), highlight: i == 1 or i == 2)), + ), + stack(dir: ltr, spacing: 0pt, + box(width: 2.2cm, height: 0.5cm, align(right + horizon, text(8pt)[swap$(3,4)$: quad])), + ..step3.enumerate().map(((i, c)) => cell(alpha-map.at(c), highlight: i == 3 or i == 4)), + ), + )) + }, + caption: [Grouping by Swapping on $x = #source-str$: three effective adjacent swaps turn the alternating string into $y = #step3-str$. The remaining two slots in $p = (#config.map(str).join(", "))$ are no-ops at position 5.], + ) + + The final row has exactly one block of $a$, one block of $b$, and one block of $c$, so it satisfies the grouping constraint within the allotted budget. + ] + ] +} + #{ let x = load-model-example("LongestCommonSubsequence") let strings = x.instance.strings diff --git a/problemreductions-cli/src/cli.rs b/problemreductions-cli/src/cli.rs index af87c708..4f97d9d3 100644 --- a/problemreductions-cli/src/cli.rs +++ b/problemreductions-cli/src/cli.rs @@ -286,6 +286,7 @@ Flags by problem type: AdditionalKey --num-attributes, --dependencies, --relation-attrs [--known-keys] ConsistencyOfDatabaseFrequencyTables --num-objects, --attribute-domains, --frequency-tables [--known-values] SubgraphIsomorphism --graph (host), --pattern (pattern) + GroupingBySwapping --string, --bound [--alphabet-size] LCS --strings, --bound [--alphabet-size] FAS --arcs [--weights] [--num-vertices] FVS --arcs [--weights] [--num-vertices] @@ -330,6 +331,7 @@ Examples: pred create GeneralizedHex --graph 0-1,0-2,0-3,1-4,2-4,3-4,4-5 --source 0 --sink 5 pred create IntegralFlowWithMultipliers --arcs \"0>1,0>2,1>3,2>3\" --capacities 1,1,2,2 --source 0 --sink 3 --multipliers 1,2,3,1 --requirement 2 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 + pred create GroupingBySwapping --string \"0,1,2,0,1,2\" --bound 5 | pred solve - --solver brute-force pred create StringToStringCorrection --source-string \"0,1,2,3,1,0\" --target-string \"0,1,3,2,1\" --bound 2 | pred solve - --solver brute-force pred create MIS/KingsSubgraph --positions \"0,0;1,0;1,1;0,1\" pred create MIS/UnitDiskGraph --positions \"0,0;1,0;0.5,0.8\" --radius 1.5 @@ -550,7 +552,7 @@ pub struct CreateArgs { /// Required edge indices for RuralPostman (comma-separated, e.g., "0,2,4") #[arg(long)] pub required_edges: Option, - /// Bound parameter (lower bound for LongestCircuit; upper or length bound for BoundedComponentSpanningForest, LengthBoundedDisjointPaths, LongestCommonSubsequence, MultipleCopyFileAllocation, MultipleChoiceBranching, OptimalLinearArrangement, RootedTreeArrangement, RuralPostman, ShortestCommonSupersequence, or StringToStringCorrection) + /// Bound parameter (lower bound for LongestCircuit; upper or length bound for BoundedComponentSpanningForest, GroupingBySwapping, LengthBoundedDisjointPaths, LongestCommonSubsequence, MultipleCopyFileAllocation, MultipleChoiceBranching, OptimalLinearArrangement, RootedTreeArrangement, RuralPostman, ShortestCommonSupersequence, or StringToStringCorrection) #[arg(long, allow_hyphen_values = true)] pub bound: Option, /// Upper bound on expected retrieval latency for ExpectedRetrievalCost @@ -577,6 +579,9 @@ pub struct CreateArgs { /// Input strings for LCS (e.g., "ABAC;BACA" or "0,1,0;1,0,1") or SCS (e.g., "0,1,2;1,2,0") #[arg(long)] pub strings: Option, + /// Input string for GroupingBySwapping (comma-separated symbol indices, e.g., "0,1,2,0,1,2") + #[arg(long)] + pub string: Option, /// Task costs for SequencingToMinimizeMaximumCumulativeCost (comma-separated, e.g., "2,-1,3,-2,1,-3") #[arg(long, allow_hyphen_values = true)] pub costs: Option, @@ -670,7 +675,7 @@ pub struct CreateArgs { /// Task availability rows for TimetableDesign (semicolon-separated 0/1 rows) #[arg(long)] pub task_avail: Option, - /// Alphabet size for LCS, SCS, StringToStringCorrection, or TwoDimensionalConsecutiveSets (optional; inferred from the input strings if omitted) + /// Alphabet size for GroupingBySwapping, LCS, SCS, StringToStringCorrection, or TwoDimensionalConsecutiveSets (optional; inferred from the input strings if omitted) #[arg(long)] pub alphabet_size: Option, @@ -735,6 +740,7 @@ Examples: pred solve reduced.json # solve a reduction bundle pred solve reduced.json -o solution.json # save result to file pred create MIS --graph 0-1,1-2 | pred solve - # read from stdin when an ILP path exists + pred create GroupingBySwapping --string \"0,1,2,0,1,2\" --bound 5 | pred solve - --solver brute-force pred create StringToStringCorrection --source-string \"0,1,2,3,1,0\" --target-string \"0,1,3,2,1\" --bound 2 | pred solve - --solver brute-force pred create TwoDimensionalConsecutiveSets --alphabet-size 6 --sets \"0,1,2;3,4,5;1,3;2,4;0,5\" | pred solve - --solver brute-force pred solve problem.json --timeout 10 # abort after 10 seconds @@ -750,8 +756,9 @@ Solve via explicit reduction: Input: a problem JSON from `pred create`, or a reduction bundle from `pred reduce`. When given a bundle, the target is solved and the solution is mapped back to the source. The ILP solver auto-reduces non-ILP problems before solving. -Problems without an ILP reduction path, such as `LengthBoundedDisjointPaths`, -`MinMaxMulticenter`, and `StringToStringCorrection`, currently need `--solver brute-force`. +Problems without an ILP reduction path, such as `GroupingBySwapping`, +`LengthBoundedDisjointPaths`, `MinMaxMulticenter`, and `StringToStringCorrection`, +currently need `--solver brute-force`. ILP backend (default: HiGHS). To use a different backend: cargo install problemreductions-cli --features coin-cbc diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index 3829c601..9dfef0d4 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -22,7 +22,7 @@ use problemreductions::models::graph::{ use problemreductions::models::misc::{ AdditionalKey, BinPacking, BoyceCoddNormalFormViolation, CapacityAssignment, CbqRelation, ConjunctiveBooleanQuery, ConsistencyOfDatabaseFrequencyTables, EnsembleComputation, - ExpectedRetrievalCost, FlowShopScheduling, FrequencyTable, KnownValue, + ExpectedRetrievalCost, FlowShopScheduling, FrequencyTable, GroupingBySwapping, KnownValue, LongestCommonSubsequence, MinimumTardinessSequencing, MultiprocessorScheduling, PaintShop, PartiallyOrderedKnapsack, QueryArg, RectilinearPictureCompression, ResourceConstrainedScheduling, SchedulingWithIndividualDeadlines, @@ -125,6 +125,7 @@ fn all_data_flags_empty(args: &CreateArgs) -> bool { && args.delay_budget.is_none() && args.pattern.is_none() && args.strings.is_none() + && args.string.is_none() && args.costs.is_none() && args.arc_costs.is_none() && args.arcs.is_none() @@ -680,6 +681,7 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str { "LongestCommonSubsequence" => { "--strings \"010110;100101;001011\" --bound 3 --alphabet-size 2" } + "GroupingBySwapping" => "--string \"0,1,2,0,1,2\" --bound 5", "MinimumCardinalityKey" => { "--num-attributes 6 --dependencies \"0,1>2;0,2>3;1,3>4;2,4>5\" --bound 2" } @@ -805,6 +807,7 @@ fn help_flag_hint( ("LongestCommonSubsequence", "strings") => { "raw strings: \"ABAC;BACA\" or symbol lists: \"0,1,0;1,0,1\"" } + ("GroupingBySwapping", "string") => "symbol list: \"0,1,2,0,1,2\"", ("ShortestCommonSupersequence", "strings") => "symbol lists: \"0,1,2;1,2,0\"", ("MultipleChoiceBranching", "partition") => "semicolon-separated groups: \"0,1;2,3\"", ("IntegralFlowHomologousArcs", "homologous_pairs") => { @@ -2919,6 +2922,56 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { ) } + // GroupingBySwapping + "GroupingBySwapping" => { + let usage = + "Usage: pred create GroupingBySwapping --string \"0,1,2,0,1,2\" --bound 5 [--alphabet-size 3]"; + let string_str = args.string.as_deref().ok_or_else(|| { + anyhow::anyhow!("GroupingBySwapping requires --string\n\n{usage}") + })?; + let bound = parse_nonnegative_usize_bound( + args.bound.ok_or_else(|| { + anyhow::anyhow!("GroupingBySwapping requires --bound\n\n{usage}") + })?, + "GroupingBySwapping", + usage, + )?; + + let string = if string_str.trim().is_empty() { + Vec::new() + } else { + string_str + .split(',') + .map(|value| { + value + .trim() + .parse::() + .context("invalid symbol index") + }) + .collect::>>()? + }; + let inferred = string.iter().copied().max().map_or(0, |value| value + 1); + let alphabet_size = args.alphabet_size.unwrap_or(inferred); + anyhow::ensure!( + alphabet_size >= inferred, + "--alphabet-size {} is smaller than max symbol + 1 ({}) in the input string", + alphabet_size, + inferred + ); + anyhow::ensure!( + alphabet_size > 0 || string.is_empty(), + "GroupingBySwapping requires a positive alphabet for non-empty strings.\n\n{usage}" + ); + anyhow::ensure!( + !string.is_empty() || bound == 0, + "GroupingBySwapping requires --bound 0 when --string is empty.\n\n{usage}" + ); + ( + ser(GroupingBySwapping::new(alphabet_size, string, bound))?, + resolved_variant.clone(), + ) + } + // ClosestVectorProblem "ClosestVectorProblem" => { let basis_str = args.basis.as_deref().ok_or_else(|| { @@ -7222,6 +7275,7 @@ mod tests { delay_budget: None, pattern: None, strings: None, + string: None, arc_costs: None, arcs: None, values: None, diff --git a/problemreductions-cli/src/problem_name.rs b/problemreductions-cli/src/problem_name.rs index 92e2265a..94817e75 100644 --- a/problemreductions-cli/src/problem_name.rs +++ b/problemreductions-cli/src/problem_name.rs @@ -17,6 +17,9 @@ pub fn resolve_alias(input: &str) -> String { if input.eq_ignore_ascii_case("UndirectedFlowLowerBounds") { return "UndirectedFlowLowerBounds".to_string(); } + if input.eq_ignore_ascii_case("GroupingBySwapping") { + return "GroupingBySwapping".to_string(); + } if let Some(pt) = problemreductions::registry::find_problem_type_by_alias(input) { return pt.canonical_name.to_string(); } diff --git a/problemreductions-cli/tests/cli_tests.rs b/problemreductions-cli/tests/cli_tests.rs index b4ce70b5..7a95af09 100644 --- a/problemreductions-cli/tests/cli_tests.rs +++ b/problemreductions-cli/tests/cli_tests.rs @@ -3548,6 +3548,69 @@ fn test_create_string_to_string_correction_rejects_negative_bound() { assert!(stderr.contains("nonnegative --bound"), "stderr: {stderr}"); } +#[test] +fn test_create_grouping_by_swapping() { + let output = pred() + .args([ + "create", + "GroupingBySwapping", + "--string", + "0,1,2,0,1,2", + "--bound", + "5", + ]) + .output() + .unwrap(); + assert!( + output.status.success(), + "stderr: {}", + String::from_utf8_lossy(&output.stderr) + ); + let stdout = String::from_utf8(output.stdout).unwrap(); + let json: serde_json::Value = serde_json::from_str(&stdout).unwrap(); + assert_eq!(json["type"], "GroupingBySwapping"); + assert_eq!(json["data"]["alphabet_size"], 3); + assert_eq!( + json["data"]["string"], + serde_json::json!([0, 1, 2, 0, 1, 2]) + ); + assert_eq!(json["data"]["budget"], 5); +} + +#[test] +fn test_create_model_example_grouping_by_swapping() { + let output = pred() + .args(["create", "--example", "GroupingBySwapping"]) + .output() + .unwrap(); + assert!( + output.status.success(), + "stderr: {}", + String::from_utf8_lossy(&output.stderr) + ); + let stdout = String::from_utf8(output.stdout).unwrap(); + let json: serde_json::Value = serde_json::from_str(&stdout).unwrap(); + assert_eq!(json["type"], "GroupingBySwapping"); + assert_eq!(json["data"]["alphabet_size"], 3); + assert_eq!( + json["data"]["string"], + serde_json::json!([0, 1, 2, 0, 1, 2]) + ); + assert_eq!(json["data"]["budget"], 5); +} + +#[test] +fn test_create_grouping_by_swapping_help_uses_cli_flags() { + let output = pred() + .args(["create", "GroupingBySwapping"]) + .output() + .unwrap(); + assert!(!output.status.success()); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("--string"), "stderr: {stderr}"); + assert!(stderr.contains("--bound"), "stderr: {stderr}"); +} + #[test] fn test_create_spinglass() { let output_file = std::env::temp_dir().join("pred_test_create_sg.json"); diff --git a/src/lib.rs b/src/lib.rs index 30c0d472..df7982d9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -69,14 +69,14 @@ pub mod prelude { pub use crate::models::misc::{ AdditionalKey, BinPacking, BoyceCoddNormalFormViolation, CapacityAssignment, CbqRelation, ConjunctiveBooleanQuery, ConjunctiveQueryFoldability, ConsistencyOfDatabaseFrequencyTables, - EnsembleComputation, ExpectedRetrievalCost, Factoring, FlowShopScheduling, Knapsack, - LongestCommonSubsequence, MinimumTardinessSequencing, MultiprocessorScheduling, PaintShop, - Partition, QueryArg, RectilinearPictureCompression, ResourceConstrainedScheduling, - SchedulingWithIndividualDeadlines, SequencingToMinimizeMaximumCumulativeCost, - SequencingToMinimizeWeightedCompletionTime, SequencingToMinimizeWeightedTardiness, - SequencingWithReleaseTimesAndDeadlines, SequencingWithinIntervals, - ShortestCommonSupersequence, StackerCrane, StaffScheduling, StringToStringCorrection, - SubsetSum, SumOfSquaresPartition, Term, TimetableDesign, + EnsembleComputation, ExpectedRetrievalCost, Factoring, FlowShopScheduling, + GroupingBySwapping, Knapsack, LongestCommonSubsequence, MinimumTardinessSequencing, + MultiprocessorScheduling, PaintShop, Partition, QueryArg, RectilinearPictureCompression, + ResourceConstrainedScheduling, SchedulingWithIndividualDeadlines, + SequencingToMinimizeMaximumCumulativeCost, SequencingToMinimizeWeightedCompletionTime, + SequencingToMinimizeWeightedTardiness, SequencingWithReleaseTimesAndDeadlines, + SequencingWithinIntervals, ShortestCommonSupersequence, StackerCrane, StaffScheduling, + StringToStringCorrection, SubsetSum, SumOfSquaresPartition, Term, TimetableDesign, }; pub use crate::models::set::{ ComparativeContainment, ConsecutiveSets, ExactCoverBy3Sets, MaximumSetPacking, diff --git a/src/models/misc/grouping_by_swapping.rs b/src/models/misc/grouping_by_swapping.rs new file mode 100644 index 00000000..34f9781b --- /dev/null +++ b/src/models/misc/grouping_by_swapping.rs @@ -0,0 +1,178 @@ +//! Grouping by Swapping problem implementation. +//! +//! Given a string over a finite alphabet and a swap budget `K`, determine +//! whether at most `K` adjacent swaps can transform the string so that every +//! symbol appears in a single contiguous block. + +use crate::registry::{FieldInfo, ProblemSchemaEntry}; +use crate::traits::{Problem, SatisfactionProblem}; +use serde::{Deserialize, Serialize}; + +inventory::submit! { + ProblemSchemaEntry { + name: "GroupingBySwapping", + display_name: "Grouping by Swapping", + aliases: &[], + dimensions: &[], + module_path: module_path!(), + description: "Group equal symbols into contiguous blocks using at most K adjacent swaps", + fields: &[ + FieldInfo { name: "alphabet_size", type_name: "usize", description: "Size of the alphabet" }, + FieldInfo { name: "string", type_name: "Vec", description: "Input string over {0, ..., alphabet_size-1}" }, + FieldInfo { name: "budget", type_name: "usize", description: "Maximum number of adjacent swaps allowed" }, + ], + } +} + +/// Grouping by Swapping. +/// +/// A configuration is a length-`budget` swap program. Each entry is either an +/// adjacent swap position `i` (swap positions `i` and `i + 1`) or the special +/// no-op value `string_len - 1`. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GroupingBySwapping { + alphabet_size: usize, + string: Vec, + budget: usize, +} + +impl GroupingBySwapping { + /// Create a new GroupingBySwapping instance. + /// + /// # Panics + /// + /// Panics if the string contains a symbol outside the declared alphabet, + /// or if the string is empty while the budget is positive. + pub fn new(alphabet_size: usize, string: Vec, budget: usize) -> Self { + assert!( + alphabet_size > 0 || string.is_empty(), + "alphabet_size must be > 0 when string is non-empty" + ); + assert!( + string.iter().all(|&symbol| symbol < alphabet_size), + "input symbols must be less than alphabet_size" + ); + assert!( + !string.is_empty() || budget == 0, + "budget must be 0 when string is empty" + ); + Self { + alphabet_size, + string, + budget, + } + } + + /// Returns the alphabet size. + pub fn alphabet_size(&self) -> usize { + self.alphabet_size + } + + /// Returns the input string. + pub fn string(&self) -> &[usize] { + &self.string + } + + /// Returns the swap budget. + pub fn budget(&self) -> usize { + self.budget + } + + /// Returns the input string length. + pub fn string_len(&self) -> usize { + self.string.len() + } + + /// Applies a swap program to the input string. + /// + /// Returns `None` if the configuration has the wrong length or contains an + /// out-of-range swap slot. + pub fn apply_swap_program(&self, config: &[usize]) -> Option> { + if config.len() != self.budget { + return None; + } + if self.string.is_empty() { + return if config.is_empty() { + Some(Vec::new()) + } else { + None + }; + } + + let no_op = self.string.len() - 1; + let mut current = self.string.clone(); + for &slot in config { + if slot >= self.string.len() { + return None; + } + if slot != no_op { + current.swap(slot, slot + 1); + } + } + Some(current) + } + + /// Returns whether every symbol in `candidate` appears in a single block. + pub fn is_grouped(&self, candidate: &[usize]) -> bool { + if candidate.iter().any(|&symbol| symbol >= self.alphabet_size) { + return false; + } + if candidate.is_empty() { + return true; + } + + let mut closed = vec![false; self.alphabet_size]; + let mut current_symbol = candidate[0]; + + for &symbol in candidate.iter().skip(1) { + if symbol == current_symbol { + continue; + } + closed[current_symbol] = true; + if closed[symbol] { + return false; + } + current_symbol = symbol; + } + + true + } +} + +impl Problem for GroupingBySwapping { + const NAME: &'static str = "GroupingBySwapping"; + type Metric = bool; + + fn dims(&self) -> Vec { + vec![self.string_len(); self.budget] + } + + fn evaluate(&self, config: &[usize]) -> bool { + self.apply_swap_program(config) + .is_some_and(|candidate| self.is_grouped(&candidate)) + } + + fn variant() -> Vec<(&'static str, &'static str)> { + crate::variant_params![] + } +} + +impl SatisfactionProblem for GroupingBySwapping {} + +crate::declare_variants! { + default sat GroupingBySwapping => "string_len ^ budget", +} + +#[cfg(feature = "example-db")] +pub(crate) fn canonical_model_example_specs() -> Vec { + vec![crate::example_db::specs::ModelExampleSpec { + id: "grouping_by_swapping", + instance: Box::new(GroupingBySwapping::new(3, vec![0, 1, 2, 0, 1, 2], 5)), + optimal_config: vec![2, 1, 3, 5, 5], + optimal_value: serde_json::json!(true), + }] +} + +#[cfg(test)] +#[path = "../../unit_tests/models/misc/grouping_by_swapping.rs"] +mod tests; diff --git a/src/models/misc/mod.rs b/src/models/misc/mod.rs index 38ae5af4..2d07dc9b 100644 --- a/src/models/misc/mod.rs +++ b/src/models/misc/mod.rs @@ -10,6 +10,7 @@ //! - [`ExpectedRetrievalCost`]: Allocate records to circular sectors within a latency bound //! - [`Factoring`]: Integer factorization //! - [`FlowShopScheduling`]: Flow Shop Scheduling (meet deadline on m processors) +//! - [`GroupingBySwapping`]: Group equal symbols into contiguous blocks by adjacent swaps //! - [`Knapsack`]: 0-1 Knapsack (maximize value subject to weight capacity) //! - [`MultiprocessorScheduling`]: Schedule tasks on processors to meet a deadline //! - [`LongestCommonSubsequence`]: Longest Common Subsequence @@ -44,6 +45,7 @@ mod ensemble_computation; pub(crate) mod expected_retrieval_cost; pub(crate) mod factoring; mod flow_shop_scheduling; +mod grouping_by_swapping; mod knapsack; mod longest_common_subsequence; mod minimum_tardiness_sequencing; @@ -81,6 +83,7 @@ pub use ensemble_computation::EnsembleComputation; pub use expected_retrieval_cost::ExpectedRetrievalCost; pub use factoring::Factoring; pub use flow_shop_scheduling::FlowShopScheduling; +pub use grouping_by_swapping::GroupingBySwapping; pub use knapsack::Knapsack; pub use longest_common_subsequence::LongestCommonSubsequence; pub use minimum_tardiness_sequencing::MinimumTardinessSequencing; @@ -116,6 +119,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec GroupingBySwapping { + GroupingBySwapping::new(3, vec![0, 1, 2, 0, 1, 2], 5) +} + +fn issue_minimum_three_swaps_instance() -> GroupingBySwapping { + GroupingBySwapping::new(3, vec![0, 1, 2, 0, 1, 2], 3) +} + +fn issue_two_swap_instance() -> GroupingBySwapping { + GroupingBySwapping::new(3, vec![0, 1, 2, 0, 1, 2], 2) +} + +#[test] +fn test_grouping_by_swapping_basic() { + let problem = issue_yes_instance(); + assert_eq!(problem.alphabet_size(), 3); + assert_eq!(problem.string(), &[0, 1, 2, 0, 1, 2]); + assert_eq!(problem.budget(), 5); + assert_eq!(problem.string_len(), 6); + assert_eq!(problem.num_variables(), 5); + assert_eq!(problem.dims(), vec![6; 5]); + assert_eq!(::NAME, "GroupingBySwapping"); + assert_eq!(::variant(), vec![]); + + let _: crate::models::misc::GroupingBySwapping = problem.clone(); + let _: crate::models::GroupingBySwapping = problem.clone(); + let _: crate::prelude::GroupingBySwapping = problem; +} + +#[test] +fn test_grouping_by_swapping_evaluate_issue_yes() { + let problem = issue_yes_instance(); + assert!(problem.evaluate(&[2, 1, 3, 5, 5])); + assert_eq!( + problem.apply_swap_program(&[2, 1, 3, 5, 5]), + Some(vec![0, 0, 1, 1, 2, 2]) + ); + assert!(!problem.evaluate(&[0, 1, 2, 3, 4])); + assert!(!problem.is_grouped(&[0, 1, 0])); + assert!(problem.is_grouped(&[0, 0, 1, 1, 2, 2])); +} + +#[test] +fn test_grouping_by_swapping_rejects_wrong_length_and_out_of_range_swaps() { + let problem = issue_yes_instance(); + assert!(!problem.evaluate(&[2, 1, 3, 5])); + assert!(!problem.evaluate(&[2, 1, 3, 5, 6])); + assert_eq!(problem.apply_swap_program(&[2, 1, 3, 5]), None); + assert_eq!(problem.apply_swap_program(&[2, 1, 3, 5, 6]), None); +} + +#[test] +fn test_grouping_by_swapping_bruteforce_yes_and_no() { + let yes_problem = issue_minimum_three_swaps_instance(); + let no_problem = issue_two_swap_instance(); + let solver = BruteForce::new(); + + let satisfying = solver + .find_satisfying(&yes_problem) + .expect("expected a satisfying 3-swap sequence"); + assert!(yes_problem.evaluate(&satisfying)); + assert!(solver + .find_all_satisfying(&yes_problem) + .iter() + .any(|config| config == &vec![2, 1, 3])); + + assert!(solver.find_satisfying(&no_problem).is_none()); + assert!(solver.find_all_satisfying(&no_problem).is_empty()); +} + +#[test] +fn test_grouping_by_swapping_paper_example() { + let problem = issue_yes_instance(); + assert!(problem.evaluate(&[2, 1, 3, 5, 5])); + + let solver = BruteForce::new(); + assert!(solver + .find_all_satisfying(&problem) + .iter() + .any(|config| config == &vec![2, 1, 3, 5, 5])); + assert!(solver.find_satisfying(&issue_two_swap_instance()).is_none()); +} + +#[test] +fn test_grouping_by_swapping_serialization() { + let problem = issue_yes_instance(); + let json = serde_json::to_value(&problem).unwrap(); + let restored: GroupingBySwapping = serde_json::from_value(json).unwrap(); + assert_eq!(restored.alphabet_size(), 3); + assert_eq!(restored.string(), &[0, 1, 2, 0, 1, 2]); + assert_eq!(restored.budget(), 5); +} + +#[test] +#[should_panic(expected = "input symbols must be less than alphabet_size")] +fn test_grouping_by_swapping_symbol_out_of_range_panics() { + GroupingBySwapping::new(3, vec![0, 1, 3], 1); +} + +#[test] +#[should_panic(expected = "budget must be 0 when string is empty")] +fn test_grouping_by_swapping_empty_string_requires_zero_budget() { + GroupingBySwapping::new(0, vec![], 1); +} From eabc3f5d11f0b9affede885315cb0e91fab99160 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Mon, 23 Mar 2026 15:01:17 +0800 Subject: [PATCH 3/3] chore: remove plan file after implementation --- docs/plans/2026-03-23-grouping-by-swapping.md | 308 ------------------ 1 file changed, 308 deletions(-) delete mode 100644 docs/plans/2026-03-23-grouping-by-swapping.md diff --git a/docs/plans/2026-03-23-grouping-by-swapping.md b/docs/plans/2026-03-23-grouping-by-swapping.md deleted file mode 100644 index 06cee0c1..00000000 --- a/docs/plans/2026-03-23-grouping-by-swapping.md +++ /dev/null @@ -1,308 +0,0 @@ -# GroupingBySwapping Implementation Plan - -> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. - -**Goal:** Add the `GroupingBySwapping` satisfaction model from issue #440, including registry/CLI/example-db/paper integration, with the issue's corrected `abcabc` example as the canonical fixture. - -**Architecture:** Implement `GroupingBySwapping` as a `misc` satisfaction model with fields `alphabet_size`, `string`, and `budget`. A configuration is a length-`budget` swap program whose entries are adjacent swap positions `0..string_len-2` plus the no-op slot `string_len-1`; `evaluate()` applies the swaps and accepts exactly when every symbol appears in a single contiguous block. Batch 1 covers model code, registration, CLI, tests, and canonical example wiring. Batch 2 covers the paper entry after the implementation is complete and exports are available. - -**Tech Stack:** Rust library crate, `inventory` schema registration, registry-backed CLI creation, serde, Typst paper, `cargo test`, `make paper`, `make test`, `make clippy` - ---- - -## Batch 1: Model, Registry, CLI, Tests, Example DB - -### Task 1: Write the failing model tests - -**Files:** -- Create: `src/unit_tests/models/misc/grouping_by_swapping.rs` -- Modify: `src/models/misc/grouping_by_swapping.rs` - -**Step 1: Write the failing test** - -Add tests that describe the issue-backed behavior before implementing the model: -- `test_grouping_by_swapping_basic` -- `test_grouping_by_swapping_evaluate_issue_yes` -- `test_grouping_by_swapping_rejects_wrong_length_and_out_of_range_swaps` -- `test_grouping_by_swapping_bruteforce_yes_and_no` -- `test_grouping_by_swapping_paper_example` -- `test_grouping_by_swapping_serialization` - -Use these concrete instances: -- YES instance: `alphabet_size = 3`, `string = [0,1,2,0,1,2]`, `budget = 5`, satisfying config `[2,1,3,5,5]` -- NO-short-budget instance: same string with `budget = 2` -- Minimum-3-swaps witness: same string with `budget = 3`, config `[2,1,3]` - -**Step 2: Run test to verify it fails** - -Run: `cargo test grouping_by_swapping --lib` -Expected: compile/test failure because `GroupingBySwapping` is not implemented or not registered yet. - -**Step 3: Write minimal implementation** - -Create `src/models/misc/grouping_by_swapping.rs` with: -- `ProblemSchemaEntry` -- `GroupingBySwapping` struct -- constructor + getters -- helper(s) to apply a swap program and test groupedness -- `Problem` + `SatisfactionProblem` impls -- `declare_variants! { default sat GroupingBySwapping => "string_len ^ budget", }` -- `#[cfg(test)]` link to the test file - -Required issue-specific semantics: -- symbols are encoded as `0..alphabet_size-1` -- each config entry is either an adjacent swap position or the no-op slot `string_len-1` -- `evaluate()` returns `false` on wrong-length configs, out-of-range values, or invalid input symbols -- groupedness means no symbol reappears after its contiguous block ends - -**Step 4: Run test to verify it passes** - -Run: `cargo test grouping_by_swapping --lib` -Expected: the new model tests pass. - -**Step 5: Commit** - -Run: -```bash -git add src/models/misc/grouping_by_swapping.rs src/unit_tests/models/misc/grouping_by_swapping.rs -git commit -m "Add GroupingBySwapping model" -``` - -### Task 2: Register the model in the crate surface - -**Files:** -- Modify: `src/models/misc/mod.rs` -- Modify: `src/models/mod.rs` -- Modify: `src/lib.rs` - -**Step 1: Write the failing test** - -Extend the new unit test file with assertions that: -- `Problem::NAME == "GroupingBySwapping"` -- `Problem::variant() == vec![]` -- the type is reachable through `crate::models::misc`, `crate::models`, and `crate::prelude` - -**Step 2: Run test to verify it fails** - -Run: `cargo test grouping_by_swapping --lib` -Expected: import/re-export assertions fail or do not compile because the type is not fully re-exported yet. - -**Step 3: Write minimal implementation** - -Register the model in: -- `src/models/misc/mod.rs` -- `src/models/mod.rs` -- `src/lib.rs` prelude exports - -Also add the example-db spec hook in `src/models/misc/mod.rs` once Task 4 creates it. - -**Step 4: Run test to verify it passes** - -Run: `cargo test grouping_by_swapping --lib` -Expected: re-export/import assertions pass. - -**Step 5: Commit** - -Run: -```bash -git add src/models/misc/mod.rs src/models/mod.rs src/lib.rs src/unit_tests/models/misc/grouping_by_swapping.rs -git commit -m "Register GroupingBySwapping exports" -``` - -### Task 3: Add CLI discovery and creation support - -**Files:** -- Modify: `problemreductions-cli/src/problem_name.rs` -- Modify: `problemreductions-cli/src/cli.rs` -- Modify: `problemreductions-cli/src/commands/create.rs` -- Modify: `problemreductions-cli/tests/cli_tests.rs` - -**Step 1: Write the failing test** - -Add CLI integration coverage for: -- `pred create GroupingBySwapping --string "0,1,2,0,1,2" --bound 5` -- optional `--alphabet-size 3` -- `pred create --example GroupingBySwapping` -- helpful usage text when `--string` or `--bound` is missing - -Prefer the same assertions used by the `LongestCommonSubsequence` and `StringToStringCorrection` tests: -- JSON `type == "GroupingBySwapping"` -- `data.alphabet_size == 3` -- `data.string == [0,1,2,0,1,2]` -- `data.budget == 5` - -**Step 2: Run test to verify it fails** - -Run: `cargo test -p problemreductions-cli grouping_by_swapping` -Expected: CLI tests fail because the alias/help/create path does not exist yet. - -**Step 3: Write minimal implementation** - -Add the canonical name wiring: -- `resolve_alias()` support for `"groupingbyswapping"` - -Add a dedicated CLI flag: -- `--string` for a comma-separated symbol list - -Update: -- `all_data_flags_empty()` -- `CreateArgs` docs / "Flags by problem type" help -- problem-specific help examples in `create.rs` -- `create()` match arm that parses `--string`, infers `alphabet_size` when omitted, validates `--alphabet-size >= max(symbol)+1`, and constructs `GroupingBySwapping` - -**Step 4: Run test to verify it passes** - -Run: `cargo test -p problemreductions-cli grouping_by_swapping` -Expected: the new CLI tests pass. - -**Step 5: Commit** - -Run: -```bash -git add problemreductions-cli/src/problem_name.rs problemreductions-cli/src/cli.rs problemreductions-cli/src/commands/create.rs problemreductions-cli/tests/cli_tests.rs -git commit -m "Add GroupingBySwapping CLI support" -``` - -### Task 4: Add the canonical example-db fixture and paper-backed test data - -**Files:** -- Modify: `src/models/misc/grouping_by_swapping.rs` -- Modify: `src/models/misc/mod.rs` - -**Step 1: Write the failing test** - -Extend `src/unit_tests/models/misc/grouping_by_swapping.rs` to require: -- `canonical_model_example_specs()` exports the issue's `abcabc`, `K=5`, config `[2,1,3,5,5]` -- the paper/example test verifies the exact issue witness is satisfying -- `BruteForce` confirms the same string is unsatisfiable for `budget = 2` - -**Step 2: Run test to verify it fails** - -Run: `cargo test grouping_by_swapping --features example-db --lib` -Expected: failure because the canonical example spec is missing or incomplete. - -**Step 3: Write minimal implementation** - -Add `canonical_model_example_specs()` to the model file and register it from `src/models/misc/mod.rs`. - -Use the issue-corrected example exactly: -- instance: `GroupingBySwapping::new(3, vec![0,1,2,0,1,2], 5)` -- optimal/satisfying config: `vec![2,1,3,5,5]` -- optimal value: `true` - -**Step 4: Run test to verify it passes** - -Run: `cargo test grouping_by_swapping --features example-db --lib` -Expected: example-db-aware tests pass. - -**Step 5: Commit** - -Run: -```bash -git add src/models/misc/grouping_by_swapping.rs src/models/misc/mod.rs src/unit_tests/models/misc/grouping_by_swapping.rs -git commit -m "Add GroupingBySwapping canonical example" -``` - -### Task 5: Batch 1 verification - -**Files:** -- Modify: none - -**Step 1: Run focused verification** - -Run: -```bash -cargo test grouping_by_swapping --workspace -cargo test -p problemreductions-cli grouping_by_swapping -``` - -Expected: all focused library and CLI tests for `GroupingBySwapping` pass. - -**Step 2: Run broader verification** - -Run: -```bash -make test -make clippy -``` - -Expected: repository tests and clippy pass without introducing regressions. - -**Step 3: Commit** - -If verification required follow-up fixes, commit them coherently before moving to Batch 2. - -## Batch 2: Paper Entry - -### Task 6: Add the Typst paper entry after exports are available - -**Files:** -- Modify: `docs/paper/reductions.typ` -- Test: `src/unit_tests/models/misc/grouping_by_swapping.rs` - -**Step 1: Write the failing test** - -Finalize `test_grouping_by_swapping_paper_example` so it matches the paper/example-db instance exactly: -- construct the canonical `abcabc`, `K=5` instance -- assert `[2,1,3,5,5]` is satisfying -- assert `budget = 2` is unsatisfiable via `BruteForce` - -**Step 2: Run test to verify it fails if needed** - -Run: `cargo test grouping_by_swapping_paper_example --lib` -Expected: if the example/test drifted during Batch 1, fix the test before touching the paper. - -**Step 3: Write minimal implementation** - -In `docs/paper/reductions.typ`: -- add `"GroupingBySwapping": [Grouping by Swapping]` to `display-name` -- add `#problem-def("GroupingBySwapping")[...]` -- use the canonical example data from the checked-in fixture flow -- describe the `abcabc -> aabbcc` witness with the issue's 3 effective swaps and 2 trailing no-ops -- include `pred-commands()` using the canonical example spec -- cite Garey & Johnson SR21 and note the brute-force bound used by the model metadata - -**Step 4: Run paper verification** - -Run: `make paper` -Expected: the Typst paper builds cleanly and the new entry renders without missing references. - -**Step 5: Commit** - -Run: -```bash -git add docs/paper/reductions.typ src/unit_tests/models/misc/grouping_by_swapping.rs -git commit -m "Document GroupingBySwapping in paper" -``` - -## Final Verification - -### Task 7: Full verification before handoff - -**Files:** -- Modify: none - -**Step 1: Run the final required checks** - -Run: -```bash -make test -make clippy -make paper -git status --short -``` - -Expected: -- tests pass -- clippy passes -- paper builds -- the tree is clean except for intentionally ignored/generated files - -**Step 2: Prepare the implementation summary** - -Summarize: -- model file and helper logic -- registration/export/CLI/example-db integration -- paper entry -- any deviations from this plan -