Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 105 additions & 0 deletions docs/paper/reductions.typ
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@
"ConjunctiveBooleanQuery": [Conjunctive Boolean Query],
"ConsecutiveBlockMinimization": [Consecutive Block Minimization],
"ConsecutiveOnesSubmatrix": [Consecutive Ones Submatrix],
"SparseMatrixCompression": [Sparse Matrix Compression],
"DirectedTwoCommodityIntegralFlow": [Directed Two-Commodity Integral Flow],
"IntegralFlowHomologousArcs": [Integral Flow with Homologous Arcs],
"IntegralFlowWithMultipliers": [Integral Flow With Multipliers],
Expand Down Expand Up @@ -6117,6 +6118,110 @@ A classical NP-complete problem from Garey and Johnson @garey1979[Ch.~3, p.~76],
]
}

#{
let x = load-model-example("SparseMatrixCompression")
let A = x.instance.matrix
let m = A.len()
let n = if m > 0 { A.at(0).len() } else { 0 }
let K = x.instance.bound_k
let cfg = x.optimal_config
let shifts = cfg.map(v => v + 1)
let storage = (4, 1, 2, 3, 1, 0)
let A-int = A.map(row => row.map(v => if v { 1 } else { 0 }))
let row-colors = (
graph-colors.at(0),
rgb("#f28e2b"),
rgb("#76b7b2"),
rgb("#e15759"),
)
[
#problem-def("SparseMatrixCompression")[
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$.
][
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.]

*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.

#pred-commands(
"pred create --example " + problem-spec(x) + " -o sparse-matrix-compression.json",
"pred solve sparse-matrix-compression.json",
"pred evaluate sparse-matrix-compression.json --config " + x.optimal_config.map(str).join(","),
)

#figure(
canvas(length: 0.7cm, {
import draw: *
let cell-size = 0.9
let gap = 0.08
let storage-x = 6.2

for i in range(m) {
for j in range(n) {
let val = A-int.at(i).at(j)
let fill = if val == 1 {
row-colors.at(i).transparentize(30%)
} else {
white
}
rect(
(j * cell-size, -i * cell-size),
(j * cell-size + cell-size - gap, -i * cell-size - cell-size + gap),
fill: fill,
stroke: 0.3pt + luma(180),
)
content(
(j * cell-size + (cell-size - gap) / 2, -i * cell-size - (cell-size - gap) / 2),
text(8pt, str(val)),
)
}
content(
(-0.55, -i * cell-size - (cell-size - gap) / 2),
text(7pt)[$r_#(i + 1)$],
)
content(
(4.6, -i * cell-size - (cell-size - gap) / 2),
text(7pt)[$s_#(i + 1) = #shifts.at(i)$],
)
}

for j in range(n) {
content(
(j * cell-size + (cell-size - gap) / 2, 0.45),
text(7pt)[$c_#(j + 1)$],
)
}

content((5.45, -1.35), text(8pt, weight: "bold")[overlay])

for j in range(storage.len()) {
let label = storage.at(j)
let fill = if label == 0 {
white
} else {
row-colors.at(label - 1).transparentize(30%)
}
rect(
(storage-x + j * cell-size, -1.5 * cell-size),
(storage-x + j * cell-size + cell-size - gap, -2.5 * cell-size + gap),
fill: fill,
stroke: 0.3pt + luma(180),
)
content(
(storage-x + j * cell-size + (cell-size - gap) / 2, -2.0 * cell-size + gap / 2),
text(8pt, str(label)),
)
content(
(storage-x + j * cell-size + (cell-size - gap) / 2, -0.8 * cell-size),
text(7pt)[$b_#(j + 1)$],
)
}
}),
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)$.],
) <fig:sparse-matrix-compression>
]
]
}

// Completeness check: warn about problem types in JSON but missing from paper
#{
let json-models = {
Expand Down
1 change: 1 addition & 0 deletions problemreductions-cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,7 @@ Flags by problem type:
BMF --matrix (0/1), --rank
ConsecutiveBlockMinimization --matrix (JSON 2D bool), --bound-k
ConsecutiveOnesSubmatrix --matrix (0/1), --k
SparseMatrixCompression --matrix (0/1), --bound
SteinerTree --graph, --edge-weights, --terminals
MultipleCopyFileAllocation --graph, --usage, --storage, --bound
AcyclicPartition --arcs [--weights] [--arc-costs] --weight-bound --cost-bound [--num-vertices]
Expand Down
101 changes: 100 additions & 1 deletion problemreductions-cli/src/commands/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ use crate::util;
use anyhow::{bail, Context, Result};
use problemreductions::export::{ModelExample, ProblemRef, ProblemSide, RuleExample};
use problemreductions::models::algebraic::{
ClosestVectorProblem, ConsecutiveBlockMinimization, ConsecutiveOnesSubmatrix, BMF,
ClosestVectorProblem, ConsecutiveBlockMinimization, ConsecutiveOnesSubmatrix,
SparseMatrixCompression, BMF,
};
use problemreductions::models::formula::Quantifier;
use problemreductions::models::graph::{
Expand Down Expand Up @@ -688,6 +689,9 @@ fn example_for(canonical: &str, graph_type: Option<&str>) -> &'static str {
"ConsecutiveBlockMinimization" => {
"--matrix '[[true,false,true],[false,true,true]]' --bound 2"
}
"SparseMatrixCompression" => {
"--matrix \"1,0,0,1;0,1,0,0;0,0,1,0;1,0,0,0\" --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\""
}
Expand Down Expand Up @@ -732,6 +736,7 @@ fn help_flag_name(canonical: &str, field_name: &str) -> String {
("PrimeAttributeName", "query_attribute") => return "query".to_string(),
("MixedChinesePostman", "arc_weights") => return "arc-costs".to_string(),
("ConsecutiveOnesSubmatrix", "bound") => return "bound".to_string(),
("SparseMatrixCompression", "bound_k") => return "bound".to_string(),
("StackerCrane", "edges") => return "graph".to_string(),
("StackerCrane", "arc_lengths") => return "arc-costs".to_string(),
("StackerCrane", "edge_lengths") => return "edge-lengths".to_string(),
Expand Down Expand Up @@ -816,6 +821,7 @@ fn help_flag_hint(
"semicolon-separated arc-index paths: \"0,2,5,8;1,4,7,9\""
}
("ConsecutiveOnesSubmatrix", "matrix") => "semicolon-separated 0/1 rows: \"1,0;0,1\"",
("SparseMatrixCompression", "matrix") => "semicolon-separated 0/1 rows: \"1,0;0,1\"",
("TimetableDesign", "craftsman_avail") | ("TimetableDesign", "task_avail") => {
"semicolon-separated 0/1 rows: \"1,1,0;0,1,1\""
}
Expand Down Expand Up @@ -2782,6 +2788,23 @@ pub fn create(args: &CreateArgs, out: &OutputConfig) -> Result<()> {
)
}

// SparseMatrixCompression
"SparseMatrixCompression" => {
let matrix = parse_bool_matrix(args)?;
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";
let bound = args.bound.ok_or_else(|| {
anyhow::anyhow!("SparseMatrixCompression requires --matrix and --bound\n\n{usage}")
})?;
let bound = parse_nonnegative_usize_bound(bound, "SparseMatrixCompression", usage)?;
if bound == 0 {
anyhow::bail!("SparseMatrixCompression requires bound >= 1\n\n{usage}");
}
(
ser(SparseMatrixCompression::new(matrix, bound))?,
resolved_variant.clone(),
)
}

// LongestCommonSubsequence
"LongestCommonSubsequence" => {
let usage =
Expand Down Expand Up @@ -7803,4 +7826,80 @@ mod tests {
"unexpected error: {err}"
);
}

#[test]
fn test_create_sparse_matrix_compression_json() {
use crate::dispatch::ProblemJsonOutput;

let mut args = empty_args();
args.problem = Some("SparseMatrixCompression".to_string());
args.matrix = Some("1,0,0,1;0,1,0,0;0,0,1,0;1,0,0,0".to_string());
args.bound = Some(2);

let output_path =
std::env::temp_dir().join(format!("smc-create-{}.json", std::process::id()));
let out = OutputConfig {
output: Some(output_path.clone()),
quiet: true,
json: false,
auto_json: false,
};

create(&args, &out).unwrap();

let json = std::fs::read_to_string(&output_path).unwrap();
let created: ProblemJsonOutput = serde_json::from_str(&json).unwrap();
assert_eq!(created.problem_type, "SparseMatrixCompression");
assert!(created.variant.is_empty());
assert_eq!(
created.data,
serde_json::json!({
"matrix": [
[true, false, false, true],
[false, true, false, false],
[false, false, true, false],
[true, false, false, false],
],
"bound_k": 2,
})
);

let _ = std::fs::remove_file(output_path);
}

#[test]
fn test_create_sparse_matrix_compression_requires_bound() {
let mut args = empty_args();
args.problem = Some("SparseMatrixCompression".to_string());
args.matrix = Some("1,0,0,1;0,1,0,0;0,0,1,0;1,0,0,0".to_string());

let out = OutputConfig {
output: None,
quiet: true,
json: false,
auto_json: false,
};

let err = create(&args, &out).unwrap_err().to_string();
assert!(err.contains("SparseMatrixCompression requires --matrix and --bound"));
assert!(err.contains("Usage: pred create SparseMatrixCompression"));
}

#[test]
fn test_create_sparse_matrix_compression_rejects_zero_bound() {
let mut args = empty_args();
args.problem = Some("SparseMatrixCompression".to_string());
args.matrix = Some("1,0;0,1".to_string());
args.bound = Some(0);

let out = OutputConfig {
output: None,
quiet: true,
json: false,
auto_json: false,
};

let err = create(&args, &out).unwrap_err().to_string();
assert!(err.contains("bound >= 1"));
}
}
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ pub mod variant;
/// Prelude module for convenient imports.
pub mod prelude {
// Problem types
pub use crate::models::algebraic::{QuadraticAssignment, BMF, QUBO};
pub use crate::models::algebraic::{QuadraticAssignment, SparseMatrixCompression, BMF, QUBO};
pub use crate::models::formula::{
CNFClause, CircuitSAT, KSatisfiability, NAESatisfiability, QuantifiedBooleanFormulas,
Satisfiability,
Expand Down
4 changes: 4 additions & 0 deletions src/models/algebraic/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
//! - [`ConsecutiveBlockMinimization`]: Consecutive Block Minimization
//! - [`ConsecutiveOnesSubmatrix`]: Consecutive Ones Submatrix (column selection with C1P)
//! - [`QuadraticAssignment`]: Quadratic Assignment Problem
//! - [`SparseMatrixCompression`]: Sparse Matrix Compression by row overlay

pub(crate) mod bmf;
pub(crate) mod closest_vector_problem;
Expand All @@ -16,6 +17,7 @@ pub(crate) mod consecutive_ones_submatrix;
pub(crate) mod ilp;
pub(crate) mod quadratic_assignment;
pub(crate) mod qubo;
pub(crate) mod sparse_matrix_compression;

pub use bmf::BMF;
pub use closest_vector_problem::{ClosestVectorProblem, VarBounds};
Expand All @@ -24,6 +26,7 @@ pub use consecutive_ones_submatrix::ConsecutiveOnesSubmatrix;
pub use ilp::{Comparison, LinearConstraint, ObjectiveSense, VariableDomain, ILP};
pub use quadratic_assignment::QuadraticAssignment;
pub use qubo::QUBO;
pub use sparse_matrix_compression::SparseMatrixCompression;

#[cfg(feature = "example-db")]
pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::ModelExampleSpec> {
Expand All @@ -35,5 +38,6 @@ pub(crate) fn canonical_model_example_specs() -> Vec<crate::example_db::specs::M
specs.extend(consecutive_block_minimization::canonical_model_example_specs());
specs.extend(consecutive_ones_submatrix::canonical_model_example_specs());
specs.extend(quadratic_assignment::canonical_model_example_specs());
specs.extend(sparse_matrix_compression::canonical_model_example_specs());
specs
}
Loading
Loading