From e89f53962730a3a4b6fc2ee59acbc3a52e9d413c Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Tue, 28 Apr 2026 21:47:39 +0800 Subject: [PATCH 1/2] Fix CBM example in README; align CBM wire format with schema The README's `pred create CBM ... | pred solve` example was broken on two fronts: the bound flag is `--bound-k` (not `--bound`), and the schema-driven factory rejected the generated JSON because `ConsecutiveBlockMinimization`'s deserialization required `num_rows`/`num_cols` fields that aren't part of its declared schema (only `matrix` and `bound` are). Drop the derived dimensions from the wire format entirely via `serde(into = ConsecutiveBlockMinimizationDef)` so serialize and deserialize both use the minimal `{matrix, bound}` form that matches `ProblemSchemaEntry`. Replace the now-irrelevant inconsistent-dimensions test with one that pins the minimal wire format and one that exercises ragged-matrix rejection. Co-Authored-By: Claude Opus 4.7 --- README.md | 2 +- .../consecutive_block_minimization.rs | 31 +++++++++---------- .../consecutive_block_minimization.rs | 16 ++++++++-- 3 files changed, 28 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 0efcb2857..31a853038 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ Try it out: pred show CBM # Create and solve a small CBM instance -pred create CBM --matrix '[[true,false,true],[false,true,true]]' --bound 2 \ +pred create CBM --matrix '[[true,false,true],[false,true,true]]' --bound-k 2 \ | pred solve - --solver brute-force ``` diff --git a/src/models/algebraic/consecutive_block_minimization.rs b/src/models/algebraic/consecutive_block_minimization.rs index 53785c485..0a5df44df 100644 --- a/src/models/algebraic/consecutive_block_minimization.rs +++ b/src/models/algebraic/consecutive_block_minimization.rs @@ -58,7 +58,10 @@ inventory::submit! { /// } /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(try_from = "ConsecutiveBlockMinimizationDef")] +#[serde( + try_from = "ConsecutiveBlockMinimizationDef", + into = "ConsecutiveBlockMinimizationDef" +)] pub struct ConsecutiveBlockMinimization { /// The binary matrix A (m x n). matrix: Vec>, @@ -184,11 +187,9 @@ crate::declare_variants! { default ConsecutiveBlockMinimization => "factorial(num_cols) * num_rows * num_cols", } -#[derive(Debug, Clone, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] struct ConsecutiveBlockMinimizationDef { matrix: Vec>, - num_rows: usize, - num_cols: usize, bound: i64, } @@ -196,20 +197,16 @@ 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 - )); + Self::try_new(value.matrix, value.bound) + } +} + +impl From for ConsecutiveBlockMinimizationDef { + fn from(value: ConsecutiveBlockMinimization) -> Self { + Self { + matrix: value.matrix, + bound: value.bound, } - Ok(problem) } } diff --git a/src/unit_tests/models/algebraic/consecutive_block_minimization.rs b/src/unit_tests/models/algebraic/consecutive_block_minimization.rs index 60fbba417..7d66b6459 100644 --- a/src/unit_tests/models/algebraic/consecutive_block_minimization.rs +++ b/src/unit_tests/models/algebraic/consecutive_block_minimization.rs @@ -91,10 +91,20 @@ fn test_consecutive_block_minimization_serialization() { } #[test] -fn test_consecutive_block_minimization_deserialization_rejects_inconsistent_dimensions() { - let json = r#"{"matrix":[[true]],"num_rows":1,"num_cols":2,"bound":1}"#; +fn test_consecutive_block_minimization_serialization_omits_derived_fields() { + let problem = ConsecutiveBlockMinimization::new(vec![vec![true, false], vec![false, true]], 2); + let value: serde_json::Value = serde_json::to_value(&problem).unwrap(); + let obj = value.as_object().unwrap(); + assert_eq!(obj.len(), 2); + assert!(obj.contains_key("matrix")); + assert!(obj.contains_key("bound")); +} + +#[test] +fn test_consecutive_block_minimization_deserialization_rejects_ragged_matrix() { + let json = r#"{"matrix":[[true,false],[true]],"bound":1}"#; let err = serde_json::from_str::(json).unwrap_err(); - assert!(err.to_string().contains("num_cols")); + assert!(err.to_string().contains("same length")); } #[test] From ea36521e500408dac2416d0a78f36489c0035e19 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Tue, 28 Apr 2026 21:54:57 +0800 Subject: [PATCH 2/2] Switch README example to MIS; revert CBM source changes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous commit on this branch fixed a CBM serde inconsistency to make the original `pred create CBM ... --bound-k 2` example run end-to-end. Use MaximumIndependentSet instead — it works against `main` as-is, and a small 4-vertex path-graph example is more recognizable to readers than CBM's 2x3 boolean matrix. This reverts the CBM model + test changes so this PR is purely a README fix; the CBM serde alignment can land separately. Verified: `pred create MIS --graph 0-1,1-2,2-3 --weights 1,1,1,1 | pred solve - --solver brute-force` returns Max(2) with witness [0,1,0,1]. Co-Authored-By: Claude Opus 4.7 --- README.md | 8 ++--- .../consecutive_block_minimization.rs | 31 ++++++++++--------- .../consecutive_block_minimization.rs | 16 ++-------- 3 files changed, 24 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index 31a853038..c660a3bd9 100644 --- a/README.md +++ b/README.md @@ -32,11 +32,11 @@ make cli # builds target/release/pred Try it out: ```bash -# Show the Consecutive Block Minimization model (alias: CBM) -pred show CBM +# Show the Maximum Independent Set model (alias: MIS) +pred show MIS -# Create and solve a small CBM instance -pred create CBM --matrix '[[true,false,true],[false,true,true]]' --bound-k 2 \ +# Create and solve MIS on the path graph 0-1-2-3 +pred create MIS --graph 0-1,1-2,2-3 --weights 1,1,1,1 \ | pred solve - --solver brute-force ``` diff --git a/src/models/algebraic/consecutive_block_minimization.rs b/src/models/algebraic/consecutive_block_minimization.rs index 0a5df44df..53785c485 100644 --- a/src/models/algebraic/consecutive_block_minimization.rs +++ b/src/models/algebraic/consecutive_block_minimization.rs @@ -58,10 +58,7 @@ inventory::submit! { /// } /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] -#[serde( - try_from = "ConsecutiveBlockMinimizationDef", - into = "ConsecutiveBlockMinimizationDef" -)] +#[serde(try_from = "ConsecutiveBlockMinimizationDef")] pub struct ConsecutiveBlockMinimization { /// The binary matrix A (m x n). matrix: Vec>, @@ -187,9 +184,11 @@ crate::declare_variants! { default ConsecutiveBlockMinimization => "factorial(num_cols) * num_rows * num_cols", } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Deserialize)] struct ConsecutiveBlockMinimizationDef { matrix: Vec>, + num_rows: usize, + num_cols: usize, bound: i64, } @@ -197,16 +196,20 @@ impl TryFrom for ConsecutiveBlockMinimization { type Error = String; fn try_from(value: ConsecutiveBlockMinimizationDef) -> Result { - Self::try_new(value.matrix, value.bound) - } -} - -impl From for ConsecutiveBlockMinimizationDef { - fn from(value: ConsecutiveBlockMinimization) -> Self { - Self { - matrix: value.matrix, - bound: value.bound, + 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) } } diff --git a/src/unit_tests/models/algebraic/consecutive_block_minimization.rs b/src/unit_tests/models/algebraic/consecutive_block_minimization.rs index 7d66b6459..60fbba417 100644 --- a/src/unit_tests/models/algebraic/consecutive_block_minimization.rs +++ b/src/unit_tests/models/algebraic/consecutive_block_minimization.rs @@ -91,20 +91,10 @@ fn test_consecutive_block_minimization_serialization() { } #[test] -fn test_consecutive_block_minimization_serialization_omits_derived_fields() { - let problem = ConsecutiveBlockMinimization::new(vec![vec![true, false], vec![false, true]], 2); - let value: serde_json::Value = serde_json::to_value(&problem).unwrap(); - let obj = value.as_object().unwrap(); - assert_eq!(obj.len(), 2); - assert!(obj.contains_key("matrix")); - assert!(obj.contains_key("bound")); -} - -#[test] -fn test_consecutive_block_minimization_deserialization_rejects_ragged_matrix() { - let json = r#"{"matrix":[[true,false],[true]],"bound":1}"#; +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("same length")); + assert!(err.to_string().contains("num_cols")); } #[test]