Skip to content

Commit b4a4e06

Browse files
authored
Merge pull request #1222 from CodingThrust/fix/ilp-nonzeros-bounds
Tighten ILP num_nonzeros bounds to per-constraint counts
2 parents 0daa9a8 + 5cab940 commit b4a4e06

108 files changed

Lines changed: 1015 additions & 923 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎docs/paper/reductions.typ‎

Lines changed: 36 additions & 48 deletions
Large diffs are not rendered by default.

‎problemreductions-cli/src/cli.rs‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -507,17 +507,17 @@ mod tests {
507507
let cli = Cli::try_parse_from([
508508
"pred",
509509
"create",
510-
"3SAT",
510+
"2SAT",
511511
"--num-vars",
512512
"3",
513513
"--clauses",
514-
"1,2,3",
514+
"1,2",
515515
])
516516
.expect("variant alias parses");
517517
let Commands::Create(args) = cli.command else {
518518
panic!("expected create command");
519519
};
520-
assert_eq!(args.problem.as_deref(), Some("KSatisfiability/K3"));
520+
assert_eq!(args.problem.as_deref(), Some("KSatisfiability/K2"));
521521
assert_eq!(args.raw("num-vars"), Some("3"));
522522
}
523523

‎src/models/algebraic/consecutive_ones_submatrix.rs‎

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ inventory::submit! {
2121
description: "Find K columns of a binary matrix that can be permuted to have the consecutive ones property",
2222
fields: &[
2323
FieldInfo { name: "matrix", type_name: "Vec<Vec<bool>>", description: "m×n binary matrix A" },
24-
FieldInfo { name: "bound", type_name: "i64", description: "Required number of columns K" },
24+
FieldInfo { name: "bound", type_name: "usize", description: "Required number of columns K" },
2525
],
2626
}
2727
}
@@ -62,13 +62,13 @@ inventory::submit! {
6262
#[serde(try_from = "ConsecutiveOnesSubmatrixData")]
6363
pub struct ConsecutiveOnesSubmatrix {
6464
matrix: Vec<Vec<bool>>,
65-
bound: i64,
65+
bound: usize,
6666
}
6767

6868
#[derive(Deserialize)]
6969
struct ConsecutiveOnesSubmatrixData {
7070
matrix: Vec<Vec<bool>>,
71-
bound: i64,
71+
bound: usize,
7272
}
7373

7474
impl TryFrom<ConsecutiveOnesSubmatrixData> for ConsecutiveOnesSubmatrix {
@@ -85,19 +85,19 @@ impl ConsecutiveOnesSubmatrix {
8585
/// # Panics
8686
///
8787
/// Panics if `bound > n`, or if rows have inconsistent lengths.
88-
pub fn new(matrix: Vec<Vec<bool>>, bound: i64) -> Self {
88+
pub fn new(matrix: Vec<Vec<bool>>, bound: usize) -> Self {
8989
Self::try_new(matrix, bound).unwrap_or_else(|error| panic!("{error}"))
9090
}
9191

9292
fn try_new(
9393
matrix: Vec<Vec<bool>>,
94-
bound: i64,
94+
bound: usize,
9595
) -> Result<Self, crate::registry::ConstructionError> {
9696
let n = matrix.first().map_or(0, Vec::len);
9797
if matrix.iter().any(|row| row.len() != n) {
9898
return Err("all matrix rows must have the same length".into());
9999
}
100-
if !(bound < 0 || usize::try_from(bound).is_ok_and(|bound| bound <= n)) {
100+
if bound > n {
101101
return Err(format!("bound ({bound}) must be <= number of columns ({n})").into());
102102
}
103103
Ok(Self { matrix, bound })
@@ -109,7 +109,7 @@ impl ConsecutiveOnesSubmatrix {
109109
}
110110

111111
/// Returns the bound (the required number of columns).
112-
pub fn bound(&self) -> i64 {
112+
pub fn bound(&self) -> usize {
113113
self.bound
114114
}
115115

@@ -222,7 +222,7 @@ impl Problem for ConsecutiveOnesSubmatrix {
222222
.filter(|(_, &v)| v)
223223
.map(|(i, _)| i)
224224
.collect();
225-
if usize::try_from(self.bound) != Ok(selected.len()) {
225+
if self.bound != selected.len() {
226226
return Ok(crate::types::Or(false));
227227
}
228228
self.any_permutation_has_c1p(&selected)

‎src/models/formula/ksat.rs‎

Lines changed: 12 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
88
use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension};
99
use crate::traits::Problem;
10-
use crate::variant::{KValue, K2, K3, KN};
10+
use crate::variant::{KValue, K2, K3};
1111
use serde::{de::Error as _, Deserialize, Deserializer, Serialize};
1212

1313
use super::{sat::validate_cnf_literals, CNFClause};
@@ -53,7 +53,7 @@ inventory::submit! {
5353
name: "KSatisfiability",
5454
display_name: "K-Satisfiability",
5555
aliases: &["KSAT"],
56-
dimensions: &[VariantDimension::new("k", "KN", &["KN", "K2", "K3"])],
56+
dimensions: &[VariantDimension::new("k", "K3", &["K3", "K2"])],
5757
category: crate::registry::ProblemCategory::Formula,
5858
module_path: module_path!(),
5959
description: "SAT with exactly k literals per clause",
@@ -124,63 +124,24 @@ impl<K: KValue> KSatisfiability<K> {
124124
/// Create a new K-SAT problem.
125125
///
126126
/// # Panics
127-
/// Panics if any clause does not have exactly K literals (when K is a
128-
/// concrete value like K2, K3). When K is KN (arbitrary), no clause-length
129-
/// validation is performed.
127+
/// Panics if any clause does not have exactly K literals.
130128
pub fn new(num_vars: usize, clauses: Vec<CNFClause>) -> Self {
131129
Self::try_new(num_vars, clauses).unwrap_or_else(|message| panic!("{message}"))
132130
}
133131

134-
/// Create a K-SAT problem after validating its clauses.
132+
/// Create a K-SAT problem after validating that every clause has exactly
133+
/// K literals.
135134
pub fn try_new(
136135
num_vars: usize,
137136
clauses: Vec<CNFClause>,
138137
) -> Result<Self, crate::registry::ConstructionError> {
139138
validate_cnf_literals(num_vars, &clauses)?;
140-
if let Some(k) = K::K {
141-
for (i, clause) in clauses.iter().enumerate() {
142-
if clause.len() != k {
143-
return Err(
144-
format!("Clause {i} has {} literals, expected {k}", clause.len()).into(),
145-
);
146-
}
147-
}
148-
}
149-
Ok(Self {
150-
num_vars,
151-
clauses,
152-
_phantom: std::marker::PhantomData,
153-
})
154-
}
155-
156-
/// Create a new K-SAT problem allowing clauses with fewer than K literals.
157-
///
158-
/// This is useful when the reduction algorithm produces clauses with
159-
/// fewer literals (e.g., when allow_less is true in the Julia implementation).
160-
///
161-
/// # Panics
162-
/// Panics if any clause has more than K literals (when K is a concrete
163-
/// value like K2, K3). When K is KN (arbitrary), no clause-length
164-
/// validation is performed.
165-
pub fn new_allow_less(num_vars: usize, clauses: Vec<CNFClause>) -> Self {
166-
Self::try_new_allow_less(num_vars, clauses).unwrap_or_else(|message| panic!("{message}"))
167-
}
168-
169-
/// Create a K-SAT problem with shorter clauses after validation.
170-
pub fn try_new_allow_less(
171-
num_vars: usize,
172-
clauses: Vec<CNFClause>,
173-
) -> Result<Self, crate::registry::ConstructionError> {
174-
validate_cnf_literals(num_vars, &clauses)?;
175-
if let Some(k) = K::K {
176-
for (i, clause) in clauses.iter().enumerate() {
177-
if clause.len() > k {
178-
return Err(format!(
179-
"Clause {i} has {} literals, expected at most {k}",
180-
clause.len()
181-
)
182-
.into());
183-
}
139+
let k = K::K.ok_or("KSatisfiability requires a fixed clause width K")?;
140+
for (i, clause) in clauses.iter().enumerate() {
141+
if clause.len() != k {
142+
return Err(
143+
format!("Clause {i} has {} literals, expected {k}", clause.len()).into(),
144+
);
184145
}
185146
}
186147
Ok(Self {
@@ -273,13 +234,11 @@ impl<K: KValue> crate::solvers::BruteForceProblem for KSatisfiability<K> {
273234
}
274235

275236
crate::declare_variants! {
276-
default KSatisfiability<KN> => "2^num_vars",
237+
default KSatisfiability<K3> => "1.307^num_vars" aliases ["3SAT"],
277238
KSatisfiability<K2> => "num_vars + num_clauses" aliases ["2SAT"],
278-
KSatisfiability<K3> => "1.307^num_vars" aliases ["3SAT"],
279239
}
280240

281241
crate::register_brute_force! {
282-
KSatisfiability<KN> decode |_, indices: Vec<usize>| crate::config::config_to_bits(&indices),
283242
KSatisfiability<K2> decode |_, indices: Vec<usize>| crate::config::config_to_bits(&indices),
284243
KSatisfiability<K3> decode |_, indices: Vec<usize>| crate::config::config_to_bits(&indices),
285244
}

‎src/rules/biconnectivityaugmentation_ilp.rs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,9 +81,9 @@ impl crate::rules::AggregateReductionResult for ReductionBiconnAugToILP {}
8181
#[reduction(transform = {
8282
exact {
8383
max_constraint_magnitude_bits = "max_numeric_magnitude_bits",
84+
num_vars = "num_potential_edges + 2 * num_vertices * (num_vertices + 1) * (num_edges + num_potential_edges)",
8485
},
8586
upper_bound {
86-
num_vars = "num_potential_edges + 2 * num_vertices * (num_vertices + 1) * (num_edges + num_potential_edges)",
8787
num_constraints = "1 + num_vertices * (num_vertices + 1) * (2 * num_edges + 4 * num_potential_edges + num_vertices)",
8888
num_nonzeros = "num_potential_edges + num_vertices * (num_vertices + 1) * (4 * num_edges + 8 * num_potential_edges)",
8989
},

‎src/rules/binpacking_ilp.rs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ impl ReductionResult for ReductionBPToILP {
5555
},
5656
upper_bound {
5757
max_constraint_magnitude_bits = "max_numeric_magnitude_bits",
58-
num_nonzeros = "(num_items * num_items + num_items) * (2 * num_items)",
58+
num_nonzeros = "2 * num_items * num_items + num_items",
5959
},
6060
})]
6161
impl ReduceTo<ILP<bool>> for BinPacking<i64> {

‎src/rules/closestsubstring_ilp.rs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ fn decode_one_hot(
125125
},
126126
upper_bound {
127127
max_constraint_magnitude_bits = "substring_length + 1",
128-
num_nonzeros = "(alphabet_size * substring_length + total_num_windows + 1) * (substring_length + num_strings + total_num_windows + 1)",
128+
num_nonzeros = "alphabet_size * substring_length + 1 + total_num_windows * (substring_length + 3)",
129129
},
130130
})]
131131
impl ReduceTo<ILP<i64, i64, Bounded>> for ClosestSubstring {

‎src/rules/clustering_ilp.rs‎

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,11 +49,15 @@ impl ReductionResult for ReductionClusteringToILP {
4949
#[crate::aggregate_reduction(ilp_feasibility)]
5050
impl crate::rules::AggregateReductionResult for ReductionClusteringToILP {}
5151

52-
#[reduction(transform = upper_bound {
53-
max_constraint_magnitude_bits = "1",
54-
num_vars = "num_elements * num_clusters",
55-
num_constraints = "num_elements + num_elements * (num_elements - 1) / 2 * num_clusters",
56-
num_nonzeros = "(num_elements * num_clusters) * (num_elements + num_elements * (num_elements - 1) / 2 * num_clusters)",
52+
#[reduction(transform = {
53+
exact {
54+
num_vars = "num_elements * num_clusters",
55+
},
56+
upper_bound {
57+
max_constraint_magnitude_bits = "1",
58+
num_constraints = "num_elements + num_elements * (num_elements - 1) / 2 * num_clusters",
59+
num_nonzeros = "num_elements * num_clusters + num_elements * (num_elements - 1) * num_clusters",
60+
},
5761
})]
5862
impl ReduceTo<ILP<bool>> for Clustering {
5963
type Result = ReductionClusteringToILP;

‎src/rules/consecutiveblockminimization_ilp.rs‎

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,15 @@ impl ReductionResult for ReductionCBMToILP {
4343
impl crate::rules::AggregateReductionResult for ReductionCBMToILP {}
4444

4545
#[reduction(
46-
transform = upper_bound {
47-
max_constraint_magnitude_bits = "num_rows * num_cols + 1",
48-
num_vars = "num_cols * num_cols + num_rows * num_cols + num_rows * num_cols",
49-
num_constraints = "num_cols + num_cols + num_rows * num_cols + num_rows + num_rows * num_cols + 1",
50-
num_nonzeros = "(num_cols * num_cols + num_rows * num_cols + num_rows * num_cols) * (num_cols + num_cols + num_rows * num_cols + num_rows + num_rows * num_cols + 1)",
46+
transform = {
47+
exact {
48+
num_vars = "num_cols * num_cols + num_rows * num_cols + num_rows * num_cols",
49+
},
50+
upper_bound {
51+
max_constraint_magnitude_bits = "num_rows * num_cols + 1",
52+
num_constraints = "num_cols + num_cols + num_rows * num_cols + num_rows + num_rows * num_cols + 1",
53+
num_nonzeros = "2 * num_cols^2 + num_rows * num_cols^2 + 5 * num_rows * num_cols",
54+
},
5155
},
5256
)]
5357
impl ReduceTo<ILP<bool>> for ConsecutiveBlockMinimization {

‎src/rules/consecutiveonessubmatrix_ilp.rs‎

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -46,19 +46,23 @@ impl ReductionResult for ReductionCOSToILP {
4646
#[crate::aggregate_reduction(ilp_feasibility)]
4747
impl crate::rules::AggregateReductionResult for ReductionCOSToILP {}
4848

49-
#[reduction(transform = upper_bound {
50-
max_constraint_magnitude_bits = "bound + num_cols + 1",
51-
num_vars = "num_cols + num_cols * bound + 5 * num_rows * bound",
52-
num_constraints = "2 + num_cols + bound + 3 * num_rows + 8 * num_rows * bound",
53-
num_nonzeros = "(num_cols + num_cols * bound + 5 * num_rows * bound) * (2 + num_cols + bound + 3 * num_rows + 8 * num_rows * bound)",
49+
#[reduction(transform = {
50+
exact {
51+
num_vars = "num_cols + num_cols * bound + 5 * num_rows * bound",
52+
},
53+
upper_bound {
54+
max_constraint_magnitude_bits = "bound + num_cols + 1",
55+
num_constraints = "2 + num_cols + bound + 3 * num_rows + 8 * num_rows * bound",
56+
num_nonzeros = "2 * num_cols + 2 * num_cols * bound + num_rows * num_cols * bound + 2 * num_rows * bound^2 + 21 * num_rows * bound",
57+
},
5458
})]
5559
impl ReduceTo<ILP<bool>> for ConsecutiveOnesSubmatrix {
5660
type Result = ReductionCOSToILP;
5761

5862
fn reduce_to(&self) -> Result<Self::Result, crate::rules::ReductionError> {
5963
let m = self.num_rows();
6064
let n = self.num_cols();
61-
let k = self.bound() as usize;
65+
let k = self.bound();
6266

6367
// Variable layout (all binary):
6468
// s_c: n vars at [0, n) — column selection

0 commit comments

Comments
 (0)