diff --git a/README.md b/README.md index 39482bbdf..256da143b 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 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/paper/reductions.typ b/docs/paper/reductions.typ index 80e4e0f43..9fcc75eb1 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -136,12 +136,13 @@ "SchedulingWithIndividualDeadlines": [Scheduling With Individual Deadlines], "StaffScheduling": [Staff Scheduling], "MinimumTardinessSequencing": [Minimum Tardiness Sequencing], + "ConsecutiveBlockMinimization": [Consecutive Block Minimization], "ConsecutiveOnesSubmatrix": [Consecutive Ones Submatrix], "SequencingToMinimizeMaximumCumulativeCost": [Sequencing to Minimize Maximum Cumulative Cost], "SequencingToMinimizeWeightedTardiness": [Sequencing to Minimize Weighted Tardiness], + "SequencingWithinIntervals": [Sequencing Within Intervals], "SumOfSquaresPartition": [Sum of Squares Partition], "TwoDimensionalConsecutiveSets": [2-Dimensional Consecutive Sets], - "SequencingWithinIntervals": [Sequencing Within Intervals], "DirectedTwoCommodityIntegralFlow": [Directed Two-Commodity Integral Flow], "ConjunctiveBooleanQuery": [Conjunctive Boolean Query], "RectilinearPictureCompression": [Rectilinear Picture Compression], @@ -1891,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)) @@ -2484,6 +2485,39 @@ 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 + let n-rows = mat.len() + let n-cols = if n-rows > 0 { mat.at(0).len() } else { 0 } + let perm = x.optimal_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 @@ -2765,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 })) [ @@ -3462,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)) @@ -3973,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/docs/paper/references.bib b/docs/paper/references.bib index 103aa5614..04538c10c 100644 --- a/docs/paper/references.bib +++ b/docs/paper/references.bib @@ -1034,6 +1034,17 @@ @article{kou1977 doi = {10.1137/0206005} } +@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} +} + @article{lawler1972, author = {Eugene L. Lawler}, title = {A Procedure for Computing the $K$ Best Solutions to Discrete Optimization Problems and Its Application to the Shortest Path Problem}, diff --git a/docs/src/cli.md b/docs/src/cli.md index c07a4e756..38fe48f8e 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 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 @@ -342,6 +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 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 @@ -366,6 +373,10 @@ pred create StrongConnectivityAugmentation --arcs "0>1,1>2,2>0,3>4,4>3,2>3,4>5,5 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`. + For problem-specific create help, run `pred create ` with no additional flags. The generic `pred create --help` output lists all flags across all problem types. diff --git a/problemreductions-cli/src/cli.rs b/problemreductions-cli/src/cli.rs index aeece57f1..f6b13c1ce 100644 --- a/problemreductions-cli/src/cli.rs +++ b/problemreductions-cli/src/cli.rs @@ -250,6 +250,7 @@ Flags by problem type: BalancedCompleteBipartiteSubgraph --left, --right, --biedges, --k BiconnectivityAugmentation --graph, --potential-edges, --budget [--num-vertices] BMF --matrix (0/1), --rank + ConsecutiveBlockMinimization --matrix (JSON 2D bool), --bound-k ConsecutiveOnesSubmatrix --matrix (0/1), --k SteinerTree --graph, --edge-weights, --terminals MultipleCopyFileAllocation --graph, --usage, --storage, --bound @@ -357,7 +358,8 @@ pub struct CreateArgs { /// Number of variables (for SAT/KSAT) #[arg(long)] pub num_vars: Option, - /// Matrix input (semicolon-separated rows; use `pred create ` for problem-specific formats) + /// 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, /// Shared integer parameter (use `pred create ` for the problem-specific meaning) @@ -552,6 +554,7 @@ pub struct CreateArgs { /// Alphabet size for LCS, SCS, StringToStringCorrection, or TwoDimensionalConsecutiveSets (optional; inferred from the input strings if omitted) #[arg(long)] pub alphabet_size: 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 35a8d1659..c71bec5c1 100644 --- a/problemreductions-cli/src/commands/create.rs +++ b/problemreductions-cli/src/commands/create.rs @@ -7,7 +7,9 @@ use crate::problem_name::{ use crate::util; use anyhow::{bail, Context, Result}; use problemreductions::export::{ModelExample, ProblemRef, ProblemSide, RuleExample}; -use problemreductions::models::algebraic::{ClosestVectorProblem, ConsecutiveOnesSubmatrix, BMF}; +use problemreductions::models::algebraic::{ + ClosestVectorProblem, ConsecutiveBlockMinimization, ConsecutiveOnesSubmatrix, BMF, +}; use problemreductions::models::graph::{ GeneralizedHex, GraphPartitioning, HamiltonianCircuit, HamiltonianPath, LengthBoundedDisjointPaths, MinimumCutIntoBoundedSets, MinimumMultiwayCut, @@ -342,7 +344,7 @@ fn type_format_hint(type_name: &str, graph_type: Option<&str>) -> &'static str { } "Vec>" => "semicolon-separated sets: \"0,1;1,2;0,2\"", "Vec" => "semicolon-separated clauses: \"1,2;-1,3\"", - "Vec>" => "semicolon-separated binary rows: \"1,1,0;0,1,1\"", + "Vec>" => "JSON 2D bool array: '[[true,false],[false,true]]'", "Vec>" => "semicolon-separated rows: \"1,0.5;0.5,2\"", "usize" | "W::Sum" => "integer", "u64" => "integer", @@ -431,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" @@ -449,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" @@ -458,13 +460,16 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str { "--alphabet-size 6 --sets \"0,1,2;3,4,5;1,3;2,4;0,5\"" } "ShortestCommonSupersequence" => "--strings \"0,1,2;1,2,0\" --bound 4", - "SequencingToMinimizeMaximumCumulativeCost" => { - "--costs 2,-1,3,-2,1,-3 --precedence-pairs \"0>2,1>2,1>3,2>4,3>5,4>5\" --bound 4" + "ConsecutiveBlockMinimization" => { + "--matrix '[[true,false,true],[false,true,true]]' --bound 2" } - "ConjunctiveQueryFoldability" => "(use --example ConjunctiveQueryFoldability)", "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\"" } + "ConjunctiveQueryFoldability" => "(use --example ConjunctiveQueryFoldability)", + "SequencingToMinimizeMaximumCumulativeCost" => { + "--costs 2,-1,3,-2,1,-3 --precedence-pairs \"0>2,1>2,1>3,2>4,3>5,4>5\" --bound 4" + } "StringToStringCorrection" => { "--source-string \"0,1,2,3,1,0\" --target-string \"0,1,3,2,1\" --bound 2" } @@ -489,12 +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(), - ("ConsecutiveOnesSubmatrix", "bound_k") => return "k".to_string(), + ("ConsecutiveOnesSubmatrix", "bound") => return "bound".to_string(), ("StaffScheduling", "shifts_per_schedule") => return "k".to_string(), _ => {} } @@ -1774,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!( @@ -1853,17 +1857,40 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> { (ser(BMF::new(matrix, rank))?, resolved_variant.clone()) } + // ConsecutiveBlockMinimization + "ConsecutiveBlockMinimization" => { + 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\n\n{usage}" + ) + })?; + 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| { + anyhow::anyhow!( + "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}" + ) + })?; + ( + ser(ConsecutiveBlockMinimization::try_new(matrix, bound) + .map_err(|err| anyhow::anyhow!("{err}\n\n{usage}"))?)?, + resolved_variant.clone(), + ) + } + // 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(), ) } @@ -1871,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 ff72ffb52..82e902a3f 100644 --- a/problemreductions-cli/tests/cli_tests.rs +++ b/problemreductions-cli/tests/cli_tests.rs @@ -381,6 +381,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": 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_evaluate_multiple_choice_branching_rejects_invalid_partition_without_panicking() { let problem_json = r#"{ @@ -410,7 +435,6 @@ fn test_evaluate_multiple_choice_branching_rejects_invalid_partition_without_pan stderr.contains("partition"), "stderr should mention the invalid partition: {stderr}" ); - std::fs::remove_file(&tmp).ok(); } @@ -614,6 +638,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", + "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#"{ @@ -1095,7 +1160,7 @@ fn test_inspect_rectilinear_picture_compression_lists_bruteforce_only() { "RectilinearPictureCompression", "--matrix", "1,1;1,1", - "--k", + "--bound", "1", ]) .output() @@ -1501,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-k"), "stderr: {stderr}"); } #[test] @@ -1519,7 +1583,7 @@ fn test_create_minimum_cardinality_key_allows_empty_lhs_dependency() { "1", "--dependencies", ">0", - "--k", + "--bound", "1", ]) .output() @@ -1534,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])); } @@ -1547,7 +1611,7 @@ fn test_create_minimum_cardinality_key_missing_num_attributes_message() { "MinimumCardinalityKey", "--dependencies", "0>0", - "--k", + "--bound", "1", ]) .output() @@ -2616,7 +2680,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-k"), "stderr: {stderr}"); } #[test] @@ -3060,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() @@ -3072,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-k"), - "help should advertise the actual CLI flag name, got: {stderr}" + stderr.contains("--bound"), + "expected '--bound' in help output, got: {stderr}" ); } @@ -3089,7 +3148,7 @@ fn test_create_rectilinear_picture_compression_rejects_ragged_matrix() { "RectilinearPictureCompression", "--matrix", "1,0;1", - "--k", + "--bound", "1", ]) .output() @@ -3160,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-k"), - "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\""), @@ -3266,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() @@ -3279,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 new file mode 100644 index 000000000..42e299f05 --- /dev/null +++ b/src/models/algebraic/consecutive_block_minimization.rs @@ -0,0 +1,251 @@ +//! 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", type_name: "i64", 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)] +#[serde(try_from = "ConsecutiveBlockMinimizationDef")] +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: i64, +} + +impl ConsecutiveBlockMinimization { + /// Create a new ConsecutiveBlockMinimization problem. + /// + /// # Arguments + /// * `matrix` - The m x n binary matrix + /// * `bound` - Upper bound on total consecutive blocks + /// + /// # Panics + /// Panics if rows have inconsistent lengths. + 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: i64) -> Result { + let (num_rows, num_cols) = validate_matrix_dimensions(&matrix)?; + Ok(Self { + matrix, + num_rows, + num_cols, + bound, + }) + } + + /// 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(&self) -> i64 { + self.bound + } + + /// 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 { + match self.count_consecutive_blocks(config) { + Some(total) => (total as i64) <= self.bound, + 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", +} + +#[derive(Debug, Clone, Deserialize)] +struct ConsecutiveBlockMinimizationDef { + matrix: Vec>, + num_rows: usize, + num_cols: usize, + bound: i64, +} + +impl TryFrom for ConsecutiveBlockMinimization { + type Error = String; + + fn try_from(value: ConsecutiveBlockMinimizationDef) -> Result { + 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 ({})", + 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 { + // 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![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, 4, 1, 3, 5], + optimal_value: serde_json::json!(true), + }] +} + +#[cfg(test)] +#[path = "../../unit_tests/models/algebraic/consecutive_block_minimization.rs"] +mod tests; 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/algebraic/mod.rs b/src/models/algebraic/mod.rs index 3a3c7c6f0..e375ab814 100644 --- a/src/models/algebraic/mod.rs +++ b/src/models/algebraic/mod.rs @@ -5,11 +5,13 @@ //! - [`ILP`]: Integer Linear Programming //! - [`ClosestVectorProblem`]: Closest Vector Problem (minimize lattice distance) //! - [`BMF`]: Boolean Matrix Factorization +//! - [`ConsecutiveBlockMinimization`]: Consecutive Block Minimization //! - [`ConsecutiveOnesSubmatrix`]: Consecutive Ones Submatrix (column selection with C1P) //! - [`QuadraticAssignment`]: Quadratic Assignment Problem pub(crate) mod bmf; pub(crate) mod closest_vector_problem; +pub(crate) mod consecutive_block_minimization; pub(crate) mod consecutive_ones_submatrix; pub(crate) mod ilp; pub(crate) mod quadratic_assignment; @@ -17,6 +19,7 @@ pub(crate) mod qubo; pub use bmf::BMF; pub use closest_vector_problem::{ClosestVectorProblem, VarBounds}; +pub use consecutive_block_minimization::ConsecutiveBlockMinimization; pub use consecutive_ones_submatrix::ConsecutiveOnesSubmatrix; pub use ilp::{Comparison, LinearConstraint, ObjectiveSense, VariableDomain, ILP}; pub use quadratic_assignment::QuadraticAssignment; @@ -29,6 +32,7 @@ pub(crate) fn canonical_model_example_specs() -> 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/mod.rs b/src/models/mod.rs index ac954567d..9017b8e8f 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -10,7 +10,8 @@ pub mod set; // Re-export commonly used types pub use algebraic::{ - ClosestVectorProblem, ConsecutiveOnesSubmatrix, QuadraticAssignment, BMF, ILP, QUBO, + ClosestVectorProblem, ConsecutiveBlockMinimization, ConsecutiveOnesSubmatrix, + QuadraticAssignment, BMF, ILP, QUBO, }; pub use formula::{CNFClause, CircuitSAT, KSatisfiability, Satisfiability}; pub use graph::{ 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/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, diff --git a/src/unit_tests/models/algebraic/consecutive_block_minimization.rs b/src/unit_tests/models/algebraic/consecutive_block_minimization.rs new file mode 100644 index 000000000..7cb9e5d2f --- /dev/null +++ b/src/unit_tests/models/algebraic/consecutive_block_minimization.rs @@ -0,0 +1,107 @@ +use super::*; +use crate::solvers::BruteForce; +use crate::traits::Problem; + +#[test] +fn test_consecutive_block_minimization_basic() { + let problem = ConsecutiveBlockMinimization::new( + vec![vec![true, false, true], vec![false, true, true]], + 2, + ); + assert_eq!(problem.num_rows(), 2); + assert_eq!(problem.num_cols(), 3); + assert_eq!(problem.bound(), 2); + assert_eq!(problem.num_variables(), 3); + assert_eq!(problem.dims(), vec![3; 3]); +} + +#[test] +fn test_consecutive_block_minimization_evaluate() { + // Matrix: + // [1, 0, 1] + // [0, 1, 1] + // Permutation [0, 2, 1] reorders columns to: + // [1, 1, 0] -> 1 block + // [0, 1, 1] -> 1 block + // Total = 2 blocks, bound = 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 = 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 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)); + } +} + +#[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(&[])); + assert!(!problem.evaluate(&[0])); +} + +#[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(), 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":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); + // Not a valid permutation => evaluate returns false + assert!(!problem.evaluate(&[0, 0])); + // Wrong length + assert!(!problem.evaluate(&[0])); +} 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); } diff --git a/src/unit_tests/trait_consistency.rs b/src/unit_tests/trait_consistency.rs index e77004e9f..60032c92c 100644 --- a/src/unit_tests/trait_consistency.rs +++ b/src/unit_tests/trait_consistency.rs @@ -76,6 +76,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",