Skip to content

Commit 792015f

Browse files
GiggleLiuisPANNclaude
authored
Fix #416: [Model] SparseMatrixCompression (#752)
* Add plan for #416: [Model] SparseMatrixCompression * Implement #416: [Model] SparseMatrixCompression * chore: remove plan file after implementation * fix: cargo fmt after merge with main * fix: reject --bound 0 in CLI instead of panicking SparseMatrixCompression::new() asserts bound_k > 0, but the CLI's parse_nonnegative_usize_bound() accepted 0 and passed it through, causing a panic. Add an explicit check before construction. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: remove unreachable second pass in storage_vector() After the first pass succeeds without collisions, the biconditional a_{ij}=1 iff b[slot]=i is already guaranteed: row labels are unique and each row only writes to its 1-entry slots. The second pass was dead code (the codecov-uncovered line). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Xiwei Pan <xiwei.pan@connect.hkust-gz.edu.cn> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 1590ea6 commit 792015f

9 files changed

Lines changed: 507 additions & 3 deletions

File tree

docs/paper/reductions.typ

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@
137137
"ConjunctiveBooleanQuery": [Conjunctive Boolean Query],
138138
"ConsecutiveBlockMinimization": [Consecutive Block Minimization],
139139
"ConsecutiveOnesSubmatrix": [Consecutive Ones Submatrix],
140+
"SparseMatrixCompression": [Sparse Matrix Compression],
140141
"DirectedTwoCommodityIntegralFlow": [Directed Two-Commodity Integral Flow],
141142
"IntegralFlowHomologousArcs": [Integral Flow with Homologous Arcs],
142143
"IntegralFlowWithMultipliers": [Integral Flow With Multipliers],
@@ -6117,6 +6118,110 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76],
61176118
]
61186119
}
61196120

6121+
#{
6122+
let x = load-model-example("SparseMatrixCompression")
6123+
let A = x.instance.matrix
6124+
let m = A.len()
6125+
let n = if m > 0 { A.at(0).len() } else { 0 }
6126+
let K = x.instance.bound_k
6127+
let cfg = x.optimal_config
6128+
let shifts = cfg.map(v => v + 1)
6129+
let storage = (4, 1, 2, 3, 1, 0)
6130+
let A-int = A.map(row => row.map(v => if v { 1 } else { 0 }))
6131+
let row-colors = (
6132+
graph-colors.at(0),
6133+
rgb("#f28e2b"),
6134+
rgb("#76b7b2"),
6135+
rgb("#e15759"),
6136+
)
6137+
[
6138+
#problem-def("SparseMatrixCompression")[
6139+
Given an $m times n$ binary matrix $A$ and a positive integer $K$, determine whether there exist a shift function $s: \{1, dots, m\} -> \{1, dots, K\}$ and a storage vector $b in \{0, 1, dots, m\}^{n + K}$ such that, for every row $i$ and column $j$, $A_(i j) = 1$ if and only if $b_(s(i) + j - 1) = i$.
6140+
][
6141+
Sparse Matrix Compression appears as problem SR13 in Garey and Johnson @garey1979. It models row-overlay compression for sparse lookup tables: rows may share storage positions only when their shifted 1-entries never demand different row labels from the same slot. The implementation in this crate searches over row shifts only, then reconstructs the implied storage vector internally. This yields the direct exact bound $O(K^m dot m dot n)$ for $m$ rows and $n$ columns.#footnote[The storage vector is not enumerated as part of the configuration space. Once the shifts are fixed, every occupied slot is forced by the 1-entries of the shifted rows.]
6142+
6143+
*Example.* Let $A = mat(#A-int.map(row => row.map(v => str(v)).join(", ")).join("; "))$ and $K = #K$. The stored config $(#cfg.map(str).join(", "))$ encodes the one-based shifts $s = (#shifts.map(str).join(", "))$. These shifts place the four row supports at positions $\{2, 5\}$, $\{3\}$, $\{4\}$, and $\{1\}$ respectively, so the supports are pairwise disjoint. The implied overlay vector is therefore $b = (#storage.map(str).join(", "))$, and this is the unique satisfying shift assignment among the $2^4 = 16$ configs in the canonical fixture.
6144+
6145+
#pred-commands(
6146+
"pred create --example " + problem-spec(x) + " -o sparse-matrix-compression.json",
6147+
"pred solve sparse-matrix-compression.json",
6148+
"pred evaluate sparse-matrix-compression.json --config " + x.optimal_config.map(str).join(","),
6149+
)
6150+
6151+
#figure(
6152+
canvas(length: 0.7cm, {
6153+
import draw: *
6154+
let cell-size = 0.9
6155+
let gap = 0.08
6156+
let storage-x = 6.2
6157+
6158+
for i in range(m) {
6159+
for j in range(n) {
6160+
let val = A-int.at(i).at(j)
6161+
let fill = if val == 1 {
6162+
row-colors.at(i).transparentize(30%)
6163+
} else {
6164+
white
6165+
}
6166+
rect(
6167+
(j * cell-size, -i * cell-size),
6168+
(j * cell-size + cell-size - gap, -i * cell-size - cell-size + gap),
6169+
fill: fill,
6170+
stroke: 0.3pt + luma(180),
6171+
)
6172+
content(
6173+
(j * cell-size + (cell-size - gap) / 2, -i * cell-size - (cell-size - gap) / 2),
6174+
text(8pt, str(val)),
6175+
)
6176+
}
6177+
content(
6178+
(-0.55, -i * cell-size - (cell-size - gap) / 2),
6179+
text(7pt)[$r_#(i + 1)$],
6180+
)
6181+
content(
6182+
(4.6, -i * cell-size - (cell-size - gap) / 2),
6183+
text(7pt)[$s_#(i + 1) = #shifts.at(i)$],
6184+
)
6185+
}
6186+
6187+
for j in range(n) {
6188+
content(
6189+
(j * cell-size + (cell-size - gap) / 2, 0.45),
6190+
text(7pt)[$c_#(j + 1)$],
6191+
)
6192+
}
6193+
6194+
content((5.45, -1.35), text(8pt, weight: "bold")[overlay])
6195+
6196+
for j in range(storage.len()) {
6197+
let label = storage.at(j)
6198+
let fill = if label == 0 {
6199+
white
6200+
} else {
6201+
row-colors.at(label - 1).transparentize(30%)
6202+
}
6203+
rect(
6204+
(storage-x + j * cell-size, -1.5 * cell-size),
6205+
(storage-x + j * cell-size + cell-size - gap, -2.5 * cell-size + gap),
6206+
fill: fill,
6207+
stroke: 0.3pt + luma(180),
6208+
)
6209+
content(
6210+
(storage-x + j * cell-size + (cell-size - gap) / 2, -2.0 * cell-size + gap / 2),
6211+
text(8pt, str(label)),
6212+
)
6213+
content(
6214+
(storage-x + j * cell-size + (cell-size - gap) / 2, -0.8 * cell-size),
6215+
text(7pt)[$b_#(j + 1)$],
6216+
)
6217+
}
6218+
}),
6219+
caption: [Canonical Sparse Matrix Compression YES instance. Row-colored 1-entries on the left are shifted into the overlay vector on the right, producing $b = (4, 1, 2, 3, 1, 0)$.],
6220+
) <fig:sparse-matrix-compression>
6221+
]
6222+
]
6223+
}
6224+
61206225
// Completeness check: warn about problem types in JSON but missing from paper
61216226
#{
61226227
let json-models = {

problemreductions-cli/src/cli.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,7 @@ Flags by problem type:
268268
BMF --matrix (0/1), --rank
269269
ConsecutiveBlockMinimization --matrix (JSON 2D bool), --bound-k
270270
ConsecutiveOnesSubmatrix --matrix (0/1), --k
271+
SparseMatrixCompression --matrix (0/1), --bound
271272
SteinerTree --graph, --edge-weights, --terminals
272273
MultipleCopyFileAllocation --graph, --usage, --storage, --bound
273274
AcyclicPartition --arcs [--weights] [--arc-costs] --weight-bound --cost-bound [--num-vertices]

problemreductions-cli/src/commands/create.rs

Lines changed: 100 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ use crate::util;
88
use anyhow::{bail, Context, Result};
99
use problemreductions::export::{ModelExample, ProblemRef, ProblemSide, RuleExample};
1010
use problemreductions::models::algebraic::{
11-
ClosestVectorProblem, ConsecutiveBlockMinimization, ConsecutiveOnesSubmatrix, BMF,
11+
ClosestVectorProblem, ConsecutiveBlockMinimization, ConsecutiveOnesSubmatrix,
12+
SparseMatrixCompression, BMF,
1213
};
1314
use problemreductions::models::formula::Quantifier;
1415
use problemreductions::models::graph::{
@@ -688,6 +689,9 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str {
688689
"ConsecutiveBlockMinimization" => {
689690
"--matrix '[[true,false,true],[false,true,true]]' --bound 2"
690691
}
692+
"SparseMatrixCompression" => {
693+
"--matrix \"1,0,0,1;0,1,0,0;0,0,1,0;1,0,0,0\" --bound 2"
694+
}
691695
"ConjunctiveBooleanQuery" => {
692696
"--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\""
693697
}
@@ -732,6 +736,7 @@ fn help_flag_name(canonical: &str, field_name: &str) -> String {
732736
("PrimeAttributeName", "query_attribute") => return "query".to_string(),
733737
("MixedChinesePostman", "arc_weights") => return "arc-costs".to_string(),
734738
("ConsecutiveOnesSubmatrix", "bound") => return "bound".to_string(),
739+
("SparseMatrixCompression", "bound_k") => return "bound".to_string(),
735740
("StackerCrane", "edges") => return "graph".to_string(),
736741
("StackerCrane", "arc_lengths") => return "arc-costs".to_string(),
737742
("StackerCrane", "edge_lengths") => return "edge-lengths".to_string(),
@@ -816,6 +821,7 @@ fn help_flag_hint(
816821
"semicolon-separated arc-index paths: \"0,2,5,8;1,4,7,9\""
817822
}
818823
("ConsecutiveOnesSubmatrix", "matrix") => "semicolon-separated 0/1 rows: \"1,0;0,1\"",
824+
("SparseMatrixCompression", "matrix") => "semicolon-separated 0/1 rows: \"1,0;0,1\"",
819825
("TimetableDesign", "craftsman_avail") | ("TimetableDesign", "task_avail") => {
820826
"semicolon-separated 0/1 rows: \"1,1,0;0,1,1\""
821827
}
@@ -2782,6 +2788,23 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> {
27822788
)
27832789
}
27842790

2791+
// SparseMatrixCompression
2792+
"SparseMatrixCompression" => {
2793+
let matrix = parse_bool_matrix(args)?;
2794+
let usage = "Usage: pred create SparseMatrixCompression --matrix \"1,0,0,1;0,1,0,0;0,0,1,0;1,0,0,0\" --bound 2";
2795+
let bound = args.bound.ok_or_else(|| {
2796+
anyhow::anyhow!("SparseMatrixCompression requires --matrix and --bound\n\n{usage}")
2797+
})?;
2798+
let bound = parse_nonnegative_usize_bound(bound, "SparseMatrixCompression", usage)?;
2799+
if bound == 0 {
2800+
anyhow::bail!("SparseMatrixCompression requires bound >= 1\n\n{usage}");
2801+
}
2802+
(
2803+
ser(SparseMatrixCompression::new(matrix, bound))?,
2804+
resolved_variant.clone(),
2805+
)
2806+
}
2807+
27852808
// LongestCommonSubsequence
27862809
"LongestCommonSubsequence" => {
27872810
let usage =
@@ -7803,4 +7826,80 @@ mod tests {
78037826
"unexpected error: {err}"
78047827
);
78057828
}
7829+
7830+
#[test]
7831+
fn test_create_sparse_matrix_compression_json() {
7832+
use crate::dispatch::ProblemJsonOutput;
7833+
7834+
let mut args = empty_args();
7835+
args.problem = Some("SparseMatrixCompression".to_string());
7836+
args.matrix = Some("1,0,0,1;0,1,0,0;0,0,1,0;1,0,0,0".to_string());
7837+
args.bound = Some(2);
7838+
7839+
let output_path =
7840+
std::env::temp_dir().join(format!("smc-create-{}.json", std::process::id()));
7841+
let out = OutputConfig {
7842+
output: Some(output_path.clone()),
7843+
quiet: true,
7844+
json: false,
7845+
auto_json: false,
7846+
};
7847+
7848+
create(&args, &out).unwrap();
7849+
7850+
let json = std::fs::read_to_string(&output_path).unwrap();
7851+
let created: ProblemJsonOutput = serde_json::from_str(&json).unwrap();
7852+
assert_eq!(created.problem_type, "SparseMatrixCompression");
7853+
assert!(created.variant.is_empty());
7854+
assert_eq!(
7855+
created.data,
7856+
serde_json::json!({
7857+
"matrix": [
7858+
[true, false, false, true],
7859+
[false, true, false, false],
7860+
[false, false, true, false],
7861+
[true, false, false, false],
7862+
],
7863+
"bound_k": 2,
7864+
})
7865+
);
7866+
7867+
let _ = std::fs::remove_file(output_path);
7868+
}
7869+
7870+
#[test]
7871+
fn test_create_sparse_matrix_compression_requires_bound() {
7872+
let mut args = empty_args();
7873+
args.problem = Some("SparseMatrixCompression".to_string());
7874+
args.matrix = Some("1,0,0,1;0,1,0,0;0,0,1,0;1,0,0,0".to_string());
7875+
7876+
let out = OutputConfig {
7877+
output: None,
7878+
quiet: true,
7879+
json: false,
7880+
auto_json: false,
7881+
};
7882+
7883+
let err = create(&args, &out).unwrap_err().to_string();
7884+
assert!(err.contains("SparseMatrixCompression requires --matrix and --bound"));
7885+
assert!(err.contains("Usage: pred create SparseMatrixCompression"));
7886+
}
7887+
7888+
#[test]
7889+
fn test_create_sparse_matrix_compression_rejects_zero_bound() {
7890+
let mut args = empty_args();
7891+
args.problem = Some("SparseMatrixCompression".to_string());
7892+
args.matrix = Some("1,0;0,1".to_string());
7893+
args.bound = Some(0);
7894+
7895+
let out = OutputConfig {
7896+
output: None,
7897+
quiet: true,
7898+
json: false,
7899+
auto_json: false,
7900+
};
7901+
7902+
let err = create(&args, &out).unwrap_err().to_string();
7903+
assert!(err.contains("bound >= 1"));
7904+
}
78067905
}

src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ pub mod variant;
4242
/// Prelude module for convenient imports.
4343
pub mod prelude {
4444
// Problem types
45-
pub use crate::models::algebraic::{QuadraticAssignment, BMF, QUBO};
45+
pub use crate::models::algebraic::{QuadraticAssignment, SparseMatrixCompression, BMF, QUBO};
4646
pub use crate::models::formula::{
4747
CNFClause, CircuitSAT, KSatisfiability, NAESatisfiability, QuantifiedBooleanFormulas,
4848
Satisfiability,

src/models/algebraic/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
//! - [`ConsecutiveBlockMinimization`]: Consecutive Block Minimization
99
//! - [`ConsecutiveOnesSubmatrix`]: Consecutive Ones Submatrix (column selection with C1P)
1010
//! - [`QuadraticAssignment`]: Quadratic Assignment Problem
11+
//! - [`SparseMatrixCompression`]: Sparse Matrix Compression by row overlay
1112
1213
pub(crate) mod bmf;
1314
pub(crate) mod closest_vector_problem;
@@ -16,6 +17,7 @@ pub(crate) mod consecutive_ones_submatrix;
1617
pub(crate) mod ilp;
1718
pub(crate) mod quadratic_assignment;
1819
pub(crate) mod qubo;
20+
pub(crate) mod sparse_matrix_compression;
1921

2022
pub use bmf::BMF;
2123
pub use closest_vector_problem::{ClosestVectorProblem, VarBounds};
@@ -24,6 +26,7 @@ pub use consecutive_ones_submatrix::ConsecutiveOnesSubmatrix;
2426
pub use ilp::{Comparison, LinearConstraint, ObjectiveSense, VariableDomain, ILP};
2527
pub use quadratic_assignment::QuadraticAssignment;
2628
pub use qubo::QUBO;
29+
pub use sparse_matrix_compression::SparseMatrixCompression;
2730

2831
#[cfg(feature = "example-db")]
2932
pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
@@ -35,5 +38,6 @@ pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::M
3538
specs.extend(consecutive_block_minimization::canonical_model_example_specs());
3639
specs.extend(consecutive_ones_submatrix::canonical_model_example_specs());
3740
specs.extend(quadratic_assignment::canonical_model_example_specs());
41+
specs.extend(sparse_matrix_compression::canonical_model_example_specs());
3842
specs
3943
}

0 commit comments

Comments
 (0)