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
84 changes: 36 additions & 48 deletions docs/paper/reductions.typ

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions problemreductions-cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -507,17 +507,17 @@ mod tests {
let cli = Cli::try_parse_from([
"pred",
"create",
"3SAT",
"2SAT",
"--num-vars",
"3",
"--clauses",
"1,2,3",
"1,2",
])
.expect("variant alias parses");
let Commands::Create(args) = cli.command else {
panic!("expected create command");
};
assert_eq!(args.problem.as_deref(), Some("KSatisfiability/K3"));
assert_eq!(args.problem.as_deref(), Some("KSatisfiability/K2"));
assert_eq!(args.raw("num-vars"), Some("3"));
}

Expand Down
16 changes: 8 additions & 8 deletions src/models/algebraic/consecutive_ones_submatrix.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,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<Vec<bool>>", description: "m×n binary matrix A" },
FieldInfo { name: "bound", type_name: "i64", description: "Required number of columns K" },
FieldInfo { name: "bound", type_name: "usize", description: "Required number of columns K" },
],
}
}
Expand Down Expand Up @@ -62,13 +62,13 @@ inventory::submit! {
#[serde(try_from = "ConsecutiveOnesSubmatrixData")]
pub struct ConsecutiveOnesSubmatrix {
matrix: Vec<Vec<bool>>,
bound: i64,
bound: usize,
}

#[derive(Deserialize)]
struct ConsecutiveOnesSubmatrixData {
matrix: Vec<Vec<bool>>,
bound: i64,
bound: usize,
}

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

fn try_new(
matrix: Vec<Vec<bool>>,
bound: i64,
bound: usize,
) -> Result<Self, crate::registry::ConstructionError> {
let n = matrix.first().map_or(0, Vec::len);
if matrix.iter().any(|row| row.len() != n) {
return Err("all matrix rows must have the same length".into());
}
if !(bound < 0 || usize::try_from(bound).is_ok_and(|bound| bound <= n)) {
if bound > n {
return Err(format!("bound ({bound}) must be <= number of columns ({n})").into());
}
Ok(Self { matrix, bound })
Expand All @@ -109,7 +109,7 @@ impl ConsecutiveOnesSubmatrix {
}

/// Returns the bound (the required number of columns).
pub fn bound(&self) -> i64 {
pub fn bound(&self) -> usize {
self.bound
}

Expand Down Expand Up @@ -222,7 +222,7 @@ impl Problem for ConsecutiveOnesSubmatrix {
.filter(|(_, &v)| v)
.map(|(i, _)| i)
.collect();
if usize::try_from(self.bound) != Ok(selected.len()) {
if self.bound != selected.len() {
return Ok(crate::types::Or(false));
}
self.any_permutation_has_c1p(&selected)
Expand Down
65 changes: 12 additions & 53 deletions src/models/formula/ksat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension};
use crate::traits::Problem;
use crate::variant::{KValue, K2, K3, KN};
use crate::variant::{KValue, K2, K3};
use serde::{de::Error as _, Deserialize, Deserializer, Serialize};

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

/// Create a K-SAT problem after validating its clauses.
/// Create a K-SAT problem after validating that every clause has exactly
/// K literals.
pub fn try_new(
num_vars: usize,
clauses: Vec<CNFClause>,
) -> Result<Self, crate::registry::ConstructionError> {
validate_cnf_literals(num_vars, &clauses)?;
if let Some(k) = K::K {
for (i, clause) in clauses.iter().enumerate() {
if clause.len() != k {
return Err(
format!("Clause {i} has {} literals, expected {k}", clause.len()).into(),
);
}
}
}
Ok(Self {
num_vars,
clauses,
_phantom: std::marker::PhantomData,
})
}

/// Create a new K-SAT problem allowing clauses with fewer than K literals.
///
/// This is useful when the reduction algorithm produces clauses with
/// fewer literals (e.g., when allow_less is true in the Julia implementation).
///
/// # Panics
/// Panics if any clause has more than K literals (when K is a concrete
/// value like K2, K3). When K is KN (arbitrary), no clause-length
/// validation is performed.
pub fn new_allow_less(num_vars: usize, clauses: Vec<CNFClause>) -> Self {
Self::try_new_allow_less(num_vars, clauses).unwrap_or_else(|message| panic!("{message}"))
}

/// Create a K-SAT problem with shorter clauses after validation.
pub fn try_new_allow_less(
num_vars: usize,
clauses: Vec<CNFClause>,
) -> Result<Self, crate::registry::ConstructionError> {
validate_cnf_literals(num_vars, &clauses)?;
if let Some(k) = K::K {
for (i, clause) in clauses.iter().enumerate() {
if clause.len() > k {
return Err(format!(
"Clause {i} has {} literals, expected at most {k}",
clause.len()
)
.into());
}
let k = K::K.ok_or("KSatisfiability requires a fixed clause width K")?;
for (i, clause) in clauses.iter().enumerate() {
if clause.len() != k {
return Err(
format!("Clause {i} has {} literals, expected {k}", clause.len()).into(),
);
}
}
Ok(Self {
Expand Down Expand Up @@ -273,13 +234,11 @@ impl<K: KValue> crate::solvers::BruteForceProblem for KSatisfiability<K> {
}

crate::declare_variants! {
default KSatisfiability<KN> => "2^num_vars",
default KSatisfiability<K3> => "1.307^num_vars" aliases ["3SAT"],
KSatisfiability<K2> => "num_vars + num_clauses" aliases ["2SAT"],
KSatisfiability<K3> => "1.307^num_vars" aliases ["3SAT"],
}

crate::register_brute_force! {
KSatisfiability<KN> decode |_, indices: Vec<usize>| crate::config::config_to_bits(&indices),
KSatisfiability<K2> decode |_, indices: Vec<usize>| crate::config::config_to_bits(&indices),
KSatisfiability<K3> decode |_, indices: Vec<usize>| crate::config::config_to_bits(&indices),
}
Expand Down
2 changes: 1 addition & 1 deletion src/rules/biconnectivityaugmentation_ilp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,9 @@ impl crate::rules::AggregateReductionResult for ReductionBiconnAugToILP {}
#[reduction(transform = {
exact {
max_constraint_magnitude_bits = "max_numeric_magnitude_bits",
num_vars = "num_potential_edges + 2 * num_vertices * (num_vertices + 1) * (num_edges + num_potential_edges)",
},
upper_bound {
num_vars = "num_potential_edges + 2 * num_vertices * (num_vertices + 1) * (num_edges + num_potential_edges)",
num_constraints = "1 + num_vertices * (num_vertices + 1) * (2 * num_edges + 4 * num_potential_edges + num_vertices)",
num_nonzeros = "num_potential_edges + num_vertices * (num_vertices + 1) * (4 * num_edges + 8 * num_potential_edges)",
},
Expand Down
14 changes: 9 additions & 5 deletions src/rules/clustering_ilp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,15 @@ impl ReductionResult for ReductionClusteringToILP {
#[crate::aggregate_reduction(ilp_feasibility)]
impl crate::rules::AggregateReductionResult for ReductionClusteringToILP {}

#[reduction(transform = upper_bound {
max_constraint_magnitude_bits = "1",
num_vars = "num_elements * num_clusters",
num_constraints = "num_elements + num_elements * (num_elements - 1) / 2 * num_clusters",
num_nonzeros = "num_elements * num_clusters + num_elements * (num_elements - 1) * num_clusters",
#[reduction(transform = {
exact {
num_vars = "num_elements * num_clusters",
},
upper_bound {
max_constraint_magnitude_bits = "1",
num_constraints = "num_elements + num_elements * (num_elements - 1) / 2 * num_clusters",
num_nonzeros = "num_elements * num_clusters + num_elements * (num_elements - 1) * num_clusters",
},
})]
impl ReduceTo<ILP<bool>> for Clustering {
type Result = ReductionClusteringToILP;
Expand Down
14 changes: 9 additions & 5 deletions src/rules/consecutiveblockminimization_ilp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,15 @@ impl ReductionResult for ReductionCBMToILP {
impl crate::rules::AggregateReductionResult for ReductionCBMToILP {}

#[reduction(
transform = upper_bound {
max_constraint_magnitude_bits = "num_rows * num_cols + 1",
num_vars = "num_cols * num_cols + num_rows * num_cols + num_rows * num_cols",
num_constraints = "num_cols + num_cols + num_rows * num_cols + num_rows + num_rows * num_cols + 1",
num_nonzeros = "2 * num_cols^2 + num_rows * num_cols^2 + 5 * num_rows * num_cols",
transform = {
exact {
num_vars = "num_cols * num_cols + num_rows * num_cols + num_rows * num_cols",
},
upper_bound {
max_constraint_magnitude_bits = "num_rows * num_cols + 1",
num_constraints = "num_cols + num_cols + num_rows * num_cols + num_rows + num_rows * num_cols + 1",
num_nonzeros = "2 * num_cols^2 + num_rows * num_cols^2 + 5 * num_rows * num_cols",
},
},
)]
impl ReduceTo<ILP<bool>> for ConsecutiveBlockMinimization {
Expand Down
16 changes: 10 additions & 6 deletions src/rules/consecutiveonessubmatrix_ilp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,19 +46,23 @@ impl ReductionResult for ReductionCOSToILP {
#[crate::aggregate_reduction(ilp_feasibility)]
impl crate::rules::AggregateReductionResult for ReductionCOSToILP {}

#[reduction(transform = upper_bound {
max_constraint_magnitude_bits = "bound + num_cols + 1",
num_vars = "num_cols + num_cols * bound + 5 * num_rows * bound",
num_constraints = "2 + num_cols + bound + 3 * num_rows + 8 * num_rows * bound",
num_nonzeros = "2 * num_cols + 2 * num_cols * bound + num_rows * num_cols * bound + 2 * num_rows * bound^2 + 21 * num_rows * bound",
#[reduction(transform = {
exact {
num_vars = "num_cols + num_cols * bound + 5 * num_rows * bound",
},
upper_bound {
max_constraint_magnitude_bits = "bound + num_cols + 1",
num_constraints = "2 + num_cols + bound + 3 * num_rows + 8 * num_rows * bound",
num_nonzeros = "2 * num_cols + 2 * num_cols * bound + num_rows * num_cols * bound + 2 * num_rows * bound^2 + 21 * num_rows * bound",
},
})]
impl ReduceTo<ILP<bool>> for ConsecutiveOnesSubmatrix {
type Result = ReductionCOSToILP;

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

// Variable layout (all binary):
// s_c: n vars at [0, n) — column selection
Expand Down
14 changes: 9 additions & 5 deletions src/rules/directedhamiltonianpath_ilp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,15 @@ impl ReductionResult for ReductionDirectedHamiltonianPathToILP {
#[crate::aggregate_reduction(ilp_feasibility)]
impl crate::rules::AggregateReductionResult for ReductionDirectedHamiltonianPathToILP {}

#[reduction(transform = upper_bound {
max_constraint_magnitude_bits = "1",
num_vars = "num_vertices^2",
num_constraints = "3 * num_vertices + num_vertices^3",
num_nonzeros = "num_vertices^2 + 2 * num_vertices^3",
#[reduction(transform = {
exact {
num_vars = "num_vertices^2",
},
upper_bound {
max_constraint_magnitude_bits = "1",
num_constraints = "3 * num_vertices + num_vertices^3",
num_nonzeros = "num_vertices^2 + 2 * num_vertices^3",
},
})]
impl ReduceTo<ILP<bool>> for DirectedHamiltonianPath {
type Result = ReductionDirectedHamiltonianPathToILP;
Expand Down
14 changes: 9 additions & 5 deletions src/rules/directedtwocommodityintegralflow_ilp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,15 @@ impl ReductionResult for ReductionD2CIFToILP {
impl crate::rules::AggregateReductionResult for ReductionD2CIFToILP {}

#[reduction(
transform = upper_bound {
max_constraint_magnitude_bits = "max_capacity * (num_arcs + 1) + 2",
num_vars = "2 * num_arcs",
num_constraints = "num_arcs + 2 * num_vertices + 2",
num_nonzeros = "(2 * num_arcs) * (num_arcs + 2 * num_vertices + 2)",
transform = {
exact {
num_vars = "2 * num_arcs",
},
upper_bound {
max_constraint_magnitude_bits = "max_capacity * (num_arcs + 1) + 2",
num_constraints = "num_arcs + 2 * num_vertices + 2",
num_nonzeros = "(2 * num_arcs) * (num_arcs + 2 * num_vertices + 2)",
},
},
)]
impl ReduceTo<ILP<i64, i64, Bounded>> for DirectedTwoCommodityIntegralFlow {
Expand Down
14 changes: 9 additions & 5 deletions src/rules/flowshopscheduling_ilp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,11 +72,15 @@ impl ReductionResult for ReductionFSSToILP {
impl crate::rules::AggregateReductionResult for ReductionFSSToILP {}

#[reduction(
transform = upper_bound {
max_constraint_magnitude_bits = "max_time_bits + 1",
num_vars = "num_jobs * (num_jobs - 1) / 2 + num_jobs * num_processors",
num_constraints = "num_jobs * (num_jobs - 1) + num_jobs + num_jobs * (num_processors - 1) + num_jobs * (num_jobs - 1) * num_processors + num_jobs",
num_nonzeros = "num_jobs * (num_jobs - 1) * (1 + 3 * num_processors) + 2 * num_jobs * num_processors",
transform = {
exact {
num_vars = "num_jobs * (num_jobs - 1) / 2 + num_jobs * num_processors",
},
upper_bound {
max_constraint_magnitude_bits = "max_time_bits + 1",
num_constraints = "num_jobs * (num_jobs - 1) + num_jobs + num_jobs * (num_processors - 1) + num_jobs * (num_jobs - 1) * num_processors + num_jobs",
num_nonzeros = "num_jobs * (num_jobs - 1) * (1 + 3 * num_processors) + 2 * num_jobs * num_processors",
},
},
)]
impl ReduceTo<ILP<i64, i64, Bounded>> for FlowShopScheduling {
Expand Down
14 changes: 9 additions & 5 deletions src/rules/hamiltoniancircuit_ilp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,15 @@ impl ReductionResult for ReductionHamiltonianCircuitToILP {
#[crate::aggregate_reduction(ilp_feasibility)]
impl crate::rules::AggregateReductionResult for ReductionHamiltonianCircuitToILP {}

#[crate::reduction(transform = upper_bound {
num_vars = "3 * num_edges",
num_constraints = "2 * num_vertices + 2 * num_edges + 1",
num_nonzeros = "10 * num_edges",
max_constraint_magnitude_bits = "num_vertices + 2",
#[crate::reduction(transform = {
exact {
num_vars = "3 * num_edges",
num_nonzeros = "10 * num_edges",
},
upper_bound {
num_constraints = "2 * num_vertices + 2 * num_edges + 1",
max_constraint_magnitude_bits = "num_vertices + 2",
},
})]
impl ReduceTo<ILP<i64, i64, Bounded>> for HamiltonianCircuit<SimpleGraph> {
type Result = ReductionHamiltonianCircuitToILP;
Expand Down
14 changes: 9 additions & 5 deletions src/rules/isomorphicspanningtree_ilp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,15 @@ impl crate::rules::AggregateReductionResult for ReductionISTToILP {}
// Two assignment families contribute 2n rows and 2n^2 terms. Each of the
// n-1 tree edges emits one two-term row per ordered non-edge, at most n(n-1).
// At n=0 both products vanish, including the otherwise negative n-1 factor.
#[reduction(transform = upper_bound {
max_constraint_magnitude_bits = "2",
num_vars = "num_vertices * num_vertices",
num_constraints = "2 * num_vertices + num_vertices * (num_vertices - 1)^2",
num_nonzeros = "2 * num_vertices^2 + 2 * num_vertices * (num_vertices - 1)^2",
#[reduction(transform = {
exact {
num_vars = "num_vertices * num_vertices",
},
upper_bound {
max_constraint_magnitude_bits = "2",
num_constraints = "2 * num_vertices + num_vertices * (num_vertices - 1)^2",
num_nonzeros = "2 * num_vertices^2 + 2 * num_vertices * (num_vertices - 1)^2",
},
})]
impl ReduceTo<ILP<bool>> for IsomorphicSpanningTree<SimpleGraph> {
type Result = ReductionISTToILP;
Expand Down
2 changes: 1 addition & 1 deletion src/rules/kclique_balancedcompletebipartitesubgraph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ impl ReductionResult for ReductionKCliqueToBCBS {
#[crate::aggregate_reduction(identity)]
impl crate::rules::AggregateReductionResult for ReductionKCliqueToBCBS {}

#[reduction(transform = upper_bound {
#[reduction(transform = exact {
left_size = "num_vertices + k * (k - 1) / 2",
right_size = "num_edges + num_vertices - k",
k = "num_vertices + k * (k - 1) / 2 - k",
Expand Down
Loading