From 6cc27307076cd86ffcc6bb4aaba190f736ce1ea9 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Mon, 16 Mar 2026 20:44:09 +0800 Subject: [PATCH 01/14] Add plan for #420: ConsecutiveBlockMinimization model --- ...26-03-16-consecutive-block-minimization.md | 167 ++++++++++++++++++ 1 file changed, 167 insertions(+) create mode 100644 docs/plans/2026-03-16-consecutive-block-minimization.md diff --git a/docs/plans/2026-03-16-consecutive-block-minimization.md b/docs/plans/2026-03-16-consecutive-block-minimization.md new file mode 100644 index 000000000..5108c839c --- /dev/null +++ b/docs/plans/2026-03-16-consecutive-block-minimization.md @@ -0,0 +1,167 @@ +# Plan: Add ConsecutiveBlockMinimization Model (#420) + +## Summary + +Add the Consecutive Block Minimization satisfaction problem (Garey & Johnson SR17) to the `algebraic/` category. Given an m×n binary matrix A and positive integer K, decide whether there exists a column permutation yielding at most K maximal blocks of consecutive 1's across all rows. + +## Issue Details + +- **Problem**: ConsecutiveBlockMinimization +- **Type**: SatisfactionProblem (Metric = bool) +- **Category**: algebraic/ (matrix input) +- **No type parameters** (no graph type, no weight type) +- **NP-complete**: Kou (1977), reduction from Hamiltonian Path +- **Complexity**: O(n^n × m × n) brute-force + +## Batch 1: Implementation (Steps 1-5.5) + +### Step 1: Category + +Place in `src/models/algebraic/` alongside BMF, QUBO, ILP, CVP. + +### Step 1.5: Size Getters + +Getter methods needed (from complexity expression and overhead expressions): +- `num_rows()` → m (matrix.len()) +- `num_cols()` → n (matrix[0].len()) +- `bound_k()` → K +- `num_variables()` → num_cols (override default) + +### Step 2: Model File + +Create `src/models/algebraic/consecutive_block_minimization.rs`: + +**Schema entry** (inventory::submit!): +- name: "ConsecutiveBlockMinimization" +- display_name: "ConsecutiveBlockMinimization" +- aliases: &["CBM"] +- dimensions: &[] (no type params) +- fields: matrix (Vec>), bound_k (usize) + +**Struct**: +```rust +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ConsecutiveBlockMinimization { + matrix: Vec>, + num_rows: usize, + num_cols: usize, + bound_k: usize, +} +``` + +**Constructor** `new(matrix, bound_k)`: +- Validate all rows have same length +- Store derived num_rows, num_cols + +**Accessors**: `matrix()`, `num_rows()`, `num_cols()`, `bound_k()` + +**Helper** `count_blocks(config)`: +- Interpret config as column permutation (config[i] = position of column i) +- Validate it's a valid permutation (all distinct, in 0..num_cols) +- For each row, count maximal runs of consecutive 1's after reordering +- Return Option: None if invalid permutation, Some(total) otherwise + +**Problem impl**: +- `NAME = "ConsecutiveBlockMinimization"` +- `Metric = bool` +- `dims() = vec![num_cols; num_cols]` +- `evaluate(config)`: call count_blocks, return false if invalid permutation, otherwise total <= bound_k +- `variant() = crate::variant_params![]` +- Override `num_variables()` → num_cols + +**SatisfactionProblem**: empty marker impl + +### Step 2.5: declare_variants! + +```rust +crate::declare_variants! { + default sat ConsecutiveBlockMinimization => "num_cols^num_cols * num_rows * num_cols", +} +``` + +### Step 3: Register in Modules + +1. `src/models/algebraic/mod.rs`: + - Add `pub(crate) mod consecutive_block_minimization;` + - Add `pub use consecutive_block_minimization::ConsecutiveBlockMinimization;` + - Update module doc comment + - Add to `canonical_model_example_specs()` aggregator + +2. `src/models/mod.rs`: + - Re-export `ConsecutiveBlockMinimization` from `algebraic` + +### Step 4: CLI Registration + +1. **Aliases**: Already handled via `declare_variants!` + ProblemSchemaEntry aliases field ("CBM") + +2. **`problemreductions-cli/src/commands/create.rs`**: + - Add match arm for "ConsecutiveBlockMinimization" + - Parse `--matrix` (JSON 2D bool array) and `--bound-k` (integer) + - Add to `after_help` flag table + - Add to `all_data_flags_empty` + - Add to `example_for` with a small example instance + +3. **`problemreductions-cli/src/cli.rs`**: + - Add `--matrix` flag: `Option` for JSON-encoded matrix + - Add `--bound-k` flag: `Option` + +### Step 4.6: Example-DB + +Add `canonical_model_example_specs()` in the model file: +- Use the YES instance from the issue (Instance 2: path graph adjacency matrix, K=6) +- Or use a simpler small instance that has a satisfying permutation +- Use `satisfaction_example(problem, vec![valid_config])` + +### Step 5: Unit Tests + +Create `src/unit_tests/models/algebraic/consecutive_block_minimization.rs`: + +- `test_consecutive_block_minimization_creation`: constructor, getters +- `test_consecutive_block_minimization_evaluation`: test with known YES and NO configs +- `test_consecutive_block_minimization_invalid_permutation`: non-permutation configs return false +- `test_consecutive_block_minimization_serialization`: serde round-trip +- `test_consecutive_block_minimization_solver`: BruteForce::find_satisfying on small instance +- `test_consecutive_block_minimization_paper_example`: test with the issue's example instances +- `test_consecutive_block_minimization_empty_matrix`: edge case + +Add `#[path]` link in model file: +```rust +#[cfg(test)] +#[path = "../../unit_tests/models/algebraic/consecutive_block_minimization.rs"] +mod tests; +``` + +### Step 5.5: Trait Consistency + +Add entry in `src/unit_tests/trait_consistency.rs`: +```rust +check_problem_trait( + &ConsecutiveBlockMinimization::new(matrix, bound_k), + "ConsecutiveBlockMinimization", +); +``` + +## Batch 2: Paper Entry (Step 6) + +### Step 6: Paper Documentation + +In `docs/paper/reductions.typ`: + +1. Add to `display-name` dict: + ```typst + "ConsecutiveBlockMinimization": [Consecutive Block Minimization], + ``` + +2. Add `problem-def` block after related algebraic problems: + - Formal definition: m×n binary matrix, positive integer K, column permutation, blocks + - Background: GJ SR17, information retrieval, scheduling, glass cutting + - Reference to Kou (1977), Booth (1975), Haddadi & Layouni (2008) + - Connection to consecutive ones property (PQ-trees) + +## Verification + +After each batch, run: +```bash +make check # fmt + clippy + test +make paper # build paper +``` From 5c70fffa186092fdae2673e37704232b39befbeb Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Mon, 16 Mar 2026 21:08:09 +0800 Subject: [PATCH 02/14] Implement #420: Add ConsecutiveBlockMinimization model MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add the Consecutive Block Minimization satisfaction problem (Garey & Johnson SR17) to the algebraic/ category. Given an m×n binary matrix and bound K, decide whether a column permutation exists yielding at most K maximal blocks of consecutive 1's across all rows. - Model with SatisfactionProblem trait, dims=[n;n] permutation space - CLI create with --matrix (JSON) and --bound-k flags - Unit tests: creation, evaluation, count_blocks, brute_force, serialization, invalid permutation, empty matrix - Paper entry with citations (Kou 1977, Booth 1975, Haddadi 2008) - Example DB fixture with 2×3 matrix instance Co-Authored-By: Claude Opus 4.6 --- docs/paper/reductions.typ | 35 +++ docs/paper/references.bib | 29 +++ docs/src/reductions/problem_schemas.json | 16 ++ docs/src/reductions/reduction_graph.json | 246 ++++++++++-------- problemreductions-cli/src/cli.rs | 5 + problemreductions-cli/src/commands/create.rs | 30 ++- src/example_db/fixtures/examples.json | 1 + .../consecutive_block_minimization.rs | 207 +++++++++++++++ src/models/algebraic/mod.rs | 4 + src/models/mod.rs | 2 +- .../consecutive_block_minimization.rs | 96 +++++++ src/unit_tests/trait_consistency.rs | 4 + 12 files changed, 559 insertions(+), 116 deletions(-) create mode 100644 src/models/algebraic/consecutive_block_minimization.rs create mode 100644 src/unit_tests/models/algebraic/consecutive_block_minimization.rs diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index ce08d7605..5751a28ca 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -105,6 +105,7 @@ "PartitionIntoTriangles": [Partition Into Triangles], "FlowShopScheduling": [Flow Shop Scheduling], "MinimumTardinessSequencing": [Minimum Tardiness Sequencing], + "ConsecutiveBlockMinimization": [Consecutive Block Minimization], ) // Definition label: "def:" — each definition block must have a matching label @@ -1496,6 +1497,40 @@ NP-completeness was established by Garey, Johnson, and Stockmeyer @gareyJohnsonS ] } +#{ + let x = load-model-example("ConsecutiveBlockMinimization") + let mat = x.instance.matrix + let K = x.instance.bound_k + let n-rows = mat.len() + let n-cols = if n-rows > 0 { mat.at(0).len() } else { 0 } + let sol = x.optimal.at(0) + let perm = sol.config + // Count blocks under the satisfying permutation + let total-blocks = 0 + for row in mat { + let in-block = false + for p in perm { + if row.at(p) { + if not in-block { + total-blocks += 1 + in-block = true + } + } else { + in-block = false + } + } + } + [ + #problem-def("ConsecutiveBlockMinimization")[ + Given an $m times n$ binary matrix $A$ and a positive integer $K$, determine whether there exists a permutation of the columns of $A$ such that the resulting matrix has at most $K$ maximal blocks of consecutive 1-entries (summed over all rows). A _block_ is a maximal contiguous run of 1-entries within a single row. + ][ + Consecutive Block Minimization (SR17 in Garey & Johnson) arises in consecutive file organization for information retrieval systems, where records stored on a linear medium must be arranged so that each query's relevant records form a contiguous segment. Applications also include scheduling, production planning, the glass cutting industry, and data compression. NP-complete by reduction from Hamiltonian Path @kou1977. When $K$ equals the number of non-all-zero rows, the problem reduces to testing the _consecutive ones property_, solvable in polynomial time via PQ-trees @booth1975. A 1.5-approximation is known @haddadi2008. The best known exact algorithm runs in $O^*(n!)$ by brute-force enumeration of all column permutations. + + *Example.* Let $A$ be the #n-rows$times$#n-cols matrix with rows #mat.enumerate().map(((i, row)) => [$r_#i = (#row.map(v => if v {$1$} else {$0$}).join($,$))$]).join(", ") and $K = #K$. The column permutation $pi = (#perm.map(p => str(p)).join(", "))$ yields #total-blocks total blocks, so #total-blocks $<= #K$ and the answer is YES. + ] + ] +} + #{ let x = load-model-example("PaintShop") let n-cars = x.instance.num_cars diff --git a/docs/paper/references.bib b/docs/paper/references.bib index 7186ea641..f6c87bc93 100644 --- a/docs/paper/references.bib +++ b/docs/paper/references.bib @@ -672,3 +672,32 @@ @article{papadimitriou1982 year = {1982}, doi = {10.1145/322307.322309} } + +@article{kou1977, + author = {Lawrence T. Kou}, + title = {Polynomial Complete Consecutive Information Retrieval Problems}, + journal = {SIAM Journal on Computing}, + volume = {6}, + number = {1}, + pages = {67--75}, + year = {1977}, + doi = {10.1137/0206005} +} + +@phdthesis{booth1975, + author = {Kellogg S. Booth}, + title = {PQ Tree Algorithms}, + school = {University of California, Berkeley}, + year = {1975} +} + +@article{haddadi2008, + author = {Salim Haddadi and Zohra Layouni}, + title = {Consecutive block minimization is 1.5-approximable}, + journal = {Information Processing Letters}, + volume = {108}, + number = {3}, + pages = {161--163}, + year = {2008}, + doi = {10.1016/j.ipl.2008.05.003} +} diff --git a/docs/src/reductions/problem_schemas.json b/docs/src/reductions/problem_schemas.json index 5949a528f..0bf2cc3e0 100644 --- a/docs/src/reductions/problem_schemas.json +++ b/docs/src/reductions/problem_schemas.json @@ -89,6 +89,22 @@ } ] }, + { + "name": "ConsecutiveBlockMinimization", + "description": "Permute columns of a binary matrix to have at most K consecutive blocks of 1s", + "fields": [ + { + "name": "matrix", + "type_name": "Vec>", + "description": "Binary matrix A (m x n)" + }, + { + "name": "bound_k", + "type_name": "usize", + "description": "Upper bound K on total consecutive blocks" + } + ] + }, { "name": "ExactCoverBy3Sets", "description": "Determine if a collection of 3-element subsets contains an exact cover", diff --git a/docs/src/reductions/reduction_graph.json b/docs/src/reductions/reduction_graph.json index cd80f3f06..fa539800b 100644 --- a/docs/src/reductions/reduction_graph.json +++ b/docs/src/reductions/reduction_graph.json @@ -1,3 +1,14 @@ +{ + "nodes": [ + { + "name": "BMF", + "variant": {}, + "category": "algebraic", + "doc_path": "models/algebraic/struct.BMF.html", + "complexity": "2^(rows * rank + r +Exported to: docs/src/reductions/reduction_graph.json + +JSON content: { "nodes": [ { @@ -57,6 +68,13 @@ "doc_path": "models/algebraic/struct.ClosestVectorProblem.html", "complexity": "2^num_basis_vectors" }, + { + "name": "ConsecutiveBlockMinimization", + "variant": {}, + "category": "algebraic", + "doc_path": "models/algebraic/struct.ConsecutiveBlockMinimization.html", + "complexity": "factorial(num_cols) * num_rows * num_cols" + }, { "name": "ExactCoverBy3Sets", "variant": {}, @@ -539,7 +557,7 @@ "edges": [ { "source": 3, - "target": 12, + "target": 13, "overhead": [ { "field": "num_vars", @@ -554,7 +572,7 @@ }, { "source": 4, - "target": 12, + "target": 13, "overhead": [ { "field": "num_vars", @@ -569,7 +587,7 @@ }, { "source": 4, - "target": 54, + "target": 55, "overhead": [ { "field": "num_spins", @@ -583,7 +601,7 @@ "doc_path": "rules/circuit_spinglass/index.html" }, { - "source": 8, + "source": 9, "target": 4, "overhead": [ { @@ -598,8 +616,8 @@ "doc_path": "rules/factoring_circuit/index.html" }, { - "source": 8, - "target": 13, + "source": 9, + "target": 14, "overhead": [ { "field": "num_vars", @@ -613,8 +631,8 @@ "doc_path": "rules/factoring_ilp/index.html" }, { - "source": 12, - "target": 13, + "source": 13, + "target": 14, "overhead": [ { "field": "num_vars", @@ -628,8 +646,8 @@ "doc_path": "rules/ilp_bool_ilp_i32/index.html" }, { - "source": 12, - "target": 49, + "source": 13, + "target": 50, "overhead": [ { "field": "num_vars", @@ -639,8 +657,8 @@ "doc_path": "rules/ilp_qubo/index.html" }, { - "source": 16, - "target": 19, + "source": 17, + "target": 20, "overhead": [ { "field": "num_vertices", @@ -654,8 +672,8 @@ "doc_path": "rules/kcoloring_casts/index.html" }, { - "source": 19, - "target": 12, + "source": 20, + "target": 13, "overhead": [ { "field": "num_vars", @@ -669,8 +687,8 @@ "doc_path": "rules/coloring_ilp/index.html" }, { - "source": 19, - "target": 49, + "source": 20, + "target": 50, "overhead": [ { "field": "num_vars", @@ -680,8 +698,8 @@ "doc_path": "rules/coloring_qubo/index.html" }, { - "source": 20, - "target": 22, + "source": 21, + "target": 23, "overhead": [ { "field": "num_vars", @@ -695,8 +713,8 @@ "doc_path": "rules/ksatisfiability_casts/index.html" }, { - "source": 20, - "target": 49, + "source": 21, + "target": 50, "overhead": [ { "field": "num_vars", @@ -706,8 +724,8 @@ "doc_path": "rules/ksatisfiability_qubo/index.html" }, { - "source": 21, - "target": 22, + "source": 22, + "target": 23, "overhead": [ { "field": "num_vars", @@ -721,8 +739,8 @@ "doc_path": "rules/ksatisfiability_casts/index.html" }, { - "source": 21, - "target": 49, + "source": 22, + "target": 50, "overhead": [ { "field": "num_vars", @@ -732,8 +750,8 @@ "doc_path": "rules/ksatisfiability_qubo/index.html" }, { - "source": 21, - "target": 58, + "source": 22, + "target": 59, "overhead": [ { "field": "num_elements", @@ -743,8 +761,8 @@ "doc_path": "rules/ksatisfiability_subsetsum/index.html" }, { - "source": 22, - "target": 51, + "source": 23, + "target": 52, "overhead": [ { "field": "num_clauses", @@ -762,8 +780,8 @@ "doc_path": "rules/sat_ksat/index.html" }, { - "source": 23, - "target": 49, + "source": 24, + "target": 50, "overhead": [ { "field": "num_vars", @@ -773,8 +791,8 @@ "doc_path": "rules/knapsack_qubo/index.html" }, { - "source": 24, - "target": 12, + "source": 25, + "target": 13, "overhead": [ { "field": "num_vars", @@ -788,8 +806,8 @@ "doc_path": "rules/longestcommonsubsequence_ilp/index.html" }, { - "source": 25, - "target": 54, + "source": 26, + "target": 55, "overhead": [ { "field": "num_spins", @@ -803,8 +821,8 @@ "doc_path": "rules/spinglass_maxcut/index.html" }, { - "source": 27, - "target": 12, + "source": 28, + "target": 13, "overhead": [ { "field": "num_vars", @@ -818,8 +836,8 @@ "doc_path": "rules/maximumclique_ilp/index.html" }, { - "source": 27, - "target": 31, + "source": 28, + "target": 32, "overhead": [ { "field": "num_vertices", @@ -833,8 +851,8 @@ "doc_path": "rules/maximumclique_maximumindependentset/index.html" }, { - "source": 28, - "target": 29, + "source": 29, + "target": 30, "overhead": [ { "field": "num_vertices", @@ -848,8 +866,8 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 28, - "target": 33, + "source": 29, + "target": 34, "overhead": [ { "field": "num_vertices", @@ -863,8 +881,8 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 29, - "target": 34, + "source": 30, + "target": 35, "overhead": [ { "field": "num_vertices", @@ -878,8 +896,8 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 30, - "target": 28, + "source": 31, + "target": 29, "overhead": [ { "field": "num_vertices", @@ -893,8 +911,8 @@ "doc_path": "rules/maximumindependentset_gridgraph/index.html" }, { - "source": 30, - "target": 31, + "source": 31, + "target": 32, "overhead": [ { "field": "num_vertices", @@ -908,8 +926,8 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 30, - "target": 32, + "source": 31, + "target": 33, "overhead": [ { "field": "num_vertices", @@ -923,8 +941,8 @@ "doc_path": "rules/maximumindependentset_triangular/index.html" }, { - "source": 30, - "target": 36, + "source": 31, + "target": 37, "overhead": [ { "field": "num_sets", @@ -938,8 +956,8 @@ "doc_path": "rules/maximumindependentset_maximumsetpacking/index.html" }, { - "source": 31, - "target": 27, + "source": 32, + "target": 28, "overhead": [ { "field": "num_vertices", @@ -953,8 +971,8 @@ "doc_path": "rules/maximumindependentset_maximumclique/index.html" }, { - "source": 31, - "target": 38, + "source": 32, + "target": 39, "overhead": [ { "field": "num_sets", @@ -968,8 +986,8 @@ "doc_path": "rules/maximumindependentset_maximumsetpacking/index.html" }, { - "source": 31, - "target": 45, + "source": 32, + "target": 46, "overhead": [ { "field": "num_vertices", @@ -983,8 +1001,8 @@ "doc_path": "rules/minimumvertexcover_maximumindependentset/index.html" }, { - "source": 32, - "target": 34, + "source": 33, + "target": 35, "overhead": [ { "field": "num_vertices", @@ -998,8 +1016,8 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 33, - "target": 30, + "source": 34, + "target": 31, "overhead": [ { "field": "num_vertices", @@ -1013,8 +1031,8 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 33, - "target": 34, + "source": 34, + "target": 35, "overhead": [ { "field": "num_vertices", @@ -1028,8 +1046,8 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 34, - "target": 31, + "source": 35, + "target": 32, "overhead": [ { "field": "num_vertices", @@ -1043,8 +1061,8 @@ "doc_path": "rules/maximumindependentset_casts/index.html" }, { - "source": 35, - "target": 12, + "source": 36, + "target": 13, "overhead": [ { "field": "num_vars", @@ -1058,8 +1076,8 @@ "doc_path": "rules/maximummatching_ilp/index.html" }, { - "source": 35, - "target": 38, + "source": 36, + "target": 39, "overhead": [ { "field": "num_sets", @@ -1073,8 +1091,8 @@ "doc_path": "rules/maximummatching_maximumsetpacking/index.html" }, { - "source": 36, - "target": 30, + "source": 37, + "target": 31, "overhead": [ { "field": "num_vertices", @@ -1088,8 +1106,8 @@ "doc_path": "rules/maximumindependentset_maximumsetpacking/index.html" }, { - "source": 36, - "target": 38, + "source": 37, + "target": 39, "overhead": [ { "field": "num_sets", @@ -1103,8 +1121,8 @@ "doc_path": "rules/maximumsetpacking_casts/index.html" }, { - "source": 37, - "target": 49, + "source": 38, + "target": 50, "overhead": [ { "field": "num_vars", @@ -1114,8 +1132,8 @@ "doc_path": "rules/maximumsetpacking_qubo/index.html" }, { - "source": 38, - "target": 12, + "source": 39, + "target": 13, "overhead": [ { "field": "num_vars", @@ -1129,8 +1147,8 @@ "doc_path": "rules/maximumsetpacking_ilp/index.html" }, { - "source": 38, - "target": 31, + "source": 39, + "target": 32, "overhead": [ { "field": "num_vertices", @@ -1144,8 +1162,8 @@ "doc_path": "rules/maximumindependentset_maximumsetpacking/index.html" }, { - "source": 38, - "target": 37, + "source": 39, + "target": 38, "overhead": [ { "field": "num_sets", @@ -1159,8 +1177,8 @@ "doc_path": "rules/maximumsetpacking_casts/index.html" }, { - "source": 39, - "target": 12, + "source": 40, + "target": 13, "overhead": [ { "field": "num_vars", @@ -1174,8 +1192,8 @@ "doc_path": "rules/minimumdominatingset_ilp/index.html" }, { - "source": 42, - "target": 12, + "source": 43, + "target": 13, "overhead": [ { "field": "num_vars", @@ -1189,8 +1207,8 @@ "doc_path": "rules/minimumsetcovering_ilp/index.html" }, { - "source": 45, - "target": 31, + "source": 46, + "target": 32, "overhead": [ { "field": "num_vertices", @@ -1204,8 +1222,8 @@ "doc_path": "rules/minimumvertexcover_maximumindependentset/index.html" }, { - "source": 45, - "target": 42, + "source": 46, + "target": 43, "overhead": [ { "field": "num_sets", @@ -1219,8 +1237,8 @@ "doc_path": "rules/minimumvertexcover_minimumsetcovering/index.html" }, { - "source": 49, - "target": 12, + "source": 50, + "target": 13, "overhead": [ { "field": "num_vars", @@ -1234,8 +1252,8 @@ "doc_path": "rules/qubo_ilp/index.html" }, { - "source": 49, - "target": 53, + "source": 50, + "target": 54, "overhead": [ { "field": "num_spins", @@ -1245,7 +1263,7 @@ "doc_path": "rules/spinglass_qubo/index.html" }, { - "source": 51, + "source": 52, "target": 4, "overhead": [ { @@ -1260,8 +1278,8 @@ "doc_path": "rules/sat_circuitsat/index.html" }, { - "source": 51, - "target": 16, + "source": 52, + "target": 17, "overhead": [ { "field": "num_vertices", @@ -1275,8 +1293,8 @@ "doc_path": "rules/sat_coloring/index.html" }, { - "source": 51, - "target": 21, + "source": 52, + "target": 22, "overhead": [ { "field": "num_clauses", @@ -1290,8 +1308,8 @@ "doc_path": "rules/sat_ksat/index.html" }, { - "source": 51, - "target": 30, + "source": 52, + "target": 31, "overhead": [ { "field": "num_vertices", @@ -1305,8 +1323,8 @@ "doc_path": "rules/sat_maximumindependentset/index.html" }, { - "source": 51, - "target": 39, + "source": 52, + "target": 40, "overhead": [ { "field": "num_vertices", @@ -1320,8 +1338,8 @@ "doc_path": "rules/sat_minimumdominatingset/index.html" }, { - "source": 53, - "target": 49, + "source": 54, + "target": 50, "overhead": [ { "field": "num_vars", @@ -1331,8 +1349,8 @@ "doc_path": "rules/spinglass_qubo/index.html" }, { - "source": 54, - "target": 25, + "source": 55, + "target": 26, "overhead": [ { "field": "num_vertices", @@ -1346,8 +1364,8 @@ "doc_path": "rules/spinglass_maxcut/index.html" }, { - "source": 54, - "target": 53, + "source": 55, + "target": 54, "overhead": [ { "field": "num_spins", @@ -1361,8 +1379,8 @@ "doc_path": "rules/spinglass_casts/index.html" }, { - "source": 59, - "target": 12, + "source": 60, + "target": 13, "overhead": [ { "field": "num_vars", @@ -1376,8 +1394,8 @@ "doc_path": "rules/travelingsalesman_ilp/index.html" }, { - "source": 59, - "target": 49, + "source": 60, + "target": 50, "overhead": [ { "field": "num_vars", @@ -1387,4 +1405,4 @@ "doc_path": "rules/travelingsalesman_qubo/index.html" } ] -} \ No newline at end of file +} diff --git a/problemreductions-cli/src/cli.rs b/problemreductions-cli/src/cli.rs index e2dbd5b54..055ab27cf 100644 --- a/problemreductions-cli/src/cli.rs +++ b/problemreductions-cli/src/cli.rs @@ -233,6 +233,7 @@ Flags by problem type: X3C (ExactCoverBy3Sets) --universe, --sets (3 elements each) BicliqueCover --left, --right, --biedges, --k BMF --matrix (0/1), --rank + ConsecutiveBlockMinimization --matrix (JSON 2D bool), --bound-k SteinerTree --graph, --edge-weights, --terminals CVP --basis, --target-vec [--bounds] OptimalLinearArrangement --graph, --bound @@ -407,6 +408,10 @@ pub struct CreateArgs { /// Alphabet size for SCS (optional; inferred from max symbol + 1 if omitted) #[arg(long)] pub alphabet_size: Option, + + /// Upper bound K on consecutive blocks for ConsecutiveBlockMinimization + #[arg(long)] + pub bound_k: Option, } #[derive(clap::Args)] diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index db0e6c587..30360d67a 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -5,7 +5,9 @@ use crate::problem_name::{resolve_problem_ref, unknown_problem_error}; use crate::util; use anyhow::{bail, Context, Result}; use problemreductions::export::{ModelExample, ProblemRef, ProblemSide, RuleExample}; -use problemreductions::models::algebraic::{ClosestVectorProblem, BMF}; +use problemreductions::models::algebraic::{ + ClosestVectorProblem, ConsecutiveBlockMinimization, BMF, +}; use problemreductions::models::graph::{GraphPartitioning, HamiltonianPath}; use problemreductions::models::misc::{ BinPacking, FlowShopScheduling, LongestCommonSubsequence, MinimumTardinessSequencing, @@ -64,6 +66,7 @@ fn all_data_flags_empty(args: &CreateArgs) -> bool { && args.deadline.is_none() && args.num_processors.is_none() && args.alphabet_size.is_none() + && args.bound_k.is_none() } fn emit_problem_output(output: &ProblemJsonOutput, out: &OutputConfig) -> Result<()> { @@ -249,6 +252,9 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str { "SubgraphIsomorphism" => "--graph 0-1,1-2,2-0 --pattern 0-1", "SubsetSum" => "--sizes 3,7,1,8,2,4 --target 11", "ShortestCommonSupersequence" => "--strings \"0,1,2;1,2,0\" --bound 4", + "ConsecutiveBlockMinimization" => { + "--matrix '[[true,false,true],[false,true,true]]' --bound-k 2" + } _ => "", } } @@ -752,6 +758,28 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { (ser(BMF::new(matrix, rank))?, resolved_variant.clone()) } + // ConsecutiveBlockMinimization + "ConsecutiveBlockMinimization" => { + let matrix_str = args.matrix.as_deref().ok_or_else(|| { + anyhow::anyhow!( + "ConsecutiveBlockMinimization requires --matrix and --bound-k\n\n\ + Usage: pred create ConsecutiveBlockMinimization --matrix '[[true,false,true],[false,true,true]]' --bound-k 2" + ) + })?; + let bound_k = args.bound_k.ok_or_else(|| { + anyhow::anyhow!( + "ConsecutiveBlockMinimization requires --bound-k\n\n\ + Usage: pred create ConsecutiveBlockMinimization --matrix '[[true,false,true],[false,true,true]]' --bound-k 2" + ) + })?; + let matrix: Vec> = serde_json::from_str(matrix_str) + .context("Failed to parse --matrix as JSON 2D bool array")?; + ( + ser(ConsecutiveBlockMinimization::new(matrix, bound_k))?, + resolved_variant.clone(), + ) + } + // LongestCommonSubsequence "LongestCommonSubsequence" => { let strings_str = args.strings.as_deref().ok_or_else(|| { diff --git a/src/example_db/fixtures/examples.json b/src/example_db/fixtures/examples.json index 83015b13d..b509fad3c 100644 --- a/src/example_db/fixtures/examples.json +++ b/src/example_db/fixtures/examples.json @@ -4,6 +4,7 @@ {"problem":"BicliqueCover","variant":{},"instance":{"graph":{"edges":[[0,0],[0,1],[1,1],[1,2]],"left_size":2,"right_size":3},"k":2},"samples":[{"config":[1,0,0,1,1,0,1,1,0,1],"metric":{"Valid":6}}],"optimal":[{"config":[0,1,0,1,0,1,0,1,0,1],"metric":{"Valid":5}},{"config":[1,0,1,0,1,0,1,0,1,0],"metric":{"Valid":5}}]}, {"problem":"CircuitSAT","variant":{},"instance":{"circuit":{"assignments":[{"expr":{"op":{"And":[{"op":{"Var":"x1"}},{"op":{"Var":"x2"}}]}},"outputs":["a"]},{"expr":{"op":{"Or":[{"op":{"Var":"x1"}},{"op":{"Var":"x2"}}]}},"outputs":["b"]},{"expr":{"op":{"Xor":[{"op":{"Var":"a"}},{"op":{"Var":"b"}}]}},"outputs":["c"]}]},"variables":["a","b","c","x1","x2"]},"samples":[{"config":[0,1,1,0,1],"metric":true},{"config":[0,1,1,1,0],"metric":true}],"optimal":[{"config":[0,0,0,0,0],"metric":true},{"config":[0,1,1,0,1],"metric":true},{"config":[0,1,1,1,0],"metric":true},{"config":[1,1,0,1,1],"metric":true}]}, {"problem":"ClosestVectorProblem","variant":{"weight":"i32"},"instance":{"basis":[[2,0],[1,2]],"bounds":[{"lower":-2,"upper":4},{"lower":-2,"upper":4}],"target":[2.8,1.5]},"samples":[{"config":[3,3],"metric":{"Valid":0.5385164807134505}}],"optimal":[{"config":[3,3],"metric":{"Valid":0.5385164807134505}}]}, + {"problem":"ConsecutiveBlockMinimization","variant":{},"instance":{"bound_k":2,"matrix":[[true,false,true],[false,true,true]],"num_cols":3,"num_rows":2},"samples":[{"config":[0,2,1],"metric":true}],"optimal":[{"config":[0,2,1],"metric":true},{"config":[1,2,0],"metric":true}]}, {"problem":"ExactCoverBy3Sets","variant":{},"instance":{"subsets":[[0,1,2],[0,2,4],[3,4,5],[3,5,7],[6,7,8],[1,4,6],[2,5,8]],"universe_size":9},"samples":[{"config":[1,0,1,0,1,0,0],"metric":true}],"optimal":[{"config":[1,0,1,0,1,0,0],"metric":true}]}, {"problem":"Factoring","variant":{},"instance":{"m":2,"n":3,"target":15},"samples":[{"config":[1,1,1,0,1],"metric":{"Valid":0}}],"optimal":[{"config":[1,1,1,0,1],"metric":{"Valid":0}}]}, {"problem":"HamiltonianPath","variant":{"graph":"SimpleGraph"},"instance":{"graph":{"inner":{"edge_property":"undirected","edges":[[0,1,null],[0,2,null],[1,3,null],[2,3,null],[3,4,null],[3,5,null],[4,2,null],[5,1,null]],"node_holes":[],"nodes":[null,null,null,null,null,null]}}},"samples":[{"config":[0,2,4,3,1,5],"metric":true}],"optimal":[{"config":[0,1,5,3,2,4],"metric":true},{"config":[0,1,5,3,4,2],"metric":true},{"config":[0,2,4,3,1,5],"metric":true},{"config":[0,2,4,3,5,1],"metric":true},{"config":[1,0,2,4,3,5],"metric":true},{"config":[1,5,3,4,2,0],"metric":true},{"config":[2,0,1,5,3,4],"metric":true},{"config":[2,4,3,5,1,0],"metric":true},{"config":[3,4,2,0,1,5],"metric":true},{"config":[3,5,1,0,2,4],"metric":true},{"config":[4,2,0,1,3,5],"metric":true},{"config":[4,2,0,1,5,3],"metric":true},{"config":[4,2,3,5,1,0],"metric":true},{"config":[4,3,2,0,1,5],"metric":true},{"config":[4,3,5,1,0,2],"metric":true},{"config":[5,1,0,2,3,4],"metric":true},{"config":[5,1,0,2,4,3],"metric":true},{"config":[5,1,3,4,2,0],"metric":true},{"config":[5,3,1,0,2,4],"metric":true},{"config":[5,3,4,2,0,1],"metric":true}]}, diff --git a/src/models/algebraic/consecutive_block_minimization.rs b/src/models/algebraic/consecutive_block_minimization.rs new file mode 100644 index 000000000..f53be4803 --- /dev/null +++ b/src/models/algebraic/consecutive_block_minimization.rs @@ -0,0 +1,207 @@ +//! Consecutive Block Minimization (CBM) problem implementation. +//! +//! Given an m x n binary matrix A and a positive integer K, +//! determine whether there exists a permutation of the columns of A +//! such that the resulting matrix has at most K maximal blocks of +//! consecutive 1-entries (summed over all rows). +//! +//! A "block" is a maximal contiguous run of 1-entries in a row. +//! This is problem SR17 in Garey & Johnson. + +use crate::registry::{FieldInfo, ProblemSchemaEntry}; +use crate::traits::{Problem, SatisfactionProblem}; +use serde::{Deserialize, Serialize}; + +inventory::submit! { + ProblemSchemaEntry { + name: "ConsecutiveBlockMinimization", + display_name: "Consecutive Block Minimization", + aliases: &["CBM"], + dimensions: &[], + module_path: module_path!(), + description: "Permute columns of a binary matrix to have at most K consecutive blocks of 1s", + fields: &[ + FieldInfo { name: "matrix", type_name: "Vec>", description: "Binary matrix A (m x n)" }, + FieldInfo { name: "bound_k", type_name: "usize", description: "Upper bound K on total consecutive blocks" }, + ], + } +} + +/// Consecutive Block Minimization (CBM) problem. +/// +/// Given an m x n binary matrix A and a positive integer K, +/// determine whether there exists a permutation of the columns of A +/// such that the resulting matrix has at most K maximal blocks of +/// consecutive 1-entries (summed over all rows). +/// +/// # Example +/// +/// ``` +/// use problemreductions::models::algebraic::ConsecutiveBlockMinimization; +/// use problemreductions::{Problem, Solver, BruteForce}; +/// +/// // 2x3 binary matrix +/// let problem = ConsecutiveBlockMinimization::new( +/// vec![ +/// vec![true, false, true], +/// vec![false, true, true], +/// ], +/// 2, +/// ); +/// +/// let solver = BruteForce::new(); +/// let solutions = solver.find_all_satisfying(&problem); +/// +/// // Verify solutions satisfy the block bound +/// for sol in solutions { +/// assert!(problem.evaluate(&sol)); +/// } +/// ``` +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ConsecutiveBlockMinimization { + /// The binary matrix A (m x n). + matrix: Vec>, + /// Number of rows (m). + num_rows: usize, + /// Number of columns (n). + num_cols: usize, + /// Upper bound K on total consecutive blocks. + bound_k: usize, +} + +impl ConsecutiveBlockMinimization { + /// Create a new ConsecutiveBlockMinimization problem. + /// + /// # Arguments + /// * `matrix` - The m x n binary matrix + /// * `bound_k` - Upper bound on total consecutive blocks + /// + /// # Panics + /// Panics if rows have inconsistent lengths. + pub fn new(matrix: Vec>, bound_k: usize) -> Self { + let num_rows = matrix.len(); + let num_cols = if num_rows > 0 { matrix[0].len() } else { 0 }; + + for row in &matrix { + assert_eq!(row.len(), num_cols, "All rows must have the same length"); + } + + Self { + matrix, + num_rows, + num_cols, + bound_k, + } + } + + /// Get the binary matrix. + pub fn matrix(&self) -> &[Vec] { + &self.matrix + } + + /// Get the number of rows. + pub fn num_rows(&self) -> usize { + self.num_rows + } + + /// Get the number of columns. + pub fn num_cols(&self) -> usize { + self.num_cols + } + + /// Get the upper bound K. + pub fn bound_k(&self) -> usize { + self.bound_k + } + + /// Count the total number of maximal consecutive blocks of 1s + /// when columns are permuted according to `config`. + /// + /// `config[position] = column_index` defines the column permutation. + /// Returns `Some(total_blocks)` if the config is a valid permutation, + /// or `None` if it is not (wrong length, duplicate columns, or out-of-range). + pub fn count_consecutive_blocks(&self, config: &[usize]) -> Option { + if config.len() != self.num_cols { + return None; + } + + // Validate permutation: all values distinct and in 0..num_cols. + let mut seen = vec![false; self.num_cols]; + for &col in config { + if col >= self.num_cols || seen[col] { + return None; + } + seen[col] = true; + } + + let mut total_blocks = 0; + for row in &self.matrix { + let mut in_block = false; + for &pos in config { + if row[pos] { + if !in_block { + total_blocks += 1; + in_block = true; + } + } else { + in_block = false; + } + } + } + + Some(total_blocks) + } +} + +impl Problem for ConsecutiveBlockMinimization { + const NAME: &'static str = "ConsecutiveBlockMinimization"; + type Metric = bool; + + fn dims(&self) -> Vec { + vec![self.num_cols; self.num_cols] + } + + fn evaluate(&self, config: &[usize]) -> bool { + if self.num_cols == 0 { + // Empty matrix: zero blocks, always satisfies any bound. + return true; + } + + match self.count_consecutive_blocks(config) { + Some(total) => total <= self.bound_k, + None => false, + } + } + + fn variant() -> Vec<(&'static str, &'static str)> { + crate::variant_params![] + } + + fn num_variables(&self) -> usize { + self.num_cols + } +} + +impl SatisfactionProblem for ConsecutiveBlockMinimization {} + +crate::declare_variants! { + default sat ConsecutiveBlockMinimization => "factorial(num_cols) * num_rows * num_cols", +} + +#[cfg(feature = "example-db")] +pub(crate) fn canonical_model_example_specs() -> Vec { + vec![crate::example_db::specs::ModelExampleSpec { + id: "consecutive_block_minimization", + build: || { + let problem = ConsecutiveBlockMinimization::new( + vec![vec![true, false, true], vec![false, true, true]], + 2, + ); + crate::example_db::specs::satisfaction_example(problem, vec![vec![0, 2, 1]]) + }, + }] +} + +#[cfg(test)] +#[path = "../../unit_tests/models/algebraic/consecutive_block_minimization.rs"] +mod tests; diff --git a/src/models/algebraic/mod.rs b/src/models/algebraic/mod.rs index a4945487d..56727d2b4 100644 --- a/src/models/algebraic/mod.rs +++ b/src/models/algebraic/mod.rs @@ -5,14 +5,17 @@ //! - [`ILP`]: Integer Linear Programming //! - [`ClosestVectorProblem`]: Closest Vector Problem (minimize lattice distance) //! - [`BMF`]: Boolean Matrix Factorization +//! - [`ConsecutiveBlockMinimization`]: Consecutive Block Minimization pub(crate) mod bmf; pub(crate) mod closest_vector_problem; +pub(crate) mod consecutive_block_minimization; pub(crate) mod ilp; pub(crate) mod qubo; pub use bmf::BMF; pub use closest_vector_problem::{ClosestVectorProblem, VarBounds}; +pub use consecutive_block_minimization::ConsecutiveBlockMinimization; pub use ilp::{Comparison, LinearConstraint, ObjectiveSense, VariableDomain, ILP}; pub use qubo::QUBO; @@ -23,5 +26,6 @@ pub(crate) fn canonical_model_example_specs() -> Vec 1 block + // [0, 1, 1] -> 1 block + // Total = 2 blocks, bound_k = 2 => satisfies + let problem = ConsecutiveBlockMinimization::new( + vec![vec![true, false, true], vec![false, true, true]], + 2, + ); + assert!(problem.evaluate(&[0, 2, 1])); + + // Identity permutation [0, 1, 2]: + // [1, 0, 1] -> 2 blocks + // [0, 1, 1] -> 1 block + // Total = 3 blocks, bound_k = 2 => does not satisfy + assert!(!problem.evaluate(&[0, 1, 2])); +} + +#[test] +fn test_consecutive_block_minimization_count_blocks() { + let problem = ConsecutiveBlockMinimization::new( + vec![vec![true, false, true], vec![false, true, true]], + 2, + ); + assert_eq!(problem.count_consecutive_blocks(&[0, 2, 1]), Some(2)); + assert_eq!(problem.count_consecutive_blocks(&[0, 1, 2]), Some(3)); + // Invalid: duplicate column + assert_eq!(problem.count_consecutive_blocks(&[0, 0, 1]), None); + // Invalid: wrong length + assert_eq!(problem.count_consecutive_blocks(&[0, 1]), None); + // Invalid: out of range + assert_eq!(problem.count_consecutive_blocks(&[0, 1, 5]), None); +} + +#[test] +fn test_consecutive_block_minimization_brute_force() { + let problem = ConsecutiveBlockMinimization::new( + vec![vec![true, false, true], vec![false, true, true]], + 2, + ); + let solver = BruteForce::new(); + let solutions = solver.find_all_satisfying(&problem); + assert!(!solutions.is_empty()); + for sol in &solutions { + assert!(problem.evaluate(sol)); + } +} + +#[test] +fn test_consecutive_block_minimization_empty_matrix() { + let problem = ConsecutiveBlockMinimization::new(vec![], 0); + assert_eq!(problem.num_rows(), 0); + assert_eq!(problem.num_cols(), 0); + assert!(problem.evaluate(&[])); +} + +#[test] +fn test_consecutive_block_minimization_serialization() { + let problem = ConsecutiveBlockMinimization::new(vec![vec![true, false], vec![false, true]], 2); + let json = serde_json::to_string(&problem).unwrap(); + let deserialized: ConsecutiveBlockMinimization = serde_json::from_str(&json).unwrap(); + assert_eq!(deserialized.num_rows(), problem.num_rows()); + assert_eq!(deserialized.num_cols(), problem.num_cols()); + assert_eq!(deserialized.bound_k(), problem.bound_k()); + assert_eq!(deserialized.matrix(), problem.matrix()); +} + +#[test] +fn test_consecutive_block_minimization_invalid_permutation() { + let problem = ConsecutiveBlockMinimization::new(vec![vec![true, false], vec![false, true]], 2); + // Not a valid permutation => evaluate returns false + assert!(!problem.evaluate(&[0, 0])); + // Wrong length + assert!(!problem.evaluate(&[0])); +} diff --git a/src/unit_tests/trait_consistency.rs b/src/unit_tests/trait_consistency.rs index 122362efa..2d8d8c41e 100644 --- a/src/unit_tests/trait_consistency.rs +++ b/src/unit_tests/trait_consistency.rs @@ -84,6 +84,10 @@ fn test_all_problems_implement_trait_correctly() { ); check_problem_trait(&PaintShop::new(vec!["a", "a"]), "PaintShop"); check_problem_trait(&BMF::new(vec![vec![true]], 1), "BMF"); + check_problem_trait( + &ConsecutiveBlockMinimization::new(vec![vec![true, false], vec![false, true]], 2), + "ConsecutiveBlockMinimization", + ); check_problem_trait( &BicliqueCover::new(BipartiteGraph::new(2, 2, vec![(0, 0)]), 1), "BicliqueCover", From f351eb0d9f08065c3828f74941fa7763e97bd299 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Mon, 16 Mar 2026 21:08:14 +0800 Subject: [PATCH 03/14] chore: remove plan file after implementation --- ...26-03-16-consecutive-block-minimization.md | 167 ------------------ 1 file changed, 167 deletions(-) delete mode 100644 docs/plans/2026-03-16-consecutive-block-minimization.md diff --git a/docs/plans/2026-03-16-consecutive-block-minimization.md b/docs/plans/2026-03-16-consecutive-block-minimization.md deleted file mode 100644 index 5108c839c..000000000 --- a/docs/plans/2026-03-16-consecutive-block-minimization.md +++ /dev/null @@ -1,167 +0,0 @@ -# Plan: Add ConsecutiveBlockMinimization Model (#420) - -## Summary - -Add the Consecutive Block Minimization satisfaction problem (Garey & Johnson SR17) to the `algebraic/` category. Given an m×n binary matrix A and positive integer K, decide whether there exists a column permutation yielding at most K maximal blocks of consecutive 1's across all rows. - -## Issue Details - -- **Problem**: ConsecutiveBlockMinimization -- **Type**: SatisfactionProblem (Metric = bool) -- **Category**: algebraic/ (matrix input) -- **No type parameters** (no graph type, no weight type) -- **NP-complete**: Kou (1977), reduction from Hamiltonian Path -- **Complexity**: O(n^n × m × n) brute-force - -## Batch 1: Implementation (Steps 1-5.5) - -### Step 1: Category - -Place in `src/models/algebraic/` alongside BMF, QUBO, ILP, CVP. - -### Step 1.5: Size Getters - -Getter methods needed (from complexity expression and overhead expressions): -- `num_rows()` → m (matrix.len()) -- `num_cols()` → n (matrix[0].len()) -- `bound_k()` → K -- `num_variables()` → num_cols (override default) - -### Step 2: Model File - -Create `src/models/algebraic/consecutive_block_minimization.rs`: - -**Schema entry** (inventory::submit!): -- name: "ConsecutiveBlockMinimization" -- display_name: "ConsecutiveBlockMinimization" -- aliases: &["CBM"] -- dimensions: &[] (no type params) -- fields: matrix (Vec>), bound_k (usize) - -**Struct**: -```rust -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ConsecutiveBlockMinimization { - matrix: Vec>, - num_rows: usize, - num_cols: usize, - bound_k: usize, -} -``` - -**Constructor** `new(matrix, bound_k)`: -- Validate all rows have same length -- Store derived num_rows, num_cols - -**Accessors**: `matrix()`, `num_rows()`, `num_cols()`, `bound_k()` - -**Helper** `count_blocks(config)`: -- Interpret config as column permutation (config[i] = position of column i) -- Validate it's a valid permutation (all distinct, in 0..num_cols) -- For each row, count maximal runs of consecutive 1's after reordering -- Return Option: None if invalid permutation, Some(total) otherwise - -**Problem impl**: -- `NAME = "ConsecutiveBlockMinimization"` -- `Metric = bool` -- `dims() = vec![num_cols; num_cols]` -- `evaluate(config)`: call count_blocks, return false if invalid permutation, otherwise total <= bound_k -- `variant() = crate::variant_params![]` -- Override `num_variables()` → num_cols - -**SatisfactionProblem**: empty marker impl - -### Step 2.5: declare_variants! - -```rust -crate::declare_variants! { - default sat ConsecutiveBlockMinimization => "num_cols^num_cols * num_rows * num_cols", -} -``` - -### Step 3: Register in Modules - -1. `src/models/algebraic/mod.rs`: - - Add `pub(crate) mod consecutive_block_minimization;` - - Add `pub use consecutive_block_minimization::ConsecutiveBlockMinimization;` - - Update module doc comment - - Add to `canonical_model_example_specs()` aggregator - -2. `src/models/mod.rs`: - - Re-export `ConsecutiveBlockMinimization` from `algebraic` - -### Step 4: CLI Registration - -1. **Aliases**: Already handled via `declare_variants!` + ProblemSchemaEntry aliases field ("CBM") - -2. **`problemreductions-cli/src/commands/create.rs`**: - - Add match arm for "ConsecutiveBlockMinimization" - - Parse `--matrix` (JSON 2D bool array) and `--bound-k` (integer) - - Add to `after_help` flag table - - Add to `all_data_flags_empty` - - Add to `example_for` with a small example instance - -3. **`problemreductions-cli/src/cli.rs`**: - - Add `--matrix` flag: `Option` for JSON-encoded matrix - - Add `--bound-k` flag: `Option` - -### Step 4.6: Example-DB - -Add `canonical_model_example_specs()` in the model file: -- Use the YES instance from the issue (Instance 2: path graph adjacency matrix, K=6) -- Or use a simpler small instance that has a satisfying permutation -- Use `satisfaction_example(problem, vec![valid_config])` - -### Step 5: Unit Tests - -Create `src/unit_tests/models/algebraic/consecutive_block_minimization.rs`: - -- `test_consecutive_block_minimization_creation`: constructor, getters -- `test_consecutive_block_minimization_evaluation`: test with known YES and NO configs -- `test_consecutive_block_minimization_invalid_permutation`: non-permutation configs return false -- `test_consecutive_block_minimization_serialization`: serde round-trip -- `test_consecutive_block_minimization_solver`: BruteForce::find_satisfying on small instance -- `test_consecutive_block_minimization_paper_example`: test with the issue's example instances -- `test_consecutive_block_minimization_empty_matrix`: edge case - -Add `#[path]` link in model file: -```rust -#[cfg(test)] -#[path = "../../unit_tests/models/algebraic/consecutive_block_minimization.rs"] -mod tests; -``` - -### Step 5.5: Trait Consistency - -Add entry in `src/unit_tests/trait_consistency.rs`: -```rust -check_problem_trait( - &ConsecutiveBlockMinimization::new(matrix, bound_k), - "ConsecutiveBlockMinimization", -); -``` - -## Batch 2: Paper Entry (Step 6) - -### Step 6: Paper Documentation - -In `docs/paper/reductions.typ`: - -1. Add to `display-name` dict: - ```typst - "ConsecutiveBlockMinimization": [Consecutive Block Minimization], - ``` - -2. Add `problem-def` block after related algebraic problems: - - Formal definition: m×n binary matrix, positive integer K, column permutation, blocks - - Background: GJ SR17, information retrieval, scheduling, glass cutting - - Reference to Kou (1977), Booth (1975), Haddadi & Layouni (2008) - - Connection to consecutive ones property (PQ-trees) - -## Verification - -After each batch, run: -```bash -make check # fmt + clippy + test -make paper # build paper -``` From 27a69baa2dce9cd450dc10690e0f4ce9d1873c4a Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Mon, 16 Mar 2026 23:49:42 +0800 Subject: [PATCH 04/14] fix: validate empty CBM permutations --- src/models/algebraic/consecutive_block_minimization.rs | 5 ----- .../models/algebraic/consecutive_block_minimization.rs | 1 + 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/models/algebraic/consecutive_block_minimization.rs b/src/models/algebraic/consecutive_block_minimization.rs index f53be4803..2b7376592 100644 --- a/src/models/algebraic/consecutive_block_minimization.rs +++ b/src/models/algebraic/consecutive_block_minimization.rs @@ -162,11 +162,6 @@ impl Problem for ConsecutiveBlockMinimization { } fn evaluate(&self, config: &[usize]) -> bool { - if self.num_cols == 0 { - // Empty matrix: zero blocks, always satisfies any bound. - return true; - } - match self.count_consecutive_blocks(config) { Some(total) => total <= self.bound_k, None => false, diff --git a/src/unit_tests/models/algebraic/consecutive_block_minimization.rs b/src/unit_tests/models/algebraic/consecutive_block_minimization.rs index 4e48fe56d..791929858 100644 --- a/src/unit_tests/models/algebraic/consecutive_block_minimization.rs +++ b/src/unit_tests/models/algebraic/consecutive_block_minimization.rs @@ -73,6 +73,7 @@ fn test_consecutive_block_minimization_empty_matrix() { assert_eq!(problem.num_rows(), 0); assert_eq!(problem.num_cols(), 0); assert!(problem.evaluate(&[])); + assert!(!problem.evaluate(&[0])); } #[test] From 79204ff4d336b6e9535391a59cb61f96700ae189 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Tue, 17 Mar 2026 00:08:16 +0800 Subject: [PATCH 05/14] fix: harden CBM CLI and deserialization --- problemreductions-cli/src/cli.rs | 3 +- problemreductions-cli/src/commands/create.rs | 16 +++-- problemreductions-cli/tests/cli_tests.rs | 66 +++++++++++++++++++ .../consecutive_block_minimization.rs | 57 +++++++++++++--- .../consecutive_block_minimization.rs | 14 +++- 5 files changed, 138 insertions(+), 18 deletions(-) diff --git a/problemreductions-cli/src/cli.rs b/problemreductions-cli/src/cli.rs index 11ce16ef3..cd2744507 100644 --- a/problemreductions-cli/src/cli.rs +++ b/problemreductions-cli/src/cli.rs @@ -318,7 +318,8 @@ pub struct CreateArgs { /// Number of variables (for SAT/KSAT) #[arg(long)] pub num_vars: Option, - /// Matrix for QUBO (semicolon-separated rows, e.g., "1,0.5;0.5,2") + /// Matrix input. QUBO uses semicolon-separated numeric rows ("1,0.5;0.5,2"); + /// ConsecutiveBlockMinimization uses a JSON 2D bool array ('[[true,false],[false,true]]') #[arg(long)] pub matrix: Option, /// Number of colors for KColoring diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index 4123b6745..dc36a2dd0 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -218,6 +218,7 @@ fn type_format_hint(type_name: &str, graph_type: Option<&str>) -> &'static str { "Vec" => "comma-separated integers: 1,1,2", "Vec" => "comma-separated: 1,2,3", "Vec" => "semicolon-separated clauses: \"1,2;-1,3\"", + "Vec>" => "JSON 2D bool array: '[[true,false],[false,true]]'", "Vec>" => "semicolon-separated rows: \"1,0.5;0.5,2\"", "usize" => "integer", "u64" => "integer", @@ -989,22 +990,23 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { // ConsecutiveBlockMinimization "ConsecutiveBlockMinimization" => { + let usage = "Usage: pred create ConsecutiveBlockMinimization --matrix '[[true,false,true],[false,true,true]]' --bound-k 2"; let matrix_str = args.matrix.as_deref().ok_or_else(|| { anyhow::anyhow!( - "ConsecutiveBlockMinimization requires --matrix and --bound-k\n\n\ - Usage: pred create ConsecutiveBlockMinimization --matrix '[[true,false,true],[false,true,true]]' --bound-k 2" + "ConsecutiveBlockMinimization requires --matrix as a JSON 2D bool array and --bound-k\n\n{usage}" ) })?; let bound_k = args.bound_k.ok_or_else(|| { + anyhow::anyhow!("ConsecutiveBlockMinimization requires --bound-k\n\n{usage}") + })?; + let matrix: Vec> = serde_json::from_str(matrix_str).map_err(|err| { anyhow::anyhow!( - "ConsecutiveBlockMinimization requires --bound-k\n\n\ - Usage: pred create ConsecutiveBlockMinimization --matrix '[[true,false,true],[false,true,true]]' --bound-k 2" + "ConsecutiveBlockMinimization requires --matrix as a JSON 2D bool array (e.g., '[[true,false,true],[false,true,true]]')\n\n{usage}\n\nFailed to parse --matrix: {err}" ) })?; - let matrix: Vec> = serde_json::from_str(matrix_str) - .context("Failed to parse --matrix as JSON 2D bool array")?; ( - ser(ConsecutiveBlockMinimization::new(matrix, bound_k))?, + ser(ConsecutiveBlockMinimization::try_new(matrix, bound_k) + .map_err(|err| anyhow::anyhow!("{err}\n\n{usage}"))?)?, resolved_variant.clone(), ) } diff --git a/problemreductions-cli/tests/cli_tests.rs b/problemreductions-cli/tests/cli_tests.rs index 8d481fccb..6947c6d8f 100644 --- a/problemreductions-cli/tests/cli_tests.rs +++ b/problemreductions-cli/tests/cli_tests.rs @@ -310,6 +310,31 @@ fn test_evaluate_sat() { std::fs::remove_file(&tmp).ok(); } +#[test] +fn test_evaluate_consecutive_block_minimization_rejects_inconsistent_dimensions() { + let problem_json = r#"{ + "type": "ConsecutiveBlockMinimization", + "data": { + "matrix": [[true]], + "num_rows": 1, + "num_cols": 2, + "bound_k": 1 + } + }"#; + let tmp = std::env::temp_dir().join("pred_test_eval_cbm_invalid_dims.json"); + std::fs::write(&tmp, problem_json).unwrap(); + + let output = pred() + .args(["evaluate", tmp.to_str().unwrap(), "--config", "0,1"]) + .output() + .unwrap(); + assert!(!output.status.success()); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("num_cols must match matrix column count")); + assert!(!stderr.contains("panicked at"), "stderr: {stderr}"); + std::fs::remove_file(&tmp).ok(); +} + #[test] fn test_create_undirected_two_commodity_integral_flow() { let output = pred() @@ -510,6 +535,47 @@ fn test_create_undirected_two_commodity_integral_flow_rejects_out_of_range_termi assert!(!stderr.contains("panicked at"), "stderr: {stderr}"); } +#[test] +fn test_create_consecutive_block_minimization_rejects_ragged_matrix() { + let output = pred() + .args([ + "create", + "ConsecutiveBlockMinimization", + "--matrix", + "[[true],[true,false]]", + "--bound-k", + "2", + ]) + .output() + .unwrap(); + assert!(!output.status.success()); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("all matrix rows must have the same length")); + assert!(stderr.contains("Usage: pred create ConsecutiveBlockMinimization")); + assert!(!stderr.contains("panicked at"), "stderr: {stderr}"); +} + +#[test] +fn test_create_consecutive_block_minimization_help_mentions_json_matrix_format() { + let output = pred() + .args(["create", "ConsecutiveBlockMinimization"]) + .output() + .unwrap(); + assert!(!output.status.success()); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("JSON 2D bool array")); + assert!(stderr.contains("[[true,false,true],[false,true,true]]")); +} + +#[test] +fn test_create_help_mentions_consecutive_block_minimization_matrix_format() { + let output = pred().args(["create", "--help"]).output().unwrap(); + assert!(output.status.success()); + let stdout = String::from_utf8(output.stdout).unwrap(); + assert!(stdout.contains("ConsecutiveBlockMinimization")); + assert!(stdout.contains("JSON 2D bool array")); +} + #[test] fn test_reduce() { let problem_json = r#"{ diff --git a/src/models/algebraic/consecutive_block_minimization.rs b/src/models/algebraic/consecutive_block_minimization.rs index 2b7376592..ba83a91a2 100644 --- a/src/models/algebraic/consecutive_block_minimization.rs +++ b/src/models/algebraic/consecutive_block_minimization.rs @@ -58,6 +58,7 @@ inventory::submit! { /// } /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "ConsecutiveBlockMinimizationDef")] pub struct ConsecutiveBlockMinimization { /// The binary matrix A (m x n). matrix: Vec>, @@ -79,19 +80,19 @@ impl ConsecutiveBlockMinimization { /// # Panics /// Panics if rows have inconsistent lengths. pub fn new(matrix: Vec>, bound_k: usize) -> Self { - let num_rows = matrix.len(); - let num_cols = if num_rows > 0 { matrix[0].len() } else { 0 }; - - for row in &matrix { - assert_eq!(row.len(), num_cols, "All rows must have the same length"); - } + Self::try_new(matrix, bound_k).unwrap_or_else(|err| panic!("{err}")) + } - Self { + /// Create a new ConsecutiveBlockMinimization problem, returning an error + /// instead of panicking when the matrix is ragged. + pub fn try_new(matrix: Vec>, bound_k: usize) -> Result { + let (num_rows, num_cols) = validate_matrix_dimensions(&matrix)?; + Ok(Self { matrix, num_rows, num_cols, bound_k, - } + }) } /// Get the binary matrix. @@ -183,6 +184,46 @@ crate::declare_variants! { default sat ConsecutiveBlockMinimization => "factorial(num_cols) * num_rows * num_cols", } +#[derive(Debug, Clone, Deserialize)] +struct ConsecutiveBlockMinimizationDef { + matrix: Vec>, + num_rows: usize, + num_cols: usize, + bound_k: usize, +} + +impl TryFrom for ConsecutiveBlockMinimization { + type Error = String; + + fn try_from(value: ConsecutiveBlockMinimizationDef) -> Result { + let problem = Self::try_new(value.matrix, value.bound_k)?; + if value.num_rows != problem.num_rows { + return Err(format!( + "num_rows must match matrix row count ({})", + problem.num_rows + )); + } + if value.num_cols != problem.num_cols { + return Err(format!( + "num_cols must match matrix column count ({})", + problem.num_cols + )); + } + Ok(problem) + } +} + +fn validate_matrix_dimensions(matrix: &[Vec]) -> Result<(usize, usize), String> { + let num_rows = matrix.len(); + let num_cols = matrix.first().map_or(0, Vec::len); + + if matrix.iter().any(|row| row.len() != num_cols) { + return Err("all matrix rows must have the same length".to_string()); + } + + Ok((num_rows, num_cols)) +} + #[cfg(feature = "example-db")] pub(crate) fn canonical_model_example_specs() -> Vec { vec![crate::example_db::specs::ModelExampleSpec { diff --git a/src/unit_tests/models/algebraic/consecutive_block_minimization.rs b/src/unit_tests/models/algebraic/consecutive_block_minimization.rs index 791929858..a4c054887 100644 --- a/src/unit_tests/models/algebraic/consecutive_block_minimization.rs +++ b/src/unit_tests/models/algebraic/consecutive_block_minimization.rs @@ -60,8 +60,11 @@ fn test_consecutive_block_minimization_brute_force() { 2, ); let solver = BruteForce::new(); - let solutions = solver.find_all_satisfying(&problem); - assert!(!solutions.is_empty()); + let mut solutions = solver.find_all_satisfying(&problem); + solutions.sort(); + let mut expected = vec![vec![0, 2, 1], vec![1, 2, 0]]; + expected.sort(); + assert_eq!(solutions, expected); for sol in &solutions { assert!(problem.evaluate(sol)); } @@ -87,6 +90,13 @@ fn test_consecutive_block_minimization_serialization() { assert_eq!(deserialized.matrix(), problem.matrix()); } +#[test] +fn test_consecutive_block_minimization_deserialization_rejects_inconsistent_dimensions() { + let json = r#"{"matrix":[[true]],"num_rows":1,"num_cols":2,"bound_k":1}"#; + let err = serde_json::from_str::(json).unwrap_err(); + assert!(err.to_string().contains("num_cols")); +} + #[test] fn test_consecutive_block_minimization_invalid_permutation() { let problem = ConsecutiveBlockMinimization::new(vec![vec![true, false], vec![false, true]], 2); From 80dd74ba70d069577344686b74be9a2fec358bec Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Tue, 17 Mar 2026 00:29:40 +0800 Subject: [PATCH 06/14] docs: surface CBM CLI usage --- README.md | 11 +++++++++++ docs/src/cli.md | 11 +++++++++++ 2 files changed, 22 insertions(+) diff --git a/README.md b/README.md index 39482bbdf..91d1077a3 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,17 @@ make cli # builds target/release/pred See the [Getting Started](https://codingthrust.github.io/problem-reductions/getting-started.html) guide for usage examples, the reduction workflow, and [CLI usage](https://codingthrust.github.io/problem-reductions/cli.html). +Try a model directly from the CLI: + +```bash +# Show the Consecutive Block Minimization model (alias: CBM) +pred show CBM + +# Create and solve a small CBM instance (currently with brute-force) +pred create CBM --matrix '[[true,false,true],[false,true,true]]' --bound-k 2 \ + | pred solve - --solver brute-force +``` + ## MCP Server (AI Integration) The `pred` CLI includes a built-in [MCP](https://modelcontextprotocol.io/) server for AI assistant integration (Claude Code, Cursor, Windsurf, OpenCode, etc.). diff --git a/docs/src/cli.md b/docs/src/cli.md index 99e709043..fbe12f3b1 100644 --- a/docs/src/cli.md +++ b/docs/src/cli.md @@ -58,6 +58,12 @@ pred create SteinerTree --graph 0-1,0-3,1-2,1-3,2-3,2-4,3-4 --edge-weights 2,5,2 # Create a Length-Bounded Disjoint Paths instance pred create LengthBoundedDisjointPaths --graph 0-1,1-6,0-2,2-3,3-6,0-4,4-5,5-6 --source 0 --sink 6 --num-paths-required 2 --bound 3 -o lbdp.json +# Create a Consecutive Block Minimization instance (alias: CBM) +pred create CBM --matrix '[[true,false,true],[false,true,true]]' --bound-k 2 -o cbm.json + +# CBM currently needs the brute-force solver +pred solve cbm.json --solver brute-force + # Or start from a canonical model example pred create --example MIS/SimpleGraph/i32 -o example.json @@ -288,6 +294,7 @@ pred create MIS --graph 0-1,1-2,2-3 -o problem.json pred create MIS --graph 0-1,1-2,2-3 --weights 2,1,3,1 -o problem.json pred create SAT --num-vars 3 --clauses "1,2;-1,3" -o sat.json pred create QUBO --matrix "1,0.5;0.5,2" -o qubo.json +pred create CBM --matrix '[[true,false,true],[false,true,true]]' --bound-k 2 -o cbm.json pred create KColoring --k 3 --graph 0-1,1-2,2-0 -o kcol.json pred create SpinGlass --graph 0-1,1-2 -o sg.json pred create MaxCut --graph 0-1,1-2,2-0 -o maxcut.json @@ -303,6 +310,10 @@ pred create MinimumTardinessSequencing --n 5 --deadlines 5,5,5,3,3 --precedence- For `LengthBoundedDisjointPaths`, the CLI flag `--bound` maps to the JSON field `max_length`. +For `ConsecutiveBlockMinimization`, the `--matrix` flag expects a JSON 2D bool array such as +`'[[true,false,true],[false,true,true]]'`. The example above shows the accepted shape, and solving +CBM instances currently requires `--solver brute-force`. + Canonical examples are useful when you want a known-good instance from the paper/example database. For model examples, `pred create --example ` emits the canonical instance for that graph node. From fb3b9ae060a791f99ace5270e43cdc60bacca174 Mon Sep 17 00:00:00 2001 From: Xiwei Pan Date: Fri, 20 Mar 2026 17:01:59 +0800 Subject: [PATCH 07/14] Fix canonical_model_example_specs for new ModelExampleSpec API The ModelExampleSpec struct changed on main (build closure -> instance/optimal_config/optimal_value fields). Co-Authored-By: Claude Opus 4.6 (1M context) --- .../algebraic/consecutive_block_minimization.rs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/models/algebraic/consecutive_block_minimization.rs b/src/models/algebraic/consecutive_block_minimization.rs index ba83a91a2..ccd252f4e 100644 --- a/src/models/algebraic/consecutive_block_minimization.rs +++ b/src/models/algebraic/consecutive_block_minimization.rs @@ -228,13 +228,12 @@ fn validate_matrix_dimensions(matrix: &[Vec]) -> Result<(usize, usize), St pub(crate) fn canonical_model_example_specs() -> Vec { vec![crate::example_db::specs::ModelExampleSpec { id: "consecutive_block_minimization", - build: || { - let problem = ConsecutiveBlockMinimization::new( - vec![vec![true, false, true], vec![false, true, true]], - 2, - ); - crate::example_db::specs::satisfaction_example(problem, vec![vec![0, 2, 1]]) - }, + instance: Box::new(ConsecutiveBlockMinimization::new( + vec![vec![true, false, true], vec![false, true, true]], + 2, + )), + optimal_config: vec![0, 2, 1], + optimal_value: serde_json::json!(true), }] } From 5e44dec6e2d28710e4c525ddeb83f35f8bf93a12 Mon Sep 17 00:00:00 2001 From: Xiwei Pan Date: Fri, 20 Mar 2026 17:02:33 +0800 Subject: [PATCH 08/14] Run cargo fmt after merge with main Co-Authored-By: Claude Opus 4.6 (1M context) --- problemreductions-cli/src/cli.rs | 1 - src/models/set/two_dimensional_consecutive_sets.rs | 8 +++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/problemreductions-cli/src/cli.rs b/problemreductions-cli/src/cli.rs index 03e5a8590..07b18df86 100644 --- a/problemreductions-cli/src/cli.rs +++ b/problemreductions-cli/src/cli.rs @@ -555,7 +555,6 @@ pub struct CreateArgs { #[arg(long)] pub alphabet_size: Option, - /// Upper bound K on consecutive blocks for ConsecutiveBlockMinimization #[arg(long)] pub bound_k: Option, diff --git a/src/models/set/two_dimensional_consecutive_sets.rs b/src/models/set/two_dimensional_consecutive_sets.rs index ca41c02d0..a3e6b20ac 100644 --- a/src/models/set/two_dimensional_consecutive_sets.rs +++ b/src/models/set/two_dimensional_consecutive_sets.rs @@ -104,7 +104,13 @@ impl TwoDimensionalConsecutiveSets { /// Create a new 2-Dimensional Consecutive Sets instance, returning validation errors. pub fn try_new(alphabet_size: usize, subsets: Vec>) -> Result { validate(alphabet_size, &subsets)?; - let subsets = subsets.into_iter().map(|mut s| { s.sort(); s }).collect(); + let subsets = subsets + .into_iter() + .map(|mut s| { + s.sort(); + s + }) + .collect(); Ok(Self { alphabet_size, subsets, From 6cf405e55eb58c1edbea42d601278a6a9ef3a9d2 Mon Sep 17 00:00:00 2001 From: Xiwei Pan Date: Fri, 20 Mar 2026 17:23:29 +0800 Subject: [PATCH 09/14] Use issue #420 Instance 2 (P_6 adjacency matrix) as canonical example Update canonical_model_example_specs to use the 6x6 path graph adjacency matrix with K=6 from the issue. Fix paper to use new optimal_config field format. Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/paper/reductions.typ | 3 +-- .../algebraic/consecutive_block_minimization.rs | 15 ++++++++++++--- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index 962671f62..6a5085ed5 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -2491,8 +2491,7 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76], let K = x.instance.bound_k let n-rows = mat.len() let n-cols = if n-rows > 0 { mat.at(0).len() } else { 0 } - let sol = x.optimal.at(0) - let perm = sol.config + let perm = x.optimal_config // Count blocks under the satisfying permutation let total-blocks = 0 for row in mat { diff --git a/src/models/algebraic/consecutive_block_minimization.rs b/src/models/algebraic/consecutive_block_minimization.rs index ccd252f4e..4b6574032 100644 --- a/src/models/algebraic/consecutive_block_minimization.rs +++ b/src/models/algebraic/consecutive_block_minimization.rs @@ -226,13 +226,22 @@ fn validate_matrix_dimensions(matrix: &[Vec]) -> Result<(usize, usize), St #[cfg(feature = "example-db")] pub(crate) fn canonical_model_example_specs() -> Vec { + // Adjacency matrix of path graph P_6, K=6 (one block per row). + // Issue #420 Instance 2. vec![crate::example_db::specs::ModelExampleSpec { id: "consecutive_block_minimization", instance: Box::new(ConsecutiveBlockMinimization::new( - vec![vec![true, false, true], vec![false, true, true]], - 2, + vec![ + vec![false, true, false, false, false, false], + vec![true, false, true, false, false, false], + vec![false, true, false, true, false, false], + vec![false, false, true, false, true, false], + vec![false, false, false, true, false, true], + vec![false, false, false, false, true, false], + ], + 6, )), - optimal_config: vec![0, 2, 1], + optimal_config: vec![0, 2, 4, 1, 3, 5], optimal_value: serde_json::json!(true), }] } From a3a0317932e029669f9bfa19e813fab1bd3e7455 Mon Sep 17 00:00:00 2001 From: Xiwei Pan Date: Fri, 20 Mar 2026 17:43:19 +0800 Subject: [PATCH 10/14] Fix SchedulingWithIndividualDeadlines paper section for new example format Update from old x.samples.at(0).config to x.optimal_config to match the current ModelExampleSpec format. Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/paper/reductions.typ | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index 6a5085ed5..b46515e32 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -3496,8 +3496,7 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76], let nproc = x.instance.num_processors let deadlines = x.instance.deadlines let precs = x.instance.precedences - let sample = x.samples.at(0) - let start = sample.config + let start = x.optimal_config let horizon = deadlines.fold(0, (acc, d) => if d > acc { d } else { acc }) let slot-groups = range(horizon).map(slot => range(ntasks).filter(t => start.at(t) == slot)) let tight-tasks = range(ntasks).filter(t => start.at(t) + 1 == deadlines.at(t)) From 3271a813fdf46102b4f31969e5bb833e01966afe Mon Sep 17 00:00:00 2001 From: Xiwei Pan Date: Fri, 20 Mar 2026 17:50:32 +0800 Subject: [PATCH 11/14] Rename --bound-k CLI flag to --bound for ConsecutiveBlockMinimization Reuse the existing --bound flag instead of adding a separate --bound-k. Other models with bound_k fields (RectilinearPictureCompression, MinimumCardinalityKey, ConsecutiveOnesSubmatrix) already map to --k. Co-Authored-By: Claude Opus 4.6 (1M context) --- README.md | 2 +- docs/src/cli.md | 4 ++-- problemreductions-cli/src/cli.rs | 3 --- problemreductions-cli/src/commands/create.rs | 12 ++++++------ problemreductions-cli/tests/cli_tests.rs | 10 +++++----- 5 files changed, 14 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 91d1077a3..256da143b 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ Try a model directly from the CLI: pred show CBM # Create and solve a small CBM instance (currently with brute-force) -pred create CBM --matrix '[[true,false,true],[false,true,true]]' --bound-k 2 \ +pred create CBM --matrix '[[true,false,true],[false,true,true]]' --bound 2 \ | pred solve - --solver brute-force ``` diff --git a/docs/src/cli.md b/docs/src/cli.md index ca6f76796..38fe48f8e 100644 --- a/docs/src/cli.md +++ b/docs/src/cli.md @@ -59,7 +59,7 @@ pred create SteinerTree --graph 0-1,0-3,1-2,1-3,2-3,2-4,3-4 --edge-weights 2,5,2 pred create LengthBoundedDisjointPaths --graph 0-1,1-6,0-2,2-3,3-6,0-4,4-5,5-6 --source 0 --sink 6 --num-paths-required 2 --bound 3 -o lbdp.json # Create a Consecutive Block Minimization instance (alias: CBM) -pred create CBM --matrix '[[true,false,true],[false,true,true]]' --bound-k 2 -o cbm.json +pred create CBM --matrix '[[true,false,true],[false,true,true]]' --bound 2 -o cbm.json # CBM currently needs the brute-force solver pred solve cbm.json --solver brute-force @@ -348,7 +348,7 @@ pred create MIS --graph 0-1,1-2,2-3 -o problem.json pred create MIS --graph 0-1,1-2,2-3 --weights 2,1,3,1 -o problem.json pred create SAT --num-vars 3 --clauses "1,2;-1,3" -o sat.json pred create QUBO --matrix "1,0.5;0.5,2" -o qubo.json -pred create CBM --matrix '[[true,false,true],[false,true,true]]' --bound-k 2 -o cbm.json +pred create CBM --matrix '[[true,false,true],[false,true,true]]' --bound 2 -o cbm.json pred create KColoring --k 3 --graph 0-1,1-2,2-0 -o kcol.json pred create KthBestSpanningTree --graph 0-1,0-2,1-2 --edge-weights 2,3,1 --k 1 --bound 3 -o kth.json pred create SpinGlass --graph 0-1,1-2 -o sg.json diff --git a/problemreductions-cli/src/cli.rs b/problemreductions-cli/src/cli.rs index 07b18df86..f6b13c1ce 100644 --- a/problemreductions-cli/src/cli.rs +++ b/problemreductions-cli/src/cli.rs @@ -555,9 +555,6 @@ pub struct CreateArgs { #[arg(long)] pub alphabet_size: Option, - /// Upper bound K on consecutive blocks for ConsecutiveBlockMinimization - #[arg(long)] - pub bound_k: Option, /// Number of attributes for AdditionalKey or MinimumCardinalityKey #[arg(long)] pub num_attributes: Option, diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index 130d35394..20917b35c 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -120,7 +120,6 @@ fn all_data_flags_empty(args: &CreateArgs) -> bool { && args.requirements.is_none() && args.num_workers.is_none() && args.alphabet_size.is_none() - && args.bound_k.is_none() && args.num_groups.is_none() && args.dependencies.is_none() && args.num_attributes.is_none() @@ -462,7 +461,7 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str { } "ShortestCommonSupersequence" => "--strings \"0,1,2;1,2,0\" --bound 4", "ConsecutiveBlockMinimization" => { - "--matrix '[[true,false,true],[false,true,true]]' --bound-k 2" + "--matrix '[[true,false,true],[false,true,true]]' --bound 2" } "ConjunctiveBooleanQuery" => { "--domain-size 6 --relations \"2:0,3|1,3|2,4;3:0,1,5|1,2,5\" --conjuncts-spec \"0:v0,c3;0:v1,c3;1:v0,v1,c5\"" @@ -500,6 +499,7 @@ fn help_flag_name(canonical: &str, field_name: &str) -> String { ("PrimeAttributeName", "dependencies") => return "deps".to_string(), ("PrimeAttributeName", "query_attribute") => return "query".to_string(), ("MinimumCardinalityKey", "bound_k") => return "k".to_string(), + ("ConsecutiveBlockMinimization", "bound_k") => return "bound".to_string(), ("ConsecutiveOnesSubmatrix", "bound_k") => return "k".to_string(), ("StaffScheduling", "shifts_per_schedule") => return "k".to_string(), _ => {} @@ -1861,14 +1861,14 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { // ConsecutiveBlockMinimization "ConsecutiveBlockMinimization" => { - let usage = "Usage: pred create ConsecutiveBlockMinimization --matrix '[[true,false,true],[false,true,true]]' --bound-k 2"; + let usage = "Usage: pred create ConsecutiveBlockMinimization --matrix '[[true,false,true],[false,true,true]]' --bound 2"; let matrix_str = args.matrix.as_deref().ok_or_else(|| { anyhow::anyhow!( - "ConsecutiveBlockMinimization requires --matrix as a JSON 2D bool array and --bound-k\n\n{usage}" + "ConsecutiveBlockMinimization requires --matrix as a JSON 2D bool array and --bound\n\n{usage}" ) })?; - let bound_k = args.bound_k.ok_or_else(|| { - anyhow::anyhow!("ConsecutiveBlockMinimization requires --bound-k\n\n{usage}") + let bound_k = args.bound.ok_or_else(|| { + anyhow::anyhow!("ConsecutiveBlockMinimization requires --bound\n\n{usage}") })?; let matrix: Vec> = serde_json::from_str(matrix_str).map_err(|err| { anyhow::anyhow!( diff --git a/problemreductions-cli/tests/cli_tests.rs b/problemreductions-cli/tests/cli_tests.rs index 797812d24..5c1a30df8 100644 --- a/problemreductions-cli/tests/cli_tests.rs +++ b/problemreductions-cli/tests/cli_tests.rs @@ -646,7 +646,7 @@ fn test_create_consecutive_block_minimization_rejects_ragged_matrix() { "ConsecutiveBlockMinimization", "--matrix", "[[true],[true,false]]", - "--bound-k", + "--bound", "2", ]) .output() @@ -1571,7 +1571,7 @@ fn test_create_minimum_cardinality_key_problem_help_uses_supported_flags() { stderr.contains("semicolon-separated dependencies"), "stderr: {stderr}" ); - assert!(!stderr.contains("--bound-k"), "stderr: {stderr}"); + assert!(!stderr.contains("--bound"), "stderr: {stderr}"); } #[test] @@ -2681,7 +2681,7 @@ fn test_create_string_to_string_correction_help_uses_cli_flags() { assert!(stderr.contains("--source-string"), "stderr: {stderr}"); assert!(stderr.contains("--target-string"), "stderr: {stderr}"); assert!(stderr.contains("--bound"), "stderr: {stderr}"); - assert!(!stderr.contains("--bound-k"), "stderr: {stderr}"); + assert!(!stderr.contains("--bound"), "stderr: {stderr}"); } #[test] @@ -3141,7 +3141,7 @@ fn test_create_rectilinear_picture_compression_help_uses_k_flag() { "expected '--k' in help output, got: {stderr}" ); assert!( - !stderr.contains("--bound-k"), + !stderr.contains("--bound"), "help should advertise the actual CLI flag name, got: {stderr}" ); } @@ -3229,7 +3229,7 @@ fn test_create_consecutive_ones_submatrix_no_flags_uses_actual_cli_help() { "expected '--k' in help output, got: {stderr}" ); assert!( - !stderr.contains("--bound-k"), + !stderr.contains("--bound"), "help should not advertise schema field names: {stderr}" ); assert!( From 6ba19b0dddfd3f5d446284acdfba6c18625ed3f4 Mon Sep 17 00:00:00 2001 From: Xiwei Pan Date: Fri, 20 Mar 2026 17:57:42 +0800 Subject: [PATCH 12/14] Use i64 for bound_k to match CLI --bound type directly Avoids type conversion between CLI i64 and model usize. Negative bounds correctly mean no permutation can satisfy. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../algebraic/consecutive_block_minimization.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/models/algebraic/consecutive_block_minimization.rs b/src/models/algebraic/consecutive_block_minimization.rs index 4b6574032..25daf7727 100644 --- a/src/models/algebraic/consecutive_block_minimization.rs +++ b/src/models/algebraic/consecutive_block_minimization.rs @@ -22,7 +22,7 @@ inventory::submit! { description: "Permute columns of a binary matrix to have at most K consecutive blocks of 1s", fields: &[ FieldInfo { name: "matrix", type_name: "Vec>", description: "Binary matrix A (m x n)" }, - FieldInfo { name: "bound_k", type_name: "usize", description: "Upper bound K on total consecutive blocks" }, + FieldInfo { name: "bound_k", type_name: "i64", description: "Upper bound K on total consecutive blocks" }, ], } } @@ -67,7 +67,7 @@ pub struct ConsecutiveBlockMinimization { /// Number of columns (n). num_cols: usize, /// Upper bound K on total consecutive blocks. - bound_k: usize, + bound_k: i64, } impl ConsecutiveBlockMinimization { @@ -79,13 +79,13 @@ impl ConsecutiveBlockMinimization { /// /// # Panics /// Panics if rows have inconsistent lengths. - pub fn new(matrix: Vec>, bound_k: usize) -> Self { + pub fn new(matrix: Vec>, bound_k: i64) -> Self { Self::try_new(matrix, bound_k).unwrap_or_else(|err| panic!("{err}")) } /// Create a new ConsecutiveBlockMinimization problem, returning an error /// instead of panicking when the matrix is ragged. - pub fn try_new(matrix: Vec>, bound_k: usize) -> Result { + pub fn try_new(matrix: Vec>, bound_k: i64) -> Result { let (num_rows, num_cols) = validate_matrix_dimensions(&matrix)?; Ok(Self { matrix, @@ -111,7 +111,7 @@ impl ConsecutiveBlockMinimization { } /// Get the upper bound K. - pub fn bound_k(&self) -> usize { + pub fn bound_k(&self) -> i64 { self.bound_k } @@ -164,7 +164,7 @@ impl Problem for ConsecutiveBlockMinimization { fn evaluate(&self, config: &[usize]) -> bool { match self.count_consecutive_blocks(config) { - Some(total) => total <= self.bound_k, + Some(total) => (total as i64) <= self.bound_k, None => false, } } @@ -189,7 +189,7 @@ struct ConsecutiveBlockMinimizationDef { matrix: Vec>, num_rows: usize, num_cols: usize, - bound_k: usize, + bound_k: i64, } impl TryFrom for ConsecutiveBlockMinimization { From 2ea8ca9acc6f86961c014ebb7f52280bdaeb4516 Mon Sep 17 00:00:00 2001 From: Xiwei Pan Date: Fri, 20 Mar 2026 19:09:39 +0800 Subject: [PATCH 13/14] Remove contradictory --bound assertion in CLI test After renaming --bound-k to --bound, the negative assertion became contradictory with the positive one. Remove since --bound-k no longer exists. Co-Authored-By: Claude Opus 4.6 (1M context) --- problemreductions-cli/tests/cli_tests.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/problemreductions-cli/tests/cli_tests.rs b/problemreductions-cli/tests/cli_tests.rs index 5c1a30df8..813bf0dea 100644 --- a/problemreductions-cli/tests/cli_tests.rs +++ b/problemreductions-cli/tests/cli_tests.rs @@ -2681,7 +2681,6 @@ fn test_create_string_to_string_correction_help_uses_cli_flags() { assert!(stderr.contains("--source-string"), "stderr: {stderr}"); assert!(stderr.contains("--target-string"), "stderr: {stderr}"); assert!(stderr.contains("--bound"), "stderr: {stderr}"); - assert!(!stderr.contains("--bound"), "stderr: {stderr}"); } #[test] From 9d364fe2f3366e7cde24da1a15fb5e9a05ccfd73 Mon Sep 17 00:00:00 2001 From: Xiwei Pan Date: Fri, 20 Mar 2026 19:25:13 +0800 Subject: [PATCH 14/14] Rename bound_k to bound (i64) across all models Unify the field name and type for CBM, MinimumCardinalityKey, ConsecutiveOnesSubmatrix, and RectilinearPictureCompression. All now use `bound: i64` matching the CLI --bound flag directly. Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/paper/reductions.typ | 8 ++-- problemreductions-cli/src/commands/create.rs | 38 +++++++++---------- problemreductions-cli/tests/cli_tests.rs | 37 +++++++----------- .../consecutive_block_minimization.rs | 24 ++++++------ .../algebraic/consecutive_ones_submatrix.rs | 22 +++++------ .../misc/rectilinear_picture_compression.rs | 18 ++++----- src/models/set/minimum_cardinality_key.rs | 14 +++---- .../consecutive_block_minimization.rs | 10 ++--- .../algebraic/consecutive_ones_submatrix.rs | 8 ++-- .../misc/rectilinear_picture_compression.rs | 6 +-- .../models/set/minimum_cardinality_key.rs | 10 ++--- src/unit_tests/prelude.rs | 2 +- 12 files changed, 93 insertions(+), 104 deletions(-) diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index b46515e32..9fcc75eb1 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -1892,7 +1892,7 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76], let n = x.instance.num_attributes let deps = x.instance.dependencies let m = deps.len() - let bound = x.instance.bound_k + let bound = x.instance.bound let key-attrs = range(n).filter(i => x.optimal_config.at(i) == 1) let fmt-set(s) = "${" + s.map(e => str(e)).join(", ") + "}$" let fmt-fd(d) = fmt-set(d.at(0)) + " $arrow.r$ " + fmt-set(d.at(1)) @@ -2488,7 +2488,7 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76], #{ let x = load-model-example("ConsecutiveBlockMinimization") let mat = x.instance.matrix - let K = x.instance.bound_k + let K = x.instance.bound let n-rows = mat.len() let n-cols = if n-rows > 0 { mat.at(0).len() } else { 0 } let perm = x.optimal_config @@ -2799,7 +2799,7 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76], let mat = x.instance.matrix let m = mat.len() let n = mat.at(0).len() - let K = x.instance.bound_k + let K = x.instance.bound // Convert bool matrix to int for display let M = mat.map(row => row.map(v => if v { 1 } else { 0 })) [ @@ -4006,7 +4006,7 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76], let A = x.instance.matrix let m = A.len() let n = A.at(0).len() - let K = x.instance.bound_k + let K = x.instance.bound // Convert bool matrix to int for display let A-int = A.map(row => row.map(v => if v { 1 } else { 0 })) // Use the canonical witness {0, 1, 3} diff --git a/problemreductions-cli/src/commands/create.rs b/problemreductions-cli/src/commands/create.rs index 20917b35c..c71bec5c1 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -433,7 +433,7 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str { "AdditionalKey" => "--num-attributes 6 --dependencies \"0,1:2,3;2,3:4,5;4,5:0,1\" --relation-attrs 0,1,2,3,4,5 --known-keys \"0,1;2,3;4,5\"", "SubgraphIsomorphism" => "--graph 0-1,1-2,2-0 --pattern 0-1", "RectilinearPictureCompression" => { - "--matrix \"1,1,0,0;1,1,0,0;0,0,1,1;0,0,1,1\" --k 2" + "--matrix \"1,1,0,0;1,1,0,0;0,0,1,1;0,0,1,1\" --bound 2" } "SequencingToMinimizeWeightedTardiness" => { "--sizes 3,4,2,5,3 --weights 2,3,1,4,2 --deadlines 5,8,4,15,10 --bound 13" @@ -451,7 +451,7 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str { "--strings \"010110;100101;001011\" --bound 3 --alphabet-size 2" } "MinimumCardinalityKey" => { - "--num-attributes 6 --dependencies \"0,1>2;0,2>3;1,3>4;2,4>5\" --k 2" + "--num-attributes 6 --dependencies \"0,1>2;0,2>3;1,3>4;2,4>5\" --bound 2" } "PrimeAttributeName" => { "--universe 6 --deps \"0,1>2,3,4,5;2,3>0,1,4,5\" --query 3" @@ -494,13 +494,11 @@ fn help_flag_name(canonical: &str, field_name: &str) -> String { return "num-processors/--m".to_string(); } ("LengthBoundedDisjointPaths", "max_length") => return "bound".to_string(), - ("RectilinearPictureCompression", "bound_k") => return "k".to_string(), + ("RectilinearPictureCompression", "bound") => return "bound".to_string(), ("PrimeAttributeName", "num_attributes") => return "universe".to_string(), ("PrimeAttributeName", "dependencies") => return "deps".to_string(), ("PrimeAttributeName", "query_attribute") => return "query".to_string(), - ("MinimumCardinalityKey", "bound_k") => return "k".to_string(), - ("ConsecutiveBlockMinimization", "bound_k") => return "bound".to_string(), - ("ConsecutiveOnesSubmatrix", "bound_k") => return "k".to_string(), + ("ConsecutiveOnesSubmatrix", "bound") => return "bound".to_string(), ("StaffScheduling", "shifts_per_schedule") => return "k".to_string(), _ => {} } @@ -1780,12 +1778,12 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { "MinimumCardinalityKey" => { let num_attributes = args.num_attributes.ok_or_else(|| { anyhow::anyhow!( - "MinimumCardinalityKey requires --num-attributes, --dependencies, and --k\n\n\ - Usage: pred create MinimumCardinalityKey --num-attributes 6 --dependencies \"0,1>2;0,2>3;1,3>4;2,4>5\" --k 2" + "MinimumCardinalityKey requires --num-attributes, --dependencies, and --bound\n\n\ + Usage: pred create MinimumCardinalityKey --num-attributes 6 --dependencies \"0,1>2;0,2>3;1,3>4;2,4>5\" --bound 2" ) })?; - let k = args.k.ok_or_else(|| { - anyhow::anyhow!("MinimumCardinalityKey requires --k (bound on key cardinality)") + let k = args.bound.ok_or_else(|| { + anyhow::anyhow!("MinimumCardinalityKey requires --bound (bound on key cardinality)") })?; let deps_str = args.dependencies.as_deref().ok_or_else(|| { anyhow::anyhow!( @@ -1867,7 +1865,7 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { "ConsecutiveBlockMinimization requires --matrix as a JSON 2D bool array and --bound\n\n{usage}" ) })?; - let bound_k = args.bound.ok_or_else(|| { + let bound = args.bound.ok_or_else(|| { anyhow::anyhow!("ConsecutiveBlockMinimization requires --bound\n\n{usage}") })?; let matrix: Vec> = serde_json::from_str(matrix_str).map_err(|err| { @@ -1876,7 +1874,7 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { ) })?; ( - ser(ConsecutiveBlockMinimization::try_new(matrix, bound_k) + ser(ConsecutiveBlockMinimization::try_new(matrix, bound) .map_err(|err| anyhow::anyhow!("{err}\n\n{usage}"))?)?, resolved_variant.clone(), ) @@ -1885,14 +1883,14 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { // RectilinearPictureCompression "RectilinearPictureCompression" => { let matrix = parse_bool_matrix(args)?; - let k = args.k.ok_or_else(|| { + let bound = args.bound.ok_or_else(|| { anyhow::anyhow!( - "RectilinearPictureCompression requires --matrix and --k\n\n\ - Usage: pred create RectilinearPictureCompression --matrix \"1,1,0,0;1,1,0,0;0,0,1,1;0,0,1,1\" --k 2" + "RectilinearPictureCompression requires --matrix and --bound\n\n\ + Usage: pred create RectilinearPictureCompression --matrix \"1,1,0,0;1,1,0,0;0,0,1,1;0,0,1,1\" --bound 2" ) })?; ( - ser(RectilinearPictureCompression::new(matrix, k))?, + ser(RectilinearPictureCompression::new(matrix, bound))?, resolved_variant.clone(), ) } @@ -1900,14 +1898,14 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { // ConsecutiveOnesSubmatrix "ConsecutiveOnesSubmatrix" => { let matrix = parse_bool_matrix(args)?; - let k = args.k.ok_or_else(|| { + let bound = args.bound.ok_or_else(|| { anyhow::anyhow!( - "ConsecutiveOnesSubmatrix requires --matrix and --k\n\n\ - Usage: pred create ConsecutiveOnesSubmatrix --matrix \"1,1,0,1;1,0,1,1;0,1,1,0\" --k 3" + "ConsecutiveOnesSubmatrix requires --matrix and --bound\n\n\ + Usage: pred create ConsecutiveOnesSubmatrix --matrix \"1,1,0,1;1,0,1,1;0,1,1,0\" --bound 3" ) })?; ( - ser(ConsecutiveOnesSubmatrix::new(matrix, k))?, + ser(ConsecutiveOnesSubmatrix::new(matrix, bound))?, resolved_variant.clone(), ) } diff --git a/problemreductions-cli/tests/cli_tests.rs b/problemreductions-cli/tests/cli_tests.rs index 813bf0dea..82e902a3f 100644 --- a/problemreductions-cli/tests/cli_tests.rs +++ b/problemreductions-cli/tests/cli_tests.rs @@ -389,7 +389,7 @@ fn test_evaluate_consecutive_block_minimization_rejects_inconsistent_dimensions( "matrix": [[true]], "num_rows": 1, "num_cols": 2, - "bound_k": 1 + "bound": 1 } }"#; let tmp = std::env::temp_dir().join("pred_test_eval_cbm_invalid_dims.json"); @@ -1160,7 +1160,7 @@ fn test_inspect_rectilinear_picture_compression_lists_bruteforce_only() { "RectilinearPictureCompression", "--matrix", "1,1;1,1", - "--k", + "--bound", "1", ]) .output() @@ -1566,12 +1566,11 @@ fn test_create_minimum_cardinality_key_problem_help_uses_supported_flags() { let stderr = String::from_utf8_lossy(&output.stderr); assert!(stderr.contains("--num-attributes"), "stderr: {stderr}"); assert!(stderr.contains("--dependencies"), "stderr: {stderr}"); - assert!(stderr.contains("--k"), "stderr: {stderr}"); + assert!(stderr.contains("--bound"), "stderr: {stderr}"); assert!( stderr.contains("semicolon-separated dependencies"), "stderr: {stderr}" ); - assert!(!stderr.contains("--bound"), "stderr: {stderr}"); } #[test] @@ -1584,7 +1583,7 @@ fn test_create_minimum_cardinality_key_allows_empty_lhs_dependency() { "1", "--dependencies", ">0", - "--k", + "--bound", "1", ]) .output() @@ -1599,7 +1598,7 @@ fn test_create_minimum_cardinality_key_allows_empty_lhs_dependency() { let json: serde_json::Value = serde_json::from_str(&stdout).unwrap(); assert_eq!(json["type"], "MinimumCardinalityKey"); assert_eq!(json["data"]["num_attributes"], 1); - assert_eq!(json["data"]["bound_k"], 1); + assert_eq!(json["data"]["bound"], 1); assert_eq!(json["data"]["dependencies"][0][0], serde_json::json!([])); assert_eq!(json["data"]["dependencies"][0][1], serde_json::json!([0])); } @@ -1612,7 +1611,7 @@ fn test_create_minimum_cardinality_key_missing_num_attributes_message() { "MinimumCardinalityKey", "--dependencies", "0>0", - "--k", + "--bound", "1", ]) .output() @@ -3124,7 +3123,7 @@ fn test_create_set_basis_no_flags_uses_actual_cli_flag_names() { } #[test] -fn test_create_rectilinear_picture_compression_help_uses_k_flag() { +fn test_create_rectilinear_picture_compression_help_uses_bound_flag() { let output = pred() .args(["create", "RectilinearPictureCompression"]) .output() @@ -3136,12 +3135,8 @@ fn test_create_rectilinear_picture_compression_help_uses_k_flag() { "expected '--matrix' in help output, got: {stderr}" ); assert!( - stderr.contains("--k"), - "expected '--k' in help output, got: {stderr}" - ); - assert!( - !stderr.contains("--bound"), - "help should advertise the actual CLI flag name, got: {stderr}" + stderr.contains("--bound"), + "expected '--bound' in help output, got: {stderr}" ); } @@ -3153,7 +3148,7 @@ fn test_create_rectilinear_picture_compression_rejects_ragged_matrix() { "RectilinearPictureCompression", "--matrix", "1,0;1", - "--k", + "--bound", "1", ]) .output() @@ -3224,12 +3219,8 @@ fn test_create_consecutive_ones_submatrix_no_flags_uses_actual_cli_help() { "expected '--matrix' in help output, got: {stderr}" ); assert!( - stderr.contains("--k"), - "expected '--k' in help output, got: {stderr}" - ); - assert!( - !stderr.contains("--bound"), - "help should not advertise schema field names: {stderr}" + stderr.contains("--bound"), + "expected '--bound' in help output, got: {stderr}" ); assert!( stderr.contains("semicolon-separated 0/1 rows: \"1,0;0,1\""), @@ -3330,7 +3321,7 @@ fn test_create_consecutive_ones_submatrix_succeeds() { "ConsecutiveOnesSubmatrix", "--matrix", "1,1,0,1;1,0,1,1;0,1,1,0", - "--k", + "--bound", "3", ]) .output() @@ -3343,7 +3334,7 @@ fn test_create_consecutive_ones_submatrix_succeeds() { let stdout = String::from_utf8(output.stdout).unwrap(); let json: serde_json::Value = serde_json::from_str(&stdout).unwrap(); assert_eq!(json["type"], "ConsecutiveOnesSubmatrix"); - assert_eq!(json["data"]["bound_k"], 3); + assert_eq!(json["data"]["bound"], 3); assert_eq!( json["data"]["matrix"][0], serde_json::json!([true, true, false, true]) diff --git a/src/models/algebraic/consecutive_block_minimization.rs b/src/models/algebraic/consecutive_block_minimization.rs index 25daf7727..42e299f05 100644 --- a/src/models/algebraic/consecutive_block_minimization.rs +++ b/src/models/algebraic/consecutive_block_minimization.rs @@ -22,7 +22,7 @@ inventory::submit! { description: "Permute columns of a binary matrix to have at most K consecutive blocks of 1s", fields: &[ FieldInfo { name: "matrix", type_name: "Vec>", description: "Binary matrix A (m x n)" }, - FieldInfo { name: "bound_k", type_name: "i64", description: "Upper bound K on total consecutive blocks" }, + FieldInfo { name: "bound", type_name: "i64", description: "Upper bound K on total consecutive blocks" }, ], } } @@ -67,7 +67,7 @@ pub struct ConsecutiveBlockMinimization { /// Number of columns (n). num_cols: usize, /// Upper bound K on total consecutive blocks. - bound_k: i64, + bound: i64, } impl ConsecutiveBlockMinimization { @@ -75,23 +75,23 @@ impl ConsecutiveBlockMinimization { /// /// # Arguments /// * `matrix` - The m x n binary matrix - /// * `bound_k` - Upper bound on total consecutive blocks + /// * `bound` - Upper bound on total consecutive blocks /// /// # Panics /// Panics if rows have inconsistent lengths. - pub fn new(matrix: Vec>, bound_k: i64) -> Self { - Self::try_new(matrix, bound_k).unwrap_or_else(|err| panic!("{err}")) + pub fn new(matrix: Vec>, bound: i64) -> Self { + Self::try_new(matrix, bound).unwrap_or_else(|err| panic!("{err}")) } /// Create a new ConsecutiveBlockMinimization problem, returning an error /// instead of panicking when the matrix is ragged. - pub fn try_new(matrix: Vec>, bound_k: i64) -> Result { + pub fn try_new(matrix: Vec>, bound: i64) -> Result { let (num_rows, num_cols) = validate_matrix_dimensions(&matrix)?; Ok(Self { matrix, num_rows, num_cols, - bound_k, + bound, }) } @@ -111,8 +111,8 @@ impl ConsecutiveBlockMinimization { } /// Get the upper bound K. - pub fn bound_k(&self) -> i64 { - self.bound_k + pub fn bound(&self) -> i64 { + self.bound } /// Count the total number of maximal consecutive blocks of 1s @@ -164,7 +164,7 @@ impl Problem for ConsecutiveBlockMinimization { fn evaluate(&self, config: &[usize]) -> bool { match self.count_consecutive_blocks(config) { - Some(total) => (total as i64) <= self.bound_k, + Some(total) => (total as i64) <= self.bound, None => false, } } @@ -189,14 +189,14 @@ struct ConsecutiveBlockMinimizationDef { matrix: Vec>, num_rows: usize, num_cols: usize, - bound_k: i64, + bound: i64, } impl TryFrom for ConsecutiveBlockMinimization { type Error = String; fn try_from(value: ConsecutiveBlockMinimizationDef) -> Result { - let problem = Self::try_new(value.matrix, value.bound_k)?; + let problem = Self::try_new(value.matrix, value.bound)?; if value.num_rows != problem.num_rows { return Err(format!( "num_rows must match matrix row count ({})", diff --git a/src/models/algebraic/consecutive_ones_submatrix.rs b/src/models/algebraic/consecutive_ones_submatrix.rs index 280db3714..03b0feb91 100644 --- a/src/models/algebraic/consecutive_ones_submatrix.rs +++ b/src/models/algebraic/consecutive_ones_submatrix.rs @@ -20,7 +20,7 @@ inventory::submit! { description: "Find K columns of a binary matrix that can be permuted to have the consecutive ones property", fields: &[ FieldInfo { name: "matrix", type_name: "Vec>", description: "m×n binary matrix A" }, - FieldInfo { name: "bound_k", type_name: "usize", description: "Required number of columns K" }, + FieldInfo { name: "bound", type_name: "i64", description: "Required number of columns K" }, ], } } @@ -60,7 +60,7 @@ inventory::submit! { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ConsecutiveOnesSubmatrix { matrix: Vec>, - bound_k: usize, + bound: i64, } impl ConsecutiveOnesSubmatrix { @@ -68,8 +68,8 @@ impl ConsecutiveOnesSubmatrix { /// /// # Panics /// - /// Panics if `bound_k > n`, or if rows have inconsistent lengths. - pub fn new(matrix: Vec>, bound_k: usize) -> Self { + /// Panics if `bound > n`, or if rows have inconsistent lengths. + pub fn new(matrix: Vec>, bound: i64) -> Self { let n = if matrix.is_empty() { 0 } else { @@ -79,10 +79,10 @@ impl ConsecutiveOnesSubmatrix { assert_eq!(row.len(), n, "All rows must have the same length"); } assert!( - bound_k <= n, - "bound_k ({bound_k}) must be <= number of columns ({n})" + bound <= n as i64, + "bound ({bound}) must be <= number of columns ({n})" ); - Self { matrix, bound_k } + Self { matrix, bound } } /// Returns the binary matrix. @@ -90,9 +90,9 @@ impl ConsecutiveOnesSubmatrix { &self.matrix } - /// Returns K (the required number of columns). - pub fn bound_k(&self) -> usize { - self.bound_k + /// Returns the bound (the required number of columns). + pub fn bound(&self) -> i64 { + self.bound } /// Returns the number of rows (m). @@ -197,7 +197,7 @@ impl Problem for ConsecutiveOnesSubmatrix { .filter(|(_, &v)| v == 1) .map(|(i, _)| i) .collect(); - if selected.len() != self.bound_k { + if (selected.len() as i64) != self.bound { return false; } self.any_permutation_has_c1p(&selected) diff --git a/src/models/misc/rectilinear_picture_compression.rs b/src/models/misc/rectilinear_picture_compression.rs index 49ea45534..01cb2c67c 100644 --- a/src/models/misc/rectilinear_picture_compression.rs +++ b/src/models/misc/rectilinear_picture_compression.rs @@ -23,7 +23,7 @@ inventory::submit! { description: "Cover all 1-entries of a binary matrix with at most K axis-aligned all-1 rectangles", fields: &[ FieldInfo { name: "matrix", type_name: "Vec>", description: "m x n binary matrix" }, - FieldInfo { name: "bound_k", type_name: "usize", description: "Maximum number of rectangles allowed" }, + FieldInfo { name: "bound", type_name: "i64", description: "Maximum number of rectangles allowed" }, ], } } @@ -61,7 +61,7 @@ inventory::submit! { #[derive(Debug, Clone, Serialize)] pub struct RectilinearPictureCompression { matrix: Vec>, - bound_k: usize, + bound: i64, #[serde(skip)] maximal_rects: Vec, } @@ -74,10 +74,10 @@ impl<'de> Deserialize<'de> for RectilinearPictureCompression { #[derive(Deserialize)] struct Inner { matrix: Vec>, - bound_k: usize, + bound: i64, } let inner = Inner::deserialize(deserializer)?; - Ok(Self::new(inner.matrix, inner.bound_k)) + Ok(Self::new(inner.matrix, inner.bound)) } } @@ -87,7 +87,7 @@ impl RectilinearPictureCompression { /// # Panics /// /// Panics if `matrix` is empty or has inconsistent row lengths. - pub fn new(matrix: Vec>, bound_k: usize) -> Self { + pub fn new(matrix: Vec>, bound: i64) -> Self { assert!(!matrix.is_empty(), "Matrix must not be empty"); let cols = matrix[0].len(); assert!(cols > 0, "Matrix must have at least one column"); @@ -97,7 +97,7 @@ impl RectilinearPictureCompression { ); let mut instance = Self { matrix, - bound_k, + bound, maximal_rects: Vec::new(), }; instance.maximal_rects = instance.compute_maximal_rectangles(); @@ -115,8 +115,8 @@ impl RectilinearPictureCompression { } /// Returns the bound K. - pub fn bound_k(&self) -> usize { - self.bound_k + pub fn bound(&self) -> i64 { + self.bound } /// Returns a reference to the binary matrix. @@ -255,7 +255,7 @@ impl Problem for RectilinearPictureCompression { // Count selected rectangles. let selected_count: usize = config.iter().sum(); - if selected_count > self.bound_k { + if (selected_count as i64) > self.bound { return false; } diff --git a/src/models/set/minimum_cardinality_key.rs b/src/models/set/minimum_cardinality_key.rs index 2ce502791..c43bc2ff2 100644 --- a/src/models/set/minimum_cardinality_key.rs +++ b/src/models/set/minimum_cardinality_key.rs @@ -18,7 +18,7 @@ inventory::submit! { fields: &[ FieldInfo { name: "num_attributes", type_name: "usize", description: "Number of attributes in the relation" }, FieldInfo { name: "dependencies", type_name: "Vec<(Vec, Vec)>", description: "Functional dependencies as (lhs, rhs) pairs" }, - FieldInfo { name: "bound_k", type_name: "usize", description: "Upper bound on key cardinality" }, + FieldInfo { name: "bound", type_name: "i64", description: "Upper bound on key cardinality" }, ], } } @@ -37,7 +37,7 @@ pub struct MinimumCardinalityKey { /// Functional dependencies as `(lhs, rhs)` pairs. dependencies: Vec<(Vec, Vec)>, /// Upper bound on key cardinality. - bound_k: usize, + bound: i64, } impl MinimumCardinalityKey { @@ -49,7 +49,7 @@ impl MinimumCardinalityKey { pub fn new( num_attributes: usize, dependencies: Vec<(Vec, Vec)>, - bound_k: usize, + bound: i64, ) -> Self { let mut dependencies = dependencies; for (dep_index, (lhs, rhs)) in dependencies.iter_mut().enumerate() { @@ -71,7 +71,7 @@ impl MinimumCardinalityKey { Self { num_attributes, dependencies, - bound_k, + bound, } } @@ -86,8 +86,8 @@ impl MinimumCardinalityKey { } /// Return the upper bound on key cardinality. - pub fn bound_k(&self) -> usize { - self.bound_k + pub fn bound(&self) -> i64 { + self.bound } /// Return the functional dependencies. @@ -162,7 +162,7 @@ impl Problem for MinimumCardinalityKey { let selected: Vec = config.iter().map(|&v| v == 1).collect(); let count = selected.iter().filter(|&&v| v).count(); - if count > self.bound_k { + if (count as i64) > self.bound { return false; } diff --git a/src/unit_tests/models/algebraic/consecutive_block_minimization.rs b/src/unit_tests/models/algebraic/consecutive_block_minimization.rs index a4c054887..7cb9e5d2f 100644 --- a/src/unit_tests/models/algebraic/consecutive_block_minimization.rs +++ b/src/unit_tests/models/algebraic/consecutive_block_minimization.rs @@ -10,7 +10,7 @@ fn test_consecutive_block_minimization_basic() { ); assert_eq!(problem.num_rows(), 2); assert_eq!(problem.num_cols(), 3); - assert_eq!(problem.bound_k(), 2); + assert_eq!(problem.bound(), 2); assert_eq!(problem.num_variables(), 3); assert_eq!(problem.dims(), vec![3; 3]); } @@ -23,7 +23,7 @@ fn test_consecutive_block_minimization_evaluate() { // Permutation [0, 2, 1] reorders columns to: // [1, 1, 0] -> 1 block // [0, 1, 1] -> 1 block - // Total = 2 blocks, bound_k = 2 => satisfies + // Total = 2 blocks, bound = 2 => satisfies let problem = ConsecutiveBlockMinimization::new( vec![vec![true, false, true], vec![false, true, true]], 2, @@ -33,7 +33,7 @@ fn test_consecutive_block_minimization_evaluate() { // Identity permutation [0, 1, 2]: // [1, 0, 1] -> 2 blocks // [0, 1, 1] -> 1 block - // Total = 3 blocks, bound_k = 2 => does not satisfy + // Total = 3 blocks, bound = 2 => does not satisfy assert!(!problem.evaluate(&[0, 1, 2])); } @@ -86,13 +86,13 @@ fn test_consecutive_block_minimization_serialization() { let deserialized: ConsecutiveBlockMinimization = serde_json::from_str(&json).unwrap(); assert_eq!(deserialized.num_rows(), problem.num_rows()); assert_eq!(deserialized.num_cols(), problem.num_cols()); - assert_eq!(deserialized.bound_k(), problem.bound_k()); + assert_eq!(deserialized.bound(), problem.bound()); assert_eq!(deserialized.matrix(), problem.matrix()); } #[test] fn test_consecutive_block_minimization_deserialization_rejects_inconsistent_dimensions() { - let json = r#"{"matrix":[[true]],"num_rows":1,"num_cols":2,"bound_k":1}"#; + let json = r#"{"matrix":[[true]],"num_rows":1,"num_cols":2,"bound":1}"#; let err = serde_json::from_str::(json).unwrap_err(); assert!(err.to_string().contains("num_cols")); } diff --git a/src/unit_tests/models/algebraic/consecutive_ones_submatrix.rs b/src/unit_tests/models/algebraic/consecutive_ones_submatrix.rs index c40ccf315..d245c3fcb 100644 --- a/src/unit_tests/models/algebraic/consecutive_ones_submatrix.rs +++ b/src/unit_tests/models/algebraic/consecutive_ones_submatrix.rs @@ -16,7 +16,7 @@ fn test_consecutive_ones_submatrix_basic() { let problem = ConsecutiveOnesSubmatrix::new(tucker_matrix(), 3); assert_eq!(problem.num_rows(), 3); assert_eq!(problem.num_cols(), 4); - assert_eq!(problem.bound_k(), 3); + assert_eq!(problem.bound(), 3); assert_eq!(problem.dims(), vec![2; 4]); assert_eq!( ::NAME, @@ -149,13 +149,13 @@ fn test_consecutive_ones_submatrix_serialization() { [true, false, true, true], [false, true, true, false], ], - "bound_k": 3, + "bound": 3, }) ); let restored: ConsecutiveOnesSubmatrix = serde_json::from_value(json).unwrap(); assert_eq!(restored.num_rows(), 3); assert_eq!(restored.num_cols(), 4); - assert_eq!(restored.bound_k(), 3); + assert_eq!(restored.bound(), 3); } #[test] @@ -208,7 +208,7 @@ fn test_consecutive_ones_submatrix_complexity_metadata_matches_evaluator() { } #[test] -#[should_panic(expected = "bound_k")] +#[should_panic(expected = "bound")] fn test_consecutive_ones_submatrix_k_too_large() { let matrix = vec![vec![true, false]]; ConsecutiveOnesSubmatrix::new(matrix, 3); diff --git a/src/unit_tests/models/misc/rectilinear_picture_compression.rs b/src/unit_tests/models/misc/rectilinear_picture_compression.rs index 3b7d38bf6..095bfdee7 100644 --- a/src/unit_tests/models/misc/rectilinear_picture_compression.rs +++ b/src/unit_tests/models/misc/rectilinear_picture_compression.rs @@ -28,7 +28,7 @@ fn test_rectilinear_picture_compression_basic() { let problem = RectilinearPictureCompression::new(two_block_matrix(), 2); assert_eq!(problem.num_rows(), 4); assert_eq!(problem.num_cols(), 4); - assert_eq!(problem.bound_k(), 2); + assert_eq!(problem.bound(), 2); assert_eq!( ::NAME, "RectilinearPictureCompression" @@ -145,13 +145,13 @@ fn test_rectilinear_picture_compression_serialization() { [false, false, true, true], [false, false, true, true], ], - "bound_k": 2, + "bound": 2, }) ); let restored: RectilinearPictureCompression = serde_json::from_value(json).unwrap(); assert_eq!(restored.num_rows(), problem.num_rows()); assert_eq!(restored.num_cols(), problem.num_cols()); - assert_eq!(restored.bound_k(), problem.bound_k()); + assert_eq!(restored.bound(), problem.bound()); assert_eq!(restored.matrix(), problem.matrix()); } diff --git a/src/unit_tests/models/set/minimum_cardinality_key.rs b/src/unit_tests/models/set/minimum_cardinality_key.rs index 36d8841ab..f97a02c00 100644 --- a/src/unit_tests/models/set/minimum_cardinality_key.rs +++ b/src/unit_tests/models/set/minimum_cardinality_key.rs @@ -5,7 +5,7 @@ use std::collections::HashSet; /// Instance 1 from the issue: 6 attributes, FDs {0,1}->{2}, {0,2}->{3}, /// {1,3}->{4}, {2,4}->{5}. K={0,1} is a candidate key of size 2. -fn instance1(bound_k: usize) -> MinimumCardinalityKey { +fn instance1(bound: i64) -> MinimumCardinalityKey { MinimumCardinalityKey::new( 6, vec![ @@ -14,7 +14,7 @@ fn instance1(bound_k: usize) -> MinimumCardinalityKey { (vec![1, 3], vec![4]), (vec![2, 4], vec![5]), ], - bound_k, + bound, ) } @@ -29,7 +29,7 @@ fn test_minimum_cardinality_key_creation() { let problem = instance1(2); assert_eq!(problem.num_attributes(), 6); assert_eq!(problem.num_dependencies(), 4); - assert_eq!(problem.bound_k(), 2); + assert_eq!(problem.bound(), 2); assert_eq!(problem.num_variables(), 6); assert_eq!(problem.dims(), vec![2; 6]); } @@ -61,7 +61,7 @@ fn test_minimum_cardinality_key_non_minimal_rejected() { #[test] fn test_minimum_cardinality_key_exceeds_bound() { let problem = instance1(1); - // K={0,1} has |K|=2 > bound_k=1, so it must be rejected. + // K={0,1} has |K|=2 > bound=1, so it must be rejected. assert!(!problem.evaluate(&[1, 1, 0, 0, 0, 0])); } @@ -85,7 +85,7 @@ fn test_minimum_cardinality_key_serialization() { assert_eq!(deserialized.num_attributes(), problem.num_attributes()); assert_eq!(deserialized.num_dependencies(), problem.num_dependencies()); - assert_eq!(deserialized.bound_k(), problem.bound_k()); + assert_eq!(deserialized.bound(), problem.bound()); assert_eq!(deserialized.dependencies(), problem.dependencies()); } diff --git a/src/unit_tests/prelude.rs b/src/unit_tests/prelude.rs index fd89ef5c6..da240315d 100644 --- a/src/unit_tests/prelude.rs +++ b/src/unit_tests/prelude.rs @@ -3,5 +3,5 @@ use crate::prelude::*; #[test] fn test_prelude_exports_rectilinear_picture_compression() { let problem = RectilinearPictureCompression::new(vec![vec![true]], 1); - assert_eq!(problem.bound_k(), 1); + assert_eq!(problem.bound(), 1); }