From b37c13db326e36d4bd8ac54a7a56dde9ebd3b61f Mon Sep 17 00:00:00 2001 From: Xiwei Pan <90967972+isPANN@users.noreply.github.com> Date: Fri, 25 Sep 2026 20:22:25 -0700 Subject: [PATCH 1/9] Make derivable reduction parameters exact --- src/models/graph/hamiltonian_path.rs | 11 +- src/rules/bmf_bicliquecover.rs | 6 +- src/rules/bmf_ilp.rs | 4 +- src/rules/closeststring_ilp.rs | 4 +- ...onsistencyofdatabasefrequencytables_ilp.rs | 4 +- src/rules/exactcoverby3sets_ilp.rs | 4 +- src/rules/expectedretrievalcost_ilp.rs | 4 +- src/rules/feasibleregisterassignment_ilp.rs | 4 +- .../hamiltoniancircuit_hamiltonianpath.rs | 1 + src/rules/hamiltonianpath_ilp.rs | 12 +- src/rules/ilp_i64_ilp_bool.rs | 6 +- src/rules/integerknapsack_ilp.rs | 4 +- src/rules/longestcommonsubsequence_ilp.rs | 4 +- src/rules/maximumcontactmapoverlap_ilp.rs | 4 +- src/rules/maximumlikelihoodranking_ilp.rs | 4 +- src/rules/minimummatrixcover_ilp.rs | 4 +- ...mumvertexcover_longestcommonsubsequence.rs | 2 +- ...uencingtominimizeweightedcompletiontime.rs | 4 +- .../partition_multiprocessorscheduling.rs | 4 +- src/rules/quadraticassignment_ilp.rs | 10 +- src/rules/registersufficiency_ilp.rs | 4 +- src/rules/sumofsquarespartition_ilp.rs | 4 +- src/rules/threedimensionalmatching_ilp.rs | 4 +- ...partition_resourceconstrainedscheduling.rs | 6 +- .../symbolic_parameter_contracts.rs | 269 ++++++++++++++++++ 25 files changed, 315 insertions(+), 72 deletions(-) diff --git a/src/models/graph/hamiltonian_path.rs b/src/models/graph/hamiltonian_path.rs index b31d6d2ff..bf64a90b4 100644 --- a/src/models/graph/hamiltonian_path.rs +++ b/src/models/graph/hamiltonian_path.rs @@ -89,6 +89,11 @@ impl HamiltonianPath { self.graph.num_edges() } + /// Number of consecutive position pairs in a Hamiltonian path. + pub fn num_consecutive_positions(&self) -> usize { + self.num_vertices().saturating_sub(1) + } + /// Check if a configuration is a valid Hamiltonian path. pub fn is_valid_solution(&self, config: &[usize]) -> bool { is_valid_hamiltonian_path(&self.graph, config) @@ -103,7 +108,11 @@ where type Solution = Vec; type Value = crate::types::Or; - crate::problem_parameters![("num_edges", num_edges), ("num_vertices", num_vertices),]; + crate::problem_parameters![ + ("num_edges", num_edges), + ("num_vertices", num_vertices), + ("num_consecutive_positions", num_consecutive_positions), + ]; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![G] diff --git a/src/rules/bmf_bicliquecover.rs b/src/rules/bmf_bicliquecover.rs index 8f9790636..867926684 100644 --- a/src/rules/bmf_bicliquecover.rs +++ b/src/rules/bmf_bicliquecover.rs @@ -95,12 +95,12 @@ impl ReductionResult for ReductionBMFToBicliqueCover { #[reduction( transform = exact { num_vertices = "rows + cols", - num_edges = "rows * cols", + left_size = "rows", + right_size = "cols", rank = "rank", }, unavailable = { - left_size = "the exact target parameter is not represented by this reduction's symbolic transform", - right_size = "the exact target parameter is not represented by this reduction's symbolic transform", + num_edges = "the number of true matrix entries is not a registered BMF parameter", } )] impl ReduceTo for BMF { diff --git a/src/rules/bmf_ilp.rs b/src/rules/bmf_ilp.rs index 8944c45e5..c69887bb6 100644 --- a/src/rules/bmf_ilp.rs +++ b/src/rules/bmf_ilp.rs @@ -54,9 +54,7 @@ impl ReductionResult for ReductionBMFToILP { transform = exact { num_vars = "rows * rank + rank * cols + rows * rank * cols + rows * cols", num_constraints = "3 * rows * rank * cols + rank * rows * cols + rows * cols + rows * cols", - }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", + num_nonzeros = "10 * rows * rank * cols + 2 * rows * cols", } )] impl ReduceTo> for BMF { diff --git a/src/rules/closeststring_ilp.rs b/src/rules/closeststring_ilp.rs index 1f7d2a922..18cbd2c95 100644 --- a/src/rules/closeststring_ilp.rs +++ b/src/rules/closeststring_ilp.rs @@ -82,9 +82,7 @@ impl ReductionResult for ReductionClosestStringToILP { transform = exact { num_vars = "alphabet_size * string_length + 1", num_constraints = "string_length + num_strings", - }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", + num_nonzeros = "alphabet_size * string_length + num_strings * (string_length + 1)", } )] impl ReduceTo> for ClosestString { diff --git a/src/rules/consistencyofdatabasefrequencytables_ilp.rs b/src/rules/consistencyofdatabasefrequencytables_ilp.rs index 7d4d095a9..0e1853f84 100644 --- a/src/rules/consistencyofdatabasefrequencytables_ilp.rs +++ b/src/rules/consistencyofdatabasefrequencytables_ilp.rs @@ -139,9 +139,7 @@ impl crate::rules::AggregateReductionResult for ReductionCDFTToILP {} transform = exact { num_vars = "num_objects * total_domain_size + num_objects * num_frequency_cells", num_constraints = "num_objects * num_attributes + num_known_values + num_frequency_cells + 3 * num_objects * num_frequency_cells", - }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", + num_nonzeros = "num_objects * total_domain_size + num_known_values + 8 * num_objects * num_frequency_cells", } )] impl ReduceTo> for ConsistencyOfDatabaseFrequencyTables { diff --git a/src/rules/exactcoverby3sets_ilp.rs b/src/rules/exactcoverby3sets_ilp.rs index 2a7ea358d..446243f8e 100644 --- a/src/rules/exactcoverby3sets_ilp.rs +++ b/src/rules/exactcoverby3sets_ilp.rs @@ -43,9 +43,7 @@ impl crate::rules::AggregateReductionResult for ReductionX3CToILP {} transform = exact { num_vars = "num_subsets", num_constraints = "universe_size + 1", - }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", + num_nonzeros = "4 * num_subsets", } )] impl ReduceTo> for ExactCoverBy3Sets { diff --git a/src/rules/expectedretrievalcost_ilp.rs b/src/rules/expectedretrievalcost_ilp.rs index a2d76cefb..0d321f552 100644 --- a/src/rules/expectedretrievalcost_ilp.rs +++ b/src/rules/expectedretrievalcost_ilp.rs @@ -80,9 +80,7 @@ impl ReductionResult for ReductionERCToILP { transform = exact { num_vars = "num_records * num_sectors + num_records^2 * num_sectors^2", num_constraints = "num_records + 3 * num_records^2 * num_sectors^2", - }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", + num_nonzeros = "7 * num_records^2 * num_sectors^2", } )] impl ReduceTo> for ExpectedRetrievalCost { diff --git a/src/rules/feasibleregisterassignment_ilp.rs b/src/rules/feasibleregisterassignment_ilp.rs index 20ea224a6..240437300 100644 --- a/src/rules/feasibleregisterassignment_ilp.rs +++ b/src/rules/feasibleregisterassignment_ilp.rs @@ -51,9 +51,7 @@ impl crate::rules::AggregateReductionResult for ReductionFeasibleRegisterAssignm transform = exact { num_vars = "2 * num_vertices + num_vertices * (num_vertices - 1) / 2", num_constraints = "3 * num_vertices * (num_vertices - 1) / 2 + 3 * num_vertices + 2 * num_arcs + 2 * num_same_register_pairs", - }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", + num_nonzeros = "4 * num_vertices + 4 * num_arcs + 7 * num_vertices * (num_vertices - 1) / 2 + 6 * num_same_register_pairs", } )] impl ReduceTo> for FeasibleRegisterAssignment { diff --git a/src/rules/hamiltoniancircuit_hamiltonianpath.rs b/src/rules/hamiltoniancircuit_hamiltonianpath.rs index 06bd1b027..2a0ec4d69 100644 --- a/src/rules/hamiltoniancircuit_hamiltonianpath.rs +++ b/src/rules/hamiltoniancircuit_hamiltonianpath.rs @@ -90,6 +90,7 @@ impl crate::rules::AggregateReductionResult for ReductionHamiltonianCircuitToHam transform = upper_bound { num_vertices = "num_vertices + 3", num_edges = "num_edges + num_vertices + 1", + num_consecutive_positions = "num_vertices + 2", } )] impl ReduceTo> for HamiltonianCircuit { diff --git a/src/rules/hamiltonianpath_ilp.rs b/src/rules/hamiltonianpath_ilp.rs index 2cd1e8c0d..c5087553c 100644 --- a/src/rules/hamiltonianpath_ilp.rs +++ b/src/rules/hamiltonianpath_ilp.rs @@ -54,12 +54,10 @@ impl ReductionResult for ReductionHamiltonianPathToILP { impl crate::rules::AggregateReductionResult for ReductionHamiltonianPathToILP {} #[reduction( - transform = upper_bound { - num_vars = "num_vertices^2 + 2 * num_edges * num_vertices", - num_constraints = "2 * num_vertices + 6 * num_edges * num_vertices + num_vertices", - }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", + transform = exact { + num_vars = "num_vertices^2 + 2 * num_edges * num_consecutive_positions", + num_constraints = "2 * num_vertices + 6 * num_edges * num_consecutive_positions + num_consecutive_positions", + num_nonzeros = "2 * num_vertices^2 + 16 * num_edges * num_consecutive_positions", } )] impl ReduceTo> for HamiltonianPath { @@ -70,7 +68,7 @@ impl ReduceTo> for HamiltonianPath { let graph = self.graph(); let edges = graph.edges(); let m = edges.len(); - let n_pos = if n == 0 { 0 } else { n - 1 }; // number of consecutive-position pairs + let n_pos = self.num_consecutive_positions(); let num_x = n * n; let num_z = 2 * m * n_pos; diff --git a/src/rules/ilp_i64_ilp_bool.rs b/src/rules/ilp_i64_ilp_bool.rs index ea783e12f..4a6944ea2 100644 --- a/src/rules/ilp_i64_ilp_bool.rs +++ b/src/rules/ilp_i64_ilp_bool.rs @@ -111,9 +111,11 @@ impl ReductionResult for ReductionIntILPToBinaryILP { } #[reduction( - transform = unavailable { + transform = exact { + num_constraints = "num_constraints", + }, + unavailable = { num_vars = "the binary width depends on concrete variable bounds, not registered problem parameters", - num_constraints = "the exact row count is preserved but the target parameters model is unavailable until all ILP overhead declarations are migrated", num_nonzeros = "binary expansion depends on concrete variable bounds and row sparsity", }, )] diff --git a/src/rules/integerknapsack_ilp.rs b/src/rules/integerknapsack_ilp.rs index 8eb5b2aca..fc3cda6bf 100644 --- a/src/rules/integerknapsack_ilp.rs +++ b/src/rules/integerknapsack_ilp.rs @@ -36,9 +36,7 @@ impl ReductionResult for ReductionIntegerKnapsackToILP { transform = exact { num_vars = "num_items", num_constraints = "num_items + 1", - }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", + num_nonzeros = "2 * num_items", } )] impl ReduceTo> for IntegerKnapsack { diff --git a/src/rules/longestcommonsubsequence_ilp.rs b/src/rules/longestcommonsubsequence_ilp.rs index 11225f470..d156ed261 100644 --- a/src/rules/longestcommonsubsequence_ilp.rs +++ b/src/rules/longestcommonsubsequence_ilp.rs @@ -53,9 +53,7 @@ impl ReductionResult for ReductionLCSToILP { transform = exact { num_vars = "max_length * (alphabet_size + 1) + max_length * total_length", num_constraints = "max_length + num_transitions + max_length * num_strings + max_length * total_length + num_transitions * sum_triangular_lengths", - }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", + num_nonzeros = "max_length * (alphabet_size + 1 + num_strings + 3 * total_length) + 2 * num_transitions * (1 + sum_triangular_lengths)", } )] impl ReduceTo> for LongestCommonSubsequence { diff --git a/src/rules/maximumcontactmapoverlap_ilp.rs b/src/rules/maximumcontactmapoverlap_ilp.rs index 709e0053f..5970556b6 100644 --- a/src/rules/maximumcontactmapoverlap_ilp.rs +++ b/src/rules/maximumcontactmapoverlap_ilp.rs @@ -73,9 +73,7 @@ impl ReductionResult for ReductionCMOToILP { transform = exact { num_vars = "num_vertices_1 * num_vertices_2 + num_contacts_1 * num_contacts_2", num_constraints = "num_vertices_1 + num_vertices_2 + num_vertices_1 * (num_vertices_1 - 1) / 2 * num_vertices_2 * (num_vertices_2 + 1) / 2 + 2 * num_contacts_1 * num_contacts_2", - }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", + num_nonzeros = "2 * num_vertices_1 * num_vertices_2 + num_vertices_1 * (num_vertices_1 - 1) * num_vertices_2 * (num_vertices_2 + 1) / 2 + 4 * num_contacts_1 * num_contacts_2", } )] impl ReduceTo> for MaximumContactMapOverlap { diff --git a/src/rules/maximumlikelihoodranking_ilp.rs b/src/rules/maximumlikelihoodranking_ilp.rs index 954b09627..0afd9524a 100644 --- a/src/rules/maximumlikelihoodranking_ilp.rs +++ b/src/rules/maximumlikelihoodranking_ilp.rs @@ -76,10 +76,8 @@ impl ReductionResult for ReductionMaximumLikelihoodRankingToILP { transform = exact { num_vars = "num_items * (num_items - 1) / 2", num_constraints = "num_items * (num_items - 1) * (num_items - 2) / 3", + num_nonzeros = "num_items * (num_items - 1) * (num_items - 2)", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } )] impl ReduceTo> for MaximumLikelihoodRanking { type Result = ReductionMaximumLikelihoodRankingToILP; diff --git a/src/rules/minimummatrixcover_ilp.rs b/src/rules/minimummatrixcover_ilp.rs index d279e1b67..8de7cad1d 100644 --- a/src/rules/minimummatrixcover_ilp.rs +++ b/src/rules/minimummatrixcover_ilp.rs @@ -56,10 +56,8 @@ fn y_index(n: usize, i: usize, j: usize) -> usize { transform = exact { num_vars = "num_rows + num_rows * (num_rows - 1) / 2", num_constraints = "3 * num_rows * (num_rows - 1) / 2", + num_nonzeros = "7 * num_rows * (num_rows - 1) / 2", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } )] impl ReduceTo> for MinimumMatrixCover { type Result = ReductionMinimumMatrixCoverToILP; diff --git a/src/rules/minimumvertexcover_longestcommonsubsequence.rs b/src/rules/minimumvertexcover_longestcommonsubsequence.rs index e2f0a23a8..2cdc94188 100644 --- a/src/rules/minimumvertexcover_longestcommonsubsequence.rs +++ b/src/rules/minimumvertexcover_longestcommonsubsequence.rs @@ -44,11 +44,11 @@ impl ReductionResult for ReductionVCToLCS { num_strings = "num_edges + 1", max_length = "num_vertices", total_length = "num_vertices + 2 * num_edges * num_vertices - 2 * num_edges", + sum_triangular_lengths = "num_vertices * (num_vertices + 1) / 2 + num_edges * (2 * num_vertices - 2) * (2 * num_vertices - 1) / 2", }, unavailable = { cross_frequency_product = "the exact target parameter is not represented by this reduction's symbolic transform", num_transitions = "the exact target parameter is not represented by this reduction's symbolic transform", - sum_triangular_lengths = "the exact target parameter is not represented by this reduction's symbolic transform", } )] impl ReduceTo for MinimumVertexCover { diff --git a/src/rules/optimallineararrangement_sequencingtominimizeweightedcompletiontime.rs b/src/rules/optimallineararrangement_sequencingtominimizeweightedcompletiontime.rs index e7a14a680..a83378b83 100644 --- a/src/rules/optimallineararrangement_sequencingtominimizeweightedcompletiontime.rs +++ b/src/rules/optimallineararrangement_sequencingtominimizeweightedcompletiontime.rs @@ -57,9 +57,7 @@ impl ReductionResult for ReductionOLAToSequencingToMinimizeWeightedCompletionTim #[reduction( transform = exact { num_tasks = "num_vertices + num_edges", - }, - unavailable = { - num_precedences = "the exact target parameter is not represented by this reduction's symbolic transform", + num_precedences = "2 * num_edges", } )] impl ReduceTo diff --git a/src/rules/partition_multiprocessorscheduling.rs b/src/rules/partition_multiprocessorscheduling.rs index 4aa12595b..6ecdc103c 100644 --- a/src/rules/partition_multiprocessorscheduling.rs +++ b/src/rules/partition_multiprocessorscheduling.rs @@ -56,9 +56,7 @@ impl crate::rules::AggregateReductionResult for ReductionPartitionToMPS {} #[reduction( transform = exact { num_tasks = "num_elements", - }, - unavailable = { - num_processors = "the exact target parameter is not represented by this reduction's symbolic transform", + num_processors = "2", } )] impl ReduceTo for Partition { diff --git a/src/rules/quadraticassignment_ilp.rs b/src/rules/quadraticassignment_ilp.rs index aacef5d4e..f1585eb7d 100644 --- a/src/rules/quadraticassignment_ilp.rs +++ b/src/rules/quadraticassignment_ilp.rs @@ -50,12 +50,10 @@ impl ReductionResult for ReductionQAPToILP { } #[reduction( - transform = upper_bound { - num_vars = "num_facilities * num_locations + num_facilities^2 * num_locations^2", - num_constraints = "num_facilities + num_locations + 3 * num_facilities^2 * num_locations^2", - }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", + transform = exact { + num_vars = "num_facilities * num_locations + num_facilities * (num_facilities - 1) * num_locations^2", + num_constraints = "num_facilities + num_locations + 3 * num_facilities * (num_facilities - 1) * num_locations^2", + num_nonzeros = "2 * num_facilities * num_locations + 7 * num_facilities * (num_facilities - 1) * num_locations^2", } )] impl ReduceTo> for QuadraticAssignment { diff --git a/src/rules/registersufficiency_ilp.rs b/src/rules/registersufficiency_ilp.rs index 447c376c8..d6db3ee1f 100644 --- a/src/rules/registersufficiency_ilp.rs +++ b/src/rules/registersufficiency_ilp.rs @@ -48,10 +48,8 @@ impl crate::rules::AggregateReductionResult for ReductionRegisterSufficiencyToIL transform = exact { num_vars = "3 * num_vertices^2 + num_vertices * (num_vertices - 1) / 2 + 2 * num_vertices", num_constraints = "9 * num_vertices^2 + 3 * num_vertices * (num_vertices - 1) / 2 + 3 * num_vertices + 2 * num_arcs + num_sinks", + num_nonzeros = "18 * num_vertices^2 + 2 * num_vertices + 7 * num_vertices * (num_vertices - 1) / 2 + 4 * num_arcs + num_sinks", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } )] impl ReduceTo> for RegisterSufficiency { type Result = ReductionRegisterSufficiencyToILP; diff --git a/src/rules/sumofsquarespartition_ilp.rs b/src/rules/sumofsquarespartition_ilp.rs index 1d82db321..57ad1b258 100644 --- a/src/rules/sumofsquarespartition_ilp.rs +++ b/src/rules/sumofsquarespartition_ilp.rs @@ -76,10 +76,8 @@ impl ReductionResult for ReductionSSPToILP { transform = exact { num_vars = "num_elements * num_groups + num_elements^2 * num_groups", num_constraints = "num_elements + 3 * num_elements^2 * num_groups", + num_nonzeros = "7 * num_elements^2 * num_groups", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } )] impl ReduceTo> for SumOfSquaresPartition { type Result = ReductionSSPToILP; diff --git a/src/rules/threedimensionalmatching_ilp.rs b/src/rules/threedimensionalmatching_ilp.rs index b9a430bde..a7a325b9e 100644 --- a/src/rules/threedimensionalmatching_ilp.rs +++ b/src/rules/threedimensionalmatching_ilp.rs @@ -40,10 +40,8 @@ impl crate::rules::AggregateReductionResult for ReductionThreeDimensionalMatchin transform = exact { num_vars = "num_triples", num_constraints = "3 * universe_size", + num_nonzeros = "3 * num_triples", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } )] impl ReduceTo> for ThreeDimensionalMatching { type Result = ReductionThreeDimensionalMatchingToILP; diff --git a/src/rules/threepartition_resourceconstrainedscheduling.rs b/src/rules/threepartition_resourceconstrainedscheduling.rs index 48536d66c..7b4111b58 100644 --- a/src/rules/threepartition_resourceconstrainedscheduling.rs +++ b/src/rules/threepartition_resourceconstrainedscheduling.rs @@ -59,10 +59,8 @@ impl crate::rules::AggregateReductionResult for ReductionThreePartitionToRCS {} #[reduction( transform = exact { num_tasks = "num_elements", - }, - unavailable = { - deadline = "the exact target parameter is not represented by this reduction's symbolic transform", - num_resources = "the exact target parameter is not represented by this reduction's symbolic transform", + deadline = "num_groups", + num_resources = "1", } )] impl ReduceTo for ThreePartition { diff --git a/src/unit_tests/symbolic_parameter_contracts.rs b/src/unit_tests/symbolic_parameter_contracts.rs index 1ba9b0d78..0779df94a 100644 --- a/src/unit_tests/symbolic_parameter_contracts.rs +++ b/src/unit_tests/symbolic_parameter_contracts.rs @@ -152,3 +152,272 @@ fn canonical_examples_satisfy_upper_bound_parameter_contracts() { } } } + +fn check_reduced_parameters(source: S, fields: &[&str], relation: ParameterRelation) +where + S: Problem + ReduceTo, + T: Problem, +{ + let reduction = source.reduce_to().expect("reduction should succeed"); + let actual = reduction.target_problem().parameters(); + let entry = crate::rules::registry::reduction_entries() + .into_iter() + .find(|entry| { + entry.source_name == S::NAME + && entry.target_name == T::NAME + && entry.source_variant() == S::variant() + && entry.target_variant() == T::variant() + }) + .expect("direct reduction is registered"); + let contract = entry.parameter_contract().unwrap(); + let transform = contract.transform().expect("symbolic transform exists"); + assert_eq!(transform.relation(), relation, "{} -> {}", S::NAME, T::NAME); + let predicted = transform.evaluate(&source.parameters()).unwrap(); + for &field in fields { + assert_eq!( + predicted.get(field), + actual.get(field), + "{} -> {}: {field}", + S::NAME, + T::NAME + ); + assert!( + !contract + .unavailable() + .iter() + .any(|item| item.field == field), + "{} -> {}: {field} is still unavailable", + S::NAME, + T::NAME + ); + } +} + +#[test] +fn newly_exact_parameters_match_reduced_instances() { + use crate::models::algebraic::MinimumMatrixCover; + use crate::models::algebraic::{ + IntegerVariable, LinearConstraint, ObjectiveSense, QuadraticAssignment, BMF, ILP, + }; + use crate::models::graph::BicliqueCover; + use crate::models::graph::{ + HamiltonianPath, MaximumContactMapOverlap, MinimumVertexCover, OptimalLinearArrangement, + }; + use crate::models::misc::{ + ClosestString, ConsistencyOfDatabaseFrequencyTables, ExpectedRetrievalCost, + FeasibleRegisterAssignment, LongestCommonSubsequence, MaximumLikelihoodRanking, + MultiprocessorScheduling, Partition, RegisterSufficiency, ResourceConstrainedScheduling, + SequencingToMinimizeWeightedCompletionTime, SumOfSquaresPartition, ThreePartition, + }; + use crate::models::set::{IntegerKnapsack, ThreeDimensionalMatching}; + use crate::types::One; + + let exact = ParameterRelation::Exact; + check_reduced_parameters::<_, ILP>( + BMF::new(vec![vec![true, false], vec![false, true]], 2), + &["num_nonzeros"], + exact, + ); + check_reduced_parameters::<_, ILP>( + ClosestString::new(2, vec![vec![0, 1], vec![1, 0]]), + &["num_nonzeros"], + exact, + ); + check_reduced_parameters::<_, ILP>( + ConsistencyOfDatabaseFrequencyTables::new(1, vec![2, 2], vec![], vec![]), + &["num_nonzeros"], + exact, + ); + check_reduced_parameters::<_, ILP>( + ExactCoverBy3Sets::new(3, vec![[0, 1, 2]]), + &["num_nonzeros"], + exact, + ); + check_reduced_parameters::<_, ILP>( + ExpectedRetrievalCost::new(vec![0.5, 0.5], 2).unwrap(), + &["num_nonzeros"], + exact, + ); + check_reduced_parameters::<_, ILP>( + FeasibleRegisterAssignment::new(4, vec![(0, 1), (0, 2), (1, 3)], 2, vec![0, 1, 0, 0]), + &["num_nonzeros"], + exact, + ); + check_reduced_parameters::<_, ILP>( + IntegerKnapsack::new(vec![3, 4], vec![5, 6], 7).unwrap(), + &["num_nonzeros"], + exact, + ); + check_reduced_parameters::<_, ILP>( + LongestCommonSubsequence::new(2, vec![vec![0, 1], vec![1, 0, 1]]), + &["num_nonzeros"], + exact, + ); + check_reduced_parameters::<_, ILP>( + MaximumContactMapOverlap::new(3, vec![(0, 2)], 3, vec![(0, 1)]), + &["num_nonzeros"], + exact, + ); + check_reduced_parameters::<_, ILP>( + MaximumLikelihoodRanking::new(vec![vec![0, 1, 2], vec![2, 0, 1], vec![1, 2, 0]]), + &["num_nonzeros"], + exact, + ); + check_reduced_parameters::<_, ILP>( + MinimumMatrixCover::new(vec![vec![0, 2], vec![3, 0]]), + &["num_nonzeros"], + exact, + ); + check_reduced_parameters::<_, ILP>( + RegisterSufficiency::new(4, vec![(2, 0), (3, 1)], 2), + &["num_nonzeros"], + exact, + ); + check_reduced_parameters::<_, ILP>( + SumOfSquaresPartition::new(vec![1, 2, 3], 2), + &["num_nonzeros"], + exact, + ); + check_reduced_parameters::<_, ILP>( + ThreeDimensionalMatching::new(2, vec![(0, 1, 1), (1, 0, 0)]), + &["num_nonzeros"], + exact, + ); + check_reduced_parameters::<_, ILP>( + QuadraticAssignment::new(vec![vec![0, 1], vec![2, 0]], vec![vec![0, 3], vec![4, 0]]), + &["num_vars", "num_constraints", "num_nonzeros"], + exact, + ); + check_reduced_parameters::<_, ILP>( + HamiltonianPath::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)])), + &["num_vars", "num_constraints", "num_nonzeros"], + exact, + ); + check_reduced_parameters::<_, BicliqueCover>( + BMF::new(vec![vec![true, false], vec![false, true]], 1), + &["num_vertices", "left_size", "right_size", "rank"], + exact, + ); + check_reduced_parameters::<_, ILP>( + ILP::::with_variables( + vec![IntegerVariable::new(Some(0), Some(3)).unwrap()], + vec![LinearConstraint::le(vec![(0, 1)], 2)], + vec![], + ObjectiveSense::Minimize, + ) + .unwrap(), + &["num_constraints"], + exact, + ); + check_reduced_parameters::<_, LongestCommonSubsequence>( + MinimumVertexCover::new(SimpleGraph::path(4), vec![One; 4]), + &["sum_triangular_lengths"], + exact, + ); + check_reduced_parameters::<_, SequencingToMinimizeWeightedCompletionTime>( + OptimalLinearArrangement::new(SimpleGraph::path(4)), + &["num_precedences"], + exact, + ); + check_reduced_parameters::<_, MultiprocessorScheduling>( + Partition::new(vec![1, 2, 3]).unwrap(), + &["num_processors"], + exact, + ); + check_reduced_parameters::<_, ResourceConstrainedScheduling>( + ThreePartition::new(vec![4, 5, 6, 4, 6, 5], 15), + &["deadline", "num_resources"], + exact, + ); +} + +#[test] +fn exact_parameter_formulas_cover_sparse_and_boundary_instances() { + use crate::models::algebraic::{QuadraticAssignment, BMF, ILP}; + use crate::models::graph::{ + BicliqueCover, HamiltonianCircuit, HamiltonianPath, MinimumVertexCover, + }; + use crate::models::misc::{ + ConsistencyOfDatabaseFrequencyTables, FrequencyTable, KnownValue, LongestCommonSubsequence, + MaximumLikelihoodRanking, RegisterSufficiency, + }; + + let exact = ParameterRelation::Exact; + check_reduced_parameters::<_, ILP>( + ConsistencyOfDatabaseFrequencyTables::new( + 2, + vec![2, 2], + vec![FrequencyTable::new(0, 1, vec![vec![1, 0], vec![0, 1]])], + vec![KnownValue::new(0, 0, 0)], + ), + &["num_nonzeros"], + exact, + ); + check_reduced_parameters::<_, ILP>( + LongestCommonSubsequence::new(2, vec![vec![], vec![0, 1]]), + &["num_nonzeros"], + exact, + ); + check_reduced_parameters::<_, ILP>( + MaximumLikelihoodRanking::new(vec![]), + &["num_nonzeros"], + exact, + ); + check_reduced_parameters::<_, ILP>( + RegisterSufficiency::new(0, vec![], 0), + &["num_nonzeros"], + exact, + ); + check_reduced_parameters::<_, ILP>( + QuadraticAssignment::new(vec![], vec![vec![0]]), + &["num_vars", "num_constraints", "num_nonzeros"], + exact, + ); + check_reduced_parameters::<_, ILP>( + QuadraticAssignment::new(vec![vec![0]], vec![vec![0, 1], vec![1, 0]]), + &["num_vars", "num_constraints", "num_nonzeros"], + exact, + ); + check_reduced_parameters::<_, ILP>( + HamiltonianPath::new(SimpleGraph::new(0, vec![])), + &["num_vars", "num_constraints", "num_nonzeros"], + exact, + ); + check_reduced_parameters::<_, ILP>( + HamiltonianPath::new(SimpleGraph::new(1, vec![])), + &["num_vars", "num_constraints", "num_nonzeros"], + exact, + ); + check_reduced_parameters::<_, HamiltonianPath>( + HamiltonianCircuit::new(SimpleGraph::new(0, vec![])), + &["num_consecutive_positions"], + ParameterRelation::UpperBound, + ); + check_reduced_parameters::<_, HamiltonianPath>( + HamiltonianCircuit::new(SimpleGraph::new(3, vec![(0, 1), (1, 2), (2, 0)])), + &["num_consecutive_positions"], + ParameterRelation::UpperBound, + ); + check_reduced_parameters::<_, LongestCommonSubsequence>( + MinimumVertexCover::new(SimpleGraph::new(0, vec![]), vec![]), + &["sum_triangular_lengths"], + exact, + ); + + let source = BMF::new(vec![vec![true, false], vec![false, true]], 1); + let reduction = ReduceTo::::reduce_to(&source).unwrap(); + assert_eq!( + reduction.target_problem().parameters().get("num_edges"), + Some(2) + ); + let entry = crate::rules::registry::reduction_entries() + .into_iter() + .find(|entry| entry.source_name == "BMF" && entry.target_name == "BicliqueCover") + .unwrap(); + let contract = entry.parameter_contract().unwrap(); + assert!(contract.transform().unwrap().get("num_edges").is_none()); + assert!(contract + .unavailable() + .iter() + .any(|field| field.field == "num_edges")); +} From bd6da65a1f6b99b6b2159f5f13080cfb74355e97 Mon Sep 17 00:00:00 2001 From: Xiwei Pan <90967972+isPANN@users.noreply.github.com> Date: Fri, 25 Sep 2026 20:30:59 -0700 Subject: [PATCH 2/9] Check every field in exact reduction transforms --- src/unit_tests/symbolic_parameter_contracts.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/unit_tests/symbolic_parameter_contracts.rs b/src/unit_tests/symbolic_parameter_contracts.rs index 0779df94a..19665fe1c 100644 --- a/src/unit_tests/symbolic_parameter_contracts.rs +++ b/src/unit_tests/symbolic_parameter_contracts.rs @@ -173,6 +173,20 @@ where let transform = contract.transform().expect("symbolic transform exists"); assert_eq!(transform.relation(), relation, "{} -> {}", S::NAME, T::NAME); let predicted = transform.evaluate(&source.parameters()).unwrap(); + if relation == ParameterRelation::Exact { + for (field, _) in transform.expressions() { + if fields.contains(&field) { + continue; + } + assert_eq!( + predicted.get(field), + actual.get(field), + "{} -> {}: {field}", + S::NAME, + T::NAME + ); + } + } for &field in fields { assert_eq!( predicted.get(field), From 90e41a8366b65ea0714119cf5d0513f5bfd1bbb8 Mon Sep 17 00:00:00 2001 From: Xiwei Pan <90967972+isPANN@users.noreply.github.com> Date: Fri, 25 Sep 2026 20:57:07 -0700 Subject: [PATCH 3/9] Verify exact reduction parameters on randomized instances --- src/lib.rs | 3 + src/models/algebraic/qubo.rs | 10 +- src/rules/closestvectorproblem_qubo.rs | 1 + src/rules/coloring_qubo.rs | 1 + src/rules/graphpartitioning_qubo.rs | 1 + src/rules/ilp_qubo.rs | 1 + src/rules/knapsack_qubo.rs | 1 + src/rules/ksatisfiability_qubo.rs | 6 + src/rules/maximumsetpacking_qubo.rs | 3 + ...mumdiscreteplanarinversekinematics_qubo.rs | 2 + src/rules/minimummultiwaycut_qubo.rs | 2 + src/rules/paintshop_qubo.rs | 2 + src/rules/qubo_casts.rs | 2 +- src/rules/qubo_ilp.rs | 13 +- src/rules/spinglass_qubo.rs | 16 +- src/rules/travelingsalesman_qubo.rs | 3 + src/unit_tests/exact_parameter_randomized.rs | 314 ++++++++++++++++++ src/unit_tests/models/algebraic/qubo.rs | 8 + src/unit_tests/rules/spinglass_qubo.rs | 12 + 19 files changed, 384 insertions(+), 17 deletions(-) create mode 100644 src/unit_tests/exact_parameter_randomized.rs diff --git a/src/lib.rs b/src/lib.rs index 93dfbd4b7..e9af9164d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -135,6 +135,9 @@ pub use problemreductions_macros::{ // Re-export inventory so `declare_variants!` can use `$crate::inventory::submit!` pub use inventory; +#[cfg(all(test, feature = "example-db"))] +#[path = "unit_tests/exact_parameter_randomized.rs"] +mod exact_parameter_randomized; #[cfg(all(test, feature = "example-db"))] #[path = "unit_tests/symbolic_parameter_contracts.rs"] mod symbolic_parameter_contracts; diff --git a/src/models/algebraic/qubo.rs b/src/models/algebraic/qubo.rs index 02b6a7153..91e23d51b 100644 --- a/src/models/algebraic/qubo.rs +++ b/src/models/algebraic/qubo.rs @@ -258,6 +258,11 @@ impl QUBO { self.num_vars } + /// Number of nonzero off-diagonal coefficients. + pub fn num_quadratic_terms(&self) -> usize { + self.entries.iter().filter(|(i, j, _)| i != j).count() + } + /// Nonzero upper-triangular coefficients `(i, j, Q[i][j])`, sorted by `(i, j)`. pub fn entries(&self) -> &[(usize, usize, W)] { &self.entries @@ -293,7 +298,10 @@ where type Solution = Vec; type Value = Min; - crate::problem_parameters![("num_vars", num_vars),]; + crate::problem_parameters![ + ("num_vars", num_vars), + ("num_quadratic_terms", num_quadratic_terms), + ]; fn evaluate( &self, diff --git a/src/rules/closestvectorproblem_qubo.rs b/src/rules/closestvectorproblem_qubo.rs index b5654e166..6d8986da3 100644 --- a/src/rules/closestvectorproblem_qubo.rs +++ b/src/rules/closestvectorproblem_qubo.rs @@ -233,6 +233,7 @@ fn dot(left: &[i64], right: &[i64], operation: &str) -> Result> for ClosestVectorProblem { type Result = ReductionCVPToQUBO; diff --git a/src/rules/coloring_qubo.rs b/src/rules/coloring_qubo.rs index dfb7d2a32..f064b7fc2 100644 --- a/src/rules/coloring_qubo.rs +++ b/src/rules/coloring_qubo.rs @@ -182,6 +182,7 @@ fn reduce_kcoloring_to_qubo( #[reduction( transform = exact { num_vars = "num_vertices * num_colors", + num_quadratic_terms = "num_vertices * num_colors * (num_colors - 1) / 2 + num_edges * num_colors", } )] impl ReduceTo>> for KColoring { diff --git a/src/rules/graphpartitioning_qubo.rs b/src/rules/graphpartitioning_qubo.rs index 9840bc491..9033e1503 100644 --- a/src/rules/graphpartitioning_qubo.rs +++ b/src/rules/graphpartitioning_qubo.rs @@ -36,6 +36,7 @@ impl ReductionResult for ReductionGraphPartitioningToQUBO { #[reduction(transform = exact { num_vars = "num_vertices", + num_quadratic_terms = "num_vertices * (num_vertices - 1) / 2", })] impl ReduceTo> for GraphPartitioning { type Result = ReductionGraphPartitioningToQUBO; diff --git a/src/rules/ilp_qubo.rs b/src/rules/ilp_qubo.rs index ca826cec4..f60d8eaa6 100644 --- a/src/rules/ilp_qubo.rs +++ b/src/rules/ilp_qubo.rs @@ -81,6 +81,7 @@ impl crate::rules::AggregateReductionResult for ReductionILPToQUBO { #[reduction( transform = unavailable { num_vars = "the slack-bit count depends on coefficient magnitudes and right-hand sides absent from the registered source parameters vector", + num_quadratic_terms = "the nonzero products depend on generated penalty coefficients", } )] impl ReduceTo> for ILP { diff --git a/src/rules/knapsack_qubo.rs b/src/rules/knapsack_qubo.rs index 63a0e208d..39d844644 100644 --- a/src/rules/knapsack_qubo.rs +++ b/src/rules/knapsack_qubo.rs @@ -46,6 +46,7 @@ impl ReductionResult for ReductionKnapsackToQUBO { #[reduction(transform = unavailable { num_vars = "the exact piecewise slack-bit count is not representable in the parameter-expression language", + num_quadratic_terms = "the nonzero products depend on item sizes and values", })] impl ReduceTo> for Knapsack { type Result = ReductionKnapsackToQUBO; diff --git a/src/rules/ksatisfiability_qubo.rs b/src/rules/ksatisfiability_qubo.rs index 6ebb91c9f..ab3cfcdc8 100644 --- a/src/rules/ksatisfiability_qubo.rs +++ b/src/rules/ksatisfiability_qubo.rs @@ -332,6 +332,9 @@ impl crate::rules::AggregateReductionResult for Reduction3SATToQUBO {} #[reduction( transform = exact { num_vars = "num_vars", + }, + unavailable = { + num_quadratic_terms = "clause literals can overlap and cancel in the QUBO coefficients", } )] impl ReduceTo>> for KSatisfiability { @@ -365,6 +368,9 @@ impl ReduceTo>> for KSatisfiability { #[reduction( transform = exact { num_vars = "num_vars + num_clauses", + }, + unavailable = { + num_quadratic_terms = "clause literals can overlap and cancel in the QUBO coefficients", } )] impl ReduceTo>> for KSatisfiability { diff --git a/src/rules/maximumsetpacking_qubo.rs b/src/rules/maximumsetpacking_qubo.rs index 2a7559884..69ef23985 100644 --- a/src/rules/maximumsetpacking_qubo.rs +++ b/src/rules/maximumsetpacking_qubo.rs @@ -43,6 +43,9 @@ impl ReductionResult for ReductionSPToQUBO { #[reduction( transform = exact { num_vars = "num_sets", + }, + unavailable = { + num_quadratic_terms = "the number of overlapping set pairs is not a registered source parameter", } )] impl ReduceTo> for MaximumSetPacking { diff --git a/src/rules/minimumdiscreteplanarinversekinematics_qubo.rs b/src/rules/minimumdiscreteplanarinversekinematics_qubo.rs index 6753e3d39..54e00005b 100644 --- a/src/rules/minimumdiscreteplanarinversekinematics_qubo.rs +++ b/src/rules/minimumdiscreteplanarinversekinematics_qubo.rs @@ -80,6 +80,8 @@ impl ReductionResult for ReductionMinimumDiscretePlanarInverseKinematicsToQUBO { #[reduction(transform = exact { num_vars = "num_orientation_samples", +}, unavailable = { + num_quadratic_terms = "the nonzero products depend on the sampled geometry", })] impl ReduceTo> for MinimumDiscretePlanarInverseKinematics { type Result = ReductionMinimumDiscretePlanarInverseKinematicsToQUBO; diff --git a/src/rules/minimummultiwaycut_qubo.rs b/src/rules/minimummultiwaycut_qubo.rs index d6b94f090..21088ad9e 100644 --- a/src/rules/minimummultiwaycut_qubo.rs +++ b/src/rules/minimummultiwaycut_qubo.rs @@ -86,6 +86,8 @@ impl ReductionResult for ReductionMinimumMultiwayCutToQUBO { #[reduction(transform = exact { num_vars = "num_terminals * num_vertices", +}, unavailable = { + num_quadratic_terms = "the nonzero products depend on edge weights and terminal placement", })] impl ReduceTo> for MinimumMultiwayCut { type Result = ReductionMinimumMultiwayCutToQUBO; diff --git a/src/rules/paintshop_qubo.rs b/src/rules/paintshop_qubo.rs index b8a05456c..7224e8704 100644 --- a/src/rules/paintshop_qubo.rs +++ b/src/rules/paintshop_qubo.rs @@ -40,6 +40,8 @@ impl ReductionResult for ReductionPaintShopToQUBO { #[reduction(transform = exact { num_vars = "num_cars", +}, unavailable = { + num_quadratic_terms = "adjacency contributions can cancel between repeated car pairs", })] impl ReduceTo> for PaintShop { type Result = ReductionPaintShopToQUBO; diff --git a/src/rules/qubo_casts.rs b/src/rules/qubo_casts.rs index bef444980..0593cc879 100644 --- a/src/rules/qubo_casts.rs +++ b/src/rules/qubo_casts.rs @@ -8,7 +8,7 @@ use crate::types::i64_to_exact_f64; impl_variant_reduction!( QUBO, => , - fields: [num_vars], + fields: [num_vars, num_quadratic_terms], |src| { let entries = src .entries() diff --git a/src/rules/qubo_ilp.rs b/src/rules/qubo_ilp.rs index e08a10368..9ed5218dc 100644 --- a/src/rules/qubo_ilp.rs +++ b/src/rules/qubo_ilp.rs @@ -90,15 +90,14 @@ where }) } +#[rustfmt::skip] macro_rules! impl_qubo_to_ilp { ($coefficient:ty) => { #[reduction( - transform = upper_bound { - num_vars = "num_vars^2 + num_vars", - num_constraints = "3 * num_vars^2", - }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", + transform = exact { + num_vars = "num_vars + num_quadratic_terms", + num_constraints = "3 * num_quadratic_terms", + num_nonzeros = "7 * num_quadratic_terms", } )] impl ReduceTo> for QUBO<$coefficient> { @@ -108,7 +107,7 @@ macro_rules! impl_qubo_to_ilp { reduce_qubo(self) } } - } + }; } impl_qubo_to_ilp!(i64); diff --git a/src/rules/spinglass_qubo.rs b/src/rules/spinglass_qubo.rs index 31d1c46d3..fa158c72e 100644 --- a/src/rules/spinglass_qubo.rs +++ b/src/rules/spinglass_qubo.rs @@ -39,7 +39,7 @@ impl ReductionResult for ReductionQUBOToSG { #[reduction( transform = exact { num_spins = "num_vars", - num_interactions = "num_vars^2", + num_interactions = "num_quadratic_terms", }, )] impl ReduceTo> for QUBO { @@ -63,10 +63,6 @@ impl ReduceTo> for QUBO { let mut onsite = vec![0.0; n]; for &(i, j, q) in self.entries() { - if q.abs() < 1e-10 { - continue; - } - if i == j { // Diagonal: Q_ii * x_i = Q_ii/2 * s_i + Q_ii/2 (constant) onsite[i] += q / 2.0; @@ -74,9 +70,7 @@ impl ReduceTo> for QUBO { // Off-diagonal: Q_ij * x_i * x_j // J_ij contribution let j_ij = q / 4.0; - if j_ij.abs() > 1e-10 { - interactions.push(((i, j), j_ij)); - } + interactions.push(((i, j), j_ij)); // h_i and h_j contributions onsite[i] += q / 4.0; onsite[j] += q / 4.0; @@ -129,6 +123,9 @@ where #[reduction( transform = exact { num_vars = "num_spins", + }, + unavailable = { + num_quadratic_terms = "zero or cancelling couplings determine the nonzero quadratic terms", } )] impl ReduceTo> for SpinGlass { @@ -173,6 +170,9 @@ impl ReduceTo> for SpinGlass { #[reduction( transform = exact { num_vars = "num_spins", + }, + unavailable = { + num_quadratic_terms = "zero or cancelling couplings determine the nonzero quadratic terms", } )] impl ReduceTo> for SpinGlass { diff --git a/src/rules/travelingsalesman_qubo.rs b/src/rules/travelingsalesman_qubo.rs index 6a02a8e2b..5be5c567f 100644 --- a/src/rules/travelingsalesman_qubo.rs +++ b/src/rules/travelingsalesman_qubo.rs @@ -126,6 +126,9 @@ impl crate::rules::AggregateReductionResult for ReductionTravelingSalesmanToQUBO #[reduction( transform = exact { num_vars = "num_vertices^2", + }, + unavailable = { + num_quadratic_terms = "the nonzero products depend on graph edges and edge costs", } )] impl ReduceTo> for TravelingSalesman { diff --git a/src/unit_tests/exact_parameter_randomized.rs b/src/unit_tests/exact_parameter_randomized.rs new file mode 100644 index 000000000..a5a6aeccb --- /dev/null +++ b/src/unit_tests/exact_parameter_randomized.rs @@ -0,0 +1,314 @@ +use crate::parameters::ParameterRelation; +use crate::registry::{load_dyn, DynProblem}; +use crate::rules::{registry::ReductionEntry, ReductionGraph}; +use serde_json::{json, Value}; +use std::collections::BTreeMap; + +type SourceKey = (String, BTreeMap); + +fn next(seed: &mut u64) -> u64 { + *seed ^= *seed << 13; + *seed ^= *seed >> 7; + *seed ^= *seed << 17; + *seed +} + +fn seed_for(entry: &ReductionEntry) -> u64 { + let mut seed = 0x9e37_79b9_7f4a_7c15_u64; + for byte in format!( + "{}{:?}{}{:?}", + entry.source_name, + entry.source_variant(), + entry.target_name, + entry.target_variant() + ) + .bytes() + { + seed = seed.wrapping_mul(0x100_0000_01b3) ^ u64::from(byte); + } + seed +} + +fn canonical_sources() -> BTreeMap> { + let mut sources = BTreeMap::>::new(); + let db = crate::example_db::build_example_db().unwrap(); + for model in db.models { + sources + .entry((model.problem, model.variant)) + .or_default() + .push(model.instance); + } + for rule in db.rules { + let source = rule.source; + if let Some(inner) = source.instance.get("inner") { + if let Some(name) = source.problem.strip_prefix("Decision") { + sources + .entry((name.to_string(), source.variant.clone())) + .or_default() + .push(inner.clone()); + } + } + sources + .entry((source.problem, source.variant)) + .or_default() + .push(source.instance); + } + sources +} + +// These changes preserve the element type and start from an independently +// constructed canonical instance. Deserialization and reduction must both accept +// a candidate before it is used as a test instance. +fn variations(value: &Value) -> Vec { + match value { + Value::Array(items) => { + let mut result = Vec::new(); + if items.len() > 1 { + let mut shorter = items.clone(); + shorter.pop(); + result.push(Value::Array(shorter)); + let mut reordered = items.clone(); + reordered.rotate_left(1); + result.push(Value::Array(reordered)); + } + for (index, item) in items.iter().enumerate() { + for changed in variations(item) { + let mut copy = items.clone(); + copy[index] = changed; + result.push(Value::Array(copy)); + } + } + result + } + Value::Object(fields) => fields + .iter() + .flat_map(|(key, child)| { + variations(child).into_iter().map(move |changed| { + let mut copy = fields.clone(); + copy.insert(key.clone(), changed); + Value::Object(copy) + }) + }) + .collect(), + _ => Vec::new(), + } +} + +fn random_source( + entry: &ReductionEntry, + sources: &BTreeMap>, +) -> Result, String> { + let variant = ReductionGraph::variant_to_map(&entry.source_variant()); + let registered = crate::registry::find_variant_entry(entry.source_name, &variant).unwrap(); + let mut seed = seed_for(entry); + if let Some(random) = registered.random { + let mut args = serde_json::Map::new(); + for input in (random.inputs)() { + let value = match input.name { + "num_vertices" => json!(4 + next(&mut seed) % 3), + "seed" => json!((next(&mut seed) >> 1) as i64), + "k" => json!(if variant.get("k").is_some_and(|value| value == "K3") { + 3 + } else { + 2 + }), + "bound" => json!(2), + _ if !input.required => continue, + name => return Err(format!("unsupported random input {name}")), + }; + args.insert(input.name.to_string(), value); + } + return (random.generate)(Value::Object(args)).map_err(|error| error.to_string()); + } + if entry.source_name == "MaximumLikelihoodRanking" { + let n = 3 + next(&mut seed) as usize % 3; + let mut matrix = vec![vec![0; n]; n]; + for i in 0..n { + let (earlier, later) = matrix.split_at_mut(i + 1); + for (offset, row) in later.iter_mut().enumerate() { + let j = i + offset + 1; + earlier[i][j] = (next(&mut seed) % 6) as i64; + row[i] = 5 - earlier[i][j]; + } + } + return Ok(Box::new( + crate::models::misc::MaximumLikelihoodRanking::new(matrix), + )); + } + if entry.source_name == "OptimumCommunicationSpanningTree" { + let n = 3 + next(&mut seed) as usize % 2; + let mut weights = vec![vec![0; n]; n]; + let mut requirements = vec![vec![0; n]; n]; + for i in 0..n { + for j in (i + 1)..n { + weights[i][j] = 1 + (next(&mut seed) % 4) as i64; + weights[j][i] = weights[i][j]; + requirements[i][j] = 1 + (next(&mut seed) % 3) as i64; + requirements[j][i] = requirements[i][j]; + } + } + return Ok(Box::new( + crate::models::misc::OptimumCommunicationSpanningTree::new(weights, requirements), + )); + } + if entry.source_name == "DecisionOpenShopScheduling" { + let time = 1 + (next(&mut seed) % 3) as i64; + return Ok(Box::new(crate::models::decision::Decision::new( + crate::models::misc::OpenShopScheduling::new(2, vec![vec![time, 1], vec![2, time]]), + 4, + ))); + } + if entry.source_name == "ExpectedRetrievalCost" { + let records = 2 + next(&mut seed) as usize % 3; + return Ok(Box::new( + crate::models::misc::ExpectedRetrievalCost::new(vec![1.0 / records as f64; records], 2) + .unwrap(), + )); + } + if entry.source_name == "ILP" && variant.get("variable").is_some_and(|v| v == "i64") { + use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; + let upper = 2 + (next(&mut seed) % 4) as i64; + return Ok(Box::new( + ILP::::with_variables( + vec![IntegerVariable::new(Some(0), Some(upper)).unwrap()], + vec![LinearConstraint::le(vec![(0, 1)], upper)], + vec![(0, 1)], + ObjectiveSense::Minimize, + ) + .unwrap(), + )); + } + + let key = (entry.source_name.to_string(), variant.clone()); + let examples = sources + .get(&key) + .ok_or_else(|| format!("no canonical source for {key:?}"))?; + let base = &examples[next(&mut seed) as usize % examples.len()]; + let base_params = load_dyn(entry.source_name, &variant, base.clone()) + .map_err(|error| error.to_string())? + .parameters_dyn(); + let use_constructor = (registered.construct_fn)(base.clone()).is_ok(); + let mut candidates = variations(base); + // Check a seeded permutation of the candidates, preferring an instance + // with different measured source parameters. + let mut same_size = None; + while !candidates.is_empty() { + let index = next(&mut seed) as usize % candidates.len(); + let candidate = candidates.swap_remove(index); + if candidate == *base { + continue; + } + let problem = if use_constructor { + (registered.construct_fn)(candidate).ok() + } else { + (registered.factory)(candidate).ok() + }; + let Some(problem) = problem else { + continue; + }; + if target_parameters(entry, problem.as_ref()).is_err() { + continue; + } + if problem.parameters_dyn() != base_params { + return Ok(problem); + } + same_size.get_or_insert(problem); + } + same_size.ok_or_else(|| format!("no valid variation for {key:?}: {base}")) +} + +fn target_parameters( + entry: &ReductionEntry, + source: &dyn DynProblem, +) -> Result { + let variant = ReductionGraph::variant_to_map(&entry.target_variant()); + if let Some(reduce) = entry.reduce_fn { + let reduced = reduce(source.as_any()).map_err(|error| error.to_string())?; + return Ok(ReductionGraph::compute_problem_parameters( + entry.target_name, + &variant, + reduced.target_problem_any(), + )); + } + let source_json = source.serialize_json(); + let target_json = if entry.turing { + json!({"inner": source_json, "bound": 2}) + } else if entry.source_name == "MinimumVertexCover" + && entry.target_name == "MinimumMaximalMatching" + { + json!({"graph": source_json["graph"]}) + } else if entry.source_name == "SubsetSum" && entry.target_name == "IntegerKnapsack" { + let sizes: Vec = source_json["sizes"] + .as_array() + .ok_or("SubsetSum sizes are not an array")? + .iter() + .map(|item| { + item.as_str() + .ok_or("size is not a string")? + .parse() + .map_err(|error| format!("{error}")) + }) + .collect::>()?; + let capacity: i64 = source_json["target"] + .as_str() + .ok_or("target is not a string")? + .parse() + .map_err(|error| format!("{error}"))?; + json!({"sizes": sizes, "values": sizes, "capacity": capacity}) + } else { + return Err("no executable reduction or test construction".into()); + }; + let target = + load_dyn(entry.target_name, &variant, target_json).map_err(|error| error.to_string())?; + Ok(target.parameters_dyn()) +} + +#[test] +fn every_exact_field_matches_a_random_reduced_instance() { + let sources = canonical_sources(); + let mut checked = 0; + let mut expected = 0; + let mut failures = Vec::new(); + for entry in crate::rules::registry::reduction_entries() { + let contract = entry.parameter_contract().unwrap(); + let Some(transform) = contract.transform() else { + continue; + }; + if transform.relation() != ParameterRelation::Exact { + continue; + } + expected += transform.expressions().count(); + let label = format!( + "{} {:?} -> {} {:?}", + entry.source_name, + entry.source_variant(), + entry.target_name, + entry.target_variant() + ); + let result = (|| { + let source = random_source(entry, &sources)?; + let actual = target_parameters(entry, source.as_ref())?; + let predicted = transform + .evaluate(&source.parameters_dyn()) + .map_err(|error| error.to_string())?; + for (field, _) in transform.expressions() { + if predicted.get(field) != actual.get(field) { + return Err(format!( + "{field}: predicted {:?}, measured {:?}; source {}", + predicted.get(field), + actual.get(field), + source.serialize_json() + )); + } + checked += 1; + } + Ok::<_, String>(()) + })(); + if let Err(error) = result { + failures.push(format!("{label}: {error}")); + } + } + assert!(expected > 0); + assert!(failures.is_empty(), "{}", failures.join("\n")); + assert_eq!(checked, expected, "some exact fields were not checked"); +} diff --git a/src/unit_tests/models/algebraic/qubo.rs b/src/unit_tests/models/algebraic/qubo.rs index 2c9539116..f76f651ff 100644 --- a/src/unit_tests/models/algebraic/qubo.rs +++ b/src/unit_tests/models/algebraic/qubo.rs @@ -95,6 +95,14 @@ fn test_num_variables() { assert_eq!(problem.num_variables(), 5); } +#[test] +fn quadratic_term_count_excludes_diagonal_coefficients() { + let problem = + QUBO::::from_entries(4, vec![(0, 0, 2.0), (0, 1, 3.0), (1, 3, -1.0), (3, 3, 4.0)]) + .unwrap(); + assert_eq!(problem.parameters().get("num_quadratic_terms"), Some(2)); +} + #[test] fn test_matrix_access() { let problem = QUBO::from_matrix(vec![ diff --git a/src/unit_tests/rules/spinglass_qubo.rs b/src/unit_tests/rules/spinglass_qubo.rs index 7894a553b..97de04644 100644 --- a/src/unit_tests/rules/spinglass_qubo.rs +++ b/src/unit_tests/rules/spinglass_qubo.rs @@ -71,6 +71,18 @@ fn test_reduction_structure() { assert_eq!(qubo2.num_variables(), 3); } +#[test] +fn sparse_qubo_interactions_match_nonzero_quadratic_terms() { + let qubo = QUBO::::from_entries( + 4, + vec![(0, 0, 2.0), (0, 1, 1e-11), (1, 3, -2.0), (3, 3, 4.0)], + ) + .unwrap(); + let reduction = ReduceTo::>::reduce_to(&qubo).unwrap(); + assert_eq!(qubo.num_quadratic_terms(), 2); + assert_eq!(reduction.target_problem().num_interactions(), 2); +} + #[test] fn test_jl_parity_spinglass_to_qubo() { let data: serde_json::Value = serde_json::from_str(include_str!( From 464da1ca4340c5a050564ea20ab985e706c2421a Mon Sep 17 00:00:00 2001 From: Xiwei Pan <90967972+isPANN@users.noreply.github.com> Date: Sat, 26 Sep 2026 04:36:19 -0700 Subject: [PATCH 4/9] Check exact and upper-bound reduction parameters --- src/lib.rs | 4 +-- ...zed.rs => parameter_formula_validation.rs} | 36 +++++++++++++------ 2 files changed, 28 insertions(+), 12 deletions(-) rename src/unit_tests/{exact_parameter_randomized.rs => parameter_formula_validation.rs} (90%) diff --git a/src/lib.rs b/src/lib.rs index e9af9164d..afeac7aa1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -136,8 +136,8 @@ pub use problemreductions_macros::{ pub use inventory; #[cfg(all(test, feature = "example-db"))] -#[path = "unit_tests/exact_parameter_randomized.rs"] -mod exact_parameter_randomized; +#[path = "unit_tests/parameter_formula_validation.rs"] +mod parameter_formula_validation; #[cfg(all(test, feature = "example-db"))] #[path = "unit_tests/symbolic_parameter_contracts.rs"] mod symbolic_parameter_contracts; diff --git a/src/unit_tests/exact_parameter_randomized.rs b/src/unit_tests/parameter_formula_validation.rs similarity index 90% rename from src/unit_tests/exact_parameter_randomized.rs rename to src/unit_tests/parameter_formula_validation.rs index a5a6aeccb..2f8083438 100644 --- a/src/unit_tests/exact_parameter_randomized.rs +++ b/src/unit_tests/parameter_formula_validation.rs @@ -94,9 +94,10 @@ fn variations(value: &Value) -> Vec { } } -fn random_source( +fn sampled_source( entry: &ReductionEntry, sources: &BTreeMap>, + allow_canonical: bool, ) -> Result, String> { let variant = ReductionGraph::variant_to_map(&entry.source_variant()); let registered = crate::registry::find_variant_entry(entry.source_name, &variant).unwrap(); @@ -214,7 +215,13 @@ fn random_source( } same_size.get_or_insert(problem); } - same_size.ok_or_else(|| format!("no valid variation for {key:?}: {base}")) + if let Some(problem) = same_size { + return Ok(problem); + } + if allow_canonical { + return (registered.factory)(base.clone()).map_err(|error| error.to_string()); + } + Err(format!("no valid variation for {key:?}: {base}")) } fn target_parameters( @@ -264,7 +271,7 @@ fn target_parameters( } #[test] -fn every_exact_field_matches_a_random_reduced_instance() { +fn every_parameter_formula_matches_a_constructed_target() { let sources = canonical_sources(); let mut checked = 0; let mut expected = 0; @@ -274,9 +281,6 @@ fn every_exact_field_matches_a_random_reduced_instance() { let Some(transform) = contract.transform() else { continue; }; - if transform.relation() != ParameterRelation::Exact { - continue; - } expected += transform.expressions().count(); let label = format!( "{} {:?} -> {} {:?}", @@ -286,15 +290,27 @@ fn every_exact_field_matches_a_random_reduced_instance() { entry.target_variant() ); let result = (|| { - let source = random_source(entry, &sources)?; + let source = sampled_source( + entry, + &sources, + transform.relation() == ParameterRelation::UpperBound, + )?; let actual = target_parameters(entry, source.as_ref())?; let predicted = transform .evaluate(&source.parameters_dyn()) .map_err(|error| error.to_string())?; for (field, _) in transform.expressions() { - if predicted.get(field) != actual.get(field) { + let valid = match (predicted.get(field), actual.get(field)) { + (Some(predicted), Some(actual)) => match transform.relation() { + ParameterRelation::Exact => predicted == actual, + ParameterRelation::UpperBound => predicted >= actual, + }, + _ => false, + }; + if !valid { return Err(format!( - "{field}: predicted {:?}, measured {:?}; source {}", + "{field} ({:?}): predicted {:?}, measured {:?}; source {}", + transform.relation(), predicted.get(field), actual.get(field), source.serialize_json() @@ -310,5 +326,5 @@ fn every_exact_field_matches_a_random_reduced_instance() { } assert!(expected > 0); assert!(failures.is_empty(), "{}", failures.join("\n")); - assert_eq!(checked, expected, "some exact fields were not checked"); + assert_eq!(checked, expected, "some formula fields were not checked"); } From 84f1bd2679aca4e4d72cd0bd5b9ba80f9dd26b1d Mon Sep 17 00:00:00 2001 From: Xiwei Pan <90967972+isPANN@users.noreply.github.com> Date: Sat, 26 Sep 2026 04:54:19 -0700 Subject: [PATCH 5/9] Document and simplify parameter formula validation --- .claude/CLAUDE.md | 1 + .claude/skills/add-rule/SKILL.md | 2 + .../parameter_formula_validation.rs | 268 ++++-------------- 3 files changed, 65 insertions(+), 206 deletions(-) diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 44934dcf8..aac07d49a 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -351,3 +351,4 @@ Parameter expressions describe how target problem parameters relate to source pr 3. Watch for common errors: universe elements mismatch (edge indices vs vertex indices), worst-case edge counts in intersection graphs (quadratic, not linear), constant factors in circuit constructions 4. Test with concrete small instances: construct a source problem, run the reduction, and compare target parameters against the formula 5. Ensure there is only one primitive reduction registration for each exact source/target variant pair; wrap shared helpers instead of registering duplicate endpoints +6. Every new rule's `exact` and `upper_bound` fields must be covered by `src/unit_tests/parameter_formula_validation.rs`: evaluate each formula on a valid source instance and compare it with measured target parameters (equal for `exact`, predicted ≥ measured for `upper_bound`). Provide a canonical example or registered generator so the shared test can exercise the rule; add targeted boundary cases when needed. diff --git a/.claude/skills/add-rule/SKILL.md b/.claude/skills/add-rule/SKILL.md index 3097be526..83f4cfecf 100644 --- a/.claude/skills/add-rule/SKILL.md +++ b/.claude/skills/add-rule/SKILL.md @@ -168,6 +168,8 @@ Add to `src/rules/mod.rs`: Create `src/unit_tests/rules/_.rs`: +Follow the [reduction parameter testing requirement](../../CLAUDE.md#reduction-parameter-relation): the shared test must cover every declared `exact` or `upper_bound` field using a valid source instance. + **Required: closed-loop test** (`test__to__closed_loop`): ```rust // 1. Create source problem instance diff --git a/src/unit_tests/parameter_formula_validation.rs b/src/unit_tests/parameter_formula_validation.rs index 2f8083438..8709c0c8f 100644 --- a/src/unit_tests/parameter_formula_validation.rs +++ b/src/unit_tests/parameter_formula_validation.rs @@ -1,34 +1,11 @@ use crate::parameters::ParameterRelation; -use crate::registry::{load_dyn, DynProblem}; +use crate::registry::DynProblem; use crate::rules::{registry::ReductionEntry, ReductionGraph}; use serde_json::{json, Value}; use std::collections::BTreeMap; type SourceKey = (String, BTreeMap); -fn next(seed: &mut u64) -> u64 { - *seed ^= *seed << 13; - *seed ^= *seed >> 7; - *seed ^= *seed << 17; - *seed -} - -fn seed_for(entry: &ReductionEntry) -> u64 { - let mut seed = 0x9e37_79b9_7f4a_7c15_u64; - for byte in format!( - "{}{:?}{}{:?}", - entry.source_name, - entry.source_variant(), - entry.target_name, - entry.target_variant() - ) - .bytes() - { - seed = seed.wrapping_mul(0x100_0000_01b3) ^ u64::from(byte); - } - seed -} - fn canonical_sources() -> BTreeMap> { let mut sources = BTreeMap::>::new(); let db = crate::example_db::build_example_db().unwrap(); @@ -40,13 +17,14 @@ fn canonical_sources() -> BTreeMap> { } for rule in db.rules { let source = rule.source; - if let Some(inner) = source.instance.get("inner") { - if let Some(name) = source.problem.strip_prefix("Decision") { - sources - .entry((name.to_string(), source.variant.clone())) - .or_default() - .push(inner.clone()); - } + if let (Some(name), Some(inner)) = ( + source.problem.strip_prefix("Decision"), + source.instance.get("inner"), + ) { + sources + .entry((name.to_string(), source.variant.clone())) + .or_default() + .push(inner.clone()); } sources .entry((source.problem, source.variant)) @@ -56,174 +34,6 @@ fn canonical_sources() -> BTreeMap> { sources } -// These changes preserve the element type and start from an independently -// constructed canonical instance. Deserialization and reduction must both accept -// a candidate before it is used as a test instance. -fn variations(value: &Value) -> Vec { - match value { - Value::Array(items) => { - let mut result = Vec::new(); - if items.len() > 1 { - let mut shorter = items.clone(); - shorter.pop(); - result.push(Value::Array(shorter)); - let mut reordered = items.clone(); - reordered.rotate_left(1); - result.push(Value::Array(reordered)); - } - for (index, item) in items.iter().enumerate() { - for changed in variations(item) { - let mut copy = items.clone(); - copy[index] = changed; - result.push(Value::Array(copy)); - } - } - result - } - Value::Object(fields) => fields - .iter() - .flat_map(|(key, child)| { - variations(child).into_iter().map(move |changed| { - let mut copy = fields.clone(); - copy.insert(key.clone(), changed); - Value::Object(copy) - }) - }) - .collect(), - _ => Vec::new(), - } -} - -fn sampled_source( - entry: &ReductionEntry, - sources: &BTreeMap>, - allow_canonical: bool, -) -> Result, String> { - let variant = ReductionGraph::variant_to_map(&entry.source_variant()); - let registered = crate::registry::find_variant_entry(entry.source_name, &variant).unwrap(); - let mut seed = seed_for(entry); - if let Some(random) = registered.random { - let mut args = serde_json::Map::new(); - for input in (random.inputs)() { - let value = match input.name { - "num_vertices" => json!(4 + next(&mut seed) % 3), - "seed" => json!((next(&mut seed) >> 1) as i64), - "k" => json!(if variant.get("k").is_some_and(|value| value == "K3") { - 3 - } else { - 2 - }), - "bound" => json!(2), - _ if !input.required => continue, - name => return Err(format!("unsupported random input {name}")), - }; - args.insert(input.name.to_string(), value); - } - return (random.generate)(Value::Object(args)).map_err(|error| error.to_string()); - } - if entry.source_name == "MaximumLikelihoodRanking" { - let n = 3 + next(&mut seed) as usize % 3; - let mut matrix = vec![vec![0; n]; n]; - for i in 0..n { - let (earlier, later) = matrix.split_at_mut(i + 1); - for (offset, row) in later.iter_mut().enumerate() { - let j = i + offset + 1; - earlier[i][j] = (next(&mut seed) % 6) as i64; - row[i] = 5 - earlier[i][j]; - } - } - return Ok(Box::new( - crate::models::misc::MaximumLikelihoodRanking::new(matrix), - )); - } - if entry.source_name == "OptimumCommunicationSpanningTree" { - let n = 3 + next(&mut seed) as usize % 2; - let mut weights = vec![vec![0; n]; n]; - let mut requirements = vec![vec![0; n]; n]; - for i in 0..n { - for j in (i + 1)..n { - weights[i][j] = 1 + (next(&mut seed) % 4) as i64; - weights[j][i] = weights[i][j]; - requirements[i][j] = 1 + (next(&mut seed) % 3) as i64; - requirements[j][i] = requirements[i][j]; - } - } - return Ok(Box::new( - crate::models::misc::OptimumCommunicationSpanningTree::new(weights, requirements), - )); - } - if entry.source_name == "DecisionOpenShopScheduling" { - let time = 1 + (next(&mut seed) % 3) as i64; - return Ok(Box::new(crate::models::decision::Decision::new( - crate::models::misc::OpenShopScheduling::new(2, vec![vec![time, 1], vec![2, time]]), - 4, - ))); - } - if entry.source_name == "ExpectedRetrievalCost" { - let records = 2 + next(&mut seed) as usize % 3; - return Ok(Box::new( - crate::models::misc::ExpectedRetrievalCost::new(vec![1.0 / records as f64; records], 2) - .unwrap(), - )); - } - if entry.source_name == "ILP" && variant.get("variable").is_some_and(|v| v == "i64") { - use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; - let upper = 2 + (next(&mut seed) % 4) as i64; - return Ok(Box::new( - ILP::::with_variables( - vec![IntegerVariable::new(Some(0), Some(upper)).unwrap()], - vec![LinearConstraint::le(vec![(0, 1)], upper)], - vec![(0, 1)], - ObjectiveSense::Minimize, - ) - .unwrap(), - )); - } - - let key = (entry.source_name.to_string(), variant.clone()); - let examples = sources - .get(&key) - .ok_or_else(|| format!("no canonical source for {key:?}"))?; - let base = &examples[next(&mut seed) as usize % examples.len()]; - let base_params = load_dyn(entry.source_name, &variant, base.clone()) - .map_err(|error| error.to_string())? - .parameters_dyn(); - let use_constructor = (registered.construct_fn)(base.clone()).is_ok(); - let mut candidates = variations(base); - // Check a seeded permutation of the candidates, preferring an instance - // with different measured source parameters. - let mut same_size = None; - while !candidates.is_empty() { - let index = next(&mut seed) as usize % candidates.len(); - let candidate = candidates.swap_remove(index); - if candidate == *base { - continue; - } - let problem = if use_constructor { - (registered.construct_fn)(candidate).ok() - } else { - (registered.factory)(candidate).ok() - }; - let Some(problem) = problem else { - continue; - }; - if target_parameters(entry, problem.as_ref()).is_err() { - continue; - } - if problem.parameters_dyn() != base_params { - return Ok(problem); - } - same_size.get_or_insert(problem); - } - if let Some(problem) = same_size { - return Ok(problem); - } - if allow_canonical { - return (registered.factory)(base.clone()).map_err(|error| error.to_string()); - } - Err(format!("no valid variation for {key:?}: {base}")) -} - fn target_parameters( entry: &ReductionEntry, source: &dyn DynProblem, @@ -265,11 +75,61 @@ fn target_parameters( } else { return Err("no executable reduction or test construction".into()); }; - let target = - load_dyn(entry.target_name, &variant, target_json).map_err(|error| error.to_string())?; + let target = crate::registry::load_dyn(entry.target_name, &variant, target_json) + .map_err(|error| error.to_string())?; Ok(target.parameters_dyn()) } +fn source_for( + entry: &ReductionEntry, + sources: &BTreeMap>, +) -> Result, String> { + let variant = ReductionGraph::variant_to_map(&entry.source_variant()); + let registered = crate::registry::find_variant_entry(entry.source_name, &variant).unwrap(); + let key = (entry.source_name.to_string(), variant.clone()); + if let Some(examples) = sources.get(&key) { + for example in examples { + if let Ok(source) = (registered.factory)(example.clone()) { + if target_parameters(entry, source.as_ref()).is_ok() { + return Ok(source); + } + } + } + } + if entry.source_name == "ILP" && variant.get("variable").is_some_and(|v| v == "i64") { + use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; + return Ok(Box::new( + ILP::::with_variables( + vec![IntegerVariable::new(Some(0), Some(3)).unwrap()], + vec![LinearConstraint::le(vec![(0, 1)], 3)], + vec![(0, 1)], + ObjectiveSense::Minimize, + ) + .unwrap(), + )); + } + let random = registered + .random + .ok_or_else(|| format!("no usable canonical source for {key:?}"))?; + let mut args = serde_json::Map::new(); + for input in (random.inputs)() { + let value = match input.name { + "num_vertices" => json!(5), + "seed" => json!(42), + "k" => json!(if variant.get("k").is_some_and(|v| v == "K3") { + 3 + } else { + 2 + }), + "bound" => json!(2), + _ if !input.required => continue, + name => return Err(format!("unsupported random input {name}")), + }; + args.insert(input.name.to_string(), value); + } + (random.generate)(Value::Object(args)).map_err(|error| error.to_string()) +} + #[test] fn every_parameter_formula_matches_a_constructed_target() { let sources = canonical_sources(); @@ -290,11 +150,7 @@ fn every_parameter_formula_matches_a_constructed_target() { entry.target_variant() ); let result = (|| { - let source = sampled_source( - entry, - &sources, - transform.relation() == ParameterRelation::UpperBound, - )?; + let source = source_for(entry, &sources)?; let actual = target_parameters(entry, source.as_ref())?; let predicted = transform .evaluate(&source.parameters_dyn()) From f743491893267549aeeb2bf37c614d2aa6827618 Mon Sep 17 00:00:00 2001 From: Xiwei Pan <90967972+isPANN@users.noreply.github.com> Date: Sun, 27 Sep 2026 07:53:11 -0700 Subject: [PATCH 6/9] Improve parameter prediction contracts and bound integer ILP reductions --- .claude/CLAUDE.md | 9 +- docs/src/design.md | 38 +++- problemreductions-cli/src/commands/graph.rs | 119 ++++------- problemreductions-cli/src/test_support.rs | 4 +- problemreductions-cli/tests/cli_tests.rs | 45 ++++ problemreductions-macros/src/lib.rs | 202 ++++++++++-------- src/models/decision.rs | 8 +- src/models/graph/integral_flow_bundles.rs | 2 +- .../graph/path_constrained_network_flow.rs | 2 +- src/parameters.rs | 131 +++++++++--- src/rules/acyclicpartition_ilp.rs | 18 +- .../balancedcompletebipartitesubgraph_ilp.rs | 14 +- src/rules/biconnectivityaugmentation_ilp.rs | 14 +- src/rules/binpacking_ilp.rs | 12 +- src/rules/bottlenecktravelingsalesman_ilp.rs | 21 +- .../boundedcomponentspanningforest_ilp.rs | 23 +- src/rules/capacityassignment_ilp.rs | 12 +- src/rules/circuit_ilp.rs | 14 +- src/rules/closeststring_ilp.rs | 15 +- src/rules/closestsubstring_ilp.rs | 23 +- src/rules/clustering_ilp.rs | 14 +- src/rules/consecutiveblockminimization_ilp.rs | 14 +- .../consecutiveonesmatrixaugmentation_ilp.rs | 12 +- src/rules/consecutiveonessubmatrix_ilp.rs | 14 +- src/rules/directedhamiltonianpath_ilp.rs | 14 +- .../directedtwocommodityintegralflow_ilp.rs | 28 ++- src/rules/disjointconnectingpaths_ilp.rs | 12 +- src/rules/ensemblecomputation_ilp.rs | 21 +- src/rules/eulerianpath_ilp.rs | 26 +-- src/rules/factoring_ilp.rs | 23 +- src/rules/feasibleregisterassignment_ilp.rs | 10 +- src/rules/flowshopscheduling_ilp.rs | 19 +- src/rules/graph.rs | 33 +-- src/rules/graphpartitioning_ilp.rs | 12 +- src/rules/ilp_qubo.rs | 13 +- src/rules/integerknapsack_ilp.rs | 18 +- src/rules/integralflowbundles_ilp.rs | 30 +-- src/rules/integralflowhomologousarcs_ilp.rs | 27 +-- src/rules/integralflowwithmultipliers_ilp.rs | 25 ++- src/rules/isomorphicspanningtree_ilp.rs | 14 +- src/rules/kclique_ilp.rs | 14 +- src/rules/knapsack_ilp.rs | 12 +- src/rules/ksatisfiability_qubo.rs | 20 +- src/rules/lengthboundeddisjointpaths_ilp.rs | 14 +- src/rules/longestcircuit_ilp.rs | 24 +-- src/rules/longestpath_ilp.rs | 28 ++- src/rules/maximalis_ilp.rs | 12 +- src/rules/maximum2satisfiability_ilp.rs | 12 +- src/rules/maximumclique_ilp.rs | 14 +- src/rules/maximumcokplex_ilp.rs | 24 +-- src/rules/maximumcommonedgesubgraph_ilp.rs | 14 +- src/rules/maximumdomaticnumber_ilp.rs | 12 +- src/rules/maximumedgeweightedkclique_ilp.rs | 24 +-- src/rules/maximumleafspanningtree_ilp.rs | 25 ++- src/rules/maximummatching_ilp.rs | 12 +- src/rules/maximumsetpacking_ilp.rs | 14 +- src/rules/maximumsetpacking_qubo.rs | 12 +- .../minimumcapacitatedspanningtree_ilp.rs | 31 +-- src/rules/minimumcoveringbycliques_ilp.rs | 12 +- src/rules/minimumcutintoboundedsets_ilp.rs | 12 +- ...mumdiscreteplanarinversekinematics_qubo.rs | 11 +- src/rules/minimumdominatingset_ilp.rs | 12 +- src/rules/minimumedgecostflow_ilp.rs | 31 ++- ...minimumexternalmacrodatacompression_ilp.rs | 14 +- src/rules/minimumfaultdetectiontestset_ilp.rs | 12 +- src/rules/minimumfeedbackarcset_ilp.rs | 25 ++- src/rules/minimumfeedbackvertexset_ilp.rs | 25 ++- src/rules/minimumgraphbandwidth_ilp.rs | 25 ++- src/rules/minimumhittingset_ilp.rs | 12 +- ...minimuminternalmacrodatacompression_ilp.rs | 14 +- src/rules/minimummaximalmatching_ilp.rs | 12 +- src/rules/minimummetricdimension_ilp.rs | 12 +- src/rules/minimummultiwaycut_ilp.rs | 12 +- src/rules/minimummultiwaycut_qubo.rs | 11 +- src/rules/minimumsetcovering_ilp.rs | 12 +- src/rules/minimumsummulticenter_ilp.rs | 14 +- src/rules/minimumtardinesssequencing_ilp.rs | 24 +-- ...nimumvertexcover_minimummaximalmatching.rs | 6 +- src/rules/minimumweightdecoding_ilp.rs | 32 ++- src/rules/minmaxmulticenter_ilp.rs | 23 +- src/rules/mixedchinesepostman_ilp.rs | 30 +-- src/rules/monochromatictriangle_ilp.rs | 14 +- src/rules/multiplechoicebranching_ilp.rs | 35 +-- src/rules/multiplecopyfileallocation_ilp.rs | 12 +- src/rules/multiprocessorscheduling_ilp.rs | 12 +- src/rules/naesatisfiability_ilp.rs | 12 +- .../numericalmatchingwithtargetsums_ilp.rs | 14 +- src/rules/openshopscheduling_ilp.rs | 41 ++-- src/rules/optimallineararrangement_ilp.rs | 25 ++- .../optimumcommunicationspanningtree_ilp.rs | 12 +- src/rules/paintshop_ilp.rs | 14 +- src/rules/paintshop_qubo.rs | 11 +- src/rules/partiallyorderedknapsack_ilp.rs | 12 +- src/rules/partitionintocliques_ilp.rs | 14 +- src/rules/partitionintopathsoflength2_ilp.rs | 14 +- src/rules/partitionintotriangles_ilp.rs | 14 +- src/rules/pathconstrainednetworkflow_ilp.rs | 25 ++- .../precedenceconstrainedscheduling_ilp.rs | 12 +- src/rules/preemptivescheduling_ilp.rs | 31 ++- .../rectilinearpicturecompression_ilp.rs | 14 +- src/rules/registersufficiency_ilp.rs | 12 +- src/rules/registry.rs | 38 +--- .../resourceconstrainedscheduling_ilp.rs | 12 +- src/rules/rootedtreestorageassignment_ilp.rs | 24 ++- src/rules/ruralpostman_ilp.rs | 32 ++- ...ingtominimizeweightedcompletiontime_ilp.rs | 29 ++- .../schedulingwithindividualdeadlines_ilp.rs | 12 +- ...cingtominimizemaximumcumulativecost_ilp.rs | 32 ++- ...sequencingtominimizetardytaskweight_ilp.rs | 12 +- ...ingtominimizeweightedcompletiontime_ilp.rs | 29 ++- ...quencingtominimizeweightedtardiness_ilp.rs | 19 +- ...equencingwithdeadlinesandsetuptimes_ilp.rs | 10 +- src/rules/sequencingwithinintervals_ilp.rs | 14 +- ...uencingwithreleasetimesanddeadlines_ilp.rs | 10 +- src/rules/setsplitting_ilp.rs | 12 +- src/rules/shortestcommonsupersequence_ilp.rs | 14 +- .../shortestweightconstrainedpath_ilp.rs | 24 ++- src/rules/sparsematrixcompression_ilp.rs | 14 +- src/rules/spinglass_qubo.rs | 24 +-- src/rules/stackercrane_ilp.rs | 14 +- src/rules/steinertree_ilp.rs | 12 +- src/rules/stringtostringcorrection_ilp.rs | 14 +- .../strongconnectivityaugmentation_ilp.rs | 21 +- src/rules/subgraphisomorphism_ilp.rs | 14 +- src/rules/subsetsum_integerknapsack.rs | 4 +- src/rules/timetabledesign_ilp.rs | 14 +- src/rules/travelingsalesman_ilp.rs | 12 +- src/rules/travelingsalesman_qubo.rs | 12 +- src/rules/undirectedflowlowerbounds_ilp.rs | 27 ++- .../undirectedtwocommodityintegralflow_ilp.rs | 25 ++- .../parameter_formula_validation.rs | 67 +++++- src/unit_tests/parameters.rs | 106 ++++++++- src/unit_tests/reduction_graph.rs | 9 +- src/unit_tests/rules/graph.rs | 28 ++- src/unit_tests/rules/ilp_qubo.rs | 56 +++++ src/unit_tests/rules/ksatisfiability_qubo.rs | 45 ++++ src/unit_tests/rules/maximummatching_ilp.rs | 29 +++ src/unit_tests/rules/maximumsetpacking_ilp.rs | 3 +- .../rules/minimumfeedbackarcset_ilp.rs | 23 ++ src/unit_tests/rules/registry.rs | 19 +- .../symbolic_parameter_contracts.rs | 13 +- 141 files changed, 1853 insertions(+), 1241 deletions(-) diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index aac07d49a..86e96ad3d 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -179,7 +179,7 @@ Max, Min, Sum, Or, And, Extremum, ExtremumSense - `NumericSize` supertrait bundles common numeric bounds (`Clone + Default + PartialOrd + Num + Zero + Bounded + AddAssign + 'static`) ### Parameter Relations -Each reduction declares one rule-level parameter relation using the `Expr` AST in `src/expr.rs`. The `transform` declaration is required: +Each reduction declares explicit per-field parameter relations using the `Expr` AST in `src/expr.rs`. The `transform` declaration is required: ```rust #[reduction(transform = upper_bound { num_vertices = "num_vertices + num_clauses", @@ -189,11 +189,12 @@ impl ReduceTo for Source { ... } ``` - Expression strings are parsed at compile time by a Pratt parser in the proc macro crate - Variable names are validated against the source problem's canonical parameter schema -- Use `transform = exact { ... }` when every formula is an equality and `transform = upper_bound { ... }` when every formula is only an upper bound. One expression block cannot mix relations. +- Use `transform = exact { ... }` when every formula is an equality and `transform = upper_bound { ... }` when every formula is only an upper bound. For mixed accuracy, use `transform = { exact { ... }, upper_bound { ... }, unavailable { ... } }`. Every formula RHS uses only registered source parameters; derive target relationships and substitute source expressions before declaring them. - Use `transform = unavailable { ... }` when no formula is representable, or an auxiliary `unavailable = { ... }` block for omitted target parameters. - Every target parameter must appear exactly once as a formula or as unavailable with a non-empty reason. -- `ParameterTransform` evaluates and composes formulas with exact rational and arbitrary-precision integer arithmetic. Unsafe upper-bound composition becomes unavailable; it never performs budget pruning or path ranking. +- `ParameterTransform` evaluates and composes formulas with exact rational and arbitrary-precision integer arithmetic. Composition preserves independent fields and their accuracy. An unavailable dependency or unsafe upper-bound substitution makes only the affected field unavailable; it never performs budget pruning or path ranking. - Concrete instance parameters come from each endpoint instance's `Problem::parameters()` implementation; `ReductionEntry` stores only the symbolic parameter relation. +- Rules producing `ILP` must declare known finite variable domains with `ILP::with_variables`; constraint rows alone do not supply bounds to binary encoding. Bounds on auxiliary variables must preserve feasibility and the optimum. Document genuinely unbounded variables rather than inventing a cutoff. - `VariantEntry` has both a complexity string and compiled `complexity_eval_fn` — same pattern - Expressions support: constants, variables, `+`, `-`, `*`, `/`, `^`, `exp()`, `log()`, `sqrt()`, `factorial()` - Complexity strings must use **concrete numeric values only** (e.g., `"2^(2.372 * num_vertices / 3)"`, not `"2^(omega * num_vertices / 3)"`) @@ -212,7 +213,7 @@ Reduction graph nodes use variant key-value pairs from `Problem::variant()`: - Same-name variant relations are explicit `#[reduction]` registrations - Each primitive reduction is determined by the exact `(source_variant, target_variant)` endpoint pair - Reduction edges carry `EdgeCapabilities { witness, aggregate, turing }`; graph search defaults to witness mode, aggregate mode is available through `ReductionMode::Aggregate`, and Turing (multi-query) mode via `ReductionMode::Turing` -- `#[reduction]` requires one `transform = exact`, `transform = upper_bound`, or `transform = unavailable` declaration and currently registers witness/config reductions; aggregate-only and Turing edges require manual `ReductionEntry` registration +- `#[reduction]` requires one uniform or mixed `transform` declaration and currently registers witness/config reductions; aggregate-only and Turing edges require manual `ReductionEntry` registration - `Decision

→ P` supports both mappings: compare the exact optimum to the bound, and recover a witness only if it meets the bound. `P → Decision

` is a Turing edge (binary search over decision bound). ### Extension Points diff --git a/docs/src/design.md b/docs/src/design.md index 4caa126b6..ebe73dac6 100644 --- a/docs/src/design.md +++ b/docs/src/design.md @@ -423,10 +423,10 @@ All path-finding operates on **exact variant nodes**. Use `ReductionGraph::varia | `find_all_paths(src, src_var, dst, dst_var)` | All simple paths | Enumerate every route | | `compose_path_parameter_transform(path)` | Symbolic composition | Compose each rule's exact or upper-bound parameter relation while preserving its promise | -A rule has one relation for all of its formulas: either an exact equality or an upper -bound. Composition keeps exact formulas exact only when every step is exact; every other -combination is an upper bound. Concrete-instance measurement remains a separate execution -API. +Each formula has its own relation: exact equality or upper bound. Composition preserves +exactness when the formula and its required inputs are exact. Bounded inputs require sound +upper-bound substitution. Unavailable fields affect only formulas that depend on them; +independent formulas survive. Concrete-instance measurement remains a separate execution API. **Example:** Finding a path from `MIS{KingsSubgraph, i64}` to `VC{SimpleGraph, i64}`: @@ -450,8 +450,10 @@ The returned `ReductionChain` stores each intermediate reduction and extracts th

Parameter contracts -Each reduction declares one relation for all represented target-parameter fields and may mark -other fields unavailable with a reason. The `#[reduction]` macro parses every formula into +Each reduction classifies every target parameter exactly once as exact, upper bound, or +unavailable with a reason. Every formula uses only registered source parameters on its RHS. +Target structural relationships may justify a formula, but source expressions must be +substituted before registration; there is no automatic model-level inference. The `#[reduction]` macro parses every formula into the canonical `Expr` DAG at compile time: ```rust,ignore @@ -467,6 +469,28 @@ unavailable = { impl ReduceTo for Source { ... } ``` +Rules can mix accuracy explicitly while existing uniform declarations remain supported: + +```rust,ignore +#[reduction(transform = { + exact { num_vars = "num_vars" }, + upper_bound { num_quadratic_terms = "num_vars * (num_vars - 1) / 2" }, +})] +impl ReduceTo>> for KSatisfiability { ... } +``` + +Here both RHS expressions refer to the SAT source's `num_vars`. +Likewise, an ILP's canonical constraint matrix has at most variables times constraints +nonzeros. If a rule predicts those dimensions by source expressions `f` and `g`, it can +explicitly declare `num_nonzeros <= f * g`. Such structural bounds remain valid when +coefficients cancel; exact sparsity can still require additional source information. + +`ReductionParameterDeclarations::fields` stores `(name, relation, expression)` triples. +Use `ParameterTransform::relation(field)` to inspect a formula's accuracy and +`unavailable(field)` for a composition failure and its upstream cause. The uniform +`ParameterTransform::new` constructor remains available; `from_fields` accepts mixed relations. +CLI contract JSON stores `relation` within each formula entry in `fields`. + `ParameterTransform` uses exact rational and arbitrary-precision integer arithmetic. Exact relations must evaluate to non-negative integers, while upper-bound results round rational values upward. Missing fields, negative or non-integral exact results, division by zero, @@ -485,7 +509,7 @@ first fully expanded and like monomials are combined; terms with non-positive co are then removed before substitution. For example, `m <= n^2` followed by `k = 10 - m` produces the sound bound `k <= 10`, while `e' = v(v - 1)/2 - e` produces `e' <= v^2/2`. A non-polynomial downstream formula cannot -propagate symbolic upper bounds and reports an error. Projection to `Growth` is a separate descriptive terminal operation used for +propagate symbolic upper bounds and makes that field unavailable, preserving the other fields. Projection to `Growth` is a separate descriptive terminal operation used for Big-O display; it does not rank or filter paths.
diff --git a/problemreductions-cli/src/commands/graph.rs b/problemreductions-cli/src/commands/graph.rs index 465b740a4..53d94812c 100644 --- a/problemreductions-cli/src/commands/graph.rs +++ b/problemreductions-cli/src/commands/graph.rs @@ -2,6 +2,7 @@ use crate::dispatch::{load_problem, read_input, ProblemJson}; use crate::output::OutputConfig; use crate::problem_name::{aliases_for, parse_problem_spec, resolve_problem_ref}; use anyhow::Result; +use problemreductions::parameters::ParameterRelation; use problemreductions::registry::collect_schemas; use problemreductions::registry::ProblemCategory; use problemreductions::rules::{ExecutedPath, ReductionGraph, ReductionPath, TraversalFlow}; @@ -584,13 +585,9 @@ fn strongest_contract_fields( let mut fields = BTreeMap::new(); if let Some(transform) = contract.transform() { for (field, expression) in transform.expressions() { - let relation = match transform.relation() { - problemreductions::parameters::ParameterRelation::Exact => { - StrongestContractRelation::Exact(expression) - } - problemreductions::parameters::ParameterRelation::UpperBound => { - StrongestContractRelation::UpperBound(expression) - } + let relation = match transform.relation(field).expect("declared formula") { + ParameterRelation::Exact => StrongestContractRelation::Exact(expression), + ParameterRelation::UpperBound => StrongestContractRelation::UpperBound(expression), }; fields.insert(field, relation); } @@ -670,11 +667,11 @@ pub(crate) fn parameter_contract_to_json( ) -> serde_json::Value { match contract { Ok(contract) => serde_json::json!({ - "relation": contract.transform().map(|transform| transform.relation()), "fields": contract.transform().map(|transform| transform.expressions().map(|(field, expression)| { serde_json::json!({ "field": field, "formula": expression.to_string(), + "relation": transform.relation(field), "big_o": big_o_of(expression), }) }).collect::>()).unwrap_or_default(), @@ -731,88 +728,50 @@ struct PreparedParameterField { relation: PreparedParameterRelation, } -fn terminal_parameter_contract( - graph: &ReductionGraph, - path: &ReductionPath, -) -> Option { - path.steps - .windows(2) - .last() - .and_then(|pair| { - graph.find_entry( - &pair[0].name, - &pair[0].variant, - &pair[1].name, - &pair[1].variant, - ) - }) - .and_then(|entry| entry.parameter_contract.ok()) -} - fn prepare_overall_parameters( graph: &ReductionGraph, path: &ReductionPath, ) -> Vec { - let Some(target) = path.target() else { + let Some(target) = path.steps.last() else { return Vec::new(); }; let composed = graph.compose_path_parameter_transform(path); - let terminal_contract = terminal_parameter_contract(graph, path); - - graph - .parameter_names(target) - .into_iter() - .map(|field| { - let expression = composed - .as_ref() - .ok() - .and_then(|transform| transform.as_ref()) - .and_then(|transform| { - transform - .get(&field) - .map(|expression| (transform.relation(), expression)) - }); - let relation = if let Some((relation, expression)) = expression { - match relation { - problemreductions::parameters::ParameterRelation::Exact => { - PreparedParameterRelation::Exact(expression.to_string()) - } - problemreductions::parameters::ParameterRelation::UpperBound => { - PreparedParameterRelation::UpperBound(expression.to_string()) + let fields = problemreductions::registry::find_variant_entry(&target.name, &target.variant) + .map(|entry| entry.parameter_names()) + .unwrap_or_default(); + fields + .iter() + .map(|&field| { + let relation = match &composed { + Ok(Some(transform)) => { + if let Some(expression) = transform.get(field) { + match transform.relation(field).expect("declared formula") { + ParameterRelation::Exact => { + PreparedParameterRelation::Exact(expression.to_string()) + } + ParameterRelation::UpperBound => { + PreparedParameterRelation::UpperBound(expression.to_string()) + } + } + } else { + let reason = transform + .unavailable(field) + .map(ToString::to_string) + .unwrap_or_else(|| { + format!("no symbolic parameter relation is registered for target field {field}") + }); + PreparedParameterRelation::Unavailable(reason) } } - } else if let Some(unavailable) = terminal_contract.as_ref().and_then(|contract| { - contract - .unavailable() - .iter() - .find(|unavailable| unavailable.field == field) - }) { - PreparedParameterRelation::Unavailable(unavailable.reason.to_string()) - } else if terminal_contract - .as_ref() - .and_then(|contract| contract.transform()) - .is_some_and(|transform| transform.get(&field).is_some()) - { - PreparedParameterRelation::Unavailable(match &composed { - Err(error) => error.to_string(), - Ok(_) => { - format!( - "no composed parameter relation is available for target field {field}" - ) - } - }) - } else { - let reason = match &composed { - Err(error) => error.to_string(), - Ok(_) => { - format!( - "no symbolic parameter relation is registered for target field {field}" - ) - } - }; - PreparedParameterRelation::Unavailable(reason) + Err(error) => PreparedParameterRelation::Unavailable(error.to_string()), + Ok(None) => PreparedParameterRelation::Unavailable(format!( + "no composed parameter relation is available for target field {field}" + )), }; - PreparedParameterField { field, relation } + PreparedParameterField { + field: field.to_string(), + relation, + } }) .collect() } diff --git a/problemreductions-cli/src/test_support.rs b/problemreductions-cli/src/test_support.rs index 7592dc02c..aaaeaf946 100644 --- a/problemreductions-cli/src/test_support.rs +++ b/problemreductions-cli/src/test_support.rs @@ -399,7 +399,7 @@ problemreductions::inventory::submit! { source_variant_fn: AggregateValueSource::variant, target_variant_fn: AggregateValueTarget::variant, parameter_declarations_fn: || ReductionParameterDeclarations { - relation: None, + fields: vec![], unavailable: vec![problemreductions::rules::registry::UnavailableParameterField { field: "num_values", @@ -430,7 +430,7 @@ problemreductions::inventory::submit! { source_variant_fn: AggregateValueSource::variant, target_variant_fn: ILP::::variant, parameter_declarations_fn: || ReductionParameterDeclarations { - relation: None, + fields: vec![], unavailable: vec![ problemreductions::rules::registry::UnavailableParameterField { diff --git a/problemreductions-cli/tests/cli_tests.rs b/problemreductions-cli/tests/cli_tests.rs index 4f9cd1949..e5641a0a5 100644 --- a/problemreductions-cli/tests/cli_tests.rs +++ b/problemreductions-cli/tests/cli_tests.rs @@ -5553,6 +5553,51 @@ fn test_path_overall_unavailable_is_reported_per_field_without_internal_modes() assert!(overall.get("bound_composition_error").is_none()); } +#[test] +fn test_path_preserves_exact_variables_and_bounded_quadratic_terms() { + for target in ["DecisionQUBO", "QUBO"] { + let output = pred() + .args([ + "path", + "KSatisfiability/K2", + target, + "--limit", + "1", + "--json", + ]) + .output() + .unwrap(); + assert!( + output.status.success(), + "{}", + String::from_utf8_lossy(&output.stderr) + ); + let envelope: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap(); + let fields = envelope["paths"][0]["overall_parameters"]["fields"] + .as_array() + .unwrap(); + let relations = fields + .iter() + .map(|field| { + ( + field["field"].as_str().unwrap(), + field["relation"].as_str().unwrap(), + ) + }) + .collect::>(); + // The reduction preserves variables; distinct off-diagonal pairs bound + // quadratic terms even when contributions cancel. + assert_eq!( + relations, + std::collections::BTreeMap::from([ + ("num_vars", "exact"), + ("num_quadratic_terms", "upper_bound"), + ]), + "prediction relations for {target}" + ); + } +} + #[test] fn test_path_overall_preserves_unavailable_fields_alongside_exact_fields() { let output = pred() diff --git a/problemreductions-macros/src/lib.rs b/problemreductions-macros/src/lib.rs index 8d9154c9f..a0493ded6 100644 --- a/problemreductions-macros/src/lib.rs +++ b/problemreductions-macros/src/lib.rs @@ -202,7 +202,8 @@ fn option_inner_type(ty: &Type) -> Option<&Type> { /// # Attributes /// /// - `transform = exact { field = expression, ... }` — exact target-parameter equalities -/// - `transform = upper_bound { field = expression, ... }` — one rule-level upper bound +/// - `transform = upper_bound { field = expression, ... }` — uniform upper bounds +/// - `transform = { exact { ... }, upper_bound { ... }, unavailable { ... } }` — per-field relations /// - `transform = unavailable { field = "reason", ... }` — no symbolic parameter transform /// - `unavailable = { field = "reason", ... }` — fields that cannot be propagated /// @@ -350,17 +351,10 @@ fn generate_aggregate_entry(result: &Type) -> TokenStream2 { } } -#[derive(Clone)] -struct ParsedExpressionField { - name: String, - expression: problemreductions_expr::Expr, -} - /// Parsed attributes from #[reduction(...)] struct ReductionAttrs { transform_declared: bool, - relation: Option, - fields: Option>, + fields: Vec<(String, ParameterRelationAttr, String)>, unavailable: Option>, } @@ -374,8 +368,7 @@ impl syn::parse::Parse for ReductionAttrs { fn parse(input: syn::parse::ParseStream) -> syn::Result { let mut attrs = ReductionAttrs { transform_declared: false, - relation: None, - fields: None, + fields: Vec::new(), unavailable: None, }; @@ -392,27 +385,17 @@ impl syn::parse::Parse for ReductionAttrs { )); } attrs.transform_declared = true; - let relation: syn::Ident = input.parse()?; - let content; - syn::braced!(content in input); - match relation.to_string().as_str() { - "exact" => { - attrs.relation = Some(ParameterRelationAttr::Exact); - attrs.fields = Some(parse_expression_fields(&content)?); - } - "upper_bound" => { - attrs.relation = Some(ParameterRelationAttr::UpperBound); - attrs.fields = Some(parse_expression_fields(&content)?); - } - "unavailable" => { - attrs.unavailable = Some(parse_unavailable_fields(&content)?); - } - _ => { - return Err(syn::Error::new( - relation.span(), - "expected `exact`, `upper_bound`, or `unavailable`", - )); + if input.peek(syn::token::Brace) { + let groups; + syn::braced!(groups in input); + while !groups.is_empty() { + attrs.parse_group(&groups)?; + if groups.peek(syn::Token![,]) { + groups.parse::()?; + } } + } else { + attrs.parse_group(input)?; } } "unavailable" => { @@ -450,6 +433,40 @@ impl syn::parse::Parse for ReductionAttrs { } } +impl ReductionAttrs { + fn parse_group(&mut self, input: syn::parse::ParseStream) -> syn::Result<()> { + let relation: syn::Ident = input.parse()?; + let content; + syn::braced!(content in input); + let kind = match relation.to_string().as_str() { + "exact" => ParameterRelationAttr::Exact, + "upper_bound" => ParameterRelationAttr::UpperBound, + "unavailable" => { + if self.unavailable.is_some() { + return Err(syn::Error::new( + relation.span(), + "duplicate `unavailable` declaration", + )); + } + self.unavailable = Some(parse_unavailable_fields(&content)?); + return Ok(()); + } + _ => { + return Err(syn::Error::new( + relation.span(), + "expected `exact`, `upper_bound`, or `unavailable`", + )) + } + }; + self.fields.extend( + parse_expression_fields(&content)? + .into_iter() + .map(|(name, expression)| (name, kind, expression)), + ); + Ok(()) + } +} + fn parse_expression_fields(content: syn::parse::ParseStream) -> syn::Result> { let mut fields = Vec::new(); while !content.is_empty() { @@ -576,37 +593,6 @@ fn make_variant_fn_body(ty: &Type, type_generics: &HashSet) -> syn::Resu Ok(quote! { <#ty as crate::traits::Problem>::variant() }) } -/// Parse one explicit exact or bound field declaration into the canonical expression DAG. -fn parse_expression_fields_to_expr( - fields: &[(String, String)], -) -> syn::Result> { - fields - .iter() - .map(|(name, source)| { - let expression = problemreductions_expr::Expr::try_parse(source).map_err(|error| { - syn::Error::new( - proc_macro2::Span::call_site(), - format!("error parsing parameter expression \"{source}\": {error}"), - ) - })?; - Ok(ParsedExpressionField { - name: name.clone(), - expression, - }) - }) - .collect() -} - -fn generate_expression_fields(fields: &[ParsedExpressionField]) -> TokenStream2 { - let field_tokens = fields.iter().map(|field| { - let expression = expr_tokens(&field.expression); - let name = field.name.as_str(); - quote! { (#name, #expression) } - }); - - quote! { vec![#(#field_tokens),*] } -} - /// Generate the reduction entry code fn generate_reduction_entry( attrs: &ReductionAttrs, @@ -637,17 +623,28 @@ fn generate_reduction_entry( let source_variant_body = make_variant_fn_body(source_type, &type_generics)?; let target_variant_body = make_variant_fn_body(&target_type, &type_generics)?; - let fields = parse_expression_fields_to_expr(attrs.fields.as_deref().unwrap_or_default())?; - let field_tokens = generate_expression_fields(&fields); - let relation_tokens = match attrs.relation { - Some(ParameterRelationAttr::Exact) => { - quote! { Some(crate::parameters::ParameterRelation::Exact) } - } - Some(ParameterRelationAttr::UpperBound) => { - quote! { Some(crate::parameters::ParameterRelation::UpperBound) } - } - None => quote! { None }, - }; + let field_tokens = attrs + .fields + .iter() + .map(|(name, relation, source)| { + let parsed = problemreductions_expr::Expr::try_parse(source).map_err(|error| { + syn::Error::new( + proc_macro2::Span::call_site(), + format!("error parsing parameter expression {source:?}: {error}"), + ) + })?; + let expression = expr_tokens(&parsed); + let relation = match relation { + ParameterRelationAttr::Exact => { + quote! { crate::parameters::ParameterRelation::Exact } + } + ParameterRelationAttr::UpperBound => { + quote! { crate::parameters::ParameterRelation::UpperBound } + } + }; + Ok(quote! { (#name, #relation, #expression) }) + }) + .collect::>>()?; let unavailable_tokens = attrs .unavailable .as_deref() @@ -666,8 +663,7 @@ fn generate_reduction_entry( source_variant_fn: || { #source_variant_body }, target_variant_fn: || { #target_variant_body }, parameter_declarations_fn: || crate::rules::registry::ReductionParameterDeclarations { - relation: #relation_tokens, - fields: #field_tokens, + fields: vec![#(#field_tokens),*], unavailable: vec![#(#unavailable_tokens),*], }, module_path: module_path!(), @@ -1097,10 +1093,10 @@ mod tests { #[test] fn parameters_report_expression_domain_errors() { - let fields = vec![("num_vertices".to_string(), "0 / 0".to_string())]; - let Err(error) = parse_expression_fields_to_expr(&fields) else { - panic!("invalid parameter expression was accepted"); - }; + let attrs: ReductionAttrs = + syn::parse_quote! { transform = exact { num_vertices = "0 / 0" } }; + let implementation: ItemImpl = syn::parse_quote! { impl ReduceTo for Source {} }; + let error = generate_reduction_entry(&attrs, &implementation).unwrap_err(); assert!(error.to_string().contains("division by zero")); } @@ -1423,6 +1419,37 @@ mod tests { assert!(generate_aggregate_impl(&inherent).is_err()); } + #[test] + fn mixed_transform_accepts_independent_relations_and_unavailable_fields() { + let attrs: ReductionAttrs = syn::parse_quote! { + transform = { + exact { n = "n" }, + upper_bound { pairs = "n * (n - 1) / 2" }, + unavailable { bits = "input magnitudes are not registered" }, + } + }; + assert_eq!(attrs.fields[0].1, ParameterRelationAttr::Exact); + assert_eq!(attrs.fields[1].1, ParameterRelationAttr::UpperBound); + assert_eq!(attrs.unavailable.unwrap()[0].0, "bits"); + } + + #[test] + fn mixed_transform_rejects_invalid_relations_and_duplicate_unavailable_groups() { + for declaration in [ + quote! { transform = { approximate { n = "n" } } }, + quote! { transform = { + unavailable { n = "not represented" }, + unavailable { m = "not represented" }, + } }, + quote! { + unavailable = { n = "not represented" }, + transform = unavailable { m = "not represented" }, + }, + ] { + assert!(syn::parse2::(declaration).is_err()); + } + } + #[test] fn reduction_accepts_explicit_transform_attributes() { let attrs: ReductionAttrs = syn::parse_quote! { @@ -1431,12 +1458,19 @@ mod tests { }; assert_eq!( attrs.fields, - Some(vec![ - ("n".to_string(), "n".to_string()), - ("squared".to_string(), "n^2".to_string()), - ]) + vec![ + ( + "n".to_string(), + ParameterRelationAttr::UpperBound, + "n".to_string() + ), + ( + "squared".to_string(), + ParameterRelationAttr::UpperBound, + "n^2".to_string() + ), + ] ); - assert_eq!(attrs.relation, Some(ParameterRelationAttr::UpperBound)); assert_eq!( attrs.unavailable, Some(vec![( diff --git a/src/models/decision.rs b/src/models/decision.rs index ce1f9849d..9bd4ded89 100644 --- a/src/models/decision.rs +++ b/src/models/decision.rs @@ -79,10 +79,10 @@ macro_rules! register_decision_variant { source_variant_fn: <$crate::models::decision::Decision<$inner> as $crate::traits::Problem>::variant, target_variant_fn: <$inner as $crate::traits::Problem>::variant, parameter_declarations_fn: || $crate::rules::registry::ReductionParameterDeclarations { - relation: Some($crate::parameters::ParameterRelation::Exact), + fields: <$inner as $crate::traits::Problem>::parameter_names() .iter() - .map(|&name| (name, $crate::expr::Expr::variable(name))) + .map(|&name| (name, $crate::parameters::ParameterRelation::Exact, $crate::expr::Expr::variable(name))) .collect(), unavailable: vec![], }, @@ -124,10 +124,10 @@ macro_rules! register_decision_variant { source_variant_fn: <$inner as $crate::traits::Problem>::variant, target_variant_fn: <$crate::models::decision::Decision<$inner> as $crate::traits::Problem>::variant, parameter_declarations_fn: || $crate::rules::registry::ReductionParameterDeclarations { - relation: Some($crate::parameters::ParameterRelation::Exact), + fields: <$inner as $crate::traits::Problem>::parameter_names() .iter() - .map(|&name| (name, $crate::expr::Expr::variable(name))) + .map(|&name| (name, $crate::parameters::ParameterRelation::Exact, $crate::expr::Expr::variable(name))) .collect(), unavailable: vec![], }, diff --git a/src/models/graph/integral_flow_bundles.rs b/src/models/graph/integral_flow_bundles.rs index 25d4fc572..8e76d423c 100644 --- a/src/models/graph/integral_flow_bundles.rs +++ b/src/models/graph/integral_flow_bundles.rs @@ -252,7 +252,7 @@ impl IntegralFlowBundles { Ok(self.evaluate_solution(config)?.0) } - fn arc_upper_bounds(&self) -> Vec { + pub(crate) fn arc_upper_bounds(&self) -> Vec { let mut upper_bounds = vec![i64::MAX; self.num_arcs()]; for (bundle, &capacity) in self.bundles.iter().zip(&self.bundle_capacities) { for &arc_index in bundle { diff --git a/src/models/graph/path_constrained_network_flow.rs b/src/models/graph/path_constrained_network_flow.rs index fa94bc4cf..79a3ab483 100644 --- a/src/models/graph/path_constrained_network_flow.rs +++ b/src/models/graph/path_constrained_network_flow.rs @@ -224,7 +224,7 @@ impl PathConstrainedNetworkFlow { Ok(()) } - fn path_bottleneck(&self, path: &[usize]) -> i64 { + pub(crate) fn path_bottleneck(&self, path: &[usize]) -> i64 { path.iter() .map(|&arc_idx| self.capacities[arc_idx]) .min() diff --git a/src/parameters.rs b/src/parameters.rs index 2cf4ec93e..0ab7a513d 100644 --- a/src/parameters.rs +++ b/src/parameters.rs @@ -8,7 +8,7 @@ use num_traits::{One, Signed, Zero}; use std::collections::{BTreeMap, HashMap, HashSet}; use std::sync::Arc; -/// What one reduction rule promises about all of its declared parameter formulas. +/// What a reduction promises about one parameter formula. #[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] #[serde(rename_all = "snake_case")] pub enum ParameterRelation { @@ -16,28 +16,19 @@ pub enum ParameterRelation { UpperBound, } -impl ParameterRelation { - fn compose(self, next: Self) -> Self { - if self == Self::Exact && next == Self::Exact { - Self::Exact - } else { - Self::UpperBound - } - } -} - -/// One rule-level symbolic transformation. Its relation applies to every formula. +/// Symbolic predictions with independent accuracy and availability per field. #[derive(Clone, Debug)] pub struct ParameterTransform { edge: Box, - relation: ParameterRelation, fields: Vec, + unavailable: BTreeMap, ParameterTransformError>, } #[derive(Clone, Debug)] struct ParameterField { name: Box, expression: Expr, + relation: ParameterRelation, plan: Plan, } @@ -68,11 +59,28 @@ impl ParameterTransform { where I: IntoIterator, N: Into>, + { + Self::from_fields( + edge, + fields + .into_iter() + .map(|(name, expression)| (name, relation, expression)), + ) + } + + /// Construct predictions whose relations may differ by target field. + pub fn from_fields( + edge: impl Into>, + fields: I, + ) -> Result + where + I: IntoIterator, + N: Into>, { let edge = edge.into(); let mut names = HashSet::new(); let mut raw_fields = Vec::new(); - for (name, expression) in fields { + for (name, relation, expression) in fields { let name = name.into(); if let Err(error) = Symbol::new(name.clone()) { return Err(ParameterTransformError::InvalidTargetField { @@ -84,24 +92,25 @@ impl ParameterTransform { if !names.insert(name.clone()) { return Err(ParameterTransformError::DuplicateTargetField { edge, field: name }); } - raw_fields.push((name, expression)); + raw_fields.push((name, relation, expression)); } let expressions = raw_fields .iter() - .map(|(_, expression)| expression) + .map(|(_, _, expression)| expression) .collect::>(); let analysis = AlgebraicAnalysis::new(&expressions); let mut plans = HashMap::new(); let fields = raw_fields .into_iter() - .map(|(name, expression)| { + .map(|(name, relation, expression)| { let plan = compile(&expression, &analysis, &mut plans).map_err(|failure| { validation_error(edge.clone(), name.clone(), expression.to_string(), failure) })?; Ok(ParameterField { name, expression, + relation, plan, }) }) @@ -109,8 +118,8 @@ impl ParameterTransform { Ok(Self { edge, - relation, fields, + unavailable: BTreeMap::new(), }) } @@ -118,8 +127,27 @@ impl ParameterTransform { &self.edge } - pub fn relation(&self) -> ParameterRelation { - self.relation + pub fn relation(&self, target_field: &str) -> Option { + self.fields + .iter() + .find(|field| field.name.as_ref() == target_field) + .map(|field| field.relation) + } + + /// Why this target field could not be predicted, including upstream causes. + pub fn unavailable(&self, target_field: &str) -> Option<&ParameterTransformError> { + self.unavailable.get(target_field) + } + + pub(crate) fn declare_unavailable(&mut self, field: &str, reason: &str) { + self.unavailable.insert( + field.into(), + ParameterTransformError::Unavailable { + edge: self.edge.clone(), + field: field.into(), + reason: reason.into(), + }, + ); } pub fn expressions(&self) -> impl Iterator { @@ -152,7 +180,7 @@ impl ParameterTransform { value, }); } - let value = if self.relation == ParameterRelation::Exact { + let value = if field.relation == ParameterRelation::Exact { if !value.is_integer() { return Err(ParameterTransformError::NonIntegralResult { edge: self.edge.clone(), @@ -181,11 +209,23 @@ impl ParameterTransform { ) -> Result { let edge = edge.into(); let replacements: HashMap<&str, &Expr> = self.expressions().collect(); - let fields = next - .fields - .iter() - .map(|field| { - let expression = if self.relation == ParameterRelation::UpperBound { + let mut fields = Vec::new(); + let mut unavailable = next.unavailable.clone(); + for field in &next.fields { + let result = (|| { + let mut bounded_input = false; + for input in field.expression.variables() { + if let Some(cause) = self.unavailable(input) { + return Err(ParameterTransformError::UnavailableInput { + edge: next.edge.clone(), + field: field.name.clone(), + input_field: input.into(), + cause: Box::new(cause.clone()), + }); + } + bounded_input |= self.relation(input) == Some(ParameterRelation::UpperBound); + } + let expression = if bounded_input { positive_polynomial_hull(&field.expression).ok_or_else(|| { ParameterTransformError::CannotPropagateUpperBound { edge: next.edge.clone(), @@ -200,14 +240,27 @@ impl ParameterTransform { expression .substitute_complete(&replacements) .map_err(|error| ParameterTransformError::MissingCompositionInput { - edge: edge.clone(), + edge: next.edge.clone(), field: field.name.clone(), input_fields: error.missing_variables().map(Box::::from).collect(), })?; - Ok((field.name.clone(), expression)) - }) - .collect::, ParameterTransformError>>()?; - Self::new(edge, self.relation.compose(next.relation), fields) + let relation = if bounded_input { + ParameterRelation::UpperBound + } else { + field.relation + }; + Ok((field.name.clone(), relation, expression)) + })(); + match result { + Ok(prediction) => fields.push(prediction), + Err(error) => { + unavailable.insert(field.name.clone(), error); + } + } + } + let mut composed = Self::from_fields(edge, fields)?; + composed.unavailable = unavailable; + Ok(composed) } } @@ -477,6 +530,22 @@ fn evaluation_error( /// Validation, composition, or evaluation failure for a [`ParameterTransform`]. #[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)] pub enum ParameterTransformError { + #[error("reduction `{edge}` target field `{field}` is unavailable: {reason}")] + Unavailable { + edge: Box, + field: Box, + reason: Box, + }, + #[error( + "reduction `{edge}` target field `{field}` depends on unavailable `{input_field}`: {cause}" + )] + UnavailableInput { + edge: Box, + field: Box, + input_field: Box, + #[source] + cause: Box, + }, #[error("reduction `{edge}` has invalid target parameter field `{field}`: {reason}")] InvalidTargetField { edge: Box, diff --git a/src/rules/acyclicpartition_ilp.rs b/src/rules/acyclicpartition_ilp.rs index 9baca7955..f042f7b37 100644 --- a/src/rules/acyclicpartition_ilp.rs +++ b/src/rules/acyclicpartition_ilp.rs @@ -4,7 +4,7 @@ //! crossing flags y_t, and partition labels used directly as a topological order. //! See the paper entry for the full formulation. -use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::AcyclicPartition; use crate::reduction; use crate::rules::ilp_helpers::mccormick_product; @@ -43,15 +43,15 @@ impl ReductionResult for ReductionAcyclicPartitionToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionAcyclicPartitionToILP {} -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_vertices * num_vertices + num_arcs * num_vertices + num_arcs + num_vertices", num_constraints = "num_vertices^2 + 4 * num_vertices + 3 * num_arcs * num_vertices + 2 * num_arcs + 1", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + num_nonzeros = "(num_vertices * num_vertices + num_arcs * num_vertices + num_arcs + num_vertices) * (num_vertices^2 + 4 * num_vertices + 3 * num_arcs * num_vertices + 2 * num_arcs + 1)", + }, +})] impl ReduceTo> for AcyclicPartition { type Result = ReductionAcyclicPartitionToILP; @@ -146,7 +146,9 @@ impl ReduceTo> for AcyclicPartition { constraints.push(LinearConstraint::le(terms, 0)); } - let target = ILP::new(num_vars, constraints, vec![], ObjectiveSense::Minimize) + let variables = vec![IntegerVariable::binary(); num_vars]; + + let target = ILP::with_variables(variables, constraints, vec![], ObjectiveSense::Minimize) .map_err(Self::target_construction)?; Ok(ReductionAcyclicPartitionToILP { target, n }) diff --git a/src/rules/balancedcompletebipartitesubgraph_ilp.rs b/src/rules/balancedcompletebipartitesubgraph_ilp.rs index 5857dfc7e..a14d79e83 100644 --- a/src/rules/balancedcompletebipartitesubgraph_ilp.rs +++ b/src/rules/balancedcompletebipartitesubgraph_ilp.rs @@ -45,15 +45,11 @@ impl ReductionResult for ReductionBCBSToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionBCBSToILP {} -#[reduction( - transform = upper_bound { - num_vars = "num_vertices", - num_constraints = "num_vertices^2 + 2", - }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] +#[reduction(transform = upper_bound { + num_vars = "num_vertices", + num_constraints = "num_vertices^2 + 2", + num_nonzeros = "num_vertices * (num_vertices^2 + 2)", +})] impl ReduceTo> for BalancedCompleteBipartiteSubgraph { type Result = ReductionBCBSToILP; diff --git a/src/rules/biconnectivityaugmentation_ilp.rs b/src/rules/biconnectivityaugmentation_ilp.rs index 37e43cae5..af0513832 100644 --- a/src/rules/biconnectivityaugmentation_ilp.rs +++ b/src/rules/biconnectivityaugmentation_ilp.rs @@ -75,15 +75,11 @@ impl ReductionResult for ReductionBiconnAugToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionBiconnAugToILP {} -#[reduction( - transform = 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)", - }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] +#[reduction(transform = 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 + 2 * num_vertices * (num_vertices + 1) * (num_edges + num_potential_edges)) * (1 + num_vertices * (num_vertices + 1) * (2 * num_edges + 4 * num_potential_edges + num_vertices))", +})] impl ReduceTo> for BiconnectivityAugmentation { type Result = ReductionBiconnAugToILP; diff --git a/src/rules/binpacking_ilp.rs b/src/rules/binpacking_ilp.rs index 24d3cc5a8..bef93355a 100644 --- a/src/rules/binpacking_ilp.rs +++ b/src/rules/binpacking_ilp.rs @@ -47,15 +47,15 @@ impl ReductionResult for ReductionBPToILP { } } -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_items * num_items + num_items", num_constraints = "2 * num_items", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + num_nonzeros = "(num_items * num_items + num_items) * (2 * num_items)", + }, +})] impl ReduceTo> for BinPacking { type Result = ReductionBPToILP; diff --git a/src/rules/bottlenecktravelingsalesman_ilp.rs b/src/rules/bottlenecktravelingsalesman_ilp.rs index 50b2e2146..5c36ad7d8 100644 --- a/src/rules/bottlenecktravelingsalesman_ilp.rs +++ b/src/rules/bottlenecktravelingsalesman_ilp.rs @@ -1,6 +1,6 @@ //! Bottleneck TSP to ILP using cyclic positions and a selected maximum edge. -use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::BottleneckTravelingSalesman; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -79,15 +79,15 @@ impl ReductionBTSPToILP { } } -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_vertices^2 + 2 * num_edges * num_vertices + num_edges", num_constraints = "num_vertices^2 + 6 * num_edges * num_vertices + 4 * num_edges + 3 * num_vertices + 1", }, - unavailable = { - num_nonzeros = "threshold comparisons depend on the ordering of edge weights", - } -)] + upper_bound { + num_nonzeros = "(num_vertices^2 + 2 * num_edges * num_vertices + num_edges) * (num_vertices^2 + 6 * num_edges * num_vertices + 4 * num_edges + 3 * num_vertices + 1)", + }, +})] impl ReduceTo> for BottleneckTravelingSalesman { type Result = ReductionBTSPToILP; @@ -174,8 +174,11 @@ impl ReduceTo> for BottleneckTravelingSalesman { .enumerate() .map(|(edge, weight)| (q(edge), weight)) .collect(); - let target = ILP::new(num_vars, constraints, objective, ObjectiveSense::Minimize) - .map_err(Self::target_construction)?; + let variables = vec![IntegerVariable::binary(); num_vars]; + + let target = + ILP::with_variables(variables, constraints, objective, ObjectiveSense::Minimize) + .map_err(Self::target_construction)?; Ok(ReductionBTSPToILP { target, num_vertices: n, diff --git a/src/rules/boundedcomponentspanningforest_ilp.rs b/src/rules/boundedcomponentspanningforest_ilp.rs index 3a8242872..093f35112 100644 --- a/src/rules/boundedcomponentspanningforest_ilp.rs +++ b/src/rules/boundedcomponentspanningforest_ilp.rs @@ -4,7 +4,7 @@ //! connectivity inside each used component via flow. //! See the paper entry for the full formulation. -use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::BoundedComponentSpanningForest; use crate::reduction; use crate::rules::ilp_helpers::one_hot_decode_rows; @@ -45,15 +45,15 @@ impl ReductionResult for ReductionBCSFToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionBCSFToILP {} -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "3 * num_vertices * max_components + 2 * max_components + 2 * num_edges * max_components", num_constraints = "num_vertices + 5 * max_components + 6 * num_vertices * max_components + 6 * num_edges * max_components", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + num_nonzeros = "(3 * num_vertices * max_components + 2 * max_components + 2 * num_edges * max_components) * (num_vertices + 5 * max_components + 6 * num_vertices * max_components + 6 * num_edges * max_components)", + }, +})] impl ReduceTo> for BoundedComponentSpanningForest { type Result = ReductionBCSFToILP; @@ -188,7 +188,14 @@ impl ReduceTo> for BoundedComponentSpanningForest { } } - let target = ILP::new(num_vars, constraints, vec![], ObjectiveSense::Minimize) + let mut variables = vec![IntegerVariable::binary(); num_vars]; + variables[2 * n * k + k..3 * n * k + 2 * k] + .fill(IntegerVariable::new(Some(0), Some(n_i64)).map_err(Self::target_construction)?); + variables[3 * n * k + 2 * k..].fill( + IntegerVariable::new(Some(0), Some(cap.max(0))).map_err(Self::target_construction)?, + ); + + let target = ILP::with_variables(variables, constraints, vec![], ObjectiveSense::Minimize) .map_err(Self::target_construction)?; Ok(ReductionBCSFToILP { target, n, k }) } diff --git a/src/rules/capacityassignment_ilp.rs b/src/rules/capacityassignment_ilp.rs index 1b348b59a..f38073a82 100644 --- a/src/rules/capacityassignment_ilp.rs +++ b/src/rules/capacityassignment_ilp.rs @@ -49,15 +49,15 @@ impl ReductionResult for ReductionCAToILP { } } -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_links * num_capacities", num_constraints = "num_links + 1", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + num_nonzeros = "(num_links * num_capacities) * (num_links + 1)", + }, +})] impl ReduceTo> for CapacityAssignment { type Result = ReductionCAToILP; diff --git a/src/rules/circuit_ilp.rs b/src/rules/circuit_ilp.rs index bcde39553..aa7169b5a 100644 --- a/src/rules/circuit_ilp.rs +++ b/src/rules/circuit_ilp.rs @@ -194,15 +194,11 @@ impl ILPBuilder { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionCircuitToILP {} -#[reduction( - transform = upper_bound { - num_vars = "num_variables + 2 * num_expression_nodes", - num_constraints = "5 * num_expression_nodes + num_assignment_outputs", - }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] +#[reduction(transform = upper_bound { + num_vars = "num_variables + 2 * num_expression_nodes", + num_constraints = "5 * num_expression_nodes + num_assignment_outputs", + num_nonzeros = "(num_variables + 2 * num_expression_nodes) * (5 * num_expression_nodes + num_assignment_outputs)", +})] impl ReduceTo> for CircuitSAT { type Result = ReductionCircuitToILP; diff --git a/src/rules/closeststring_ilp.rs b/src/rules/closeststring_ilp.rs index 18cbd2c95..852f45288 100644 --- a/src/rules/closeststring_ilp.rs +++ b/src/rules/closeststring_ilp.rs @@ -21,7 +21,7 @@ //! substring problems," Journal of the ACM 49(2):157-171, 2002. //! -use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::ClosestString; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -125,8 +125,17 @@ impl ReduceTo> for ClosestString { // Objective: minimize R. let objective = vec![(r_idx, 1)]; - let target = ILP::new(num_vars, constraints, objective, ObjectiveSense::Minimize) - .map_err(Self::target_construction)?; + // Every center has Hamming distance at most the string length, so an optimum has R <= m. + let mut variables = vec![IntegerVariable::binary(); num_vars]; + variables[r_idx] = IntegerVariable::new( + Some(0), + Some(Self::exact_i64(m, "bounding the Hamming radius")?), + ) + .map_err(Self::target_construction)?; + + let target = + ILP::with_variables(variables, constraints, objective, ObjectiveSense::Minimize) + .map_err(Self::target_construction)?; Ok(ReductionClosestStringToILP { target, diff --git a/src/rules/closestsubstring_ilp.rs b/src/rules/closestsubstring_ilp.rs index 65416c54c..8bb7589b9 100644 --- a/src/rules/closestsubstring_ilp.rs +++ b/src/rules/closestsubstring_ilp.rs @@ -29,7 +29,7 @@ //! substring problems," Journal of the ACM 49(2):157-171, 2002. //! -use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::ClosestSubstring; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -118,15 +118,15 @@ fn decode_one_hot( Ok(index) } -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "alphabet_size * substring_length + total_num_windows + 1", num_constraints = "substring_length + num_strings + total_num_windows + 1", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + num_nonzeros = "(alphabet_size * substring_length + total_num_windows + 1) * (substring_length + num_strings + total_num_windows + 1)", + }, +})] impl ReduceTo> for ClosestSubstring { type Result = ReductionClosestSubstringToILP; @@ -200,8 +200,13 @@ impl ReduceTo> for ClosestSubstring { // Objective: minimize R. let objective = vec![(r_idx, 1)]; - let target = ILP::new(num_vars, constraints, objective, ObjectiveSense::Minimize) - .map_err(Self::target_construction)?; + let mut variables = vec![IntegerVariable::binary(); num_vars]; + variables[r_idx] = + IntegerVariable::new(Some(0), Some(ell_i64)).map_err(Self::target_construction)?; + + let target = + ILP::with_variables(variables, constraints, objective, ObjectiveSense::Minimize) + .map_err(Self::target_construction)?; Ok(ReductionClosestSubstringToILP { target, diff --git a/src/rules/clustering_ilp.rs b/src/rules/clustering_ilp.rs index 00b2e91ee..95a457064 100644 --- a/src/rules/clustering_ilp.rs +++ b/src/rules/clustering_ilp.rs @@ -49,15 +49,11 @@ impl ReductionResult for ReductionClusteringToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionClusteringToILP {} -#[reduction( - transform = upper_bound { - num_vars = "num_elements * num_clusters", - num_constraints = "num_elements + num_elements * (num_elements - 1) / 2 * num_clusters", - }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] +#[reduction(transform = upper_bound { + 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 * (num_elements - 1) / 2 * num_clusters)", +})] impl ReduceTo> for Clustering { type Result = ReductionClusteringToILP; diff --git a/src/rules/consecutiveblockminimization_ilp.rs b/src/rules/consecutiveblockminimization_ilp.rs index f61d3e4bb..08eb9974e 100644 --- a/src/rules/consecutiveblockminimization_ilp.rs +++ b/src/rules/consecutiveblockminimization_ilp.rs @@ -42,15 +42,11 @@ impl ReductionResult for ReductionCBMToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionCBMToILP {} -#[reduction( - transform = upper_bound { - 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", - }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] +#[reduction(transform = upper_bound { + 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 = "(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)", +})] impl ReduceTo> for ConsecutiveBlockMinimization { type Result = ReductionCBMToILP; diff --git a/src/rules/consecutiveonesmatrixaugmentation_ilp.rs b/src/rules/consecutiveonesmatrixaugmentation_ilp.rs index 999a91899..18dbd6535 100644 --- a/src/rules/consecutiveonesmatrixaugmentation_ilp.rs +++ b/src/rules/consecutiveonesmatrixaugmentation_ilp.rs @@ -43,15 +43,15 @@ impl ReductionResult for ReductionCOMAToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionCOMAToILP {} -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_cols * num_cols + 5 * num_rows * num_cols", num_constraints = "num_cols + num_cols + num_rows * num_cols + 2 * num_rows + num_rows + 3 * num_rows * num_cols + 4 * num_rows * num_cols + 1", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + num_nonzeros = "(num_cols * num_cols + 5 * num_rows * num_cols) * (num_cols + num_cols + num_rows * num_cols + 2 * num_rows + num_rows + 3 * num_rows * num_cols + 4 * num_rows * num_cols + 1)", + }, +})] impl ReduceTo> for ConsecutiveOnesMatrixAugmentation { type Result = ReductionCOMAToILP; diff --git a/src/rules/consecutiveonessubmatrix_ilp.rs b/src/rules/consecutiveonessubmatrix_ilp.rs index a1affc72d..04b40ce82 100644 --- a/src/rules/consecutiveonessubmatrix_ilp.rs +++ b/src/rules/consecutiveonessubmatrix_ilp.rs @@ -46,15 +46,11 @@ impl ReductionResult for ReductionCOSToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionCOSToILP {} -#[reduction( - transform = upper_bound { - num_vars = "num_cols + num_cols * bound + 5 * num_rows * bound", - num_constraints = "2 + num_cols + bound + 3 * num_rows + 8 * num_rows * bound", - }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] +#[reduction(transform = upper_bound { + 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 = "(num_cols + num_cols * bound + 5 * num_rows * bound) * (2 + num_cols + bound + 3 * num_rows + 8 * num_rows * bound)", +})] impl ReduceTo> for ConsecutiveOnesSubmatrix { type Result = ReductionCOSToILP; diff --git a/src/rules/directedhamiltonianpath_ilp.rs b/src/rules/directedhamiltonianpath_ilp.rs index d201e5bb2..fb4415956 100644 --- a/src/rules/directedhamiltonianpath_ilp.rs +++ b/src/rules/directedhamiltonianpath_ilp.rs @@ -53,15 +53,11 @@ impl ReductionResult for ReductionDirectedHamiltonianPathToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionDirectedHamiltonianPathToILP {} -#[reduction( - transform = upper_bound { - num_vars = "num_vertices^2", - num_constraints = "3 * num_vertices + num_vertices^3", - }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] +#[reduction(transform = upper_bound { + num_vars = "num_vertices^2", + num_constraints = "3 * num_vertices + num_vertices^3", + num_nonzeros = "(num_vertices^2) * (3 * num_vertices + num_vertices^3)", +})] impl ReduceTo> for DirectedHamiltonianPath { type Result = ReductionDirectedHamiltonianPathToILP; diff --git a/src/rules/directedtwocommodityintegralflow_ilp.rs b/src/rules/directedtwocommodityintegralflow_ilp.rs index ff9089ec1..de6b9e0ef 100644 --- a/src/rules/directedtwocommodityintegralflow_ilp.rs +++ b/src/rules/directedtwocommodityintegralflow_ilp.rs @@ -12,7 +12,7 @@ //! Objective: Minimize 0 (feasibility). //! Extraction: Direct 2*|A| variables. -use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::DirectedTwoCommodityIntegralFlow; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -55,15 +55,11 @@ impl ReductionResult for ReductionD2CIFToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionD2CIFToILP {} -#[reduction( - transform = upper_bound { - num_vars = "2 * num_arcs", - num_constraints = "num_arcs + 2 * num_vertices + 2", - }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] +#[reduction(transform = upper_bound { + num_vars = "2 * num_arcs", + num_constraints = "num_arcs + 2 * num_vertices + 2", + num_nonzeros = "(2 * num_arcs) * (num_arcs + 2 * num_vertices + 2)", +})] impl ReduceTo> for DirectedTwoCommodityIntegralFlow { type Result = ReductionD2CIFToILP; @@ -155,8 +151,18 @@ impl ReduceTo> for DirectedTwoCommodityIntegralFlow { } constraints.push(LinearConstraint::ge(sink2_terms, self.requirement_2())); + let variables = self + .capacities() + .iter() + .copied() + .cycle() + .take(num_vars) + .map(|capacity| IntegerVariable::new(Some(0), Some(capacity))) + .collect::, _>>() + .map_err(Self::target_construction)?; + Ok(ReductionD2CIFToILP { - target: ILP::new(num_vars, constraints, vec![], ObjectiveSense::Minimize) + target: ILP::with_variables(variables, constraints, vec![], ObjectiveSense::Minimize) .map_err(Self::target_construction)?, num_arcs: m, }) diff --git a/src/rules/disjointconnectingpaths_ilp.rs b/src/rules/disjointconnectingpaths_ilp.rs index 7dfdbed51..674db108b 100644 --- a/src/rules/disjointconnectingpaths_ilp.rs +++ b/src/rules/disjointconnectingpaths_ilp.rs @@ -93,15 +93,15 @@ impl ReductionResult for ReductionDCPToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionDCPToILP {} -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_pairs * 2 * num_edges", num_constraints = "num_pairs * num_vertices + num_vertices", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + num_nonzeros = "(num_pairs * 2 * num_edges) * (num_pairs * num_vertices + num_vertices)", + }, +})] impl ReduceTo> for DisjointConnectingPaths { type Result = ReductionDCPToILP; diff --git a/src/rules/ensemblecomputation_ilp.rs b/src/rules/ensemblecomputation_ilp.rs index 2ed359613..edd086456 100644 --- a/src/rules/ensemblecomputation_ilp.rs +++ b/src/rules/ensemblecomputation_ilp.rs @@ -1,6 +1,6 @@ //! Polynomial-size circuit-slot reduction from EnsembleComputation to `ILP`. -use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::EnsembleComputation; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -80,15 +80,15 @@ impl ReductionResult for ReductionEnsembleComputationToILP { } } -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "3 * budget * universe_size + budget * (budget - 1) * (universe_size + 1) + num_subsets * budget + budget", num_constraints = "5 * budget - 1 + budget * (budget - 1) * (1 + 3 * universe_size) + 2 * budget * universe_size + num_subsets * budget * (universe_size + 2) + num_subsets", }, - unavailable = { - num_nonzeros = "depends on the cardinalities and duplicate structure of the required subsets", - } -)] + upper_bound { + num_nonzeros = "(3 * budget * universe_size + budget * (budget - 1) * (universe_size + 1) + num_subsets * budget + budget) * (5 * budget - 1 + budget * (budget - 1) * (1 + 3 * universe_size) + 2 * budget * universe_size + num_subsets * budget * (universe_size + 2) + num_subsets)", + }, +})] impl ReduceTo> for EnsembleComputation { type Result = ReductionEnsembleComputationToILP; @@ -279,8 +279,11 @@ impl ReduceTo> for EnsembleComputation { } let objective = (0..budget).map(|step| (activity_base + step, 1)).collect(); - let target = ILP::new(num_vars, constraints, objective, ObjectiveSense::Minimize) - .map_err(Self::target_construction)?; + let variables = vec![IntegerVariable::binary(); num_vars]; + + let target = + ILP::with_variables(variables, constraints, objective, ObjectiveSense::Minimize) + .map_err(Self::target_construction)?; Ok(ReductionEnsembleComputationToILP { target, universe_size: u, diff --git a/src/rules/eulerianpath_ilp.rs b/src/rules/eulerianpath_ilp.rs index 5543806d8..64741308a 100644 --- a/src/rules/eulerianpath_ilp.rs +++ b/src/rules/eulerianpath_ilp.rs @@ -23,7 +23,7 @@ //! Bang-Jensen and Gutin, *Digraphs: Theory, Algorithms and Applications*, //! 2nd ed., Springer (2009). -use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::EulerianPath; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -147,15 +147,11 @@ fn compatible_pairs(arcs: &[(usize, usize)]) -> Vec<(usize, usize)> { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionEulerianPathToILP {} -#[reduction( - transform = upper_bound { - num_vars = "3 * num_arcs + num_arcs * num_arcs", - num_constraints = "5 * num_arcs + 2 * num_arcs * num_arcs + 2", - }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] +#[reduction(transform = upper_bound { + num_vars = "3 * num_arcs + num_arcs * num_arcs", + num_constraints = "5 * num_arcs + 2 * num_arcs * num_arcs + 2", + num_nonzeros = "(3 * num_arcs + num_arcs * num_arcs) * (5 * num_arcs + 2 * num_arcs * num_arcs + 2)", +})] impl ReduceTo> for EulerianPath { type Result = ReductionEulerianPathToILP; @@ -239,8 +235,14 @@ impl ReduceTo> for EulerianPath { constraints.push(LinearConstraint::eq(start_sum, 1)); constraints.push(LinearConstraint::eq(end_sum, 1)); - let target = ILP::new(num_vars, constraints, Vec::new(), ObjectiveSense::Minimize) - .map_err(Self::target_construction)?; + let mut variables = vec![IntegerVariable::binary(); num_vars]; + variables[p + 2 * m..].fill( + IntegerVariable::new(Some(0), Some(m_i64 - 1)).map_err(Self::target_construction)?, + ); + + let target = + ILP::with_variables(variables, constraints, Vec::new(), ObjectiveSense::Minimize) + .map_err(Self::target_construction)?; Ok(ReductionEulerianPathToILP { target, diff --git a/src/rules/factoring_ilp.rs b/src/rules/factoring_ilp.rs index f7e6fe5fb..a9ee5b30f 100644 --- a/src/rules/factoring_ilp.rs +++ b/src/rules/factoring_ilp.rs @@ -19,7 +19,7 @@ //! 4. Binary bounds: p_i ≤ 1, q_j ≤ 1 //! 5. Carry bounds: 0 ≤ c_k ≤ min(m, n) -use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::Factoring; use crate::reduction; use crate::rules::ilp_helpers::mccormick_product; @@ -113,15 +113,11 @@ impl ReductionResult for ReductionFactoringToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionFactoringToILP {} -#[reduction( - transform = upper_bound { +#[reduction(transform = upper_bound { num_vars = "num_bits_first * num_bits_second + 2 * num_bits_first + 2 * num_bits_second + target_bits", num_constraints = "3 * num_bits_first * num_bits_second + 4 * num_bits_first + 4 * num_bits_second + 3 * target_bits + 1", -}, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + num_nonzeros = "(num_bits_first * num_bits_second + 2 * num_bits_first + 2 * num_bits_second + target_bits) * (3 * num_bits_first * num_bits_second + 4 * num_bits_first + 4 * num_bits_second + 3 * target_bits + 1)", +})] impl ReduceTo> for Factoring { type Result = ReductionFactoringToILP; @@ -223,8 +219,15 @@ impl ReduceTo> for Factoring { // Objective: feasibility problem (minimize 0) let objective: Vec<(usize, i64)> = vec![]; - let ilp = ILP::::new(num_vars, constraints, objective, ObjectiveSense::Minimize) - .map_err(>>::target_construction)?; + let mut variables = vec![IntegerVariable::binary(); num_vars]; + variables[num_p + num_q + num_z..].fill( + IntegerVariable::new(Some(0), Some(carry_upper)) + .map_err(>>::target_construction)?, + ); + + let ilp = + ILP::::with_variables(variables, constraints, objective, ObjectiveSense::Minimize) + .map_err(>>::target_construction)?; Ok(ReductionFactoringToILP { target: ilp, m, n }) } diff --git a/src/rules/feasibleregisterassignment_ilp.rs b/src/rules/feasibleregisterassignment_ilp.rs index 240437300..d56068386 100644 --- a/src/rules/feasibleregisterassignment_ilp.rs +++ b/src/rules/feasibleregisterassignment_ilp.rs @@ -10,7 +10,7 @@ //! interval non-overlap: if `u` is before `v`, then `v` must be scheduled no //! earlier than the latest dependent of `u`. -use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::FeasibleRegisterAssignment; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -135,8 +135,14 @@ impl ReduceTo> for FeasibleRegisterAssignment { )); } + let mut variables = vec![IntegerVariable::binary(); num_vars]; + variables[..2 * n].fill( + IntegerVariable::new(Some(0), Some(last_position)) + .map_err(Self::target_construction)?, + ); + Ok(ReductionFeasibleRegisterAssignmentToILP { - target: ILP::new(num_vars, constraints, vec![], ObjectiveSense::Minimize) + target: ILP::with_variables(variables, constraints, vec![], ObjectiveSense::Minimize) .map_err(Self::target_construction)?, num_vertices: n, }) diff --git a/src/rules/flowshopscheduling_ilp.rs b/src/rules/flowshopscheduling_ilp.rs index 8bbce62e5..fd91dcb2d 100644 --- a/src/rules/flowshopscheduling_ilp.rs +++ b/src/rules/flowshopscheduling_ilp.rs @@ -5,7 +5,7 @@ //! Machine-chain and big-M disjunctive constraints enforce a valid flow-shop //! schedule; the deadline becomes a makespan bound. -use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::FlowShopScheduling; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -71,15 +71,11 @@ impl ReductionResult for ReductionFSSToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionFSSToILP {} -#[reduction( - transform = upper_bound { +#[reduction(transform = upper_bound { 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", -}, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + num_nonzeros = "(num_jobs * (num_jobs - 1) / 2 + num_jobs * num_processors) * (num_jobs * (num_jobs - 1) + num_jobs + num_jobs * (num_processors - 1) + num_jobs * (num_jobs - 1) * num_processors + num_jobs)", +})] impl ReduceTo> for FlowShopScheduling { type Result = ReductionFSSToILP; @@ -191,8 +187,13 @@ impl ReduceTo> for FlowShopScheduling { } } + let mut variables = vec![IntegerVariable::binary(); num_vars]; + variables[num_order_vars..].fill( + IntegerVariable::new(Some(0), Some(deadline)).map_err(Self::target_construction)?, + ); + Ok(ReductionFSSToILP { - target: ILP::new(num_vars, constraints, vec![], ObjectiveSense::Minimize) + target: ILP::with_variables(variables, constraints, vec![], ObjectiveSense::Minimize) .map_err(Self::target_construction)?, num_jobs: n, num_machines: m, diff --git a/src/rules/graph.rs b/src/rules/graph.rs index a0f50f77e..5f922aad6 100644 --- a/src/rules/graph.rs +++ b/src/rules/graph.rs @@ -954,14 +954,22 @@ impl ReductionGraph { target_problem: path.steps[index + 1].name.clone(), error: Box::new(error.clone()), })?; - contract - .transform() - .cloned() - .ok_or_else(|| PathParameterError::Unavailable { - step: index + 1, - source_problem: path.steps[index].name.clone(), - target_problem: path.steps[index + 1].name.clone(), - }) + let mut transform = contract.transform().cloned().unwrap_or_else(|| { + crate::parameters::ParameterTransform::new( + format!( + "{} -> {}", + path.steps[index].name, + path.steps[index + 1].name + ), + crate::parameters::ParameterRelation::Exact, + Vec::<(&str, crate::expr::Expr)>::new(), + ) + .expect("empty transform is valid") + }); + for field in contract.unavailable() { + transform.declare_unavailable(field.field, field.reason); + } + Ok(transform) }) .collect() } @@ -1370,14 +1378,13 @@ impl ReductionGraph { let mut parameters = Vec::new(); if let Ok(contract) = contract { if let Some(transform) = contract.transform() { - let relation = match transform.relation() { - crate::parameters::ParameterRelation::Exact => "exact", - crate::parameters::ParameterRelation::UpperBound => "upper_bound", - }; parameters.extend(transform.expressions().map(|(field, expression)| { ParameterFieldJson { field: field.to_string(), - contract: relation, + contract: match transform.relation(field).expect("declared formula") { + crate::parameters::ParameterRelation::Exact => "exact", + crate::parameters::ParameterRelation::UpperBound => "upper_bound", + }, formula: Some(expression.to_string()), reason: None, } diff --git a/src/rules/graphpartitioning_ilp.rs b/src/rules/graphpartitioning_ilp.rs index 3186696a2..d6cceea7a 100644 --- a/src/rules/graphpartitioning_ilp.rs +++ b/src/rules/graphpartitioning_ilp.rs @@ -43,15 +43,15 @@ impl ReductionResult for ReductionGraphPartitioningToILP { } } -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_vertices + num_edges", num_constraints = "2 * num_edges + 1", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + num_nonzeros = "(num_vertices + num_edges) * (2 * num_edges + 1)", + }, +})] impl ReduceTo> for GraphPartitioning { type Result = ReductionGraphPartitioningToILP; diff --git a/src/rules/ilp_qubo.rs b/src/rules/ilp_qubo.rs index f60d8eaa6..32408abe6 100644 --- a/src/rules/ilp_qubo.rs +++ b/src/rules/ilp_qubo.rs @@ -78,12 +78,13 @@ impl crate::rules::AggregateReductionResult for ReductionILPToQUBO { } } -#[reduction( - transform = unavailable { - num_vars = "the slack-bit count depends on coefficient magnitudes and right-hand sides absent from the registered source parameters vector", - num_quadratic_terms = "the nonzero products depend on generated penalty coefficients", - } -)] +// Each successfully computed positive i64 slack range needs at most 63 bits. +// Distinct off-diagonal pairs bound the resulting quadratic terms, including +// when penalty contributions cancel. Both bounds use only source parameters. +#[reduction(transform = upper_bound { + num_vars = "num_vars + 63 * num_constraints", + num_quadratic_terms = "(num_vars + 63 * num_constraints) * (num_vars + 63 * num_constraints - 1) / 2", +})] impl ReduceTo> for ILP { type Result = ReductionILPToQUBO; diff --git a/src/rules/integerknapsack_ilp.rs b/src/rules/integerknapsack_ilp.rs index fc3cda6bf..42f56afa1 100644 --- a/src/rules/integerknapsack_ilp.rs +++ b/src/rules/integerknapsack_ilp.rs @@ -4,7 +4,7 @@ //! capacity inequality is kept directly, and explicit upper bounds //! `c_i <= floor(B / s_i)` preserve the exact witness domain of the source. -use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::set::IntegerKnapsack; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -64,9 +64,21 @@ impl ReduceTo> for IntegerKnapsack { let objective = values.iter().copied().enumerate().collect(); + let variables = self + .sizes() + .iter() + .map(|&size| IntegerVariable::new(Some(0), Some(self.capacity() / size))) + .collect::, _>>() + .map_err(Self::target_construction)?; + Ok(ReductionIntegerKnapsackToILP { - target: ILP::new(num_vars, constraints, objective, ObjectiveSense::Maximize) - .map_err(Self::target_construction)?, + target: ILP::with_variables( + variables, + constraints, + objective, + ObjectiveSense::Maximize, + ) + .map_err(Self::target_construction)?, }) } } diff --git a/src/rules/integralflowbundles_ilp.rs b/src/rules/integralflowbundles_ilp.rs index e9d4ee032..98038f27c 100644 --- a/src/rules/integralflowbundles_ilp.rs +++ b/src/rules/integralflowbundles_ilp.rs @@ -4,7 +4,7 @@ //! the bundle-capacity inequalities, flow-conservation equalities at //! nonterminals, and the sink inflow lower bound from the source problem. -use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::IntegralFlowBundles; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -41,15 +41,15 @@ impl ReductionResult for ReductionIFBToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionIFBToILP {} -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_arcs", num_constraints = "num_bundles + num_vertices - 1", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + num_nonzeros = "num_arcs * (num_bundles + num_vertices - 1)", + }, +})] impl ReduceTo> for IntegralFlowBundles { type Result = ReductionIFBToILP; @@ -90,14 +90,16 @@ impl ReduceTo> for IntegralFlowBundles { } constraints.push(LinearConstraint::ge(sink_terms, self.requirement())); + let variables = self + .arc_upper_bounds() + .into_iter() + .map(|capacity| IntegerVariable::new(Some(0), Some(capacity))) + .collect::, _>>() + .map_err(Self::target_construction)?; + Ok(ReductionIFBToILP { - target: ILP::new( - self.num_arcs(), - constraints, - vec![], - ObjectiveSense::Minimize, - ) - .map_err(Self::target_construction)?, + target: ILP::with_variables(variables, constraints, vec![], ObjectiveSense::Minimize) + .map_err(Self::target_construction)?, }) } } diff --git a/src/rules/integralflowhomologousarcs_ilp.rs b/src/rules/integralflowhomologousarcs_ilp.rs index 502e80405..2ec627726 100644 --- a/src/rules/integralflowhomologousarcs_ilp.rs +++ b/src/rules/integralflowhomologousarcs_ilp.rs @@ -3,7 +3,7 @@ //! One integer flow variable per arc. Capacity bounds, conservation at //! non-terminals, homologous-pair equality, and sink inflow requirement. -use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::IntegralFlowHomologousArcs; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -40,21 +40,16 @@ impl ReductionResult for ReductionIFHAToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionIFHAToILP {} -#[reduction( - transform = upper_bound { - num_vars = "num_arcs", - num_constraints = "num_arcs^2 + num_arcs + num_vertices + 1", - }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] +#[reduction(transform = upper_bound { + num_vars = "num_arcs", + num_constraints = "num_arcs^2 + num_arcs + num_vertices + 1", + num_nonzeros = "num_arcs * (num_arcs^2 + num_arcs + num_vertices + 1)", +})] impl ReduceTo> for IntegralFlowHomologousArcs { type Result = ReductionIFHAToILP; fn reduce_to(&self) -> Result { let arcs = self.graph().arcs(); - let num_arcs = self.num_arcs(); let num_vertices = self.num_vertices(); let mut constraints = Vec::new(); @@ -98,8 +93,16 @@ impl ReduceTo> for IntegralFlowHomologousArcs { } constraints.push(LinearConstraint::ge(sink_terms, self.requirement())); + let variables = self + .capacities() + .iter() + .copied() + .map(|capacity| IntegerVariable::new(Some(0), Some(capacity))) + .collect::, _>>() + .map_err(Self::target_construction)?; + Ok(ReductionIFHAToILP { - target: ILP::new(num_arcs, constraints, vec![], ObjectiveSense::Minimize) + target: ILP::with_variables(variables, constraints, vec![], ObjectiveSense::Minimize) .map_err(Self::target_construction)?, }) } diff --git a/src/rules/integralflowwithmultipliers_ilp.rs b/src/rules/integralflowwithmultipliers_ilp.rs index 780e28dfb..dc41b9fcf 100644 --- a/src/rules/integralflowwithmultipliers_ilp.rs +++ b/src/rules/integralflowwithmultipliers_ilp.rs @@ -3,7 +3,7 @@ //! One integer flow variable per arc. Capacity bounds, multiplier-scaled //! conservation at non-terminals, and sink inflow requirement. -use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::IntegralFlowWithMultipliers; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -40,21 +40,20 @@ impl ReductionResult for ReductionIFWMToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionIFWMToILP {} -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_arcs", num_constraints = "num_arcs + num_vertices - 1", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + num_nonzeros = "num_arcs * (num_arcs + num_vertices - 1)", + }, +})] impl ReduceTo> for IntegralFlowWithMultipliers { type Result = ReductionIFWMToILP; fn reduce_to(&self) -> Result { let arcs = self.graph().arcs(); - let num_arcs = self.num_arcs(); let num_vertices = self.num_vertices(); let mut constraints = Vec::new(); @@ -96,8 +95,16 @@ impl ReduceTo> for IntegralFlowWithMultipliers { } constraints.push(LinearConstraint::ge(sink_terms, self.requirement())); + let variables = self + .capacities() + .iter() + .copied() + .map(|capacity| IntegerVariable::new(Some(0), Some(capacity))) + .collect::, _>>() + .map_err(Self::target_construction)?; + Ok(ReductionIFWMToILP { - target: ILP::new(num_arcs, constraints, vec![], ObjectiveSense::Minimize) + target: ILP::with_variables(variables, constraints, vec![], ObjectiveSense::Minimize) .map_err(Self::target_construction)?, }) } diff --git a/src/rules/isomorphicspanningtree_ilp.rs b/src/rules/isomorphicspanningtree_ilp.rs index b86657b1f..fadee1715 100644 --- a/src/rules/isomorphicspanningtree_ilp.rs +++ b/src/rules/isomorphicspanningtree_ilp.rs @@ -42,15 +42,11 @@ impl ReductionResult for ReductionISTToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionISTToILP {} -#[reduction( - transform = upper_bound { - num_vars = "num_vertices * num_vertices", - num_constraints = "2 * num_vertices + 2 * (num_vertices - 1) * num_vertices * num_vertices", - }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] +#[reduction(transform = upper_bound { + num_vars = "num_vertices * num_vertices", + num_constraints = "2 * num_vertices + 2 * (num_vertices - 1) * num_vertices * num_vertices", + num_nonzeros = "(num_vertices * num_vertices) * (2 * num_vertices + 2 * (num_vertices - 1) * num_vertices * num_vertices)", +})] impl ReduceTo> for IsomorphicSpanningTree { type Result = ReductionISTToILP; diff --git a/src/rules/kclique_ilp.rs b/src/rules/kclique_ilp.rs index 4ce5c4cf7..0ed5ae82e 100644 --- a/src/rules/kclique_ilp.rs +++ b/src/rules/kclique_ilp.rs @@ -57,15 +57,11 @@ impl ReductionResult for ReductionKCliqueToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionKCliqueToILP {} -#[reduction( - transform = upper_bound { - num_vars = "num_vertices", - num_constraints = "num_vertices^2 + 1", - }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] +#[reduction(transform = upper_bound { + num_vars = "num_vertices", + num_constraints = "num_vertices^2 + 1", + num_nonzeros = "num_vertices * (num_vertices^2 + 1)", +})] impl ReduceTo> for KClique { type Result = ReductionKCliqueToILP; diff --git a/src/rules/knapsack_ilp.rs b/src/rules/knapsack_ilp.rs index 2601ce5f9..1505e7a76 100644 --- a/src/rules/knapsack_ilp.rs +++ b/src/rules/knapsack_ilp.rs @@ -34,15 +34,15 @@ impl ReductionResult for ReductionKnapsackToILP { } } -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_items", num_constraints = "1", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + num_nonzeros = "num_items * 1", + }, +})] impl ReduceTo> for Knapsack { type Result = ReductionKnapsackToILP; diff --git a/src/rules/ksatisfiability_qubo.rs b/src/rules/ksatisfiability_qubo.rs index ab3cfcdc8..f6d4d2337 100644 --- a/src/rules/ksatisfiability_qubo.rs +++ b/src/rules/ksatisfiability_qubo.rs @@ -330,11 +330,9 @@ impl crate::rules::AggregateReductionResult for ReductionKSatToQUBO {} impl crate::rules::AggregateReductionResult for Reduction3SATToQUBO {} #[reduction( - transform = exact { - num_vars = "num_vars", - }, - unavailable = { - num_quadratic_terms = "clause literals can overlap and cancel in the QUBO coefficients", + transform = { + exact { num_vars = "num_vars" }, + upper_bound { num_quadratic_terms = "num_vars * (num_vars - 1) / 2" }, } )] impl ReduceTo>> for KSatisfiability { @@ -365,14 +363,14 @@ impl ReduceTo>> for KSatisfiability { } } -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_vars + num_clauses", }, - unavailable = { - num_quadratic_terms = "clause literals can overlap and cancel in the QUBO coefficients", - } -)] + upper_bound { + num_quadratic_terms = "(num_vars + num_clauses) * ((num_vars + num_clauses) - 1) / 2", + }, +})] impl ReduceTo>> for KSatisfiability { type Result = Reduction3SATToQUBO; diff --git a/src/rules/lengthboundeddisjointpaths_ilp.rs b/src/rules/lengthboundeddisjointpaths_ilp.rs index 5745f463e..2c330682c 100644 --- a/src/rules/lengthboundeddisjointpaths_ilp.rs +++ b/src/rules/lengthboundeddisjointpaths_ilp.rs @@ -100,15 +100,11 @@ impl ReductionResult for ReductionLBDPToILP { } } -#[reduction( - transform = upper_bound { - num_vars = "max_paths * 2 * num_edges + max_paths", - num_constraints = "max_paths * num_vertices + max_paths * num_edges + max_paths + num_edges + num_vertices + max_paths", - }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] +#[reduction(transform = upper_bound { + num_vars = "max_paths * 2 * num_edges + max_paths", + num_constraints = "max_paths * num_vertices + max_paths * num_edges + max_paths + num_edges + num_vertices + max_paths", + num_nonzeros = "(max_paths * 2 * num_edges + max_paths) * (max_paths * num_vertices + max_paths * num_edges + max_paths + num_edges + num_vertices + max_paths)", +})] impl ReduceTo> for LengthBoundedDisjointPaths { type Result = ReductionLBDPToILP; diff --git a/src/rules/longestcircuit_ilp.rs b/src/rules/longestcircuit_ilp.rs index b122df15a..cbac8605f 100644 --- a/src/rules/longestcircuit_ilp.rs +++ b/src/rules/longestcircuit_ilp.rs @@ -56,15 +56,15 @@ impl ReductionResult for ReductionLongestCircuitToILP { } } -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_edges + 2 * num_vertices + 2 * num_edges * num_vertices", num_constraints = "2 + num_vertices + 2 * num_vertices^2 + 2 * num_edges * num_vertices", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + num_nonzeros = "(num_edges + 2 * num_vertices + 2 * num_edges * num_vertices) * (2 + num_vertices + 2 * num_vertices^2 + 2 * num_edges * num_vertices)", + }, +})] impl ReduceTo> for LongestCircuit { type Result = ReductionLongestCircuitToILP; @@ -209,15 +209,15 @@ impl ReductionResult for ReductionDecisionLongestCircuitToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionDecisionLongestCircuitToILP {} -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_edges + 2 * num_vertices + 2 * num_edges * num_vertices", num_constraints = "3 + num_vertices + 2 * num_vertices^2 + 2 * num_edges * num_vertices", }, - unavailable = { - num_nonzeros = "depends on the graph and nonzero edge lengths", - } -)] + upper_bound { + num_nonzeros = "(num_edges + 2 * num_vertices + 2 * num_edges * num_vertices) * (3 + num_vertices + 2 * num_vertices^2 + 2 * num_edges * num_vertices)", + }, +})] impl ReduceTo> for Decision> { type Result = ReductionDecisionLongestCircuitToILP; diff --git a/src/rules/longestpath_ilp.rs b/src/rules/longestpath_ilp.rs index ef0246564..e2e99c0da 100644 --- a/src/rules/longestpath_ilp.rs +++ b/src/rules/longestpath_ilp.rs @@ -5,7 +5,7 @@ //! path positions. Flow-balance constraints force a single directed `s-t` path, //! while MTZ-style ordering constraints eliminate detached cycles. -use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::LongestPath; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -48,15 +48,15 @@ impl ReductionResult for ReductionLongestPathToILP { } } -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "2 * num_edges + num_vertices", num_constraints = "5 * num_edges + 4 * num_vertices + 1", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + num_nonzeros = "(2 * num_edges + num_vertices) * (5 * num_edges + 4 * num_vertices + 1)", + }, +})] impl ReduceTo> for LongestPath { type Result = ReductionLongestPathToILP; @@ -172,9 +172,19 @@ impl ReduceTo> for LongestPath { objective.push((ReductionLongestPathToILP::arc_var(edge_idx, 1), coeff)); } + let mut variables = vec![IntegerVariable::binary(); num_vars]; + variables[2 * num_edges..].fill( + IntegerVariable::new(Some(0), Some(max_order)).map_err(Self::target_construction)?, + ); + Ok(ReductionLongestPathToILP { - target: ILP::new(num_vars, constraints, objective, ObjectiveSense::Maximize) - .map_err(Self::target_construction)?, + target: ILP::with_variables( + variables, + constraints, + objective, + ObjectiveSense::Maximize, + ) + .map_err(Self::target_construction)?, num_edges, }) } diff --git a/src/rules/maximalis_ilp.rs b/src/rules/maximalis_ilp.rs index 1d2e3f93c..0830cbd6e 100644 --- a/src/rules/maximalis_ilp.rs +++ b/src/rules/maximalis_ilp.rs @@ -32,15 +32,15 @@ impl ReductionResult for ReductionMxISToILP { } } -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_vertices", num_constraints = "num_edges + num_vertices", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + num_nonzeros = "num_vertices * (num_edges + num_vertices)", + }, +})] impl ReduceTo> for MaximalIS { type Result = ReductionMxISToILP; diff --git a/src/rules/maximum2satisfiability_ilp.rs b/src/rules/maximum2satisfiability_ilp.rs index cfa12df13..a9445762b 100644 --- a/src/rules/maximum2satisfiability_ilp.rs +++ b/src/rules/maximum2satisfiability_ilp.rs @@ -40,15 +40,15 @@ impl ReductionResult for ReductionMaximum2SatisfiabilityToILP { } } -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_vars + num_clauses", num_constraints = "num_clauses", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + num_nonzeros = "(num_vars + num_clauses) * num_clauses", + }, +})] impl ReduceTo> for Maximum2Satisfiability { type Result = ReductionMaximum2SatisfiabilityToILP; diff --git a/src/rules/maximumclique_ilp.rs b/src/rules/maximumclique_ilp.rs index 7f62e15b1..f4922efd5 100644 --- a/src/rules/maximumclique_ilp.rs +++ b/src/rules/maximumclique_ilp.rs @@ -45,15 +45,11 @@ impl ReductionResult for ReductionCliqueToILP { } } -#[reduction( - transform = upper_bound { - num_vars = "num_vertices", - num_constraints = "num_vertices^2", - }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] +#[reduction(transform = upper_bound { + num_vars = "num_vertices", + num_constraints = "num_vertices^2", + num_nonzeros = "num_vertices * (num_vertices^2)", +})] impl ReduceTo> for MaximumClique { type Result = ReductionCliqueToILP; diff --git a/src/rules/maximumcokplex_ilp.rs b/src/rules/maximumcokplex_ilp.rs index 8021bb316..d9e6067e1 100644 --- a/src/rules/maximumcokplex_ilp.rs +++ b/src/rules/maximumcokplex_ilp.rs @@ -80,15 +80,15 @@ where }) } -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_vertices", num_constraints = "num_vertices", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + num_nonzeros = "num_vertices * num_vertices", + }, +})] impl ReduceTo> for MaximumCoKPlex { type Result = ReductionCoKPlexToILP; @@ -109,15 +109,15 @@ impl ReduceTo> for MaximumCoKPlex { } } -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_vertices", num_constraints = "num_vertices", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + num_nonzeros = "num_vertices * num_vertices", + }, +})] impl ReduceTo> for MaximumCoKPlex { type Result = ReductionCoKPlexToILP; diff --git a/src/rules/maximumcommonedgesubgraph_ilp.rs b/src/rules/maximumcommonedgesubgraph_ilp.rs index 4ab7d8858..64001e434 100644 --- a/src/rules/maximumcommonedgesubgraph_ilp.rs +++ b/src/rules/maximumcommonedgesubgraph_ilp.rs @@ -66,15 +66,11 @@ impl ReductionResult for ReductionMCESToILP { } } -#[reduction( - transform = upper_bound { - num_vars = "num_vertices_1 * num_vertices_2 + num_arcs_1 * num_arcs_2", - num_constraints = "num_vertices_1 + num_vertices_2 + 3 * num_arcs_1 * num_arcs_2", - }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] +#[reduction(transform = upper_bound { + num_vars = "num_vertices_1 * num_vertices_2 + num_arcs_1 * num_arcs_2", + num_constraints = "num_vertices_1 + num_vertices_2 + 3 * num_arcs_1 * num_arcs_2", + num_nonzeros = "(num_vertices_1 * num_vertices_2 + num_arcs_1 * num_arcs_2) * (num_vertices_1 + num_vertices_2 + 3 * num_arcs_1 * num_arcs_2)", +})] impl ReduceTo> for MaximumCommonEdgeSubgraph { type Result = ReductionMCESToILP; diff --git a/src/rules/maximumdomaticnumber_ilp.rs b/src/rules/maximumdomaticnumber_ilp.rs index 3a0221a6e..acba50a38 100644 --- a/src/rules/maximumdomaticnumber_ilp.rs +++ b/src/rules/maximumdomaticnumber_ilp.rs @@ -58,15 +58,15 @@ impl ReductionResult for ReductionDomaticNumberToILP { } } -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_vertices * num_vertices + num_vertices", num_constraints = "num_vertices + num_vertices * num_vertices + num_vertices * num_vertices", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + num_nonzeros = "(num_vertices * num_vertices + num_vertices) * (num_vertices + num_vertices * num_vertices + num_vertices * num_vertices)", + }, +})] impl ReduceTo> for MaximumDomaticNumber { type Result = ReductionDomaticNumberToILP; diff --git a/src/rules/maximumedgeweightedkclique_ilp.rs b/src/rules/maximumedgeweightedkclique_ilp.rs index c7909601b..58f4cf93d 100644 --- a/src/rules/maximumedgeweightedkclique_ilp.rs +++ b/src/rules/maximumedgeweightedkclique_ilp.rs @@ -138,15 +138,15 @@ where }) } -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_vertices + num_edges", num_constraints = "1 + num_vertices * (num_vertices - 1) / 2 + 2 * num_edges", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + num_nonzeros = "(num_vertices + num_edges) * (1 + num_vertices * (num_vertices - 1) / 2 + 2 * num_edges)", + }, +})] impl ReduceTo> for MaximumEdgeWeightedKClique { type Result = ReductionMaximumEdgeWeightedKCliqueToILP; @@ -155,15 +155,15 @@ impl ReduceTo> for MaximumEdgeWeightedKClique { } } -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_vertices + num_edges", num_constraints = "1 + num_vertices * (num_vertices - 1) / 2 + 2 * num_edges", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + num_nonzeros = "(num_vertices + num_edges) * (1 + num_vertices * (num_vertices - 1) / 2 + 2 * num_edges)", + }, +})] impl ReduceTo> for MaximumEdgeWeightedKClique { type Result = ReductionMaximumEdgeWeightedKCliqueToILP; diff --git a/src/rules/maximumleafspanningtree_ilp.rs b/src/rules/maximumleafspanningtree_ilp.rs index 2623018b7..1fac86ae3 100644 --- a/src/rules/maximumleafspanningtree_ilp.rs +++ b/src/rules/maximumleafspanningtree_ilp.rs @@ -18,7 +18,7 @@ //! //! Objective: maximize sum(z_v) -use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::MaximumLeafSpanningTree; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -55,15 +55,15 @@ impl ReductionResult for ReductionMaximumLeafSpanningTreeToILP { } } -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "3 * num_edges + num_vertices", num_constraints = "3 * num_vertices + 2 * num_edges + 1", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + num_nonzeros = "(3 * num_edges + num_vertices) * (3 * num_vertices + 2 * num_edges + 1)", + }, +})] impl ReduceTo> for MaximumLeafSpanningTree { type Result = ReductionMaximumLeafSpanningTreeToILP; @@ -156,8 +156,15 @@ impl ReduceTo> for MaximumLeafSpanningTree { // Objective: maximize sum(z_v) let objective: Vec<(usize, i64)> = (0..n).map(|v| (leaf_var(v), 1)).collect(); - let target = ILP::new(num_vars, constraints, objective, ObjectiveSense::Maximize) - .map_err(Self::target_construction)?; + let mut variables = vec![IntegerVariable::binary(); num_vars]; + variables[m + n..].fill( + IntegerVariable::new(Some(0), Some((n_i64 - 1).max(0))) + .map_err(Self::target_construction)?, + ); + + let target = + ILP::with_variables(variables, constraints, objective, ObjectiveSense::Maximize) + .map_err(Self::target_construction)?; Ok(ReductionMaximumLeafSpanningTreeToILP { target, diff --git a/src/rules/maximummatching_ilp.rs b/src/rules/maximummatching_ilp.rs index a4e27b0d9..84f01cdb4 100644 --- a/src/rules/maximummatching_ilp.rs +++ b/src/rules/maximummatching_ilp.rs @@ -45,15 +45,15 @@ impl ReductionResult for ReductionMatchingToILP { } } -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_edges", + }, + upper_bound { num_constraints = "num_vertices", + num_nonzeros = "2 * num_edges", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] +})] impl ReduceTo> for MaximumMatching { type Result = ReductionMatchingToILP; diff --git a/src/rules/maximumsetpacking_ilp.rs b/src/rules/maximumsetpacking_ilp.rs index b2195358a..be215a8f8 100644 --- a/src/rules/maximumsetpacking_ilp.rs +++ b/src/rules/maximumsetpacking_ilp.rs @@ -39,15 +39,11 @@ impl ReductionResult for ReductionSPToILP { } } -#[reduction( - transform = upper_bound { - num_vars = "num_sets", - num_constraints = "universe_size", - }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] +#[reduction(transform = upper_bound { + num_vars = "num_sets", + num_constraints = "universe_size", + num_nonzeros = "num_sets * universe_size", +})] impl ReduceTo> for MaximumSetPacking { type Result = ReductionSPToILP; diff --git a/src/rules/maximumsetpacking_qubo.rs b/src/rules/maximumsetpacking_qubo.rs index 69ef23985..fe5ffd594 100644 --- a/src/rules/maximumsetpacking_qubo.rs +++ b/src/rules/maximumsetpacking_qubo.rs @@ -40,14 +40,14 @@ impl ReductionResult for ReductionSPToQUBO { } } -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_sets", }, - unavailable = { - num_quadratic_terms = "the number of overlapping set pairs is not a registered source parameter", - } -)] + upper_bound { + num_quadratic_terms = "num_sets * (num_sets - 1) / 2", + }, +})] impl ReduceTo> for MaximumSetPacking { type Result = ReductionSPToQUBO; diff --git a/src/rules/minimumcapacitatedspanningtree_ilp.rs b/src/rules/minimumcapacitatedspanningtree_ilp.rs index 0886ad247..42f2263e1 100644 --- a/src/rules/minimumcapacitatedspanningtree_ilp.rs +++ b/src/rules/minimumcapacitatedspanningtree_ilp.rs @@ -24,7 +24,7 @@ //! //! Objective: minimize sum(w_e * y_e) -use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::MinimumCapacitatedSpanningTree; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -62,15 +62,11 @@ impl ReductionResult for ReductionMinimumCapacitatedSpanningTreeToILP { } } -#[reduction( - transform = upper_bound { - num_vars = "5 * num_edges", - num_constraints = "5 * num_edges + 2 * num_vertices + 1", - }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] +#[reduction(transform = upper_bound { + num_vars = "5 * num_edges", + num_constraints = "5 * num_edges + 2 * num_vertices + 1", + num_nonzeros = "(5 * num_edges) * (5 * num_edges + 2 * num_vertices + 1)", +})] impl ReduceTo> for MinimumCapacitatedSpanningTree { type Result = ReductionMinimumCapacitatedSpanningTreeToILP; @@ -208,8 +204,19 @@ impl ReduceTo> for MinimumCapacitatedSpanningTree { .map(|(edge_idx, weight)| (edge_var(edge_idx), weight.to_sum())) .collect(); - let target = ILP::new(num_vars, constraints, objective, ObjectiveSense::Minimize) - .map_err(Self::target_construction)?; + let mut variables = vec![IntegerVariable::binary(); num_vars]; + variables[m..3 * m].fill( + IntegerVariable::new(Some(0), Some(cap.min(total_req).max(0))) + .map_err(Self::target_construction)?, + ); + variables[3 * m..].fill( + IntegerVariable::new(Some(0), Some(connectivity_total)) + .map_err(Self::target_construction)?, + ); + + let target = + ILP::with_variables(variables, constraints, objective, ObjectiveSense::Minimize) + .map_err(Self::target_construction)?; Ok(ReductionMinimumCapacitatedSpanningTreeToILP { target, diff --git a/src/rules/minimumcoveringbycliques_ilp.rs b/src/rules/minimumcoveringbycliques_ilp.rs index fc139660b..63278b39d 100644 --- a/src/rules/minimumcoveringbycliques_ilp.rs +++ b/src/rules/minimumcoveringbycliques_ilp.rs @@ -61,15 +61,15 @@ impl ReductionResult for ReductionMinimumCoveringByCliquesToILP { } } -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_vertices * num_edges + num_edges + num_edges * num_edges", num_constraints = "num_vertices * num_edges + (num_vertices * (num_vertices - 1) / 2 - num_edges) * num_edges + 3 * num_edges * num_edges + num_edges", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + num_nonzeros = "(num_vertices * num_edges + num_edges + num_edges * num_edges) * (num_vertices * num_edges + (num_vertices * (num_vertices - 1) / 2 - num_edges) * num_edges + 3 * num_edges * num_edges + num_edges)", + }, +})] impl ReduceTo> for MinimumCoveringByCliques { type Result = ReductionMinimumCoveringByCliquesToILP; diff --git a/src/rules/minimumcutintoboundedsets_ilp.rs b/src/rules/minimumcutintoboundedsets_ilp.rs index bb0331be8..0386d9f6a 100644 --- a/src/rules/minimumcutintoboundedsets_ilp.rs +++ b/src/rules/minimumcutintoboundedsets_ilp.rs @@ -39,15 +39,15 @@ impl ReductionResult for ReductionMinCutBSToILP { } } -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_vertices + num_edges", num_constraints = "2 + 2 + 2 * num_edges", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + num_nonzeros = "(num_vertices + num_edges) * (2 + 2 + 2 * num_edges)", + }, +})] impl ReduceTo> for MinimumCutIntoBoundedSets { type Result = ReductionMinCutBSToILP; diff --git a/src/rules/minimumdiscreteplanarinversekinematics_qubo.rs b/src/rules/minimumdiscreteplanarinversekinematics_qubo.rs index 54e00005b..5597a550a 100644 --- a/src/rules/minimumdiscreteplanarinversekinematics_qubo.rs +++ b/src/rules/minimumdiscreteplanarinversekinematics_qubo.rs @@ -78,10 +78,13 @@ impl ReductionResult for ReductionMinimumDiscretePlanarInverseKinematicsToQUBO { } } -#[reduction(transform = exact { - num_vars = "num_orientation_samples", -}, unavailable = { - num_quadratic_terms = "the nonzero products depend on the sampled geometry", +#[reduction(transform = { + exact { + num_vars = "num_orientation_samples", + }, + upper_bound { + num_quadratic_terms = "num_orientation_samples * (num_orientation_samples - 1) / 2", + }, })] impl ReduceTo> for MinimumDiscretePlanarInverseKinematics { type Result = ReductionMinimumDiscretePlanarInverseKinematicsToQUBO; diff --git a/src/rules/minimumdominatingset_ilp.rs b/src/rules/minimumdominatingset_ilp.rs index 5b1d3f945..83ad5fb96 100644 --- a/src/rules/minimumdominatingset_ilp.rs +++ b/src/rules/minimumdominatingset_ilp.rs @@ -46,15 +46,15 @@ impl ReductionResult for ReductionDSToILP { } } -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_vertices", num_constraints = "num_vertices", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + num_nonzeros = "num_vertices * num_vertices", + }, +})] impl ReduceTo> for MinimumDominatingSet { type Result = ReductionDSToILP; diff --git a/src/rules/minimumedgecostflow_ilp.rs b/src/rules/minimumedgecostflow_ilp.rs index dd30eb04a..9588f942b 100644 --- a/src/rules/minimumedgecostflow_ilp.rs +++ b/src/rules/minimumedgecostflow_ilp.rs @@ -18,7 +18,7 @@ //! Objective: minimize Σ p(a) · y_a. //! Extraction: first m variables are the flow values. -use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::MinimumEdgeCostFlow; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -53,15 +53,15 @@ impl ReductionResult for ReductionMECFToILP { } } -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "2 * num_edges", num_constraints = "2 * num_edges + num_vertices - 1", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + num_nonzeros = "(2 * num_edges) * (2 * num_edges + num_vertices - 1)", + }, +})] impl ReduceTo> for MinimumEdgeCostFlow { type Result = ReductionMECFToILP; @@ -124,9 +124,22 @@ impl ReduceTo> for MinimumEdgeCostFlow { // Objective: minimize Σ p(a) · y_a let objective: Vec<(usize, i64)> = (0..m).map(|a| (y(a), self.prices()[a])).collect(); + let mut variables = self + .capacities() + .iter() + .map(|&capacity| IntegerVariable::new(Some(0), Some(capacity))) + .collect::, _>>() + .map_err(Self::target_construction)?; + variables.resize(num_vars, IntegerVariable::binary()); + Ok(ReductionMECFToILP { - target: ILP::new(num_vars, constraints, objective, ObjectiveSense::Minimize) - .map_err(Self::target_construction)?, + target: ILP::with_variables( + variables, + constraints, + objective, + ObjectiveSense::Minimize, + ) + .map_err(Self::target_construction)?, num_edges: m, }) } diff --git a/src/rules/minimumexternalmacrodatacompression_ilp.rs b/src/rules/minimumexternalmacrodatacompression_ilp.rs index ba34bba53..5f6acb6c8 100644 --- a/src/rules/minimumexternalmacrodatacompression_ilp.rs +++ b/src/rules/minimumexternalmacrodatacompression_ilp.rs @@ -213,15 +213,11 @@ fn encode_pointer(n: usize, start: usize, len: usize) -> usize { idx + len - 1 } -#[reduction( - transform = upper_bound { - num_vars = "string_length * alphabet_size + 2 * string_length + string_length ^ 3", - num_constraints = "string_length + string_length * alphabet_size + string_length + string_length + 1 + string_length ^ 3 * string_length", - }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] +#[reduction(transform = upper_bound { + num_vars = "string_length * alphabet_size + 2 * string_length + string_length ^ 3", + num_constraints = "string_length + string_length * alphabet_size + string_length + string_length + 1 + string_length ^ 3 * string_length", + num_nonzeros = "(string_length * alphabet_size + 2 * string_length + string_length ^ 3) * (string_length + string_length * alphabet_size + string_length + string_length + 1 + string_length ^ 3 * string_length)", +})] impl ReduceTo> for MinimumExternalMacroDataCompression { type Result = ReductionEMDCToILP; diff --git a/src/rules/minimumfaultdetectiontestset_ilp.rs b/src/rules/minimumfaultdetectiontestset_ilp.rs index bf0893c70..c98bc4051 100644 --- a/src/rules/minimumfaultdetectiontestset_ilp.rs +++ b/src/rules/minimumfaultdetectiontestset_ilp.rs @@ -43,15 +43,15 @@ impl ReductionResult for ReductionMFDTSToILP { } } -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_inputs * num_outputs", num_constraints = "num_vertices - num_inputs - num_outputs", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + num_nonzeros = "(num_inputs * num_outputs) * (num_vertices - num_inputs - num_outputs)", + }, +})] impl ReduceTo> for MinimumFaultDetectionTestSet { type Result = ReductionMFDTSToILP; diff --git a/src/rules/minimumfeedbackarcset_ilp.rs b/src/rules/minimumfeedbackarcset_ilp.rs index 2de455d85..b2f236d5b 100644 --- a/src/rules/minimumfeedbackarcset_ilp.rs +++ b/src/rules/minimumfeedbackarcset_ilp.rs @@ -9,7 +9,7 @@ //! - Objective: Minimize Σ w_a * y_a //! - Variable layout: first |A| are y_a, next |V| are o_v -use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::MinimumFeedbackArcSet; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -54,15 +54,15 @@ impl ReductionResult for ReductionFASToILP { } } -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_arcs + num_vertices", num_constraints = "num_arcs + num_arcs + num_vertices", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + num_nonzeros = "(num_arcs + num_vertices) * (num_arcs + num_arcs + num_vertices)", + }, +})] impl ReduceTo> for MinimumFeedbackArcSet { type Result = ReductionFASToILP; @@ -109,8 +109,15 @@ impl ReduceTo> for MinimumFeedbackArcSet { .map(|(arc, &weight)| (arc, weight)) .collect(); - let target = ILP::new(num_vars, constraints, objective, ObjectiveSense::Minimize) - .map_err(Self::target_construction)?; + let mut variables = vec![IntegerVariable::binary(); num_vars]; + variables[m..].fill( + IntegerVariable::new(Some(0), Some((n_i64 - 1).max(0))) + .map_err(Self::target_construction)?, + ); + + let target = + ILP::with_variables(variables, constraints, objective, ObjectiveSense::Minimize) + .map_err(Self::target_construction)?; Ok(ReductionFASToILP { target, diff --git a/src/rules/minimumfeedbackvertexset_ilp.rs b/src/rules/minimumfeedbackvertexset_ilp.rs index 8b712b1a1..b6e0b88e7 100644 --- a/src/rules/minimumfeedbackvertexset_ilp.rs +++ b/src/rules/minimumfeedbackvertexset_ilp.rs @@ -6,7 +6,7 @@ //! Plus binary bounds (x_i <= 1) and order bounds (o_i <= n-1) //! - Objective: Minimize the weighted sum of removed vertices -use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::MinimumFeedbackVertexSet; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -51,15 +51,15 @@ impl ReductionResult for ReductionMFVSToILP { } } -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "2 * num_vertices", num_constraints = "num_arcs + 2 * num_vertices", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + num_nonzeros = "(2 * num_vertices) * (num_arcs + 2 * num_vertices)", + }, +})] impl ReduceTo> for MinimumFeedbackVertexSet { type Result = ReductionMFVSToILP; @@ -106,8 +106,15 @@ impl ReduceTo> for MinimumFeedbackVertexSet { .map(|(vertex, &weight)| (vertex, weight)) .collect(); - let target = ILP::new(num_vars, constraints, objective, ObjectiveSense::Minimize) - .map_err(>>::target_construction)?; + let mut variables = vec![IntegerVariable::binary(); num_vars]; + variables[n..].fill( + IntegerVariable::new(Some(0), Some((n_i64 - 1).max(0))) + .map_err(>>::target_construction)?, + ); + + let target = + ILP::with_variables(variables, constraints, objective, ObjectiveSense::Minimize) + .map_err(>>::target_construction)?; Ok(ReductionMFVSToILP { target, diff --git a/src/rules/minimumgraphbandwidth_ilp.rs b/src/rules/minimumgraphbandwidth_ilp.rs index 76e1b5692..9dc8f2914 100644 --- a/src/rules/minimumgraphbandwidth_ilp.rs +++ b/src/rules/minimumgraphbandwidth_ilp.rs @@ -7,7 +7,7 @@ //! - For each edge (u,v): pos_u - pos_v <= B, pos_v - pos_u <= B //! - Objective: minimize B -use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::MinimumGraphBandwidth; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -49,15 +49,15 @@ impl ReductionResult for ReductionMGBToILP { } } -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_vertices^2 + num_vertices + 1", num_constraints = "2 * num_vertices + num_vertices^2 + num_vertices + num_vertices + 1 + 2 * num_edges", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + num_nonzeros = "(num_vertices^2 + num_vertices + 1) * (2 * num_vertices + num_vertices^2 + num_vertices + num_vertices + 1 + 2 * num_edges)", + }, +})] impl ReduceTo> for MinimumGraphBandwidth { type Result = ReductionMGBToILP; @@ -131,8 +131,15 @@ impl ReduceTo> for MinimumGraphBandwidth { // Objective: minimize B let objective = vec![(b_idx, 1)]; - let target = ILP::new(num_vars, constraints, objective, ObjectiveSense::Minimize) - .map_err(Self::target_construction)?; + let mut variables = vec![IntegerVariable::binary(); num_vars]; + variables[num_x..].fill( + IntegerVariable::new(Some(0), Some((n_i64 - 1).max(0))) + .map_err(Self::target_construction)?, + ); + + let target = + ILP::with_variables(variables, constraints, objective, ObjectiveSense::Minimize) + .map_err(Self::target_construction)?; Ok(ReductionMGBToILP { target, diff --git a/src/rules/minimumhittingset_ilp.rs b/src/rules/minimumhittingset_ilp.rs index fe8f498d9..e9771539d 100644 --- a/src/rules/minimumhittingset_ilp.rs +++ b/src/rules/minimumhittingset_ilp.rs @@ -31,15 +31,15 @@ impl ReductionResult for ReductionHSToILP { } } -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "universe_size", num_constraints = "num_sets", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + num_nonzeros = "universe_size * num_sets", + }, +})] impl ReduceTo> for MinimumHittingSet { type Result = ReductionHSToILP; diff --git a/src/rules/minimuminternalmacrodatacompression_ilp.rs b/src/rules/minimuminternalmacrodatacompression_ilp.rs index 0ff986409..1c58d96b1 100644 --- a/src/rules/minimuminternalmacrodatacompression_ilp.rs +++ b/src/rules/minimuminternalmacrodatacompression_ilp.rs @@ -159,15 +159,11 @@ impl ReductionResult for ReductionIMDCToILP { } } -#[reduction( - transform = upper_bound { - num_vars = "string_len + string_len ^ 3", - num_constraints = "string_len + 1", - }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] +#[reduction(transform = upper_bound { + num_vars = "string_len + string_len ^ 3", + num_constraints = "string_len + 1", + num_nonzeros = "(string_len + string_len ^ 3) * (string_len + 1)", +})] impl ReduceTo> for MinimumInternalMacroDataCompression { type Result = ReductionIMDCToILP; diff --git a/src/rules/minimummaximalmatching_ilp.rs b/src/rules/minimummaximalmatching_ilp.rs index 33e1f2afe..93dcd0cf3 100644 --- a/src/rules/minimummaximalmatching_ilp.rs +++ b/src/rules/minimummaximalmatching_ilp.rs @@ -48,15 +48,15 @@ impl ReductionResult for ReductionMMMToILP { } } -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_edges", num_constraints = "num_vertices + num_edges", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + num_nonzeros = "num_edges * (num_vertices + num_edges)", + }, +})] impl ReduceTo> for MinimumMaximalMatching { type Result = ReductionMMMToILP; diff --git a/src/rules/minimummetricdimension_ilp.rs b/src/rules/minimummetricdimension_ilp.rs index a10a1e9a5..12fef2119 100644 --- a/src/rules/minimummetricdimension_ilp.rs +++ b/src/rules/minimummetricdimension_ilp.rs @@ -48,15 +48,15 @@ impl ReductionResult for ReductionMDToILP { } } -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_vertices", num_constraints = "num_vertices * (num_vertices - 1) / 2", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + num_nonzeros = "num_vertices * (num_vertices * (num_vertices - 1) / 2)", + }, +})] impl ReduceTo> for MinimumMetricDimension { type Result = ReductionMDToILP; diff --git a/src/rules/minimummultiwaycut_ilp.rs b/src/rules/minimummultiwaycut_ilp.rs index 51c157007..4600bf0ef 100644 --- a/src/rules/minimummultiwaycut_ilp.rs +++ b/src/rules/minimummultiwaycut_ilp.rs @@ -57,15 +57,15 @@ impl ReductionResult for ReductionMMCToILP { } } -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_terminals * num_vertices + num_edges", num_constraints = "num_vertices + 2 * num_terminals * num_edges + num_terminals * num_terminals", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + num_nonzeros = "(num_terminals * num_vertices + num_edges) * (num_vertices + 2 * num_terminals * num_edges + num_terminals * num_terminals)", + }, +})] impl ReduceTo> for MinimumMultiwayCut { type Result = ReductionMMCToILP; diff --git a/src/rules/minimummultiwaycut_qubo.rs b/src/rules/minimummultiwaycut_qubo.rs index 21088ad9e..bdfb81d6e 100644 --- a/src/rules/minimummultiwaycut_qubo.rs +++ b/src/rules/minimummultiwaycut_qubo.rs @@ -84,10 +84,13 @@ impl ReductionResult for ReductionMinimumMultiwayCutToQUBO { } } -#[reduction(transform = exact { - num_vars = "num_terminals * num_vertices", -}, unavailable = { - num_quadratic_terms = "the nonzero products depend on edge weights and terminal placement", +#[reduction(transform = { + exact { + num_vars = "num_terminals * num_vertices", + }, + upper_bound { + num_quadratic_terms = "(num_terminals * num_vertices) * ((num_terminals * num_vertices) - 1) / 2", + }, })] impl ReduceTo> for MinimumMultiwayCut { type Result = ReductionMinimumMultiwayCutToQUBO; diff --git a/src/rules/minimumsetcovering_ilp.rs b/src/rules/minimumsetcovering_ilp.rs index 6f208be75..7b830ac62 100644 --- a/src/rules/minimumsetcovering_ilp.rs +++ b/src/rules/minimumsetcovering_ilp.rs @@ -43,15 +43,15 @@ impl ReductionResult for ReductionSCToILP { } } -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_sets", num_constraints = "universe_size", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + num_nonzeros = "num_sets * universe_size", + }, +})] impl ReduceTo> for MinimumSetCovering { type Result = ReductionSCToILP; diff --git a/src/rules/minimumsummulticenter_ilp.rs b/src/rules/minimumsummulticenter_ilp.rs index bb6f82fd6..edfa101cd 100644 --- a/src/rules/minimumsummulticenter_ilp.rs +++ b/src/rules/minimumsummulticenter_ilp.rs @@ -117,15 +117,11 @@ fn weighted_distances_msmc( dist } -#[reduction( - transform = upper_bound { - num_vars = "num_vertices + num_vertices^2", - num_constraints = "num_vertices^2 + 2 * num_vertices + 1", - }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] +#[reduction(transform = upper_bound { + num_vars = "num_vertices + num_vertices^2", + num_constraints = "num_vertices^2 + 2 * num_vertices + 1", + num_nonzeros = "(num_vertices + num_vertices^2) * (num_vertices^2 + 2 * num_vertices + 1)", +})] impl ReduceTo> for MinimumSumMulticenter { type Result = ReductionMSMCToILP; diff --git a/src/rules/minimumtardinesssequencing_ilp.rs b/src/rules/minimumtardinesssequencing_ilp.rs index e8533153d..bbf61f745 100644 --- a/src/rules/minimumtardinesssequencing_ilp.rs +++ b/src/rules/minimumtardinesssequencing_ilp.rs @@ -104,15 +104,15 @@ fn build_common_constraints( } // Unit-length variant -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_tasks * num_tasks + num_tasks", num_constraints = "2 * num_tasks + num_precedences + num_tasks", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + num_nonzeros = "(num_tasks * num_tasks + num_tasks) * (2 * num_tasks + num_precedences + num_tasks)", + }, +})] impl ReduceTo> for MinimumTardinessSequencing { type Result = ReductionMTSToILP; @@ -150,15 +150,15 @@ impl ReduceTo> for MinimumTardinessSequencing { } // Arbitrary-length variant -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_tasks * num_tasks + num_tasks", num_constraints = "2 * num_tasks + num_precedences + num_tasks * num_tasks", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + num_nonzeros = "(num_tasks * num_tasks + num_tasks) * (2 * num_tasks + num_precedences + num_tasks * num_tasks)", + }, +})] impl ReduceTo> for MinimumTardinessSequencing { type Result = ReductionMTSWeightedToILP; diff --git a/src/rules/minimumvertexcover_minimummaximalmatching.rs b/src/rules/minimumvertexcover_minimummaximalmatching.rs index 9338eae91..4fccecbdd 100644 --- a/src/rules/minimumvertexcover_minimummaximalmatching.rs +++ b/src/rules/minimumvertexcover_minimummaximalmatching.rs @@ -21,10 +21,10 @@ inventory::submit! { source_variant_fn: as Problem>::variant, target_variant_fn: as Problem>::variant, parameter_declarations_fn: || ReductionParameterDeclarations { - relation: Some(crate::parameters::ParameterRelation::Exact), + fields: vec![ - ("num_vertices", crate::expr::Expr::variable("num_vertices")), - ("num_edges", crate::expr::Expr::variable("num_edges")), + ("num_vertices", crate::parameters::ParameterRelation::Exact, crate::expr::Expr::variable("num_vertices")), + ("num_edges", crate::parameters::ParameterRelation::Exact, crate::expr::Expr::variable("num_edges")), ], unavailable: vec![], }, diff --git a/src/rules/minimumweightdecoding_ilp.rs b/src/rules/minimumweightdecoding_ilp.rs index 970470c71..606fe757d 100644 --- a/src/rules/minimumweightdecoding_ilp.rs +++ b/src/rules/minimumweightdecoding_ilp.rs @@ -16,7 +16,7 @@ //! Objective: minimize Σ x_j (Hamming weight). use crate::models::algebraic::MinimumWeightDecoding; -use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -53,15 +53,15 @@ impl ReductionResult for ReductionMinimumWeightDecodingToILP { } } -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_cols + num_rows", num_constraints = "num_rows + num_cols", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + num_nonzeros = "(num_cols + num_rows) * (num_rows + num_cols)", + }, +})] impl ReduceTo> for MinimumWeightDecoding { type Result = ReductionMinimumWeightDecodingToILP; @@ -96,9 +96,23 @@ impl ReduceTo> for MinimumWeightDecoding { // Objective: minimize Σ x_j let objective: Vec<(usize, i64)> = (0..m).map(|j| (x(j), 1)).collect(); + let mut variables = vec![IntegerVariable::binary(); num_vars]; + variables[m..].fill( + IntegerVariable::new( + Some(0), + Some(Self::exact_i64(m / 2, "bounding parity quotients")?), + ) + .map_err(Self::target_construction)?, + ); + Ok(ReductionMinimumWeightDecodingToILP { - target: ILP::new(num_vars, constraints, objective, ObjectiveSense::Minimize) - .map_err(Self::target_construction)?, + target: ILP::with_variables( + variables, + constraints, + objective, + ObjectiveSense::Minimize, + ) + .map_err(Self::target_construction)?, num_cols: m, }) } diff --git a/src/rules/minmaxmulticenter_ilp.rs b/src/rules/minmaxmulticenter_ilp.rs index f4360ac63..82b521f56 100644 --- a/src/rules/minmaxmulticenter_ilp.rs +++ b/src/rules/minmaxmulticenter_ilp.rs @@ -24,7 +24,7 @@ //! Note: All-pairs shortest-path distances are computed using weighted shortest //! paths over `edge_lengths`. Unreachable assignment variables are forced to 0. -use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::MinMaxMulticenter; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -121,15 +121,15 @@ fn weighted_distances_mmc( dist } -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_vertices + num_vertices^2 + 1", num_constraints = "2 * num_vertices^2 + 3 * num_vertices + 2", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + num_nonzeros = "(num_vertices + num_vertices^2 + 1) * (2 * num_vertices^2 + 3 * num_vertices + 2)", + }, +})] impl ReduceTo> for MinMaxMulticenter { type Result = ReductionMMCToILP; @@ -232,8 +232,13 @@ impl ReduceTo> for MinMaxMulticenter { // Objective: minimize z let objective = vec![(z_var, 1)]; - let target = ILP::new(num_vars, constraints, objective, ObjectiveSense::Minimize) - .map_err(Self::target_construction)?; + let mut variables = vec![IntegerVariable::binary(); num_vars]; + variables[z_var] = + IntegerVariable::new(Some(0), Some(z_upper)).map_err(Self::target_construction)?; + + let target = + ILP::with_variables(variables, constraints, objective, ObjectiveSense::Minimize) + .map_err(Self::target_construction)?; Ok(ReductionMMCToILP { target, num_vertices: n, diff --git a/src/rules/mixedchinesepostman_ilp.rs b/src/rules/mixedchinesepostman_ilp.rs index 88da8843a..a12819f2d 100644 --- a/src/rules/mixedchinesepostman_ilp.rs +++ b/src/rules/mixedchinesepostman_ilp.rs @@ -5,7 +5,7 @@ //! within the length bound. Uses connectivity flow constraints on both //! forward and reverse directions. -use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::MixedChinesePostman; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -42,15 +42,11 @@ impl ReductionResult for ReductionMCPToILP { } } -#[reduction( - transform = upper_bound { - num_vars = "num_edges + 4 * (num_arcs + 2 * num_edges) + 3 * num_vertices + 1", - num_constraints = "num_edges + 8 * (num_arcs + 2 * num_edges) + 10 * num_vertices + 2", - }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] +#[reduction(transform = upper_bound { + num_vars = "num_edges + 4 * (num_arcs + 2 * num_edges) + 3 * num_vertices + 1", + num_constraints = "num_edges + 8 * (num_arcs + 2 * num_edges) + 10 * num_vertices + 2", + num_nonzeros = "(num_edges + 4 * (num_arcs + 2 * num_edges) + 3 * num_vertices + 1) * (num_edges + 8 * (num_arcs + 2 * num_edges) + 10 * num_vertices + 2)", +})] impl ReduceTo> for MixedChinesePostman { type Result = ReductionMCPToILP; @@ -371,8 +367,18 @@ impl ReduceTo> for MixedChinesePostman { } } - let target = ILP::new(num_vars, constraints, objective, ObjectiveSense::Minimize) - .map_err(Self::target_construction)?; + let mut variables = vec![IntegerVariable::binary(); num_vars]; + variables[q..q + l] + .fill(IntegerVariable::new(Some(0), Some(big_g)).map_err(Self::target_construction)?); + variables[s_idx..q + 2 * l + 3 * n + 1] + .fill(IntegerVariable::new(Some(0), Some(n_i64)).map_err(Self::target_construction)?); + variables[q + 2 * l + 3 * n + 1..].fill( + IntegerVariable::new(Some(0), Some(flow_big_m)).map_err(Self::target_construction)?, + ); + + let target = + ILP::with_variables(variables, constraints, objective, ObjectiveSense::Minimize) + .map_err(Self::target_construction)?; Ok(ReductionMCPToILP { target, diff --git a/src/rules/monochromatictriangle_ilp.rs b/src/rules/monochromatictriangle_ilp.rs index 137f5d4b3..5bdcc3449 100644 --- a/src/rules/monochromatictriangle_ilp.rs +++ b/src/rules/monochromatictriangle_ilp.rs @@ -43,15 +43,11 @@ impl ReductionResult for ReductionMonochromaticTriangleToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionMonochromaticTriangleToILP {} -#[reduction( - transform = upper_bound { - num_vars = "num_edges", - num_constraints = "2 * num_triangles + num_vertices^5 / 8", - }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] +#[reduction(transform = upper_bound { + num_vars = "num_edges", + num_constraints = "2 * num_triangles + num_vertices^5 / 8", + num_nonzeros = "num_edges * (2 * num_triangles + num_vertices^5 / 8)", +})] impl ReduceTo> for MonochromaticTriangle { type Result = ReductionMonochromaticTriangleToILP; diff --git a/src/rules/multiplechoicebranching_ilp.rs b/src/rules/multiplechoicebranching_ilp.rs index e341e994c..63e8b1121 100644 --- a/src/rules/multiplechoicebranching_ilp.rs +++ b/src/rules/multiplechoicebranching_ilp.rs @@ -1,6 +1,6 @@ //! Reduction from MultipleChoiceBranching with integer weights to integer ILP. -use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::MultipleChoiceBranching; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -39,15 +39,15 @@ impl ReductionResult for ReductionMultipleChoiceBranchingToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionMultipleChoiceBranchingToILP {} -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_arcs + num_vertices", num_constraints = "2 * num_arcs + 2 * num_vertices + num_partition_groups + 1", }, - unavailable = { - num_nonzeros = "zero weights and loop normalization determine the exact nonzero count", - } -)] + upper_bound { + num_nonzeros = "(num_arcs + num_vertices) * (2 * num_arcs + 2 * num_vertices + num_partition_groups + 1)", + }, +})] impl ReduceTo> for MultipleChoiceBranching { type Result = ReductionMultipleChoiceBranchingToILP; @@ -101,13 +101,20 @@ impl ReduceTo> for MultipleChoiceBranching { *self.threshold(), )); - let target = ILP::new( - num_arcs + num_vertices, - constraints, - vec![], - ObjectiveSense::Minimize, - ) - .map_err(>>::target_construction)?; + let mut variables = vec![IntegerVariable::binary(); num_arcs + num_vertices]; + variables[num_arcs..].fill( + IntegerVariable::new( + Some(0), + Some(Self::exact_i64( + num_vertices.saturating_sub(1), + "bounding topological labels", + )?), + ) + .map_err(Self::target_construction)?, + ); + + let target = ILP::with_variables(variables, constraints, vec![], ObjectiveSense::Minimize) + .map_err(>>::target_construction)?; Ok(ReductionMultipleChoiceBranchingToILP { target, num_arcs }) } } diff --git a/src/rules/multiplecopyfileallocation_ilp.rs b/src/rules/multiplecopyfileallocation_ilp.rs index 335aa7a91..9b294b615 100644 --- a/src/rules/multiplecopyfileallocation_ilp.rs +++ b/src/rules/multiplecopyfileallocation_ilp.rs @@ -68,15 +68,15 @@ fn bfs_distances(graph: &SimpleGraph, source: usize, n: usize) -> Vec { dist } -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_vertices + num_vertices^2", num_constraints = "num_vertices^2 + num_vertices", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + num_nonzeros = "(num_vertices + num_vertices^2) * (num_vertices^2 + num_vertices)", + }, +})] impl ReduceTo> for MultipleCopyFileAllocation { type Result = ReductionMCFAToILP; diff --git a/src/rules/multiprocessorscheduling_ilp.rs b/src/rules/multiprocessorscheduling_ilp.rs index 520ee4674..be832401b 100644 --- a/src/rules/multiprocessorscheduling_ilp.rs +++ b/src/rules/multiprocessorscheduling_ilp.rs @@ -56,15 +56,15 @@ impl ReductionResult for ReductionMSToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionMSToILP {} -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_tasks * num_processors", num_constraints = "num_tasks + num_processors", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + num_nonzeros = "(num_tasks * num_processors) * (num_tasks + num_processors)", + }, +})] impl ReduceTo> for MultiprocessorScheduling { type Result = ReductionMSToILP; diff --git a/src/rules/naesatisfiability_ilp.rs b/src/rules/naesatisfiability_ilp.rs index 8affee9e1..03d6a3b72 100644 --- a/src/rules/naesatisfiability_ilp.rs +++ b/src/rules/naesatisfiability_ilp.rs @@ -44,15 +44,15 @@ impl ReductionResult for ReductionNAESATToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionNAESATToILP {} -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_vars", num_constraints = "2 * num_clauses", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + num_nonzeros = "num_vars * (2 * num_clauses)", + }, +})] impl ReduceTo> for NAESatisfiability { type Result = ReductionNAESATToILP; diff --git a/src/rules/numericalmatchingwithtargetsums_ilp.rs b/src/rules/numericalmatchingwithtargetsums_ilp.rs index 617233292..670905866 100644 --- a/src/rules/numericalmatchingwithtargetsums_ilp.rs +++ b/src/rules/numericalmatchingwithtargetsums_ilp.rs @@ -70,15 +70,11 @@ impl ReductionResult for ReductionNMTSToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionNMTSToILP {} -#[reduction( - transform = upper_bound { - num_vars = "num_pairs * num_pairs * num_pairs", - num_constraints = "3 * num_pairs", - }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] +#[reduction(transform = upper_bound { + num_vars = "num_pairs * num_pairs * num_pairs", + num_constraints = "3 * num_pairs", + num_nonzeros = "(num_pairs * num_pairs * num_pairs) * (3 * num_pairs)", +})] impl ReduceTo> for NumericalMatchingWithTargetSums { type Result = ReductionNMTSToILP; diff --git a/src/rules/openshopscheduling_ilp.rs b/src/rules/openshopscheduling_ilp.rs index 5a7d01091..514ff218a 100644 --- a/src/rules/openshopscheduling_ilp.rs +++ b/src/rules/openshopscheduling_ilp.rs @@ -26,7 +26,7 @@ //! //! **Objective:** Minimize C. -use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::OpenShopScheduling; use crate::models::Decision; use crate::reduction; @@ -103,15 +103,15 @@ impl ReductionResult for ReductionOSSToILP { } } -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_jobs * (num_jobs - 1) / 2 * num_machines + num_jobs * num_machines + num_jobs * num_machines * (num_machines - 1) / 2 + 1", num_constraints = "num_jobs * (num_jobs - 1) / 2 * num_machines + num_jobs * num_machines + 1 + 2 * num_jobs * (num_jobs - 1) / 2 * num_machines + num_jobs * num_machines * (num_machines - 1) / 2 + 2 * num_jobs * num_machines * (num_machines - 1) / 2 + num_jobs * num_machines", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + num_nonzeros = "(num_jobs * (num_jobs - 1) / 2 * num_machines + num_jobs * num_machines + num_jobs * num_machines * (num_machines - 1) / 2 + 1) * (num_jobs * (num_jobs - 1) / 2 * num_machines + num_jobs * num_machines + 1 + 2 * num_jobs * (num_jobs - 1) / 2 * num_machines + num_jobs * num_machines * (num_machines - 1) / 2 + 2 * num_jobs * num_machines * (num_machines - 1) / 2 + num_jobs * num_machines)", + }, +})] impl ReduceTo> for OpenShopScheduling { type Result = ReductionOSSToILP; @@ -267,9 +267,20 @@ impl ReduceTo> for OpenShopScheduling { // Objective: minimize C let objective = vec![(c_var, 1)]; + let mut variables = vec![IntegerVariable::binary(); num_vars]; + let time_domain = + IntegerVariable::new(Some(0), Some(total_p)).map_err(Self::target_construction)?; + variables[num_order_vars..num_order_vars + num_start_vars].fill(time_domain); + variables[c_var] = time_domain; + Ok(ReductionOSSToILP { - target: ILP::new(num_vars, constraints, objective, ObjectiveSense::Minimize) - .map_err(Self::target_construction)?, + target: ILP::with_variables( + variables, + constraints, + objective, + ObjectiveSense::Minimize, + ) + .map_err(Self::target_construction)?, num_jobs: n, num_machines: m, num_order_vars, @@ -305,15 +316,15 @@ impl ReductionResult for ReductionDecisionOpenShopSchedulingToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionDecisionOpenShopSchedulingToILP {} -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_jobs * (num_jobs - 1) / 2 * num_machines + num_jobs * num_machines + num_jobs * num_machines * (num_machines - 1) / 2 + 1", num_constraints = "3 * num_jobs * (num_jobs - 1) / 2 * num_machines + 2 * num_jobs * num_machines + 3 * num_jobs * num_machines * (num_machines - 1) / 2 + 2", }, - unavailable = { - num_nonzeros = "depends on the generated scheduling constraints", - } -)] + upper_bound { + num_nonzeros = "(num_jobs * (num_jobs - 1) / 2 * num_machines + num_jobs * num_machines + num_jobs * num_machines * (num_machines - 1) / 2 + 1) * (3 * num_jobs * (num_jobs - 1) / 2 * num_machines + 2 * num_jobs * num_machines + 3 * num_jobs * num_machines * (num_machines - 1) / 2 + 2)", + }, +})] impl ReduceTo> for Decision { type Result = ReductionDecisionOpenShopSchedulingToILP; diff --git a/src/rules/optimallineararrangement_ilp.rs b/src/rules/optimallineararrangement_ilp.rs index 934dd35b1..e37b18c83 100644 --- a/src/rules/optimallineararrangement_ilp.rs +++ b/src/rules/optimallineararrangement_ilp.rs @@ -7,7 +7,7 @@ //! - abs_diff_le constraints: z_{u,v} >= p_u - p_v, z_{u,v} >= p_v - p_u //! - Minimize: sum z_{u,v} -use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::OptimalLinearArrangement; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -49,15 +49,15 @@ impl ReductionResult for ReductionOLAToILP { } } -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_vertices^2 + num_vertices + num_edges", num_constraints = "2 * num_vertices + num_vertices^2 + num_vertices + num_vertices + 3 * num_edges", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + num_nonzeros = "(num_vertices^2 + num_vertices + num_edges) * (2 * num_vertices + num_vertices^2 + num_vertices + num_vertices + 3 * num_edges)", + }, +})] impl ReduceTo> for OptimalLinearArrangement { type Result = ReductionOLAToILP; @@ -132,8 +132,15 @@ impl ReduceTo> for OptimalLinearArrangement { // Objective: minimize sum z_e let objective: Vec<(usize, i64)> = (0..m).map(|e| (z_idx(e), 1)).collect(); - let target = ILP::new(num_vars, constraints, objective, ObjectiveSense::Minimize) - .map_err(>>::target_construction)?; + let mut variables = vec![IntegerVariable::binary(); num_vars]; + variables[num_x..].fill( + IntegerVariable::new(Some(0), Some((n_i64 - 1).max(0))) + .map_err(>>::target_construction)?, + ); + + let target = + ILP::with_variables(variables, constraints, objective, ObjectiveSense::Minimize) + .map_err(>>::target_construction)?; Ok(ReductionOLAToILP { target, diff --git a/src/rules/optimumcommunicationspanningtree_ilp.rs b/src/rules/optimumcommunicationspanningtree_ilp.rs index ee0c48bb7..b586d3211 100644 --- a/src/rules/optimumcommunicationspanningtree_ilp.rs +++ b/src/rules/optimumcommunicationspanningtree_ilp.rs @@ -46,15 +46,15 @@ impl ReductionResult for ReductionOptimumCommunicationSpanningTreeToILP { } } -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_edges + 2 * num_edges * num_vertices * (num_vertices - 1) / 2", num_constraints = "1 + num_vertices * num_vertices * (num_vertices - 1) / 2 + 2 * num_edges * num_vertices * (num_vertices - 1) / 2", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + num_nonzeros = "(num_edges + 2 * num_edges * num_vertices * (num_vertices - 1) / 2) * (1 + num_vertices * num_vertices * (num_vertices - 1) / 2 + 2 * num_edges * num_vertices * (num_vertices - 1) / 2)", + }, +})] impl ReduceTo> for OptimumCommunicationSpanningTree { type Result = ReductionOptimumCommunicationSpanningTreeToILP; diff --git a/src/rules/paintshop_ilp.rs b/src/rules/paintshop_ilp.rs index b5f455827..6e51ae1a3 100644 --- a/src/rules/paintshop_ilp.rs +++ b/src/rules/paintshop_ilp.rs @@ -37,15 +37,11 @@ impl ReductionResult for ReductionPaintShopToILP { } } -#[reduction( - transform = upper_bound { - num_vars = "num_cars + 2 * num_sequence", - num_constraints = "num_sequence + 2 * num_sequence", - }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] +#[reduction(transform = upper_bound { + num_vars = "num_cars + 2 * num_sequence", + num_constraints = "num_sequence + 2 * num_sequence", + num_nonzeros = "(num_cars + 2 * num_sequence) * (num_sequence + 2 * num_sequence)", +})] impl ReduceTo> for PaintShop { type Result = ReductionPaintShopToILP; diff --git a/src/rules/paintshop_qubo.rs b/src/rules/paintshop_qubo.rs index 7224e8704..fd6bbeced 100644 --- a/src/rules/paintshop_qubo.rs +++ b/src/rules/paintshop_qubo.rs @@ -38,10 +38,13 @@ impl ReductionResult for ReductionPaintShopToQUBO { } } -#[reduction(transform = exact { - num_vars = "num_cars", -}, unavailable = { - num_quadratic_terms = "adjacency contributions can cancel between repeated car pairs", +#[reduction(transform = { + exact { + num_vars = "num_cars", + }, + upper_bound { + num_quadratic_terms = "num_cars * (num_cars - 1) / 2", + }, })] impl ReduceTo> for PaintShop { type Result = ReductionPaintShopToQUBO; diff --git a/src/rules/partiallyorderedknapsack_ilp.rs b/src/rules/partiallyorderedknapsack_ilp.rs index ecfef47b6..546d20027 100644 --- a/src/rules/partiallyorderedknapsack_ilp.rs +++ b/src/rules/partiallyorderedknapsack_ilp.rs @@ -31,15 +31,15 @@ impl ReductionResult for ReductionPOKToILP { } } -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_items", num_constraints = "num_precedences + 1", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + num_nonzeros = "num_items * (num_precedences + 1)", + }, +})] impl ReduceTo> for PartiallyOrderedKnapsack { type Result = ReductionPOKToILP; diff --git a/src/rules/partitionintocliques_ilp.rs b/src/rules/partitionintocliques_ilp.rs index f15013c49..7ef0c820d 100644 --- a/src/rules/partitionintocliques_ilp.rs +++ b/src/rules/partitionintocliques_ilp.rs @@ -49,15 +49,11 @@ impl ReductionResult for ReductionPartitionIntoCliquesToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionPartitionIntoCliquesToILP {} -#[reduction( - transform = upper_bound { - num_vars = "num_vertices^2", - num_constraints = "num_vertices + num_vertices^3", - }, - unavailable = { - num_nonzeros = "the exact target parameter depends on the source clique bound and non-edges", - } -)] +#[reduction(transform = upper_bound { + num_vars = "num_vertices^2", + num_constraints = "num_vertices + num_vertices^3", + num_nonzeros = "(num_vertices^2) * (num_vertices + num_vertices^3)", +})] impl ReduceTo> for PartitionIntoCliques { type Result = ReductionPartitionIntoCliquesToILP; diff --git a/src/rules/partitionintopathsoflength2_ilp.rs b/src/rules/partitionintopathsoflength2_ilp.rs index d08b94113..30b32901d 100644 --- a/src/rules/partitionintopathsoflength2_ilp.rs +++ b/src/rules/partitionintopathsoflength2_ilp.rs @@ -67,15 +67,11 @@ impl ReductionResult for ReductionPIPL2ToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionPIPL2ToILP {} -#[reduction( - transform = upper_bound { - num_vars = "num_vertices^2 + num_edges * num_vertices", - num_constraints = "num_vertices^2 + num_edges * num_vertices + num_vertices", - }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] +#[reduction(transform = upper_bound { + num_vars = "num_vertices^2 + num_edges * num_vertices", + num_constraints = "num_vertices^2 + num_edges * num_vertices + num_vertices", + num_nonzeros = "(num_vertices^2 + num_edges * num_vertices) * (num_vertices^2 + num_edges * num_vertices + num_vertices)", +})] impl ReduceTo> for PartitionIntoPathsOfLength2 { type Result = ReductionPIPL2ToILP; diff --git a/src/rules/partitionintotriangles_ilp.rs b/src/rules/partitionintotriangles_ilp.rs index 644eb9458..3ec37775c 100644 --- a/src/rules/partitionintotriangles_ilp.rs +++ b/src/rules/partitionintotriangles_ilp.rs @@ -60,15 +60,11 @@ impl ReductionResult for ReductionPITToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionPITToILP {} -#[reduction( - transform = upper_bound { - num_vars = "num_vertices^2", - num_constraints = "num_vertices^2 * num_vertices", - }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] +#[reduction(transform = upper_bound { + num_vars = "num_vertices^2", + num_constraints = "num_vertices^2 * num_vertices", + num_nonzeros = "(num_vertices^2) * (num_vertices^2 * num_vertices)", +})] impl ReduceTo> for PartitionIntoTriangles { type Result = ReductionPITToILP; diff --git a/src/rules/pathconstrainednetworkflow_ilp.rs b/src/rules/pathconstrainednetworkflow_ilp.rs index bf9b50ebd..635030112 100644 --- a/src/rules/pathconstrainednetworkflow_ilp.rs +++ b/src/rules/pathconstrainednetworkflow_ilp.rs @@ -3,7 +3,7 @@ //! One integer variable per prescribed path. Arc capacity aggregation //! across paths and total flow requirement. -use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::PathConstrainedNetworkFlow; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -40,15 +40,11 @@ impl ReductionResult for ReductionPCNFToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionPCNFToILP {} -#[reduction( - transform = upper_bound { - num_vars = "num_paths", - num_constraints = "num_arcs + 1", - }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] +#[reduction(transform = upper_bound { + num_vars = "num_paths", + num_constraints = "num_arcs + 1", + num_nonzeros = "num_paths * (num_arcs + 1)", +})] impl ReduceTo> for PathConstrainedNetworkFlow { type Result = ReductionPCNFToILP; @@ -75,8 +71,15 @@ impl ReduceTo> for PathConstrainedNetworkFlow { let total_terms: Vec<(usize, i64)> = (0..num_paths).map(|i| (i, 1)).collect(); constraints.push(LinearConstraint::ge(total_terms, self.requirement())); + let variables = self + .paths() + .iter() + .map(|path| IntegerVariable::new(Some(0), Some(self.path_bottleneck(path)))) + .collect::, _>>() + .map_err(Self::target_construction)?; + Ok(ReductionPCNFToILP { - target: ILP::new(num_paths, constraints, vec![], ObjectiveSense::Minimize) + target: ILP::with_variables(variables, constraints, vec![], ObjectiveSense::Minimize) .map_err(Self::target_construction)?, }) } diff --git a/src/rules/precedenceconstrainedscheduling_ilp.rs b/src/rules/precedenceconstrainedscheduling_ilp.rs index 4de993d6f..5416b8e6d 100644 --- a/src/rules/precedenceconstrainedscheduling_ilp.rs +++ b/src/rules/precedenceconstrainedscheduling_ilp.rs @@ -61,15 +61,15 @@ impl ReductionResult for ReductionPCSToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionPCSToILP {} -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_tasks * deadline", num_constraints = "num_tasks + deadline + num_precedences", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + num_nonzeros = "(num_tasks * deadline) * (num_tasks + deadline + num_precedences)", + }, +})] impl ReduceTo> for PrecedenceConstrainedScheduling { type Result = ReductionPCSToILP; diff --git a/src/rules/preemptivescheduling_ilp.rs b/src/rules/preemptivescheduling_ilp.rs index f6d65c971..badad6f82 100644 --- a/src/rules/preemptivescheduling_ilp.rs +++ b/src/rules/preemptivescheduling_ilp.rs @@ -21,7 +21,7 @@ //! Note: `ILP` treats all variables as non-negative integers. Binary constraints //! on x_{t,u} are enforced by x_{t,u} ≤ 1. -use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::PreemptiveScheduling; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -67,15 +67,15 @@ impl ReductionResult for ReductionPSToILP { } } -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_tasks * d_max + 1", num_constraints = "num_tasks + d_max + num_precedences * d_max + 2 * num_tasks * d_max", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + num_nonzeros = "(num_tasks * d_max + 1) * (num_tasks + d_max + num_precedences * d_max + 2 * num_tasks * d_max)", + }, +})] impl ReduceTo> for PreemptiveScheduling { type Result = ReductionPSToILP; @@ -149,9 +149,22 @@ impl ReduceTo> for PreemptiveScheduling { // Objective: minimize M let objective = vec![(m_var, 1)]; + // All task slots end by d; lowering the makespan to d preserves every feasible schedule. + let mut variables = vec![IntegerVariable::binary(); num_vars]; + variables[m_var] = IntegerVariable::new( + Some(0), + Some(Self::exact_i64(d, "bounding the schedule makespan")?), + ) + .map_err(Self::target_construction)?; + Ok(ReductionPSToILP { - target: ILP::new(num_vars, constraints, objective, ObjectiveSense::Minimize) - .map_err(Self::target_construction)?, + target: ILP::with_variables( + variables, + constraints, + objective, + ObjectiveSense::Minimize, + ) + .map_err(Self::target_construction)?, num_tasks: n, d_max: d, }) diff --git a/src/rules/rectilinearpicturecompression_ilp.rs b/src/rules/rectilinearpicturecompression_ilp.rs index 78334ddf4..751aee09e 100644 --- a/src/rules/rectilinearpicturecompression_ilp.rs +++ b/src/rules/rectilinearpicturecompression_ilp.rs @@ -39,15 +39,11 @@ impl ReductionResult for ReductionRPCToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionRPCToILP {} -#[reduction( - transform = upper_bound { - num_vars = "num_rows^2 * num_cols^2", - num_constraints = "num_rows * num_cols + 1", - }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] +#[reduction(transform = upper_bound { + num_vars = "num_rows^2 * num_cols^2", + num_constraints = "num_rows * num_cols + 1", + num_nonzeros = "(num_rows^2 * num_cols^2) * (num_rows * num_cols + 1)", +})] impl ReduceTo> for RectilinearPictureCompression { type Result = ReductionRPCToILP; diff --git a/src/rules/registersufficiency_ilp.rs b/src/rules/registersufficiency_ilp.rs index d6db3ee1f..c5595f662 100644 --- a/src/rules/registersufficiency_ilp.rs +++ b/src/rules/registersufficiency_ilp.rs @@ -7,7 +7,7 @@ //! - binary threshold/live indicators to count how many values are live after //! each evaluation step -use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::RegisterSufficiency; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -184,8 +184,16 @@ impl ReduceTo> for RegisterSufficiency { )); } + let mut variables = vec![IntegerVariable::binary(); num_vars]; + variables[time_offset..latest_offset].fill( + IntegerVariable::new(Some(0), Some(maximum_time)).map_err(Self::target_construction)?, + ); + variables[latest_offset..order_offset].fill( + IntegerVariable::new(Some(0), Some(latest_time)).map_err(Self::target_construction)?, + ); + Ok(ReductionRegisterSufficiencyToILP { - target: ILP::new(num_vars, constraints, vec![], ObjectiveSense::Minimize) + target: ILP::with_variables(variables, constraints, vec![], ObjectiveSense::Minimize) .map_err(Self::target_construction)?, num_vertices: n, }) diff --git a/src/rules/registry.rs b/src/rules/registry.rs index 7247687d4..4c46f27d5 100644 --- a/src/rules/registry.rs +++ b/src/rules/registry.rs @@ -16,8 +16,7 @@ pub struct UnavailableParameterField { /// Raw symbolic declaration emitted by the reduction proc macro. #[derive(Clone, Debug, Default)] pub struct ReductionParameterDeclarations { - pub relation: Option, - pub fields: Vec<(&'static str, Expr)>, + pub fields: Vec<(&'static str, ParameterRelation, Expr)>, pub unavailable: Vec, } @@ -37,7 +36,7 @@ impl ReductionParameterContract { let formula_names: HashSet<_> = declarations .fields .iter() - .map(|(field, _)| *field) + .map(|(field, _, _)| *field) .collect(); let mut unavailable_names = HashSet::new(); for unavailable in &declarations.unavailable { @@ -56,16 +55,13 @@ impl ReductionParameterContract { }); } } - let transform = match (declarations.relation, declarations.fields.is_empty()) { - (Some(relation), false) => Some(ParameterTransform::new( - edge, - relation, - declarations.fields, - )?), - (None, true) if !declarations.unavailable.is_empty() => None, - (None, true) => return Err(ParameterContractError::EmptyContract { edge }), - (Some(_), true) => return Err(ParameterContractError::EmptyTransform { edge }), - (None, false) => return Err(ParameterContractError::MissingRelation { edge }), + let transform = if declarations.fields.is_empty() { + if declarations.unavailable.is_empty() { + return Err(ParameterContractError::EmptyContract { edge }); + } + None + } else { + Some(ParameterTransform::from_fields(edge, declarations.fields)?) }; Ok(Self { transform, @@ -86,8 +82,6 @@ impl ReductionParameterContract { pub enum ParameterContractError { Transform(ParameterTransformError), EmptyContract { edge: Box }, - EmptyTransform { edge: Box }, - MissingRelation { edge: Box }, DuplicateClassification { edge: Box, field: Box }, EmptyUnavailableReason { edge: Box, field: Box }, } @@ -100,16 +94,6 @@ impl std::fmt::Display for ParameterContractError { formatter, "reduction `{edge}` has no parameter formulas or unavailable fields" ), - Self::EmptyTransform { edge } => { - write!( - formatter, - "reduction `{edge}` declares an empty parameter transform" - ) - } - Self::MissingRelation { edge } => write!( - formatter, - "reduction `{edge}` declares parameter formulas without a relation" - ), Self::DuplicateClassification { edge, field } => { write!( formatter, @@ -353,7 +337,7 @@ pub fn validate_reduction_parameter_schemas() -> Result<(), Vec> { for field in declarations .fields .iter() - .flat_map(|(_, expression)| expression.variables()) + .flat_map(|(_, _, expression)| expression.variables()) { if !source_fields.contains(field) { errors.push(format!( @@ -366,7 +350,7 @@ pub fn validate_reduction_parameter_schemas() -> Result<(), Vec> { let declared_target_fields = declarations .fields .iter() - .map(|(field, _)| *field) + .map(|(field, _, _)| *field) .chain(declarations.unavailable.iter().map(|field| field.field)) .collect::>(); for field in &declared_target_fields { diff --git a/src/rules/resourceconstrainedscheduling_ilp.rs b/src/rules/resourceconstrainedscheduling_ilp.rs index 99b8510ad..37ef1f54f 100644 --- a/src/rules/resourceconstrainedscheduling_ilp.rs +++ b/src/rules/resourceconstrainedscheduling_ilp.rs @@ -52,15 +52,15 @@ impl ReductionResult for ReductionRCSToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionRCSToILP {} -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_tasks * deadline", num_constraints = "num_tasks + deadline + num_resources * deadline", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + num_nonzeros = "(num_tasks * deadline) * (num_tasks + deadline + num_resources * deadline)", + }, +})] impl ReduceTo> for ResourceConstrainedScheduling { type Result = ReductionRCSToILP; diff --git a/src/rules/rootedtreestorageassignment_ilp.rs b/src/rules/rootedtreestorageassignment_ilp.rs index 0ca97f3a7..dce49eff9 100644 --- a/src/rules/rootedtreestorageassignment_ilp.rs +++ b/src/rules/rootedtreestorageassignment_ilp.rs @@ -4,7 +4,7 @@ //! a_{u,v}, transitive-closure helpers h_{u,v,w}, and per-subset gadgets //! (top/bottom selectors, pair selectors, endpoint depths, extension costs). -use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::set::RootedTreeStorageAssignment; use crate::reduction; use crate::rules::ilp_helpers::{mccormick_product, one_hot_decode_rows}; @@ -90,15 +90,11 @@ impl ReductionResult for ReductionRTSAToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionRTSAToILP {} -#[reduction( - transform = upper_bound { - num_vars = "universe_size * universe_size * universe_size + 2 * universe_size * universe_size + universe_size + num_subsets * (universe_size * universe_size + 2 * universe_size + 3)", - num_constraints = "4 * universe_size^3 + 6 * universe_size^2 + 5 * universe_size + 2 + num_subsets * (2 * universe_size^3 + 5 * universe_size^2 + 8 * universe_size + 8)", - }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] +#[reduction(transform = upper_bound { + num_vars = "universe_size * universe_size * universe_size + 2 * universe_size * universe_size + universe_size + num_subsets * (universe_size * universe_size + 2 * universe_size + 3)", + num_constraints = "4 * universe_size^3 + 6 * universe_size^2 + 5 * universe_size + 2 + num_subsets * (2 * universe_size^3 + 5 * universe_size^2 + 8 * universe_size + 8)", + num_nonzeros = "(universe_size * universe_size * universe_size + 2 * universe_size * universe_size + universe_size + num_subsets * (universe_size * universe_size + 2 * universe_size + 3)) * (4 * universe_size^3 + 6 * universe_size^2 + 5 * universe_size + 2 + num_subsets * (2 * universe_size^3 + 5 * universe_size^2 + 8 * universe_size + 8))", +})] impl ReduceTo> for RootedTreeStorageAssignment { type Result = ReductionRTSAToILP; @@ -391,7 +387,13 @@ impl ReduceTo> for RootedTreeStorageAssignment { let cost_terms: Vec<(usize, i64)> = (0..r).map(|s| (idx_c(n, r, s), 1)).collect(); constraints.push(LinearConstraint::le(cost_terms, bound)); - let target = ILP::new(nv, constraints, vec![], ObjectiveSense::Minimize) + let mut variables = vec![IntegerVariable::binary(); nv]; + let depth_domain = + IntegerVariable::new(Some(0), Some(big_m_depth)).map_err(Self::target_construction)?; + variables[n * n..n * n + n].fill(depth_domain); + variables[idx_big_t(n, r, 0)..].fill(depth_domain); + + let target = ILP::with_variables(variables, constraints, vec![], ObjectiveSense::Minimize) .map_err(Self::target_construction)?; Ok(ReductionRTSAToILP { target, n }) } diff --git a/src/rules/ruralpostman_ilp.rs b/src/rules/ruralpostman_ilp.rs index ad8e63a1d..f056d2f7a 100644 --- a/src/rules/ruralpostman_ilp.rs +++ b/src/rules/ruralpostman_ilp.rs @@ -4,7 +4,7 @@ //! connectivity flow constraints to encode an Eulerian connected subgraph //! covering all required edges within the length bound. -use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::RuralPostman; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -40,15 +40,15 @@ impl ReductionResult for ReductionRPToILP { } } -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_edges + num_vertices + num_edges + num_vertices + 2 * num_edges", num_constraints = "2 * num_edges + num_required_edges + num_vertices + 2 * num_edges + num_vertices + 2 * num_edges + num_vertices + num_edges + num_edges + num_vertices", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + num_nonzeros = "(num_edges + num_vertices + num_edges + num_vertices + 2 * num_edges) * (2 * num_edges + num_required_edges + num_vertices + 2 * num_edges + num_vertices + 2 * num_edges + num_vertices + num_edges + num_edges + num_vertices)", + }, +})] impl ReduceTo> for RuralPostman { type Result = ReductionRPToILP; @@ -204,8 +204,22 @@ impl ReduceTo> for RuralPostman { let objective: Vec<(usize, i64)> = (0..m) .map(|e| (t_idx(e), edge_lengths[e].to_sum())) .collect(); - let target = ILP::new(num_vars, constraints, objective, ObjectiveSense::Minimize) - .map_err(Self::target_construction)?; + let mut variables = vec![IntegerVariable::binary(); num_vars]; + variables[..m] + .fill(IntegerVariable::new(Some(0), Some(2)).map_err(Self::target_construction)?); + variables[m..m + n].fill( + IntegerVariable::new( + Some(0), + Some(Self::exact_i64(m, "bounding half the traversal degree")?), + ) + .map_err(Self::target_construction)?, + ); + variables[2 * m + 2 * n..] + .fill(IntegerVariable::new(Some(0), Some(big_m)).map_err(Self::target_construction)?); + + let target = + ILP::with_variables(variables, constraints, objective, ObjectiveSense::Minimize) + .map_err(Self::target_construction)?; Ok(ReductionRPToILP { target, diff --git a/src/rules/schedulingtominimizeweightedcompletiontime_ilp.rs b/src/rules/schedulingtominimizeweightedcompletiontime_ilp.rs index caefabe83..9b5aaf957 100644 --- a/src/rules/schedulingtominimizeweightedcompletiontime_ilp.rs +++ b/src/rules/schedulingtominimizeweightedcompletiontime_ilp.rs @@ -5,7 +5,7 @@ //! ordering variables `y_{i,j}` for each task pair. Big-M constraints //! enforce that tasks sharing a processor do not overlap. -use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::SchedulingToMinimizeWeightedCompletionTime; use crate::reduction; use crate::rules::ilp_helpers::one_hot_decode_rows; @@ -62,15 +62,15 @@ impl ReductionResult for ReductionSMWCTToILP { } } -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_tasks * num_processors + num_tasks + num_tasks * (num_tasks - 1) / 2", num_constraints = "num_tasks + num_tasks * num_processors + 2 * num_tasks + 2 * num_tasks * (num_tasks - 1) / 2 * num_processors + num_tasks * (num_tasks - 1) / 2", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + num_nonzeros = "(num_tasks * num_processors + num_tasks + num_tasks * (num_tasks - 1) / 2) * (num_tasks + num_tasks * num_processors + 2 * num_tasks + 2 * num_tasks * (num_tasks - 1) / 2 * num_processors + num_tasks * (num_tasks - 1) / 2)", + }, +})] impl ReduceTo> for SchedulingToMinimizeWeightedCompletionTime { type Result = ReductionSMWCTToILP; @@ -190,9 +190,20 @@ impl ReduceTo> for SchedulingToMinimizeWeightedCompletionTime { .map(|(task, weight)| (result.c_var(task), weight)) .collect(); - Ok(ReductionSMWCTToILP { - target: ILP::new(num_vars, constraints, objective, ObjectiveSense::Minimize) + let mut variables = vec![IntegerVariable::binary(); num_vars]; + variables[n * m..n * m + n].fill( + IntegerVariable::new(Some(0), Some(total_processing_time)) .map_err(Self::target_construction)?, + ); + + Ok(ReductionSMWCTToILP { + target: ILP::with_variables( + variables, + constraints, + objective, + ObjectiveSense::Minimize, + ) + .map_err(Self::target_construction)?, num_tasks: n, num_processors: m, }) diff --git a/src/rules/schedulingwithindividualdeadlines_ilp.rs b/src/rules/schedulingwithindividualdeadlines_ilp.rs index c073ba6c1..034bc4c89 100644 --- a/src/rules/schedulingwithindividualdeadlines_ilp.rs +++ b/src/rules/schedulingwithindividualdeadlines_ilp.rs @@ -57,15 +57,15 @@ impl ReductionResult for ReductionSWIDToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionSWIDToILP {} -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_tasks * max_deadline", num_constraints = "num_tasks + max_deadline + num_precedences + 1", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + num_nonzeros = "(num_tasks * max_deadline) * (num_tasks + max_deadline + num_precedences + 1)", + }, +})] impl ReduceTo> for SchedulingWithIndividualDeadlines { type Result = ReductionSWIDToILP; diff --git a/src/rules/sequencingtominimizemaximumcumulativecost_ilp.rs b/src/rules/sequencingtominimizemaximumcumulativecost_ilp.rs index 1c87717e3..2e5bc5947 100644 --- a/src/rules/sequencingtominimizemaximumcumulativecost_ilp.rs +++ b/src/rules/sequencingtominimizemaximumcumulativecost_ilp.rs @@ -4,7 +4,7 @@ //! Permutation constraints, precedence constraints, and prefix cumulative-cost //! bounds at every position. -use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::SequencingToMinimizeMaximumCumulativeCost; use crate::reduction; use crate::rules::ilp_helpers::one_hot_decode; @@ -45,14 +45,15 @@ impl ReductionResult for ReductionSTMMCCToILP { } } -#[reduction(transform = exact { - num_vars = "num_tasks^2 + 1", - num_constraints = "num_tasks^2 + 3 * num_tasks + num_precedences + 1", -}, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] +#[reduction(transform = { + exact { + num_vars = "num_tasks^2 + 1", + num_constraints = "num_tasks^2 + 3 * num_tasks + num_precedences + 1", + }, + upper_bound { + num_nonzeros = "(num_tasks^2 + 1) * (num_tasks^2 + 3 * num_tasks + num_precedences + 1)", + }, +})] impl ReduceTo> for SequencingToMinimizeMaximumCumulativeCost { type Result = ReductionSTMMCCToILP; @@ -130,9 +131,18 @@ impl ReduceTo> for SequencingToMinimizeMaximumCumulativeCost { // Objective: minimize z (the maximum cumulative cost) let objective = vec![(z_var, 1)]; + let mut variables = vec![IntegerVariable::binary(); num_vars]; + variables[z_var] = + IntegerVariable::new(Some(0), Some(z_upper)).map_err(Self::target_construction)?; + Ok(ReductionSTMMCCToILP { - target: ILP::new(num_vars, constraints, objective, ObjectiveSense::Minimize) - .map_err(Self::target_construction)?, + target: ILP::with_variables( + variables, + constraints, + objective, + ObjectiveSense::Minimize, + ) + .map_err(Self::target_construction)?, num_tasks: n, }) } diff --git a/src/rules/sequencingtominimizetardytaskweight_ilp.rs b/src/rules/sequencingtominimizetardytaskweight_ilp.rs index 4bf8c68f0..b2f3c8a0a 100644 --- a/src/rules/sequencingtominimizetardytaskweight_ilp.rs +++ b/src/rules/sequencingtominimizetardytaskweight_ilp.rs @@ -46,15 +46,15 @@ impl ReductionResult for ReductionSTMTTWToILP { } } -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_tasks * num_tasks + num_tasks", num_constraints = "2 * num_tasks + 2 * num_tasks * num_tasks", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + num_nonzeros = "(num_tasks * num_tasks + num_tasks) * (2 * num_tasks + 2 * num_tasks * num_tasks)", + }, +})] impl ReduceTo> for SequencingToMinimizeTardyTaskWeight { type Result = ReductionSTMTTWToILP; diff --git a/src/rules/sequencingtominimizeweightedcompletiontime_ilp.rs b/src/rules/sequencingtominimizeweightedcompletiontime_ilp.rs index 1171c160a..d0e1f4a4e 100644 --- a/src/rules/sequencingtominimizeweightedcompletiontime_ilp.rs +++ b/src/rules/sequencingtominimizeweightedcompletiontime_ilp.rs @@ -5,7 +5,7 @@ //! For each unordered pair `{i, j}`, a pair of big-M constraints forces one //! task to finish before the other starts. -use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::SequencingToMinimizeWeightedCompletionTime; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -51,15 +51,15 @@ impl ReductionResult for ReductionSTMWCTToILP { } } -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_tasks + num_tasks * (num_tasks - 1) / 2", num_constraints = "2 * num_tasks + 3 * num_tasks * (num_tasks - 1) / 2 + num_precedences", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + num_nonzeros = "(num_tasks + num_tasks * (num_tasks - 1) / 2) * (2 * num_tasks + 3 * num_tasks * (num_tasks - 1) / 2 + num_precedences)", + }, +})] impl ReduceTo> for SequencingToMinimizeWeightedCompletionTime { type Result = ReductionSTMWCTToILP; @@ -132,9 +132,20 @@ impl ReduceTo> for SequencingToMinimizeWeightedCompletionTime { let objective = weights.iter().copied().enumerate().collect(); - Ok(Self::Result { - target: ILP::new(num_vars, constraints, objective, ObjectiveSense::Minimize) + let mut variables = vec![IntegerVariable::binary(); num_vars]; + variables[..num_tasks].fill( + IntegerVariable::new(Some(0), Some(total_processing_time)) .map_err(Self::target_construction)?, + ); + + Ok(Self::Result { + target: ILP::with_variables( + variables, + constraints, + objective, + ObjectiveSense::Minimize, + ) + .map_err(Self::target_construction)?, num_tasks, }) } diff --git a/src/rules/sequencingtominimizeweightedtardiness_ilp.rs b/src/rules/sequencingtominimizeweightedtardiness_ilp.rs index feb90dd53..de1d41b38 100644 --- a/src/rules/sequencingtominimizeweightedtardiness_ilp.rs +++ b/src/rules/sequencingtominimizeweightedtardiness_ilp.rs @@ -4,7 +4,7 @@ //! and nonnegative tardiness variables T_j. Big-M disjunctive constraints //! force a single-machine order; the weighted tardiness sum is bounded by K. -use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::SequencingToMinimizeWeightedTardiness; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -57,15 +57,11 @@ impl ReductionResult for ReductionSTMWTToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionSTMWTToILP {} -#[reduction( - transform = upper_bound { +#[reduction(transform = upper_bound { num_vars = "num_tasks^2 + 2 * num_tasks", num_constraints = "2 * num_tasks^2 + 3 * num_tasks + 1", -}, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + num_nonzeros = "(num_tasks^2 + 2 * num_tasks) * (2 * num_tasks^2 + 3 * num_tasks + 1)", +})] impl ReduceTo> for SequencingToMinimizeWeightedTardiness { type Result = ReductionSTMWTToILP; @@ -156,8 +152,13 @@ impl ReduceTo> for SequencingToMinimizeWeightedTardiness { let terms: Vec<(usize, i64)> = (0..n).map(|j| (t_var(j), weights[j])).collect(); constraints.push(LinearConstraint::le(terms, bound)); + // Left-justify the job order: completion and tardiness are <= horizon, with no increase in cost. + let mut variables = vec![IntegerVariable::binary(); num_vars]; + variables[num_order_vars..] + .fill(IntegerVariable::new(Some(0), Some(horizon)).map_err(Self::target_construction)?); + Ok(ReductionSTMWTToILP { - target: ILP::new(num_vars, constraints, vec![], ObjectiveSense::Minimize) + target: ILP::with_variables(variables, constraints, vec![], ObjectiveSense::Minimize) .map_err(Self::target_construction)?, num_tasks: n, num_order_vars, diff --git a/src/rules/sequencingwithdeadlinesandsetuptimes_ilp.rs b/src/rules/sequencingwithdeadlinesandsetuptimes_ilp.rs index 85dc2be3e..2829801bf 100644 --- a/src/rules/sequencingwithdeadlinesandsetuptimes_ilp.rs +++ b/src/rules/sequencingwithdeadlinesandsetuptimes_ilp.rs @@ -58,15 +58,11 @@ impl ReductionResult for ReductionSWDSTToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionSWDSTToILP {} -#[reduction( - transform = upper_bound { +#[reduction(transform = upper_bound { num_vars = "2 * num_tasks^2 + num_tasks", num_constraints = "2 * num_tasks + num_tasks^2 * (num_tasks - 1) + 3 * num_tasks * (num_tasks - 1) + num_tasks * num_tasks", -}, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + num_nonzeros = "(2 * num_tasks^2 + num_tasks) * (2 * num_tasks + num_tasks^2 * (num_tasks - 1) + 3 * num_tasks * (num_tasks - 1) + num_tasks * num_tasks)", +})] impl ReduceTo> for SequencingWithDeadlinesAndSetUpTimes { type Result = ReductionSWDSTToILP; diff --git a/src/rules/sequencingwithinintervals_ilp.rs b/src/rules/sequencingwithinintervals_ilp.rs index 663628d5f..4182c20a7 100644 --- a/src/rules/sequencingwithinintervals_ilp.rs +++ b/src/rules/sequencingwithinintervals_ilp.rs @@ -76,15 +76,11 @@ impl ReductionResult for ReductionSWIToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionSWIToILP {} -#[reduction( - transform = upper_bound { - num_vars = "num_start_slots", - num_constraints = "num_start_slots^2 + num_tasks", - }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] +#[reduction(transform = upper_bound { + num_vars = "num_start_slots", + num_constraints = "num_start_slots^2 + num_tasks", + num_nonzeros = "num_start_slots * (num_start_slots^2 + num_tasks)", +})] impl ReduceTo> for SequencingWithinIntervals { type Result = ReductionSWIToILP; diff --git a/src/rules/sequencingwithreleasetimesanddeadlines_ilp.rs b/src/rules/sequencingwithreleasetimesanddeadlines_ilp.rs index 91c5d8109..6d001eca3 100644 --- a/src/rules/sequencingwithreleasetimesanddeadlines_ilp.rs +++ b/src/rules/sequencingwithreleasetimesanddeadlines_ilp.rs @@ -58,15 +58,11 @@ impl ReductionResult for ReductionSWRTDToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionSWRTDToILP {} -#[reduction( - transform = upper_bound { +#[reduction(transform = upper_bound { num_vars = "num_tasks * time_horizon", num_constraints = "num_tasks * time_horizon + num_tasks + time_horizon", -}, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + num_nonzeros = "(num_tasks * time_horizon) * (num_tasks * time_horizon + num_tasks + time_horizon)", +})] impl ReduceTo> for SequencingWithReleaseTimesAndDeadlines { type Result = ReductionSWRTDToILP; diff --git a/src/rules/setsplitting_ilp.rs b/src/rules/setsplitting_ilp.rs index ecccd257b..ef0667f47 100644 --- a/src/rules/setsplitting_ilp.rs +++ b/src/rules/setsplitting_ilp.rs @@ -46,15 +46,15 @@ impl ReductionResult for ReductionSetSplittingToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionSetSplittingToILP {} -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "universe_size", num_constraints = "2 * num_subsets", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + num_nonzeros = "universe_size * (2 * num_subsets)", + }, +})] impl ReduceTo> for SetSplitting { type Result = ReductionSetSplittingToILP; diff --git a/src/rules/shortestcommonsupersequence_ilp.rs b/src/rules/shortestcommonsupersequence_ilp.rs index d1c713717..784371a04 100644 --- a/src/rules/shortestcommonsupersequence_ilp.rs +++ b/src/rules/shortestcommonsupersequence_ilp.rs @@ -45,15 +45,11 @@ impl ReductionResult for ReductionSCSToILP { } } -#[reduction( - transform = upper_bound { - num_vars = "max_length * (alphabet_size + 1) + total_length * max_length", - num_constraints = "max_length + total_length + total_length * max_length + total_length + max_length", - }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] +#[reduction(transform = upper_bound { + num_vars = "max_length * (alphabet_size + 1) + total_length * max_length", + num_constraints = "max_length + total_length + total_length * max_length + total_length + max_length", + num_nonzeros = "(max_length * (alphabet_size + 1) + total_length * max_length) * (max_length + total_length + total_length * max_length + total_length + max_length)", +})] impl ReduceTo> for ShortestCommonSupersequence { type Result = ReductionSCSToILP; diff --git a/src/rules/shortestweightconstrainedpath_ilp.rs b/src/rules/shortestweightconstrainedpath_ilp.rs index fe8ff92be..c94aab980 100644 --- a/src/rules/shortestweightconstrainedpath_ilp.rs +++ b/src/rules/shortestweightconstrainedpath_ilp.rs @@ -6,7 +6,7 @@ //! bound constraint enforces the weight limit, and the objective minimizes //! total path length. -use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::ShortestWeightConstrainedPath; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -57,15 +57,15 @@ impl ReductionResult for ReductionSWCPToILP { } } -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "2 * num_edges + num_vertices", num_constraints = "5 * num_edges + 4 * num_vertices + 2", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + num_nonzeros = "(2 * num_edges + num_vertices) * (5 * num_edges + 4 * num_vertices + 2)", + }, +})] impl ReduceTo> for ShortestWeightConstrainedPath { type Result = ReductionSWCPToILP; @@ -213,8 +213,14 @@ impl ReduceTo> for ShortestWeightConstrainedPath { ] }) .collect(); - let target_ilp = ILP::new(num_vars, constraints, objective, ObjectiveSense::Minimize) - .map_err(Self::target_construction)?; + let mut variables = vec![IntegerVariable::binary(); num_vars]; + variables[2 * num_edges..].fill( + IntegerVariable::new(Some(0), Some(max_order)).map_err(Self::target_construction)?, + ); + + let target_ilp = + ILP::with_variables(variables, constraints, objective, ObjectiveSense::Minimize) + .map_err(Self::target_construction)?; Ok(ReductionSWCPToILP { target: target_ilp, diff --git a/src/rules/sparsematrixcompression_ilp.rs b/src/rules/sparsematrixcompression_ilp.rs index b03552ee3..6b493c63b 100644 --- a/src/rules/sparsematrixcompression_ilp.rs +++ b/src/rules/sparsematrixcompression_ilp.rs @@ -45,15 +45,11 @@ impl ReductionResult for ReductionSMCToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionSMCToILP {} -#[reduction( - transform = upper_bound { - num_vars = "num_rows * bound_k", - num_constraints = "num_rows + num_rows^2 * num_cols^2 * bound_k", - }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] +#[reduction(transform = upper_bound { + num_vars = "num_rows * bound_k", + num_constraints = "num_rows + num_rows^2 * num_cols^2 * bound_k", + num_nonzeros = "(num_rows * bound_k) * (num_rows + num_rows^2 * num_cols^2 * bound_k)", +})] impl ReduceTo> for SparseMatrixCompression { type Result = ReductionSMCToILP; diff --git a/src/rules/spinglass_qubo.rs b/src/rules/spinglass_qubo.rs index fa158c72e..9fbaf246a 100644 --- a/src/rules/spinglass_qubo.rs +++ b/src/rules/spinglass_qubo.rs @@ -120,14 +120,14 @@ where } } -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_spins", }, - unavailable = { - num_quadratic_terms = "zero or cancelling couplings determine the nonzero quadratic terms", - } -)] + upper_bound { + num_quadratic_terms = "num_spins * (num_spins - 1) / 2", + }, +})] impl ReduceTo> for SpinGlass { type Result = ReductionSGToQUBO; @@ -167,14 +167,14 @@ impl ReduceTo> for SpinGlass { } } -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_spins", }, - unavailable = { - num_quadratic_terms = "zero or cancelling couplings determine the nonzero quadratic terms", - } -)] + upper_bound { + num_quadratic_terms = "num_spins * (num_spins - 1) / 2", + }, +})] impl ReduceTo> for SpinGlass { type Result = ReductionSGToQUBO; diff --git a/src/rules/stackercrane_ilp.rs b/src/rules/stackercrane_ilp.rs index 5e9a0d7d9..d4e15609a 100644 --- a/src/rules/stackercrane_ilp.rs +++ b/src/rules/stackercrane_ilp.rs @@ -44,15 +44,11 @@ impl ReductionResult for ReductionSCToILP { } } -#[reduction( - transform = upper_bound { - num_vars = "num_arcs * num_arcs + num_arcs * num_arcs * num_arcs", - num_constraints = "num_arcs + num_arcs + 4 * num_arcs * num_arcs * num_arcs", - }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] +#[reduction(transform = upper_bound { + num_vars = "num_arcs * num_arcs + num_arcs * num_arcs * num_arcs", + num_constraints = "num_arcs + num_arcs + 4 * num_arcs * num_arcs * num_arcs", + num_nonzeros = "(num_arcs * num_arcs + num_arcs * num_arcs * num_arcs) * (num_arcs + num_arcs + 4 * num_arcs * num_arcs * num_arcs)", +})] impl ReduceTo> for StackerCrane { type Result = ReductionSCToILP; diff --git a/src/rules/steinertree_ilp.rs b/src/rules/steinertree_ilp.rs index 0bfa3f98d..2dbc30918 100644 --- a/src/rules/steinertree_ilp.rs +++ b/src/rules/steinertree_ilp.rs @@ -43,15 +43,15 @@ impl ReductionResult for ReductionSteinerTreeToILP { } } -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_edges + num_vertices + 2 * num_edges * (num_vertices - 1)", num_constraints = "num_vertices * (num_vertices - 1) + 2 * num_edges * num_vertices + num_terminals + 1", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + num_nonzeros = "(num_edges + num_vertices + 2 * num_edges * (num_vertices - 1)) * (num_vertices * (num_vertices - 1) + 2 * num_edges * num_vertices + num_terminals + 1)", + }, +})] impl ReduceTo> for SteinerTree { type Result = ReductionSteinerTreeToILP; diff --git a/src/rules/stringtostringcorrection_ilp.rs b/src/rules/stringtostringcorrection_ilp.rs index aa4e22978..1f99ce66c 100644 --- a/src/rules/stringtostringcorrection_ilp.rs +++ b/src/rules/stringtostringcorrection_ilp.rs @@ -115,15 +115,11 @@ impl ReductionResult for ReductionSTSCToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionSTSCToILP {} -#[reduction( - transform = upper_bound { - num_vars = "(bound + 1) * source_length^2 + (bound + 1) * source_length + 2 * bound * source_length + bound", - num_constraints = "4 * bound * source_length^3 + 2 * bound * source_length^2 + source_length^2 + 6 * bound * source_length + 5 * source_length + bound + 1", - }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] +#[reduction(transform = upper_bound { + num_vars = "(bound + 1) * source_length^2 + (bound + 1) * source_length + 2 * bound * source_length + bound", + num_constraints = "4 * bound * source_length^3 + 2 * bound * source_length^2 + source_length^2 + 6 * bound * source_length + 5 * source_length + bound + 1", + num_nonzeros = "((bound + 1) * source_length^2 + (bound + 1) * source_length + 2 * bound * source_length + bound) * (4 * bound * source_length^3 + 2 * bound * source_length^2 + source_length^2 + 6 * bound * source_length + 5 * source_length + bound + 1)", +})] impl ReduceTo> for StringToStringCorrection { type Result = ReductionSTSCToILP; diff --git a/src/rules/strongconnectivityaugmentation_ilp.rs b/src/rules/strongconnectivityaugmentation_ilp.rs index 00fbacf36..5fddf756f 100644 --- a/src/rules/strongconnectivityaugmentation_ilp.rs +++ b/src/rules/strongconnectivityaugmentation_ilp.rs @@ -4,7 +4,7 @@ //! sending flow both from a root to every vertex and back again. //! See the paper entry for the full formulation. -use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::StrongConnectivityAugmentation; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -44,15 +44,11 @@ impl ReductionResult for ReductionSCAToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionSCAToILP {} -#[reduction( - transform = upper_bound { - num_vars = "num_potential_arcs + 2 * num_vertices * (num_arcs + num_potential_arcs)", - num_constraints = "1 + num_potential_arcs + 2 * num_arcs + 2 * num_vertices * num_potential_arcs + 2 * num_vertices * num_vertices", - }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] +#[reduction(transform = upper_bound { + num_vars = "num_potential_arcs + 2 * num_vertices * (num_arcs + num_potential_arcs)", + num_constraints = "1 + num_potential_arcs + 2 * num_arcs + 2 * num_vertices * num_potential_arcs + 2 * num_vertices * num_vertices", + num_nonzeros = "(num_potential_arcs + 2 * num_vertices * (num_arcs + num_potential_arcs)) * (1 + num_potential_arcs + 2 * num_arcs + 2 * num_vertices * num_potential_arcs + 2 * num_vertices * num_vertices)", +})] impl ReduceTo> for StrongConnectivityAugmentation { type Result = ReductionSCAToILP; @@ -181,7 +177,10 @@ impl ReduceTo> for StrongConnectivityAugmentation { } } - let target = ILP::new(num_vars, constraints, vec![], ObjectiveSense::Minimize) + // Each connectivity certificate can be a simple unit-flow path; cycles are unnecessary. + let variables = vec![IntegerVariable::binary(); num_vars]; + + let target = ILP::with_variables(variables, constraints, vec![], ObjectiveSense::Minimize) .map_err(Self::target_construction)?; Ok(ReductionSCAToILP { target, diff --git a/src/rules/subgraphisomorphism_ilp.rs b/src/rules/subgraphisomorphism_ilp.rs index 7237dab2b..55a07a90b 100644 --- a/src/rules/subgraphisomorphism_ilp.rs +++ b/src/rules/subgraphisomorphism_ilp.rs @@ -57,15 +57,11 @@ impl ReductionResult for ReductionSubIsoToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionSubIsoToILP {} -#[reduction( - transform = upper_bound { - num_vars = "num_pattern_vertices * num_host_vertices", - num_constraints = "num_pattern_vertices + num_host_vertices + num_pattern_edges * num_host_vertices^2", - }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] +#[reduction(transform = upper_bound { + num_vars = "num_pattern_vertices * num_host_vertices", + num_constraints = "num_pattern_vertices + num_host_vertices + num_pattern_edges * num_host_vertices^2", + num_nonzeros = "(num_pattern_vertices * num_host_vertices) * (num_pattern_vertices + num_host_vertices + num_pattern_edges * num_host_vertices^2)", +})] impl ReduceTo> for SubgraphIsomorphism { type Result = ReductionSubIsoToILP; diff --git a/src/rules/subsetsum_integerknapsack.rs b/src/rules/subsetsum_integerknapsack.rs index b715ca836..3d4adea58 100644 --- a/src/rules/subsetsum_integerknapsack.rs +++ b/src/rules/subsetsum_integerknapsack.rs @@ -32,8 +32,8 @@ inventory::submit! { source_variant_fn: ::variant, target_variant_fn: ::variant, parameter_declarations_fn: || ReductionParameterDeclarations { - relation: Some(crate::parameters::ParameterRelation::Exact), - fields: vec![("num_items", Expr::variable("num_elements"))], + + fields: vec![("num_items", crate::parameters::ParameterRelation::Exact, Expr::variable("num_elements"))], unavailable: vec![crate::rules::registry::UnavailableParameterField { field: "capacity", reason: "the target capacity equals the SubsetSum target, which is not a registered source parameter", diff --git a/src/rules/timetabledesign_ilp.rs b/src/rules/timetabledesign_ilp.rs index 4ddfe908f..0964aeae9 100644 --- a/src/rules/timetabledesign_ilp.rs +++ b/src/rules/timetabledesign_ilp.rs @@ -64,15 +64,11 @@ impl ReductionResult for ReductionTDToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionTDToILP {} -#[reduction( - transform = upper_bound { - num_vars = "num_craftsmen * num_tasks * num_periods", - num_constraints = "num_craftsmen * num_periods + num_tasks * num_periods + num_craftsmen * num_tasks + num_craftsmen * num_tasks * num_periods", - }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] +#[reduction(transform = upper_bound { + num_vars = "num_craftsmen * num_tasks * num_periods", + num_constraints = "num_craftsmen * num_periods + num_tasks * num_periods + num_craftsmen * num_tasks + num_craftsmen * num_tasks * num_periods", + num_nonzeros = "(num_craftsmen * num_tasks * num_periods) * (num_craftsmen * num_periods + num_tasks * num_periods + num_craftsmen * num_tasks + num_craftsmen * num_tasks * num_periods)", +})] impl ReduceTo> for TimetableDesign { type Result = ReductionTDToILP; diff --git a/src/rules/travelingsalesman_ilp.rs b/src/rules/travelingsalesman_ilp.rs index ee89c2ec7..7f219ae13 100644 --- a/src/rules/travelingsalesman_ilp.rs +++ b/src/rules/travelingsalesman_ilp.rs @@ -65,15 +65,15 @@ impl ReductionResult for ReductionTSPToILP { } } -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_vertices^2 + 2 * num_vertices * num_edges", num_constraints = "num_vertices^3 + -1 * num_vertices^2 + 2 * num_vertices + 4 * num_vertices * num_edges", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + num_nonzeros = "(num_vertices^2 + 2 * num_vertices * num_edges) * (num_vertices^3 + -1 * num_vertices^2 + 2 * num_vertices + 4 * num_vertices * num_edges)", + }, +})] impl ReduceTo> for TravelingSalesman { type Result = ReductionTSPToILP; diff --git a/src/rules/travelingsalesman_qubo.rs b/src/rules/travelingsalesman_qubo.rs index 5be5c567f..2e3c66af6 100644 --- a/src/rules/travelingsalesman_qubo.rs +++ b/src/rules/travelingsalesman_qubo.rs @@ -123,14 +123,14 @@ impl crate::rules::AggregateReductionResult for ReductionTravelingSalesmanToQUBO } } -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_vertices^2", }, - unavailable = { - num_quadratic_terms = "the nonzero products depend on graph edges and edge costs", - } -)] + upper_bound { + num_quadratic_terms = "(num_vertices^2) * ((num_vertices^2) - 1) / 2", + }, +})] impl ReduceTo> for TravelingSalesman { type Result = ReductionTravelingSalesmanToQUBO; diff --git a/src/rules/undirectedflowlowerbounds_ilp.rs b/src/rules/undirectedflowlowerbounds_ilp.rs index d3310c430..b8cf95beb 100644 --- a/src/rules/undirectedflowlowerbounds_ilp.rs +++ b/src/rules/undirectedflowlowerbounds_ilp.rs @@ -23,7 +23,7 @@ //! //! Size upper bound: 3*|E| variables, 5*|E| + |V| + 1 constraints. -use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::UndirectedFlowLowerBounds; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -78,15 +78,11 @@ impl ReductionResult for ReductionUFLBToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionUFLBToILP {} -#[reduction( - transform = upper_bound { - num_vars = "3 * num_edges", - num_constraints = "5 * num_edges + num_vertices + 1", - }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] +#[reduction(transform = upper_bound { + num_vars = "3 * num_edges", + num_constraints = "5 * num_edges + num_vertices + 1", + num_nonzeros = "(3 * num_edges) * (5 * num_edges + num_vertices + 1)", +})] impl ReduceTo> for UndirectedFlowLowerBounds { type Result = ReductionUFLBToILP; @@ -177,8 +173,17 @@ impl ReduceTo> for UndirectedFlowLowerBounds { } constraints.push(LinearConstraint::ge(sink_terms, self.requirement())); + let mut variables = self + .capacities() + .iter() + .flat_map(|&capacity| std::iter::repeat_n(capacity, 2)) + .map(|capacity| IntegerVariable::new(Some(0), Some(capacity))) + .collect::, _>>() + .map_err(Self::target_construction)?; + variables.resize(num_vars, IntegerVariable::binary()); + Ok(ReductionUFLBToILP { - target: ILP::new(num_vars, constraints, vec![], ObjectiveSense::Minimize) + target: ILP::with_variables(variables, constraints, vec![], ObjectiveSense::Minimize) .map_err(Self::target_construction)?, num_edges: e, }) diff --git a/src/rules/undirectedtwocommodityintegralflow_ilp.rs b/src/rules/undirectedtwocommodityintegralflow_ilp.rs index 7cba64e21..61a69fa4d 100644 --- a/src/rules/undirectedtwocommodityintegralflow_ilp.rs +++ b/src/rules/undirectedtwocommodityintegralflow_ilp.rs @@ -24,7 +24,7 @@ //! //! Constraints per edge (7 per edge) + flow conservation (2 per non-terminal vertex) + net flow (2) -use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::UndirectedTwoCommodityIntegralFlow; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -69,15 +69,15 @@ impl ReductionResult for ReductionU2CIFToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionU2CIFToILP {} -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "6 * num_edges", num_constraints = "7 * num_edges + num_conservation_constraints + 2", }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + num_nonzeros = "(6 * num_edges) * (7 * num_edges + num_conservation_constraints + 2)", + }, +})] impl ReduceTo> for UndirectedTwoCommodityIntegralFlow { type Result = ReductionU2CIFToILP; @@ -200,8 +200,17 @@ impl ReduceTo> for UndirectedTwoCommodityIntegralFlow { } constraints.push(LinearConstraint::ge(sink2_terms, self.requirement_2())); + let mut variables = self + .capacities() + .iter() + .flat_map(|&capacity| std::iter::repeat_n(capacity, 4)) + .map(|capacity| IntegerVariable::new(Some(0), Some(capacity))) + .collect::, _>>() + .map_err(Self::target_construction)?; + variables.resize(num_vars, IntegerVariable::binary()); + Ok(ReductionU2CIFToILP { - target: ILP::new(num_vars, constraints, vec![], ObjectiveSense::Minimize) + target: ILP::with_variables(variables, constraints, vec![], ObjectiveSense::Minimize) .map_err(Self::target_construction)?, num_edges: e, }) diff --git a/src/unit_tests/parameter_formula_validation.rs b/src/unit_tests/parameter_formula_validation.rs index 8709c0c8f..183c2111f 100644 --- a/src/unit_tests/parameter_formula_validation.rs +++ b/src/unit_tests/parameter_formula_validation.rs @@ -6,6 +6,32 @@ use std::collections::BTreeMap; type SourceKey = (String, BTreeMap); +#[test] +fn matrix_targets_with_dimension_predictions_also_bound_entries() { + for entry in crate::rules::registry::reduction_entries() { + let (dimensions, count): (&[&str], &str) = match entry.target_name { + "ILP" => (&["num_vars", "num_constraints"], "num_nonzeros"), + "QUBO" | "DecisionQUBO" => (&["num_vars"], "num_quadratic_terms"), + _ => continue, + }; + let contract = entry.parameter_contract().unwrap(); + let Some(transform) = contract.transform() else { + continue; + }; + if dimensions + .iter() + .all(|field| transform.get(field).is_some()) + { + assert!( + transform.get(count).is_some(), + "{} -> {} has dimension predictions but no {count} bound", + entry.source_name, + entry.target_name + ); + } + } +} + fn canonical_sources() -> BTreeMap> { let mut sources = BTreeMap::>::new(); let db = crate::example_db::build_example_db().unwrap(); @@ -130,6 +156,43 @@ fn source_for( (random.generate)(Value::Object(args)).map_err(|error| error.to_string()) } +#[test] +fn integer_ilp_reductions_support_binary_encoding() { + use crate::models::algebraic::ILP; + use crate::rules::ReduceTo; + use crate::traits::Problem; + + let sources = canonical_sources(); + let mut failures = Vec::new(); + let mut checked = 0; + for entry in crate::rules::registry::reduction_entries() { + if entry.target_name != "ILP" || entry.target_variant() != ILP::::variant() { + continue; + } + let result = (|| { + let source = source_for(entry, &sources)?; + let reduced = + entry.reduce_fn.unwrap()(source.as_any()).map_err(|error| error.to_string())?; + let integer = reduced + .target_problem_any() + .downcast_ref::>() + .unwrap(); + ReduceTo::>::reduce_to(integer).map_err(|error| error.to_string())?; + Ok::<_, String>(()) + })(); + if let Err(error) = result { + failures.push(format!( + "{} {:?}: {error}", + entry.source_name, + entry.source_variant() + )); + } + checked += 1; + } + assert!(checked > 0); + assert!(failures.is_empty(), "{}", failures.join("\n")); +} + #[test] fn every_parameter_formula_matches_a_constructed_target() { let sources = canonical_sources(); @@ -157,7 +220,7 @@ fn every_parameter_formula_matches_a_constructed_target() { .map_err(|error| error.to_string())?; for (field, _) in transform.expressions() { let valid = match (predicted.get(field), actual.get(field)) { - (Some(predicted), Some(actual)) => match transform.relation() { + (Some(predicted), Some(actual)) => match transform.relation(field).unwrap() { ParameterRelation::Exact => predicted == actual, ParameterRelation::UpperBound => predicted >= actual, }, @@ -166,7 +229,7 @@ fn every_parameter_formula_matches_a_constructed_target() { if !valid { return Err(format!( "{field} ({:?}): predicted {:?}, measured {:?}; source {}", - transform.relation(), + transform.relation(field).unwrap(), predicted.get(field), actual.get(field), source.serialize_json() diff --git a/src/unit_tests/parameters.rs b/src/unit_tests/parameters.rs index a98eb5dfc..908a9946c 100644 --- a/src/unit_tests/parameters.rs +++ b/src/unit_tests/parameters.rs @@ -2,6 +2,101 @@ use super::{ParameterRelation, ParameterTransform, ParameterTransformError}; use crate::expr::Expr; use crate::types::ProblemParameters; +#[test] +fn composition_preserves_fields_with_available_dependencies() { + let first = ParameterTransform::new( + "A -> B", + ParameterRelation::Exact, + [("variables", Expr::parse("n"))], + ) + .unwrap(); + let next = ParameterTransform::new( + "B -> C", + ParameterRelation::Exact, + [ + ("variables", Expr::parse("variables")), + ("terms", Expr::parse("terms")), + ], + ) + .unwrap(); + let composed = first.compose(&next, "A -> C").unwrap(); + assert_eq!( + composed + .evaluate(&ProblemParameters::new(vec![("n", 4)])) + .unwrap() + .get("variables"), + Some(4) + ); + assert!(composed.get("terms").is_none()); +} + +#[test] +fn composition_tracks_only_each_fields_dependencies() { + let first = ParameterTransform::from_fields( + "A -> B", + [ + ("vertices", ParameterRelation::Exact, Expr::parse("n")), + ("edges", ParameterRelation::UpperBound, Expr::parse("n^2")), + ], + ) + .unwrap(); + let next = ParameterTransform::new( + "B -> C", + ParameterRelation::Exact, + [ + ("vertices", Expr::parse("vertices")), + ("edges", Expr::parse("edges")), + ("reciprocal", Expr::parse("1 / edges")), + ], + ) + .unwrap(); + let composed = first.compose(&next, "A -> C").unwrap(); + assert_eq!( + composed.relation("vertices"), + Some(ParameterRelation::Exact) + ); + assert_eq!( + composed.relation("edges"), + Some(ParameterRelation::UpperBound) + ); + assert!(matches!( + composed.unavailable("reciprocal"), + Some(ParameterTransformError::CannotPropagateUpperBound { .. }) + )); + let values = composed + .evaluate(&ProblemParameters::new(vec![("n", 3)])) + .unwrap(); + assert_eq!(values.get("vertices"), Some(3)); + assert_eq!(values.get("edges"), Some(9)); +} + +#[test] +fn constants_survive_unavailable_inputs_and_dependent_fields_keep_the_cause() { + let mut first = + ParameterTransform::from_fields("A -> B", Vec::<(&str, ParameterRelation, Expr)>::new()) + .unwrap(); + first.declare_unavailable("size", "input magnitudes are not registered"); + let next = ParameterTransform::new( + "B -> C", + ParameterRelation::Exact, + [("fixed", Expr::parse("3")), ("size", Expr::parse("size"))], + ) + .unwrap(); + let composed = first.compose(&next, "A -> C").unwrap(); + assert_eq!( + composed + .evaluate(&ProblemParameters::default()) + .unwrap() + .get("fixed"), + Some(3) + ); + assert!( + matches!(composed.unavailable("size"), Some(ParameterTransformError::UnavailableInput { cause, .. }) + if matches!(cause.as_ref(), ParameterTransformError::Unavailable { edge, field, .. } + if edge.as_ref() == "A -> B" && field.as_ref() == "size")) + ); +} + #[test] fn exact_transform_evaluates_exactly() { let transform = ParameterTransform::new( @@ -31,7 +126,10 @@ fn upper_bound_relation_survives_evaluation_and_composition() { ) .unwrap(); let composed = first.compose(&second, "A -> C").unwrap(); - assert_eq!(composed.relation(), ParameterRelation::UpperBound); + assert_eq!( + composed.relation("k").unwrap(), + ParameterRelation::UpperBound + ); let result = composed .evaluate(&ProblemParameters::new(vec![("n", 4)])) .unwrap(); @@ -109,10 +207,12 @@ fn symbolic_upper_bound_composition_rejects_non_polynomial_formulas() { ) .unwrap(); + let composed = bounded_transform.compose(&reciprocal, "A -> C").unwrap(); assert!(matches!( - bounded_transform.compose(&reciprocal, "A -> C"), - Err(ParameterTransformError::CannotPropagateUpperBound { .. }) + composed.unavailable("k"), + Some(ParameterTransformError::CannotPropagateUpperBound { .. }) )); + assert!(composed.get("k").is_none()); } #[test] diff --git a/src/unit_tests/reduction_graph.rs b/src/unit_tests/reduction_graph.rs index ce165c6db..0370dbb4b 100644 --- a/src/unit_tests/reduction_graph.rs +++ b/src/unit_tests/reduction_graph.rs @@ -99,7 +99,7 @@ fn exact_rule_exposes_one_transform() { .unwrap() .unwrap(); assert_eq!( - transform.relation(), + transform.relation("num_vertices").unwrap(), crate::parameters::ParameterRelation::Exact ); } @@ -1080,8 +1080,11 @@ fn test_find_paths_bounded_returns_shortest_when_truncated() { parameter_contract: ReductionParameterContract::new( "synthetic edge", ReductionParameterDeclarations { - relation: Some(crate::parameters::ParameterRelation::Exact), - fields: vec![("n", Expr::variable("n"))], + fields: vec![( + "n", + crate::parameters::ParameterRelation::Exact, + Expr::variable("n"), + )], unavailable: vec![], }, ), diff --git a/src/unit_tests/rules/graph.rs b/src/unit_tests/rules/graph.rs index 013856e87..a2a8912d6 100644 --- a/src/unit_tests/rules/graph.rs +++ b/src/unit_tests/rules/graph.rs @@ -26,7 +26,6 @@ fn empty_parameter_contract() -> Result Reductio parameter_contract: ReductionParameterContract::new( "synthetic edge", ReductionParameterDeclarations { - relation: Some(crate::parameters::ParameterRelation::Exact), fields: fields .iter() - .map(|(field, expression)| (*field, Expr::try_parse(expression).unwrap())) + .map(|(field, expression)| { + ( + *field, + crate::parameters::ParameterRelation::Exact, + Expr::try_parse(expression).unwrap(), + ) + }) .collect(), unavailable: vec![], }, @@ -653,10 +657,10 @@ fn path_parameter_contract_errors_are_typed_and_isolated() { turing: false, }, ); - assert!(matches!( - unavailable.path_parameter_transforms(&disconnected), - Err(PathParameterError::Unavailable { .. }) - )); + let transforms = unavailable + .path_parameter_transforms(&disconnected) + .unwrap(); + assert!(transforms[0].unavailable("size").is_some()); let invalid_contract = Err(ParameterContractError::EmptyUnavailableReason { edge: "A -> B".into(), @@ -720,9 +724,13 @@ fn path_size_composition_and_contract_evaluation_report_errors() { ], ); let chained = named_path(&["A", "B", "C"]); + let composed = invalid_composition + .compose_path_parameter_transform(&chained) + .unwrap() + .unwrap(); assert!(matches!( - invalid_composition.compose_path_parameter_transform(&chained), - Err(PathParameterError::Step { .. }) + composed.unavailable("z"), + Some(crate::parameters::ParameterTransformError::MissingCompositionInput { .. }) )); let valid = ReductionGraph::from_test_edges( @@ -1932,7 +1940,7 @@ fn parameter_contract_variables_are_registered_source_fields() { let input_vars: std::collections::HashSet<_> = declarations .fields .iter() - .flat_map(|(_, expression)| expression.variables()) + .flat_map(|(_, _, expression)| expression.variables()) .collect(); if input_vars.is_empty() { continue; diff --git a/src/unit_tests/rules/ilp_qubo.rs b/src/unit_tests/rules/ilp_qubo.rs index 08300e2c8..3f89c648d 100644 --- a/src/unit_tests/rules/ilp_qubo.rs +++ b/src/unit_tests/rules/ilp_qubo.rs @@ -3,6 +3,62 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense}; use crate::solvers::BruteForce; use crate::solvers::BruteForceProblem as _; +#[test] +fn parameter_bounds_cover_slack_boundaries_and_cancellation() { + use crate::parameters::ParameterRelation; + use crate::traits::Problem; + let entry = crate::rules::registry::reduction_entries() + .into_iter() + .find(|entry| { + entry.source_name == "ILP" + && entry.target_name == "QUBO" + && entry.source_variant() == ILP::::variant() + }) + .unwrap(); + let contract = entry.parameter_contract().unwrap(); + let transform = contract + .transform() + .expect("ILP must predict QUBO size bounds"); + let mut cases = vec![ + ILP::::new(0, vec![], vec![], ObjectiveSense::Minimize).unwrap(), + ILP::::new( + 2, + vec![ + LinearConstraint::eq(vec![(0, 1), (1, 1)], 0), + LinearConstraint::eq(vec![(0, 1), (1, -1)], 0), + ], + vec![], + ObjectiveSense::Minimize, + ) + .unwrap(), + ]; + for range in [0, 1, 2, 3, 4, 7, 8] { + for constraint in [ + LinearConstraint::le(vec![(0, -1)], range - 1), + LinearConstraint::ge(vec![(0, 1)], 1 - range), + ] { + cases.push( + ILP::::new(1, vec![constraint], vec![], ObjectiveSense::Minimize).unwrap(), + ); + } + } + for source in cases { + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + let actual = reduction.target_problem().parameters(); + let predicted = transform.evaluate(&source.parameters()).unwrap(); + for field in ["num_vars", "num_quadratic_terms"] { + assert_eq!( + transform.relation(field), + Some(ParameterRelation::UpperBound) + ); + assert!( + predicted.get(field).unwrap() >= actual.get(field).unwrap(), + "{field}" + ); + } + } +} + #[test] fn test_ilp_to_qubo_closed_loop() { // Binary ILP: maximize x0 + 2*x1 + 3*x2 diff --git a/src/unit_tests/rules/ksatisfiability_qubo.rs b/src/unit_tests/rules/ksatisfiability_qubo.rs index 3bb229434..2d9aae200 100644 --- a/src/unit_tests/rules/ksatisfiability_qubo.rs +++ b/src/unit_tests/rules/ksatisfiability_qubo.rs @@ -6,6 +6,51 @@ use crate::solvers::BruteForceProblem as _; use crate::traits::Problem; use crate::variant::{K2, K3}; +#[test] +fn mixed_parameter_predictions_match_constructed_qubo_paths() { + use crate::parameters::ParameterRelation; + use crate::rules::ReductionGraph; + let graph = ReductionGraph::new(); + let path = graph + .find_all_paths( + KSatisfiability::::NAME, + &ReductionGraph::variant_to_map(&KSatisfiability::::variant()), + QUBO::::NAME, + &ReductionGraph::variant_to_map(&QUBO::::variant()), + ) + .into_iter() + .find(|path| path.len() == 2) + .unwrap(); + let transform = graph + .compose_path_parameter_transform(&path) + .unwrap() + .unwrap(); + assert_eq!( + transform.relation("num_vars"), + Some(ParameterRelation::Exact) + ); + assert_eq!( + transform.relation("num_quadratic_terms"), + Some(ParameterRelation::UpperBound) + ); + for (n, clauses, expected_terms) in [ + (0, vec![], 0), + (1, vec![], 0), + (3, vec![vec![1, 2], vec![1, 3], vec![2, 3]], 3), + (2, vec![vec![1, 2], vec![-1, 2]], 0), + ] { + let source = + KSatisfiability::::new(n, clauses.into_iter().map(CNFClause::new).collect()); + let reduction = ReduceTo::>>::reduce_to(&source).unwrap(); + let final_reduction = ReduceTo::>::reduce_to(reduction.target_problem()).unwrap(); + let actual = final_reduction.target_problem().parameters(); + let predicted = transform.evaluate(&source.parameters()).unwrap(); + assert_eq!(actual.get("num_quadratic_terms"), Some(expected_terms)); + assert_eq!(predicted.get("num_vars"), actual.get("num_vars")); + assert!(predicted.get("num_quadratic_terms").unwrap() >= expected_terms); + } +} + #[test] fn test_ksatisfiability_to_qubo_closed_loop() { // 3 vars, 4 clauses (matches ground truth): diff --git a/src/unit_tests/rules/maximummatching_ilp.rs b/src/unit_tests/rules/maximummatching_ilp.rs index 3dda34bfe..913253576 100644 --- a/src/unit_tests/rules/maximummatching_ilp.rs +++ b/src/unit_tests/rules/maximummatching_ilp.rs @@ -4,6 +4,35 @@ use crate::topology::SimpleGraph; use crate::traits::Problem; use crate::types::Max; +#[test] +fn parameter_predictions_account_for_isolated_vertices_and_loops() { + use crate::parameters::ParameterRelation; + for (vertices, edges, expected_constraints) in [(3, vec![(0, 1)], 2), (1, vec![(0, 0)], 1)] { + let source = MaximumMatching::<_, i64>::unit_weights(SimpleGraph::new(vertices, edges)); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + let actual = reduction.target_problem().parameters(); + assert_eq!(actual.get("num_constraints"), Some(expected_constraints)); + let entry = crate::rules::registry::reduction_entries() + .into_iter() + .find(|entry| entry.source_name == "MaximumMatching" && entry.target_name == "ILP") + .unwrap(); + let contract = entry.parameter_contract().unwrap(); + let transform = contract.transform().unwrap(); + let predicted = transform.evaluate(&source.parameters()).unwrap(); + for (field, _) in transform.expressions() { + match transform.relation(field).unwrap() { + ParameterRelation::Exact => { + assert_eq!(predicted.get(field), actual.get(field), "{field}") + } + ParameterRelation::UpperBound => assert!( + predicted.get(field).unwrap() >= actual.get(field).unwrap(), + "{field}" + ), + } + } + } +} + #[test] fn test_reduction_creates_valid_ilp() { // Triangle graph: 3 vertices, 3 edges diff --git a/src/unit_tests/rules/maximumsetpacking_ilp.rs b/src/unit_tests/rules/maximumsetpacking_ilp.rs index e2d428dbf..d8a91d468 100644 --- a/src/unit_tests/rules/maximumsetpacking_ilp.rs +++ b/src/unit_tests/rules/maximumsetpacking_ilp.rs @@ -12,7 +12,8 @@ fn constraint_count_is_only_an_upper_bound() { .unwrap() .transform() .unwrap() - .relation(), + .relation("num_constraints") + .unwrap(), crate::parameters::ParameterRelation::UpperBound ); let problem = MaximumSetPacking::new(vec![vec![0], vec![1]]); diff --git a/src/unit_tests/rules/minimumfeedbackarcset_ilp.rs b/src/unit_tests/rules/minimumfeedbackarcset_ilp.rs index 09413a71b..eb9e8aa51 100644 --- a/src/unit_tests/rules/minimumfeedbackarcset_ilp.rs +++ b/src/unit_tests/rules/minimumfeedbackarcset_ilp.rs @@ -4,6 +4,29 @@ use crate::topology::DirectedGraph; use crate::traits::Problem; use crate::types::Min; +#[test] +fn feedback_arc_set_solves_through_integer_binary_ilp_and_qubo() { + use crate::models::algebraic::QUBO; + + let source = + MinimumFeedbackArcSet::new(DirectedGraph::new(2, vec![(0, 1), (1, 0)]), vec![2_i64, 5]); + let integer = ReduceTo::>::reduce_to(&source).unwrap(); + let binary = ReduceTo::>::reduce_to(integer.target_problem()).unwrap(); + let qubo = ReduceTo::>::reduce_to(binary.target_problem()).unwrap(); + let optimum = BruteForce::new() + .solve(qubo.target_problem()) + .unwrap() + .unwrap(); + let binary_solution = qubo.extract_solution(&optimum).unwrap(); + let integer_solution = binary.extract_solution(&binary_solution).unwrap(); + assert!(integer + .target_problem() + .is_feasible(&integer_solution) + .unwrap()); + let solution = integer.extract_solution(&integer_solution).unwrap(); + assert_eq!(source.evaluate(&solution).unwrap(), Min(Some(2))); +} + #[test] fn test_reduction_creates_valid_ilp() { // Simple 3-cycle: 0 -> 1 -> 2 -> 0 diff --git a/src/unit_tests/rules/registry.rs b/src/unit_tests/rules/registry.rs index 72de79349..9a3eeec91 100644 --- a/src/unit_tests/rules/registry.rs +++ b/src/unit_tests/rules/registry.rs @@ -114,16 +114,19 @@ fn entry_with(declarations: fn() -> ReductionParameterDeclarations) -> Reduction } #[test] -fn one_relation_applies_to_the_whole_transform() { +fn declared_field_exposes_its_relation() { let entry = entry_with(|| ReductionParameterDeclarations { - relation: Some(crate::parameters::ParameterRelation::Exact), - fields: vec![("n", Expr::variable("n"))], + fields: vec![( + "n", + crate::parameters::ParameterRelation::Exact, + Expr::variable("n"), + )], unavailable: vec![], }); let contract = entry.parameter_contract().unwrap(); let transform = contract.transform().unwrap(); assert_eq!( - transform.relation(), + transform.relation("n").unwrap(), crate::parameters::ParameterRelation::Exact ); assert!(transform.get("n").is_some()); @@ -132,8 +135,11 @@ fn one_relation_applies_to_the_whole_transform() { #[test] fn unavailable_field_cannot_overlap_a_formula() { let entry = entry_with(|| ReductionParameterDeclarations { - relation: Some(crate::parameters::ParameterRelation::Exact), - fields: vec![("n", Expr::variable("n"))], + fields: vec![( + "n", + crate::parameters::ParameterRelation::Exact, + Expr::variable("n"), + )], unavailable: vec![UnavailableParameterField { field: "n", reason: "the construction does not expose this statistic", @@ -148,7 +154,6 @@ fn unavailable_field_cannot_overlap_a_formula() { #[test] fn unavailable_field_requires_a_reason() { let entry = entry_with(|| ReductionParameterDeclarations { - relation: None, fields: vec![], unavailable: vec![UnavailableParameterField { field: "n", diff --git a/src/unit_tests/symbolic_parameter_contracts.rs b/src/unit_tests/symbolic_parameter_contracts.rs index 19665fe1c..c0411e1e0 100644 --- a/src/unit_tests/symbolic_parameter_contracts.rs +++ b/src/unit_tests/symbolic_parameter_contracts.rs @@ -123,9 +123,6 @@ fn canonical_examples_satisfy_upper_bound_parameter_contracts() { let Some(transform) = contract.transform() else { continue; }; - if transform.relation() != ParameterRelation::UpperBound { - continue; - } let source_size = ReductionGraph::compute_problem_parameters( &example.source.problem, &example.source.variant, @@ -171,7 +168,15 @@ where .expect("direct reduction is registered"); let contract = entry.parameter_contract().unwrap(); let transform = contract.transform().expect("symbolic transform exists"); - assert_eq!(transform.relation(), relation, "{} -> {}", S::NAME, T::NAME); + for (field, _) in transform.expressions() { + assert_eq!( + transform.relation(field), + Some(relation), + "{} -> {}: {field}", + S::NAME, + T::NAME + ); + } let predicted = transform.evaluate(&source.parameters()).unwrap(); if relation == ParameterRelation::Exact { for (field, _) in transform.expressions() { From 69282d170b53dd99d92f4eb5fe41d2a94eb615b2 Mon Sep 17 00:00:00 2001 From: Xiwei Pan <90967972+isPANN@users.noreply.github.com> Date: Tue, 29 Sep 2026 02:13:56 -0700 Subject: [PATCH 7/9] Derive reduction parameter bounds from constructed targets Correct exactness claims, count sparse coefficients by construction block, reject negative flow capacities, and verify metadata using existing behavior inputs and real executors. --- docs/src/design.md | 10 ++- problemreductions-cli/src/test_support.rs | 2 - src/models/decision.rs | 2 - .../graph/path_constrained_network_flow.rs | 5 +- .../graph/undirected_flow_lower_bounds.rs | 3 + src/rules/coloring_qubo.rs | 12 ++- .../hamiltoniancircuit_hamiltonianpath.rs | 14 +-- src/rules/knapsack_ilp.rs | 2 +- src/rules/lengthboundeddisjointpaths_ilp.rs | 12 ++- src/rules/maximalis_ilp.rs | 6 +- src/rules/maximumsetpacking_ilp.rs | 12 ++- .../minimumcapacitatedspanningtree_ilp.rs | 14 ++- src/rules/minimumdominatingset_ilp.rs | 4 +- src/rules/minimumedgecostflow_ilp.rs | 13 ++- src/rules/minimumfaultdetectiontestset_ilp.rs | 8 +- src/rules/minimummaximalmatching_ilp.rs | 7 +- ...nimumvertexcover_minimummaximalmatching.rs | 1 - src/rules/paintshop_ilp.rs | 14 ++- src/rules/registry.rs | 2 +- src/rules/ruralpostman_ilp.rs | 16 ++-- src/rules/spinglass_qubo.rs | 5 +- src/rules/subsetsum_integerknapsack.rs | 1 - src/rules/test_helpers.rs | 28 ++++++ src/rules/travelingsalesman_ilp.rs | 9 +- .../graph/path_constrained_network_flow.rs | 4 + .../graph/undirected_flow_lower_bounds.rs | 7 ++ .../parameter_formula_validation.rs | 89 +++++++++---------- src/unit_tests/rules/coloring_qubo.rs | 1 + .../rules/minimumedgecostflow_ilp.rs | 1 + .../rules/minimumfaultdetectiontestset_ilp.rs | 1 + .../rules/minimummaximalmatching_ilp.rs | 1 + src/unit_tests/rules/ruralpostman_ilp.rs | 1 + src/unit_tests/rules/travelingsalesman_ilp.rs | 1 + .../symbolic_parameter_contracts.rs | 36 +++----- 34 files changed, 210 insertions(+), 134 deletions(-) diff --git a/docs/src/design.md b/docs/src/design.md index ebe73dac6..cf7a7aa82 100644 --- a/docs/src/design.md +++ b/docs/src/design.md @@ -452,6 +452,10 @@ The returned `ReductionChain` stores each intermediate reduction and extracts th Each reduction classifies every target parameter exactly once as exact, upper bound, or unavailable with a reason. Every formula uses only registered source parameters on its RHS. +The guarantee describes the target built by the registered implementation. Derive it by +counting construction blocks, including early returns, skipped rows, repeated terms, and +normalization. Prefer a simple algorithmic upper bound; use `exact` only when equality holds +for every accepted input. Existing examples check the derivation but do not prove it. Target structural relationships may justify a formula, but source expressions must be substituted before registration; there is no automatic model-level inference. The `#[reduction]` macro parses every formula into the canonical `Expr` DAG at compile time: @@ -489,7 +493,11 @@ coefficients cancel; exact sparsity can still require additional source informat Use `ParameterTransform::relation(field)` to inspect a formula's accuracy and `unavailable(field)` for a composition failure and its upstream cause. The uniform `ParameterTransform::new` constructor remains available; `from_fields` accepts mixed relations. -CLI contract JSON stores `relation` within each formula entry in `fields`. +CLI contract JSON stores `relation` within each formula entry in `fields`, replacing the +former contract-level property. Callers of the former argument-free `relation()` must now +request a field. `ParameterContractError::EmptyTransform` and `MissingRelation` have been +removed. `path_parameter_transforms` retains unavailable fields within the transform +instead of returning `PathParameterError::Unavailable` for the entire path. `ParameterTransform` uses exact rational and arbitrary-precision integer arithmetic. Exact relations must evaluate to non-negative integers, while upper-bound results round rational diff --git a/problemreductions-cli/src/test_support.rs b/problemreductions-cli/src/test_support.rs index aaaeaf946..a955d2442 100644 --- a/problemreductions-cli/src/test_support.rs +++ b/problemreductions-cli/src/test_support.rs @@ -399,7 +399,6 @@ problemreductions::inventory::submit! { source_variant_fn: AggregateValueSource::variant, target_variant_fn: AggregateValueTarget::variant, parameter_declarations_fn: || ReductionParameterDeclarations { - fields: vec![], unavailable: vec![problemreductions::rules::registry::UnavailableParameterField { field: "num_values", @@ -430,7 +429,6 @@ problemreductions::inventory::submit! { source_variant_fn: AggregateValueSource::variant, target_variant_fn: ILP::::variant, parameter_declarations_fn: || ReductionParameterDeclarations { - fields: vec![], unavailable: vec![ problemreductions::rules::registry::UnavailableParameterField { diff --git a/src/models/decision.rs b/src/models/decision.rs index 9bd4ded89..26928fdc5 100644 --- a/src/models/decision.rs +++ b/src/models/decision.rs @@ -79,7 +79,6 @@ macro_rules! register_decision_variant { source_variant_fn: <$crate::models::decision::Decision<$inner> as $crate::traits::Problem>::variant, target_variant_fn: <$inner as $crate::traits::Problem>::variant, parameter_declarations_fn: || $crate::rules::registry::ReductionParameterDeclarations { - fields: <$inner as $crate::traits::Problem>::parameter_names() .iter() .map(|&name| (name, $crate::parameters::ParameterRelation::Exact, $crate::expr::Expr::variable(name))) @@ -124,7 +123,6 @@ macro_rules! register_decision_variant { source_variant_fn: <$inner as $crate::traits::Problem>::variant, target_variant_fn: <$crate::models::decision::Decision<$inner> as $crate::traits::Problem>::variant, parameter_declarations_fn: || $crate::rules::registry::ReductionParameterDeclarations { - fields: <$inner as $crate::traits::Problem>::parameter_names() .iter() .map(|&name| (name, $crate::parameters::ParameterRelation::Exact, $crate::expr::Expr::variable(name))) diff --git a/src/models/graph/path_constrained_network_flow.rs b/src/models/graph/path_constrained_network_flow.rs index 79a3ab483..a1a186205 100644 --- a/src/models/graph/path_constrained_network_flow.rs +++ b/src/models/graph/path_constrained_network_flow.rs @@ -132,7 +132,7 @@ impl PathConstrainedNetworkFlow { /// # Panics /// /// Panics if: - /// - `capacities.len() != graph.num_arcs()` + /// - `capacities.len() != graph.num_arcs()` or a capacity is negative /// - `source` or `sink` are out of range or identical /// - any prescribed path is not a valid directed simple s-t path pub fn new( @@ -164,6 +164,9 @@ impl PathConstrainedNetworkFlow { graph.num_arcs(), )); } + if capacities.iter().any(|&capacity| capacity < 0) { + return Err("capacities must be nonnegative".into()); + } if source >= num_vertices { return Err(format!("source ({source}) >= num_vertices ({num_vertices})").into()); } diff --git a/src/models/graph/undirected_flow_lower_bounds.rs b/src/models/graph/undirected_flow_lower_bounds.rs index c8a270ba6..8cd64d366 100644 --- a/src/models/graph/undirected_flow_lower_bounds.rs +++ b/src/models/graph/undirected_flow_lower_bounds.rs @@ -109,6 +109,9 @@ impl UndirectedFlowLowerBounds { } let num_vertices = graph.num_vertices(); + if capacities.iter().any(|&capacity| capacity < 0) { + return Err("capacities must be nonnegative".into()); + } if source >= num_vertices { return Err(format!("source must be less than num_vertices ({num_vertices})").into()); } diff --git a/src/rules/coloring_qubo.rs b/src/rules/coloring_qubo.rs index f064b7fc2..e6b6fdc38 100644 --- a/src/rules/coloring_qubo.rs +++ b/src/rules/coloring_qubo.rs @@ -179,10 +179,16 @@ fn reduce_kcoloring_to_qubo( } // Register only the KN variant in the reduction graph +// There are n * binomial(k, 2) within-vertex interactions and at most m*k +// edge interactions: parallel edges share coefficients and loops are diagonal. #[reduction( - transform = exact { - num_vars = "num_vertices * num_colors", - num_quadratic_terms = "num_vertices * num_colors * (num_colors - 1) / 2 + num_edges * num_colors", + transform = { + exact { + num_vars = "num_vertices * num_colors", + }, + upper_bound { + num_quadratic_terms = "num_vertices * num_colors * (num_colors - 1) / 2 + num_edges * num_colors", + }, } )] impl ReduceTo>> for KColoring { diff --git a/src/rules/hamiltoniancircuit_hamiltonianpath.rs b/src/rules/hamiltoniancircuit_hamiltonianpath.rs index 2a0ec4d69..1520088e3 100644 --- a/src/rules/hamiltoniancircuit_hamiltonianpath.rs +++ b/src/rules/hamiltoniancircuit_hamiltonianpath.rs @@ -86,13 +86,17 @@ impl ReductionResult for ReductionHamiltonianCircuitToHamiltonianPath { #[crate::aggregate_reduction(identity)] impl crate::rules::AggregateReductionResult for ReductionHamiltonianCircuitToHamiltonianPath {} -#[reduction( - transform = upper_bound { +// Both branches allocate n+3 vertices. The main branch copies m edges, +// at most m neighbor occurrences of vertex 0, and two pendant edges. +#[reduction(transform = { + exact { num_vertices = "num_vertices + 3", - num_edges = "num_edges + num_vertices + 1", num_consecutive_positions = "num_vertices + 2", - } -)] + }, + upper_bound { + num_edges = "2 * num_edges + 2", + }, +})] impl ReduceTo> for HamiltonianCircuit { type Result = ReductionHamiltonianCircuitToHamiltonianPath; diff --git a/src/rules/knapsack_ilp.rs b/src/rules/knapsack_ilp.rs index 1505e7a76..1d3e57852 100644 --- a/src/rules/knapsack_ilp.rs +++ b/src/rules/knapsack_ilp.rs @@ -40,7 +40,7 @@ impl ReductionResult for ReductionKnapsackToILP { num_constraints = "1", }, upper_bound { - num_nonzeros = "num_items * 1", + num_nonzeros = "num_items", }, })] impl ReduceTo> for Knapsack { diff --git a/src/rules/lengthboundeddisjointpaths_ilp.rs b/src/rules/lengthboundeddisjointpaths_ilp.rs index 2c330682c..505c7e3b3 100644 --- a/src/rules/lengthboundeddisjointpaths_ilp.rs +++ b/src/rules/lengthboundeddisjointpaths_ilp.rs @@ -100,10 +100,14 @@ impl ReductionResult for ReductionLBDPToILP { } } -#[reduction(transform = upper_bound { - num_vars = "max_paths * 2 * num_edges + max_paths", - num_constraints = "max_paths * num_vertices + max_paths * num_edges + max_paths + num_edges + num_vertices + max_paths", - num_nonzeros = "(max_paths * 2 * num_edges + max_paths) * (max_paths * num_vertices + max_paths * num_edges + max_paths + num_edges + num_vertices + max_paths)", +#[reduction(transform = { + exact { + num_vars = "max_paths * 2 * num_edges + max_paths", + }, + upper_bound { + num_constraints = "max_paths * num_vertices + max_paths * num_edges + max_paths + num_edges + num_vertices + max_paths", + num_nonzeros = "(max_paths * 2 * num_edges + max_paths) * (max_paths * num_vertices + max_paths * num_edges + max_paths + num_edges + num_vertices + max_paths)", + }, })] impl ReduceTo> for LengthBoundedDisjointPaths { type Result = ReductionLBDPToILP; diff --git a/src/rules/maximalis_ilp.rs b/src/rules/maximalis_ilp.rs index 0830cbd6e..7be51dd64 100644 --- a/src/rules/maximalis_ilp.rs +++ b/src/rules/maximalis_ilp.rs @@ -32,13 +32,15 @@ impl ReductionResult for ReductionMxISToILP { } } +// Independence uses at most m distinct non-loop pairs (2m coefficients); +// maximality uses n diagonal terms and at most 2m neighbor incidences. #[reduction(transform = { exact { num_vars = "num_vertices", - num_constraints = "num_edges + num_vertices", }, upper_bound { - num_nonzeros = "num_vertices * (num_edges + num_vertices)", + num_constraints = "num_edges + num_vertices", + num_nonzeros = "4 * num_edges + num_vertices", }, })] impl ReduceTo> for MaximalIS { diff --git a/src/rules/maximumsetpacking_ilp.rs b/src/rules/maximumsetpacking_ilp.rs index be215a8f8..2a6d290f0 100644 --- a/src/rules/maximumsetpacking_ilp.rs +++ b/src/rules/maximumsetpacking_ilp.rs @@ -39,10 +39,14 @@ impl ReductionResult for ReductionSPToILP { } } -#[reduction(transform = upper_bound { - num_vars = "num_sets", - num_constraints = "universe_size", - num_nonzeros = "num_sets * universe_size", +#[reduction(transform = { + exact { + num_vars = "num_sets", + }, + upper_bound { + num_constraints = "universe_size", + num_nonzeros = "num_sets * universe_size", + }, })] impl ReduceTo> for MaximumSetPacking { type Result = ReductionSPToILP; diff --git a/src/rules/minimumcapacitatedspanningtree_ilp.rs b/src/rules/minimumcapacitatedspanningtree_ilp.rs index 42f2263e1..a3362f65a 100644 --- a/src/rules/minimumcapacitatedspanningtree_ilp.rs +++ b/src/rules/minimumcapacitatedspanningtree_ilp.rs @@ -62,10 +62,16 @@ impl ReductionResult for ReductionMinimumCapacitatedSpanningTreeToILP { } } -#[reduction(transform = upper_bound { - num_vars = "5 * num_edges", - num_constraints = "5 * num_edges + 2 * num_vertices + 1", - num_nonzeros = "(5 * num_edges) * (5 * num_edges + 2 * num_vertices + 1)", +// Five edge variables per edge; all row blocks are unconditional. Nonzeros: +// cardinality m, binary m, two conservation blocks 8m, linking 6m, capacity 2m. +#[reduction(transform = { + exact { + num_vars = "5 * num_edges", + num_constraints = "5 * num_edges + 2 * num_vertices + 1", + }, + upper_bound { + num_nonzeros = "18 * num_edges", + }, })] impl ReduceTo> for MinimumCapacitatedSpanningTree { type Result = ReductionMinimumCapacitatedSpanningTreeToILP; diff --git a/src/rules/minimumdominatingset_ilp.rs b/src/rules/minimumdominatingset_ilp.rs index 83ad5fb96..f1bda8fa6 100644 --- a/src/rules/minimumdominatingset_ilp.rs +++ b/src/rules/minimumdominatingset_ilp.rs @@ -46,13 +46,15 @@ impl ReductionResult for ReductionDSToILP { } } +// Each vertex contributes its own variable plus its neighbor incidences; +// merging repeated coefficients can only reduce the n+2m nonzero bound. #[reduction(transform = { exact { num_vars = "num_vertices", num_constraints = "num_vertices", }, upper_bound { - num_nonzeros = "num_vertices * num_vertices", + num_nonzeros = "2 * num_edges + num_vertices", }, })] impl ReduceTo> for MinimumDominatingSet { diff --git a/src/rules/minimumedgecostflow_ilp.rs b/src/rules/minimumedgecostflow_ilp.rs index 9588f942b..efd85e521 100644 --- a/src/rules/minimumedgecostflow_ilp.rs +++ b/src/rules/minimumedgecostflow_ilp.rs @@ -5,15 +5,12 @@ //! y_a (a = m..2m-1) — binary indicator: y_a = 1 iff f_a > 0 //! //! Constraints: -//! f_a ≤ c(a) — capacity (m constraints) //! f_a ≤ c(a) · y_a — linking: forces y_a = 1 when f_a > 0 (m constraints) //! y_a ≤ 1 — binary bound on indicators (m constraints) -//! conservation at non-terminal vertices (|V|-2 equality constraints) +//! conservation at non-isolated non-terminal vertices (at most |V|-2 rows) //! net flow into sink ≥ R (1 constraint) //! -//! Total: 3m + |V| - 1 constraints (but we omit redundant capacity since -//! linking already implies f_a ≤ c(a) when y_a ≤ 1). -//! Actually we keep all for clarity: 2m + |V| - 1 constraints. +//! Total: at most 2m + |V| - 1 constraints. Capacity is also a variable bound. //! //! Objective: minimize Σ p(a) · y_a. //! Extraction: first m variables are the flow values. @@ -53,13 +50,15 @@ impl ReductionResult for ReductionMECFToILP { } } +// Linking and indicator rows contribute at most 3m nonzeros. Conservation +// and the sink row together use each arc at most once per endpoint: at most 2m. #[reduction(transform = { exact { num_vars = "2 * num_edges", - num_constraints = "2 * num_edges + num_vertices - 1", }, upper_bound { - num_nonzeros = "(2 * num_edges) * (2 * num_edges + num_vertices - 1)", + num_constraints = "2 * num_edges + num_vertices - 1", + num_nonzeros = "5 * num_edges", }, })] impl ReduceTo> for MinimumEdgeCostFlow { diff --git a/src/rules/minimumfaultdetectiontestset_ilp.rs b/src/rules/minimumfaultdetectiontestset_ilp.rs index c98bc4051..c41c5919d 100644 --- a/src/rules/minimumfaultdetectiontestset_ilp.rs +++ b/src/rules/minimumfaultdetectiontestset_ilp.rs @@ -43,13 +43,17 @@ impl ReductionResult for ReductionMFDTSToILP { } } +// One variable per input/output list pair and one row per non-boundary vertex. +// Boundary membership is a union, so list lengths cannot be subtracted from n. +// Nonempty terminal lists guarantee at least one boundary vertex; each remaining +// vertex contributes at most one coefficient per pair, including repeated inputs. #[reduction(transform = { exact { num_vars = "num_inputs * num_outputs", - num_constraints = "num_vertices - num_inputs - num_outputs", }, upper_bound { - num_nonzeros = "(num_inputs * num_outputs) * (num_vertices - num_inputs - num_outputs)", + num_constraints = "num_vertices - 1", + num_nonzeros = "num_inputs * num_outputs * (num_vertices - 1)", }, })] impl ReduceTo> for MinimumFaultDetectionTestSet { diff --git a/src/rules/minimummaximalmatching_ilp.rs b/src/rules/minimummaximalmatching_ilp.rs index 93dcd0cf3..54d77ae65 100644 --- a/src/rules/minimummaximalmatching_ilp.rs +++ b/src/rules/minimummaximalmatching_ilp.rs @@ -48,13 +48,16 @@ impl ReductionResult for ReductionMMMToILP { } } +// At most n nonempty incidence rows and exactly m maximality rows. Incidence +// contributes at most 2m coefficients; each maximality row lists at most m +// distinct edge variables. Normalizing loop incidences can only lower the count. #[reduction(transform = { exact { num_vars = "num_edges", - num_constraints = "num_vertices + num_edges", }, upper_bound { - num_nonzeros = "num_edges * (num_vertices + num_edges)", + num_constraints = "num_vertices + num_edges", + num_nonzeros = "2 * num_edges + num_edges^2", }, })] impl ReduceTo> for MinimumMaximalMatching { diff --git a/src/rules/minimumvertexcover_minimummaximalmatching.rs b/src/rules/minimumvertexcover_minimummaximalmatching.rs index 4fccecbdd..5568dec44 100644 --- a/src/rules/minimumvertexcover_minimummaximalmatching.rs +++ b/src/rules/minimumvertexcover_minimummaximalmatching.rs @@ -21,7 +21,6 @@ inventory::submit! { source_variant_fn: as Problem>::variant, target_variant_fn: as Problem>::variant, parameter_declarations_fn: || ReductionParameterDeclarations { - fields: vec![ ("num_vertices", crate::parameters::ParameterRelation::Exact, crate::expr::Expr::variable("num_vertices")), ("num_edges", crate::parameters::ParameterRelation::Exact, crate::expr::Expr::variable("num_edges")), diff --git a/src/rules/paintshop_ilp.rs b/src/rules/paintshop_ilp.rs index 6e51ae1a3..cc01ab912 100644 --- a/src/rules/paintshop_ilp.rs +++ b/src/rules/paintshop_ilp.rs @@ -37,10 +37,16 @@ impl ReductionResult for ReductionPaintShopToILP { } } -#[reduction(transform = upper_bound { - num_vars = "num_cars + 2 * num_sequence", - num_constraints = "num_sequence + 2 * num_sequence", - num_nonzeros = "(num_cars + 2 * num_sequence) * (num_sequence + 2 * num_sequence)", +// Position rows have two coefficients; each consecutive pair adds two rows +// of three coefficients, for at most 8 * sequence_len coefficients overall. +#[reduction(transform = { + exact { + num_vars = "num_cars + 2 * num_sequence", + }, + upper_bound { + num_constraints = "3 * num_sequence", + num_nonzeros = "8 * num_sequence", + }, })] impl ReduceTo> for PaintShop { type Result = ReductionPaintShopToILP; diff --git a/src/rules/registry.rs b/src/rules/registry.rs index 4c46f27d5..e35276e7d 100644 --- a/src/rules/registry.rs +++ b/src/rules/registry.rs @@ -165,7 +165,7 @@ pub struct ReductionEntry { pub source_variant_fn: fn() -> Vec<(&'static str, &'static str)>, /// Function to derive target variant attributes from `Problem::variant()`. pub target_variant_fn: fn() -> Vec<(&'static str, &'static str)>, - /// The rule's single parameter relation, formulas, and unavailable target fields. + /// The rule's per-field parameter relations, formulas, and unavailable target fields. pub parameter_declarations_fn: fn() -> ReductionParameterDeclarations, /// Module path where the reduction is defined (from `module_path!()`). pub module_path: &'static str, diff --git a/src/rules/ruralpostman_ilp.rs b/src/rules/ruralpostman_ilp.rs index f056d2f7a..ef1fe5355 100644 --- a/src/rules/ruralpostman_ilp.rs +++ b/src/rules/ruralpostman_ilp.rs @@ -40,14 +40,14 @@ impl ReductionResult for ReductionRPToILP { } } -#[reduction(transform = { - exact { - num_vars = "num_edges + num_vertices + num_edges + num_vertices + 2 * num_edges", - num_constraints = "2 * num_edges + num_required_edges + num_vertices + 2 * num_edges + num_vertices + 2 * num_edges + num_vertices + num_edges + num_edges + num_vertices", - }, - upper_bound { - num_nonzeros = "(num_edges + num_vertices + num_edges + num_vertices + 2 * num_edges) * (2 * num_edges + num_required_edges + num_vertices + 2 * num_edges + num_vertices + 2 * num_edges + num_vertices + num_edges + num_edges + num_vertices)", - }, +// The nonempty-required-set branch allocates 4m+2n variables and 8m+4n+r rows; +// the empty branch allocates none. Coefficients by block are bounded by: +// linking 4m, required r, parity 2m+n, edge activation 4m, vertex activation +// 2m+n, flow capacity 4m, conservation 4m+2n, and upper bounds 2m+n. +#[reduction(transform = upper_bound { + num_vars = "4 * num_edges + 2 * num_vertices", + num_constraints = "8 * num_edges + 4 * num_vertices + num_required_edges", + num_nonzeros = "22 * num_edges + 5 * num_vertices + num_required_edges", })] impl ReduceTo> for RuralPostman { type Result = ReductionRPToILP; diff --git a/src/rules/spinglass_qubo.rs b/src/rules/spinglass_qubo.rs index 9fbaf246a..e7b0da2b5 100644 --- a/src/rules/spinglass_qubo.rs +++ b/src/rules/spinglass_qubo.rs @@ -120,12 +120,13 @@ where } } +// Each source interaction contributes to at most one quadratic coefficient. #[reduction(transform = { exact { num_vars = "num_spins", }, upper_bound { - num_quadratic_terms = "num_spins * (num_spins - 1) / 2", + num_quadratic_terms = "num_interactions", }, })] impl ReduceTo> for SpinGlass { @@ -172,7 +173,7 @@ impl ReduceTo> for SpinGlass { num_vars = "num_spins", }, upper_bound { - num_quadratic_terms = "num_spins * (num_spins - 1) / 2", + num_quadratic_terms = "num_interactions", }, })] impl ReduceTo> for SpinGlass { diff --git a/src/rules/subsetsum_integerknapsack.rs b/src/rules/subsetsum_integerknapsack.rs index 3d4adea58..26c93814b 100644 --- a/src/rules/subsetsum_integerknapsack.rs +++ b/src/rules/subsetsum_integerknapsack.rs @@ -32,7 +32,6 @@ inventory::submit! { source_variant_fn: ::variant, target_variant_fn: ::variant, parameter_declarations_fn: || ReductionParameterDeclarations { - fields: vec![("num_items", crate::parameters::ParameterRelation::Exact, Expr::variable("num_elements"))], unavailable: vec![crate::rules::registry::UnavailableParameterField { field: "capacity", diff --git a/src/rules/test_helpers.rs b/src/rules/test_helpers.rs index 620f17233..473454c90 100644 --- a/src/rules/test_helpers.rs +++ b/src/rules/test_helpers.rs @@ -4,6 +4,34 @@ use crate::traits::Problem; use crate::types::SolutionAggregate; use std::collections::HashSet; +pub(crate) fn assert_parameter_predictions(source: &R::Source, reduction: &R) { + use crate::parameters::ParameterRelation; + + let entry = crate::rules::registry::reduction_entries() + .into_iter() + .find(|entry| { + entry.source_name == R::Source::NAME + && entry.source_variant() == R::Source::variant() + && entry.target_name == R::Target::NAME + && entry.target_variant() == R::Target::variant() + }) + .expect("registered reduction"); + let contract = entry.parameter_contract().unwrap(); + let transform = contract.transform().unwrap(); + let predicted = transform.evaluate(&source.parameters()).unwrap(); + let actual = reduction.target_problem().parameters(); + for (field, _) in transform.expressions() { + let predicted = predicted.get(field).unwrap(); + let actual = actual.get(field).unwrap(); + match transform.relation(field).unwrap() { + ParameterRelation::Exact => assert_eq!(predicted, actual, "{field}"), + ParameterRelation::UpperBound => { + assert!(predicted >= actual, "{field}: {predicted} < {actual}") + } + } + } +} + fn verify_optimization_round_trip( source: &Source, target_solutions: Vec, diff --git a/src/rules/travelingsalesman_ilp.rs b/src/rules/travelingsalesman_ilp.rs index 7f219ae13..6be6afa76 100644 --- a/src/rules/travelingsalesman_ilp.rs +++ b/src/rules/travelingsalesman_ilp.rs @@ -65,13 +65,18 @@ impl ReductionResult for ReductionTSPToILP { } } +// Assignment contributes 2n rows and 2n² coefficients. Absent ordered pairs +// contribute at most n²(n-1) rows with two coefficients each. The 2nm products +// each contribute three rows and at most seven normalized coefficients. +// Edge occurrences cannot be subtracted from absent pairs: duplicates and +// loops increase m without removing distinct non-loop pairs. #[reduction(transform = { exact { num_vars = "num_vertices^2 + 2 * num_vertices * num_edges", - num_constraints = "num_vertices^3 + -1 * num_vertices^2 + 2 * num_vertices + 4 * num_vertices * num_edges", }, upper_bound { - num_nonzeros = "(num_vertices^2 + 2 * num_vertices * num_edges) * (num_vertices^3 + -1 * num_vertices^2 + 2 * num_vertices + 4 * num_vertices * num_edges)", + num_constraints = "num_vertices^2 * (num_vertices - 1) + 2 * num_vertices + 6 * num_vertices * num_edges", + num_nonzeros = "2 * num_vertices^3 + 14 * num_vertices * num_edges", }, })] impl ReduceTo> for TravelingSalesman { diff --git a/src/unit_tests/models/graph/path_constrained_network_flow.rs b/src/unit_tests/models/graph/path_constrained_network_flow.rs index 6df403538..556b90daa 100644 --- a/src/unit_tests/models/graph/path_constrained_network_flow.rs +++ b/src/unit_tests/models/graph/path_constrained_network_flow.rs @@ -180,6 +180,10 @@ fn test_path_constrained_network_flow_deserialization_rejects_invalid_instances( let restored: PathConstrainedNetworkFlow = serde_json::from_value(valid.clone()).unwrap(); assert_eq!(serde_json::to_value(&restored).unwrap(), valid); + let mut negative = valid.clone(); + negative["capacities"][0] = serde_json::json!(-1); + assert!(serde_json::from_value::(negative).is_err()); + let cases = [ ( "paths", diff --git a/src/unit_tests/models/graph/undirected_flow_lower_bounds.rs b/src/unit_tests/models/graph/undirected_flow_lower_bounds.rs index 143b83fd1..b230e7b65 100644 --- a/src/unit_tests/models/graph/undirected_flow_lower_bounds.rs +++ b/src/unit_tests/models/graph/undirected_flow_lower_bounds.rs @@ -3,6 +3,13 @@ use super::*; #[test] fn test_undirected_flow_lower_bounds_invalid_inputs() { let valid = serde_json::to_value(canonical_yes_instance()).unwrap(); + let mut negative = valid.clone(); + negative["capacities"][0] = serde_json::json!(-1); + negative["lower_bounds"][0] = serde_json::json!(-1); + let spec = + serde_json::from_value::(negative.clone()).unwrap(); + assert!(UndirectedFlowLowerBounds::try_from(spec).is_err()); + assert!(serde_json::from_value::(negative).is_err()); for (field, value) in [ ("capacities", serde_json::json!([])), ("lower_bounds", serde_json::json!([])), diff --git a/src/unit_tests/parameter_formula_validation.rs b/src/unit_tests/parameter_formula_validation.rs index 183c2111f..00ade3b27 100644 --- a/src/unit_tests/parameter_formula_validation.rs +++ b/src/unit_tests/parameter_formula_validation.rs @@ -73,37 +73,15 @@ fn target_parameters( reduced.target_problem_any(), )); } - let source_json = source.serialize_json(); - let target_json = if entry.turing { - json!({"inner": source_json, "bound": 2}) - } else if entry.source_name == "MinimumVertexCover" - && entry.target_name == "MinimumMaximalMatching" - { - json!({"graph": source_json["graph"]}) - } else if entry.source_name == "SubsetSum" && entry.target_name == "IntegerKnapsack" { - let sizes: Vec = source_json["sizes"] - .as_array() - .ok_or("SubsetSum sizes are not an array")? - .iter() - .map(|item| { - item.as_str() - .ok_or("size is not a string")? - .parse() - .map_err(|error| format!("{error}")) - }) - .collect::>()?; - let capacity: i64 = source_json["target"] - .as_str() - .ok_or("target is not a string")? - .parse() - .map_err(|error| format!("{error}"))?; - json!({"sizes": sizes, "values": sizes, "capacity": capacity}) - } else { - return Err("no executable reduction or test construction".into()); - }; - let target = crate::registry::load_dyn(entry.target_name, &variant, target_json) - .map_err(|error| error.to_string())?; - Ok(target.parameters_dyn()) + if let Some(reduce) = entry.reduce_aggregate_fn { + let reduced = reduce(source.as_any()).map_err(|error| error.to_string())?; + return Ok(ReductionGraph::compute_problem_parameters( + entry.target_name, + &variant, + reduced.target_problem_any(), + )); + } + Err("no executable reduction".into()) } fn source_for( @@ -134,26 +112,29 @@ fn source_for( .unwrap(), )); } + // Reuse existing examples with the same model name when a compatible variant + // has no dedicated example; its factory still enforces the concrete type. + for ((name, _), examples) in sources { + if name != entry.source_name { + continue; + } + for example in examples { + if let Ok(source) = (registered.factory)(example.clone()) { + if target_parameters(entry, source.as_ref()).is_ok() { + return Ok(source); + } + } + } + } + // Geometric variants without canonical instances use the existing seeded + // graph generators. Do not guess values for arbitrary new generator inputs: + // a new contract should supply a usable example or explicit generator data. let random = registered .random .ok_or_else(|| format!("no usable canonical source for {key:?}"))?; - let mut args = serde_json::Map::new(); - for input in (random.inputs)() { - let value = match input.name { - "num_vertices" => json!(5), - "seed" => json!(42), - "k" => json!(if variant.get("k").is_some_and(|v| v == "K3") { - 3 - } else { - 2 - }), - "bound" => json!(2), - _ if !input.required => continue, - name => return Err(format!("unsupported random input {name}")), - }; - args.insert(input.name.to_string(), value); - } - (random.generate)(Value::Object(args)).map_err(|error| error.to_string()) + (random.generate)(json!({"num_vertices": 5, "seed": 42})).map_err(|error| { + format!("no usable canonical source for {key:?}; graph generator: {error}") + }) } #[test] @@ -194,12 +175,22 @@ fn integer_ilp_reductions_support_binary_encoding() { } #[test] -fn every_parameter_formula_matches_a_constructed_target() { +fn every_executable_parameter_formula_matches_a_constructed_target() { let sources = canonical_sources(); let mut checked = 0; let mut expected = 0; let mut failures = Vec::new(); for entry in crate::rules::registry::reduction_entries() { + // Metadata-only and Turing edges have no single target constructor. + // Their declarations are checked by symbolic_parameter_contracts; do not + // manufacture a target here and claim it verifies a real executor. + if entry.reduce_fn.is_none() && entry.reduce_aggregate_fn.is_none() { + eprintln!( + "no executable size check: {} -> {}", + entry.source_name, entry.target_name + ); + continue; + } let contract = entry.parameter_contract().unwrap(); let Some(transform) = contract.transform() else { continue; diff --git a/src/unit_tests/rules/coloring_qubo.rs b/src/unit_tests/rules/coloring_qubo.rs index 84ffdd462..9d542b00a 100644 --- a/src/unit_tests/rules/coloring_qubo.rs +++ b/src/unit_tests/rules/coloring_qubo.rs @@ -93,6 +93,7 @@ fn test_kcoloring_to_qubo_all_small_graphs_and_configurations() { for k in 0..=3 { let source = KColoring::::with_k(SimpleGraph::new(n, edges.clone()), k); let reduction = ReduceTo::>>::reduce_to(&source).unwrap(); + crate::rules::test_helpers::assert_parameter_predictions(&source, &reduction); let target = AggregateReductionResult::target_problem(&reduction).inner(); assert_eq!(target.num_vars(), n * k); let mut minimum = i64::MAX; diff --git a/src/unit_tests/rules/minimumedgecostflow_ilp.rs b/src/unit_tests/rules/minimumedgecostflow_ilp.rs index 683d75059..a2f9a9415 100644 --- a/src/unit_tests/rules/minimumedgecostflow_ilp.rs +++ b/src/unit_tests/rules/minimumedgecostflow_ilp.rs @@ -49,6 +49,7 @@ fn test_minimumedgecostflow_to_ilp_structure() { let reduction: ReductionMECFToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); + crate::rules::test_helpers::assert_parameter_predictions(&problem, &reduction); // 6 arcs → 2*6 = 12 variables assert_eq!(ilp.num_vars(), 12); diff --git a/src/unit_tests/rules/minimumfaultdetectiontestset_ilp.rs b/src/unit_tests/rules/minimumfaultdetectiontestset_ilp.rs index f2400cdfc..aff71100c 100644 --- a/src/unit_tests/rules/minimumfaultdetectiontestset_ilp.rs +++ b/src/unit_tests/rules/minimumfaultdetectiontestset_ilp.rs @@ -30,6 +30,7 @@ fn test_reduction_creates_covering_ilp() { let reduction: ReductionMFDTSToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); + crate::rules::test_helpers::assert_parameter_predictions(&problem, &reduction); assert_eq!(ilp.num_vars(), 4); assert_eq!(ilp.constraints().len(), 3); diff --git a/src/unit_tests/rules/minimummaximalmatching_ilp.rs b/src/unit_tests/rules/minimummaximalmatching_ilp.rs index 342a422fd..cefb9548b 100644 --- a/src/unit_tests/rules/minimummaximalmatching_ilp.rs +++ b/src/unit_tests/rules/minimummaximalmatching_ilp.rs @@ -97,6 +97,7 @@ fn test_empty_graph() { let reduction: ReductionMMMToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); + crate::rules::test_helpers::assert_parameter_predictions(&problem, &reduction); assert_eq!(ilp.num_vars(), 0); assert_eq!(ilp.constraints().len(), 0); diff --git a/src/unit_tests/rules/ruralpostman_ilp.rs b/src/unit_tests/rules/ruralpostman_ilp.rs index ac981b878..567aec646 100644 --- a/src/unit_tests/rules/ruralpostman_ilp.rs +++ b/src/unit_tests/rules/ruralpostman_ilp.rs @@ -78,6 +78,7 @@ fn test_ruralpostman_empty_required_set_extracts_zero_multiplicities() { vec![], ); let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + crate::rules::test_helpers::assert_parameter_predictions(&source, &reduction); let target = ILPSolver::new().solve(reduction.target_problem()).unwrap(); let extracted = reduction.extract_solution(&target).unwrap(); diff --git a/src/unit_tests/rules/travelingsalesman_ilp.rs b/src/unit_tests/rules/travelingsalesman_ilp.rs index d7c630715..04916fa25 100644 --- a/src/unit_tests/rules/travelingsalesman_ilp.rs +++ b/src/unit_tests/rules/travelingsalesman_ilp.rs @@ -21,6 +21,7 @@ fn test_reduction_creates_valid_ilp_c4() { let reduction: ReductionTSPToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); + crate::rules::test_helpers::assert_parameter_predictions(&problem, &reduction); // n=4, m=4: num_vars = 16 + 2*4*4 = 48 assert_eq!(ilp.num_vars(), 48); diff --git a/src/unit_tests/symbolic_parameter_contracts.rs b/src/unit_tests/symbolic_parameter_contracts.rs index c0411e1e0..535deeb1d 100644 --- a/src/unit_tests/symbolic_parameter_contracts.rs +++ b/src/unit_tests/symbolic_parameter_contracts.rs @@ -168,31 +168,17 @@ where .expect("direct reduction is registered"); let contract = entry.parameter_contract().unwrap(); let transform = contract.transform().expect("symbolic transform exists"); - for (field, _) in transform.expressions() { - assert_eq!( - transform.relation(field), - Some(relation), - "{} -> {}: {field}", - S::NAME, - T::NAME - ); - } let predicted = transform.evaluate(&source.parameters()).unwrap(); - if relation == ParameterRelation::Exact { - for (field, _) in transform.expressions() { - if fields.contains(&field) { - continue; - } - assert_eq!( - predicted.get(field), - actual.get(field), - "{} -> {}: {field}", - S::NAME, - T::NAME - ); + for (field, _) in transform.expressions() { + let predicted = predicted.get(field).unwrap(); + let actual = actual.get(field).unwrap(); + match transform.relation(field).unwrap() { + ParameterRelation::Exact => assert_eq!(predicted, actual, "{field}"), + ParameterRelation::UpperBound => assert!(predicted >= actual, "{field}"), } } for &field in fields { + assert_eq!(transform.relation(field), Some(relation), "{field}"); assert_eq!( predicted.get(field), actual.get(field), @@ -409,13 +395,13 @@ fn exact_parameter_formulas_cover_sparse_and_boundary_instances() { ); check_reduced_parameters::<_, HamiltonianPath>( HamiltonianCircuit::new(SimpleGraph::new(0, vec![])), - &["num_consecutive_positions"], - ParameterRelation::UpperBound, + &["num_vertices", "num_consecutive_positions"], + exact, ); check_reduced_parameters::<_, HamiltonianPath>( HamiltonianCircuit::new(SimpleGraph::new(3, vec![(0, 1), (1, 2), (2, 0)])), - &["num_consecutive_positions"], - ParameterRelation::UpperBound, + &["num_vertices", "num_consecutive_positions"], + exact, ); check_reduced_parameters::<_, LongestCommonSubsequence>( MinimumVertexCover::new(SimpleGraph::new(0, vec![]), vec![]), From 8bdddd45ba25105bcda780cd343abea9f660e87e Mon Sep 17 00:00:00 2001 From: Xiwei Pan <90967972+isPANN@users.noreply.github.com> Date: Tue, 29 Sep 2026 05:18:32 -0700 Subject: [PATCH 8/9] Consolidate bounded ILP and parameter prediction stack Squash the combined changes from PRs #1180, #1182, #1183, #1184, and #1185 into #1174. Preserve the complete stack-tip tree so subsequent corrections can be maintained on one branch. --- .claude/CLAUDE.md | 2 +- docs/paper/reductions.typ | 226 +++-- docs/paper/references.bib | 11 + docs/src/design.md | 14 +- ...hained_reduction_factoring_to_spinglass.rs | 6 +- problemreductions-cli/src/create_args.rs | 29 +- problemreductions-cli/src/dispatch.rs | 2 +- problemreductions-cli/src/problem_name.rs | 37 +- problemreductions-cli/src/test_support.rs | 4 + problemreductions-cli/tests/cli_tests.rs | 72 +- src/example_db/specs.rs | 44 +- src/lib.rs | 4 + .../algebraic/closest_vector_problem.rs | 8 + src/models/algebraic/ilp.rs | 143 +++- src/models/algebraic/mod.rs | 4 +- src/models/graph/acyclic_partition.rs | 17 +- .../graph/biconnectivity_augmentation.rs | 11 + .../bounded_component_spanning_forest.rs | 11 + src/models/graph/highly_connected_deletion.rs | 65 -- src/models/graph/integral_flow_bundles.rs | 6 + .../graph/integral_flow_with_multipliers.rs | 7 +- src/models/graph/longest_circuit.rs | 13 +- src/models/graph/min_max_multicenter.rs | 34 +- .../minimum_capacitated_spanning_tree.rs | 16 +- src/models/graph/multiple_choice_branching.rs | 11 + .../graph/shortest_weight_constrained_path.rs | 16 +- .../graph/strong_connectivity_augmentation.rs | 11 + .../graph/undirected_flow_lower_bounds.rs | 15 +- .../undirected_two_commodity_integral_flow.rs | 6 + src/models/misc/bin_packing.rs | 16 +- src/models/misc/capacity_assignment.rs | 17 +- src/models/misc/flow_shop_scheduling.rs | 17 +- src/models/misc/knapsack.rs | 17 +- .../misc/minimum_tardiness_sequencing.rs | 7 + src/models/misc/multiprocessor_scheduling.rs | 16 +- src/models/misc/open_shop_scheduling.rs | 6 + src/models/misc/partially_ordered_knapsack.rs | 11 + src/models/misc/partition.rs | 10 +- .../misc/resource_constrained_scheduling.rs | 20 +- ...ng_to_minimize_weighted_completion_time.rs | 11 +- ...ing_to_minimize_maximum_cumulative_cost.rs | 6 + ...equencing_to_minimize_tardy_task_weight.rs | 10 +- ...ng_to_minimize_weighted_completion_time.rs | 6 + ...quencing_to_minimize_weighted_tardiness.rs | 17 +- ...uencing_with_deadlines_and_set_up_times.rs | 13 +- src/models/misc/subset_sum.rs | 13 +- src/models/misc/three_partition.rs | 16 +- src/models/set/exact_cover_by_3_sets.rs | 1 - src/models/set/integer_knapsack.rs | 11 +- src/rules/acyclicpartition_ilp.rs | 17 +- .../balancedcompletebipartitesubgraph_ilp.rs | 1 + src/rules/biconnectivityaugmentation_ilp.rs | 29 +- src/rules/binpacking_ilp.rs | 2 + src/rules/bmf_bicliquecover.rs | 12 +- src/rules/bmf_ilp.rs | 1 + src/rules/bottlenecktravelingsalesman_ilp.rs | 17 +- .../boundedcomponentspanningforest_ilp.rs | 17 +- src/rules/capacityassignment_ilp.rs | 1 + src/rules/circuit_ilp.rs | 1 + src/rules/circuit_sat.rs | 12 +- src/rules/closeststring_ilp.rs | 27 +- src/rules/closestsubstring_ilp.rs | 17 +- src/rules/closestvectorproblem_qubo.rs | 8 +- src/rules/clustering_ilp.rs | 1 + src/rules/coloring_ilp.rs | 11 +- src/rules/consecutiveblockminimization_ilp.rs | 21 +- .../consecutiveonesmatrixaugmentation_ilp.rs | 8 +- src/rules/consecutiveonessubmatrix_ilp.rs | 1 + ...onsistencyofdatabasefrequencytables_ilp.rs | 11 +- ...ximumindependentset_integralflowbundles.rs | 1 + ...nminimumdominatingset_minmaxmulticenter.rs | 1 + ...onminimumvertexcover_hamiltoniancircuit.rs | 10 +- src/rules/directedhamiltonianpath_ilp.rs | 1 + .../directedtwocommodityintegralflow_ilp.rs | 45 +- src/rules/disjointconnectingpaths_ilp.rs | 1 + src/rules/ensemblecomputation_ilp.rs | 13 +- src/rules/eulerianpath_ilp.rs | 15 +- ...tcoverby3sets_algebraicequationsovergf2.rs | 4 +- src/rules/exactcoverby3sets_ilp.rs | 11 +- .../exactcoverby3sets_maximumsetpacking.rs | 12 +- src/rules/exactcoverby3sets_subsetproduct.rs | 2 +- src/rules/expectedretrievalcost_ilp.rs | 11 +- src/rules/factoring_circuit.rs | 16 +- src/rules/factoring_ilp.rs | 31 +- src/rules/feasibleregisterassignment_ilp.rs | 23 +- src/rules/flowshopscheduling_ilp.rs | 36 +- src/rules/graphpartitioning_ilp.rs | 1 + ...oniancircuit_biconnectivityaugmentation.rs | 1 + .../hamiltoniancircuit_longestcircuit.rs | 1 + ...ncircuit_strongconnectivityaugmentation.rs | 1 + src/rules/hamiltonianpath_ilp.rs | 1 + src/rules/highlyconnecteddeletion_ilp.rs | 267 +++--- src/rules/ilp_bool_ilp_i64.rs | 16 +- src/rules/ilp_bounded_ilp.rs | 59 ++ src/rules/ilp_casts.rs | 2 + src/rules/ilp_helpers.rs | 12 + src/rules/ilp_i64_ilp_bool.rs | 38 +- src/rules/ilp_qubo.rs | 10 +- src/rules/integerknapsack_ilp.rs | 29 +- src/rules/integralflowbundles_ilp.rs | 22 +- src/rules/integralflowhomologousarcs_ilp.rs | 33 +- src/rules/integralflowwithmultipliers_ilp.rs | 26 +- src/rules/isomorphicspanningtree_ilp.rs | 1 + ...lique_balancedcompletebipartitesubgraph.rs | 16 +- src/rules/kclique_ilp.rs | 1 + src/rules/knapsack_ilp.rs | 14 +- src/rules/knapsack_qubo.rs | 10 +- src/rules/ksatisfiability_acyclicpartition.rs | 1 + ...bility_directedtwocommodityintegralflow.rs | 14 +- ...tisfiability_feasibleregisterassignment.rs | 11 +- .../ksatisfiability_preemptivescheduling.rs | 16 +- ...atisfiability_simultaneousincongruences.rs | 4 +- src/rules/ksatisfiability_subsetsum.rs | 9 +- src/rules/lengthboundeddisjointpaths_ilp.rs | 7 +- src/rules/longestcircuit_ilp.rs | 22 +- src/rules/longestcommonsubsequence_ilp.rs | 1 + src/rules/longestpath_ilp.rs | 15 +- src/rules/maximalis_ilp.rs | 1 + src/rules/maximum2satisfiability_ilp.rs | 1 + src/rules/maximumclique_ilp.rs | 1 + src/rules/maximumcokplex_ilp.rs | 13 +- src/rules/maximumcommonedgesubgraph_ilp.rs | 1 + src/rules/maximumcontactmapoverlap_ilp.rs | 11 +- src/rules/maximumdomaticnumber_ilp.rs | 12 +- src/rules/maximumedgeweightedkclique_ilp.rs | 2 + src/rules/maximumleafspanningtree_ilp.rs | 13 +- src/rules/maximumlikelihoodranking_ilp.rs | 1 + src/rules/maximummatching_ilp.rs | 1 + src/rules/maximumsetpacking_ilp.rs | 5 + .../minimumcapacitatedspanningtree_ilp.rs | 17 +- src/rules/minimumcoveringbycliques_ilp.rs | 1 + src/rules/minimumcutintoboundedsets_ilp.rs | 4 +- src/rules/minimumdominatingset_ilp.rs | 1 + src/rules/minimumedgecostflow_ilp.rs | 25 +- ...minimumexternalmacrodatacompression_ilp.rs | 1 + src/rules/minimumfaultdetectiontestset_ilp.rs | 1 + src/rules/minimumfeedbackarcset_ilp.rs | 15 +- src/rules/minimumfeedbackvertexset_ilp.rs | 24 +- src/rules/minimumgraphbandwidth_ilp.rs | 17 +- src/rules/minimumhittingset_ilp.rs | 1 + ...minimuminternalmacrodatacompression_ilp.rs | 1 + src/rules/minimummatrixcover_ilp.rs | 1 + src/rules/minimummaximalmatching_ilp.rs | 1 + src/rules/minimummetricdimension_ilp.rs | 1 + src/rules/minimummultiwaycut_ilp.rs | 1 + src/rules/minimumsetcovering_ilp.rs | 1 + src/rules/minimumsummulticenter_ilp.rs | 1 + src/rules/minimumtardinesssequencing_ilp.rs | 25 +- ...mumvertexcover_longestcommonsubsequence.rs | 18 +- src/rules/minimumweightdecoding_ilp.rs | 17 +- src/rules/minmaxmulticenter_ilp.rs | 36 +- src/rules/mixedchinesepostman_ilp.rs | 29 +- src/rules/mod.rs | 1 + src/rules/monochromatictriangle_ilp.rs | 1 + src/rules/multiplechoicebranching_ilp.rs | 15 +- src/rules/multiplecopyfileallocation_ilp.rs | 1 + src/rules/multiprocessorscheduling_ilp.rs | 1 + src/rules/naesatisfiability_ilp.rs | 1 + .../numericalmatchingwithtargetsums_ilp.rs | 1 + src/rules/openshopscheduling_ilp.rs | 42 +- src/rules/optimallineararrangement_ilp.rs | 29 +- ...uencingtominimizeweightedcompletiontime.rs | 1 + .../optimumcommunicationspanningtree_ilp.rs | 1 + src/rules/paintshop_ilp.rs | 1 + src/rules/partiallyorderedknapsack_ilp.rs | 1 + src/rules/partition_binpacking.rs | 11 +- .../partition_integralflowwithmultipliers.rs | 4 +- src/rules/partition_knapsack.rs | 7 +- .../partition_multiprocessorscheduling.rs | 11 +- src/rules/partition_openshopscheduling.rs | 13 +- ...ion_sequencingtominimizetardytaskweight.rs | 1 + src/rules/partition_subsetsum.rs | 2 + src/rules/partitionintocliques_ilp.rs | 1 + ...flength2_boundedcomponentspanningforest.rs | 11 +- src/rules/partitionintopathsoflength2_ilp.rs | 1 + src/rules/partitionintotriangles_ilp.rs | 1 + src/rules/pathconstrainednetworkflow_ilp.rs | 33 +- .../precedenceconstrainedscheduling_ilp.rs | 8 +- src/rules/preemptivescheduling_ilp.rs | 21 +- src/rules/quadraticassignment_ilp.rs | 11 +- src/rules/qubo_ilp.rs | 1 + .../rectilinearpicturecompression_ilp.rs | 19 +- src/rules/registersufficiency_ilp.rs | 23 +- .../resourceconstrainedscheduling_ilp.rs | 28 +- src/rules/rootedtreestorageassignment_ilp.rs | 35 +- src/rules/ruralpostman_ilp.rs | 13 +- src/rules/sat_circuitsat.rs | 16 +- src/rules/sat_ksat.rs | 14 +- ...tisfiability_integralflowhomologousarcs.rs | 14 +- src/rules/satisfiability_naesatisfiability.rs | 14 +- ...ingtominimizeweightedcompletiontime_ilp.rs | 19 +- .../schedulingwithindividualdeadlines_ilp.rs | 7 +- ...cingtominimizemaximumcumulativecost_ilp.rs | 23 +- ...sequencingtominimizetardytaskweight_ilp.rs | 1 + ...ingtominimizeweightedcompletiontime_ilp.rs | 17 +- ...quencingtominimizeweightedtardiness_ilp.rs | 31 +- ...equencingwithdeadlinesandsetuptimes_ilp.rs | 13 +- src/rules/sequencingwithinintervals_ilp.rs | 1 + ...uencingwithreleasetimesanddeadlines_ilp.rs | 1 + src/rules/setsplitting_betweenness.rs | 10 +- src/rules/setsplitting_ilp.rs | 6 +- src/rules/shortestcommonsupersequence_ilp.rs | 1 + .../shortestweightconstrainedpath_ilp.rs | 17 +- src/rules/sparsematrixcompression_ilp.rs | 1 + src/rules/stackercrane_ilp.rs | 1 + src/rules/steinertree_ilp.rs | 1 + src/rules/stringtostringcorrection_ilp.rs | 1 + .../strongconnectivityaugmentation_ilp.rs | 27 +- src/rules/subgraphisomorphism_ilp.rs | 1 + src/rules/subsetsum_closestvectorproblem.rs | 20 +- src/rules/subsetsum_integerknapsack.rs | 7 +- src/rules/subsetsum_partition.rs | 2 + src/rules/sumofsquarespartition_ilp.rs | 9 +- src/rules/threedimensionalmatching_ilp.rs | 1 + ...threedimensionalmatching_threepartition.rs | 1 + ...partition_resourceconstrainedscheduling.rs | 1 + src/rules/timetabledesign_ilp.rs | 20 +- src/rules/travelingsalesman_ilp.rs | 1 + src/rules/undirectedflowlowerbounds_ilp.rs | 38 +- .../undirectedtwocommodityintegralflow_ilp.rs | 71 +- src/solvers/ilp/adapter.rs | 16 +- src/solvers/ilp/solver.rs | 5 +- src/solvers/pipelines.rs | 346 ++++---- src/solvers/registry.rs | 13 +- src/types.rs | 35 + src/unit_tests/example_db.rs | 2 + src/unit_tests/ilp_overhead.rs | 796 ++++++++++++++++++ src/unit_tests/models/algebraic/ilp.rs | 141 +++- .../models/graph/acyclic_partition.rs | 5 +- .../graph/integral_flow_with_multipliers.rs | 24 +- .../graph/undirected_flow_lower_bounds.rs | 2 + src/unit_tests/models/misc/bin_packing.rs | 34 + src/unit_tests/models/misc/partition.rs | 17 + src/unit_tests/models/misc/subset_sum.rs | 19 + .../parameter_formula_validation.rs | 10 +- src/unit_tests/problem_parameters.rs | 1 + src/unit_tests/reduction_graph.rs | 510 +++++++++++ src/unit_tests/rules/acyclicpartition_ilp.rs | 16 +- src/unit_tests/rules/aggregate_contracts.rs | 3 +- .../rules/biconnectivityaugmentation_ilp.rs | 16 +- .../rules/bottlenecktravelingsalesman_ilp.rs | 18 +- .../boundedcomponentspanningforest_ilp.rs | 11 +- src/unit_tests/rules/closeststring_ilp.rs | 23 +- src/unit_tests/rules/closestsubstring_ilp.rs | 23 +- .../rules/consecutiveblockminimization_ilp.rs | 26 + .../consecutiveonesmatrixaugmentation_ilp.rs | 19 + .../directedtwocommodityintegralflow_ilp.rs | 47 +- .../rules/ensemblecomputation_ilp.rs | 10 +- src/unit_tests/rules/eulerianpath_ilp.rs | 17 +- src/unit_tests/rules/factoring_ilp.rs | 39 +- .../rules/feasibleregisterassignment_ilp.rs | 13 +- .../rules/flowshopscheduling_ilp.rs | 17 +- .../rules/highlyconnecteddeletion_ilp.rs | 248 +++--- src/unit_tests/rules/ilp_bool_ilp_i64.rs | 11 +- src/unit_tests/rules/ilp_bounded_ilp.rs | 40 + src/unit_tests/rules/ilp_i64_ilp_bool.rs | 8 +- src/unit_tests/rules/ilp_qubo.rs | 14 + src/unit_tests/rules/integerknapsack_ilp.rs | 37 +- .../rules/integralflowbundles_ilp.rs | 14 +- .../rules/integralflowhomologousarcs_ilp.rs | 34 +- .../rules/integralflowwithmultipliers_ilp.rs | 49 +- src/unit_tests/rules/knapsack_ilp.rs | 23 + ...bility_directedtwocommodityintegralflow.rs | 4 +- ...tisfiability_feasibleregisterassignment.rs | 5 +- .../rules/lengthboundeddisjointpaths_ilp.rs | 16 + src/unit_tests/rules/longestpath_ilp.rs | 12 +- src/unit_tests/rules/maximumcokplex_ilp.rs | 16 + .../rules/maximumdomaticnumber_ilp.rs | 15 + .../rules/maximumleafspanningtree_ilp.rs | 20 +- src/unit_tests/rules/maximumsetpacking_ilp.rs | 15 + .../minimumcapacitatedspanningtree_ilp.rs | 18 +- .../rules/minimumcutintoboundedsets_ilp.rs | 26 + .../rules/minimumedgecostflow_ilp.rs | 44 +- .../rules/minimumfeedbackarcset_ilp.rs | 11 +- .../rules/minimumfeedbackvertexset_ilp.rs | 19 +- .../rules/minimumgraphbandwidth_ilp.rs | 11 +- .../rules/minimumweightdecoding_ilp.rs | 14 +- src/unit_tests/rules/minmaxmulticenter_ilp.rs | 12 +- .../rules/mixedchinesepostman_ilp.rs | 12 +- .../rules/multiplechoicebranching_ilp.rs | 9 +- .../rules/openshopscheduling_ilp.rs | 44 +- .../rules/optimallineararrangement_ilp.rs | 11 +- .../rules/partition_openshopscheduling.rs | 4 +- .../rules/pathconstrainednetworkflow_ilp.rs | 34 +- .../precedenceconstrainedscheduling_ilp.rs | 20 + .../rules/preemptivescheduling_ilp.rs | 13 +- .../rectilinearpicturecompression_ilp.rs | 27 + src/unit_tests/rules/reduction_path_parity.rs | 4 +- .../rules/registersufficiency_ilp.rs | 16 +- .../rules/rootedtreestorageassignment_ilp.rs | 37 +- src/unit_tests/rules/ruralpostman_ilp.rs | 12 +- ...ingtominimizeweightedcompletiontime_ilp.rs | 14 +- .../schedulingwithindividualdeadlines_ilp.rs | 24 + ...cingtominimizemaximumcumulativecost_ilp.rs | 10 +- ...ingtominimizeweightedcompletiontime_ilp.rs | 24 +- ...quencingtominimizeweightedtardiness_ilp.rs | 15 +- src/unit_tests/rules/setsplitting_ilp.rs | 29 + .../shortestweightconstrainedpath_ilp.rs | 10 +- .../strongconnectivityaugmentation_ilp.rs | 12 +- .../rules/subsetsum_integerknapsack.rs | 46 + src/unit_tests/rules/timetabledesign_ilp.rs | 31 + .../rules/undirectedflowlowerbounds_ilp.rs | 14 +- .../undirectedtwocommodityintegralflow_ilp.rs | 20 +- src/unit_tests/solvers/registry.rs | 35 +- src/unit_tests/solvers/resolver.rs | 10 +- .../symbolic_parameter_contracts.rs | 558 +++++++++++- .../suites/register_assignment_reductions.rs | 8 +- 307 files changed, 5573 insertions(+), 1795 deletions(-) create mode 100644 src/rules/ilp_bounded_ilp.rs create mode 100644 src/unit_tests/ilp_overhead.rs create mode 100644 src/unit_tests/rules/ilp_bounded_ilp.rs diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index ad8cb05d0..d44c1be19 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -164,7 +164,7 @@ impl ReduceTo for Source { ... } - Every target parameter must appear exactly once as a formula or as unavailable with a non-empty reason. - `ParameterTransform` evaluates and composes formulas with exact rational and arbitrary-precision integer arithmetic. Composition preserves independent fields and their accuracy. An unavailable dependency or unsafe upper-bound substitution makes only the affected field unavailable; it never performs budget pruning or path ranking. - Concrete instance parameters come from each endpoint instance's `Problem::parameters()` implementation; `ReductionEntry` stores only the symbolic parameter relation. -- Rules producing `ILP` must declare known finite variable domains with `ILP::with_variables`; constraint rows alone do not supply bounds to binary encoding. Bounds on auxiliary variables must preserve feasibility and the optimum. Document genuinely unbounded variables rather than inventing a cutoff. +- Rules producing ILP must target the most specific applicable registered variant: `ILP` for binary variables, `ILP` for explicit finite integer domains, and general `ILP` otherwise. Supply finite domains with `ILP::with_variables`; constraint rows alone do not supply bounds to binary encoding. Bounds on auxiliary variables must preserve feasibility and the optimum. The independent `bounds` dimension defaults to `general`; register additional concrete variants only when a reduction needs them. - `VariantEntry` has both a complexity string and compiled `complexity_eval_fn` — same pattern - Expressions support: constants, variables, `+`, `-`, `*`, `/`, `^`, `exp()`, `log()`, `sqrt()`, `factorial()` - Complexity strings must use **concrete numeric values only** (e.g., `"2^(2.372 * num_vertices / 3)"`, not `"2^(omega * num_vertices / 3)"`) diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index 5c357942a..c06aca6f3 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -2437,7 +2437,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| let witness = (2, 1, 1, 1, 1, 2, 1) [ #problem-def("UndirectedFlowLowerBounds")[ - Given an undirected graph $G = (V, E)$, specified vertices $s, t in V$, lower bounds $l: E -> ZZ_(>= 0)$, upper capacities $c: E -> ZZ^+$ with $l(e) <= c(e)$ for every edge, and a requirement $R in ZZ^+$, determine whether there exists a flow function $f: {(u, v), (v, u): {u, v} in E} -> ZZ_(>= 0)$ such that each edge carries flow in at most one direction, every edge value lies between its lower and upper bound, flow is conserved at every vertex in $V backslash {s, t}$, and the net flow into $t$ is at least $R$. + Given an undirected graph $G = (V, E)$, specified vertices $s, t in V$, lower bounds $l: E -> ZZ_(>= 0)$, upper capacities $c: E -> ZZ_(>= 0)$ with $l(e) <= c(e)$ for every edge, and a requirement $R in ZZ^+$, determine whether there exists a flow function $f: {(u, v), (v, u): {u, v} in E} -> ZZ_(>= 0)$ such that each edge carries flow in at most one direction, every edge value lies between its lower and upper bound, flow is conserved at every vertex in $V backslash {s, t}$, and the net flow into $t$ is at least $R$. ][ Undirected Flow with Lower Bounds appears as ND37 in Garey and Johnson's catalog @garey1979. Itai proved that even this single-commodity undirected feasibility problem is NP-complete, contrasting sharply with the directed lower-bounded case, which reduces to ordinary max-flow machinery @itai1978. @@ -5008,7 +5008,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| } #{ - let x = load-model-example("ILP") + let x = load-model-example("ILP", variant: (bounds: "general", coefficient: "i64", variable: "i64")) let nv = x.instance.variables.len() let obj = x.instance.objective let constraints = x.instance.constraints @@ -11496,6 +11496,8 @@ the displayed rule, extracted from the corresponding `pred path` entry. )[ This $O(n^2 m)$ reduction constructs an ILP with binary assignment variables $x_(t,p)$, integer completion-time variables $C_t$, and binary ordering variables $y_(i,j)$ for task pairs. Big-M disjunctive constraints enforce non-overlapping execution on shared processors. ][ + _Numeric magnitude._ Let $h$ be `max_processing_time_bits` and $n$ the task count. With $L$ the total processing time, the disjunction rows have magnitudes at most $3L$, and variable endpoints at most $L$. Target `max_constraint_magnitude_bits` is at most $h+n+2$. Objective weights need no parameter. + _Construction._ Let $n = |T|$ and $m$ be the number of processors. Create $n m$ binary assignment variables $x_(t,p) in {0, 1}$ (task $t$ on processor $p$), $n$ integer completion-time variables $C_t$, and $n(n-1)/2$ binary ordering variables $y_(i,j)$ for $i < j$. The constraints are: (1) Assignment: $sum_p x_(t,p) = 1$ for each $t$. (2) Completion bounds: $C_t >= ell(t)$ for each $t$. @@ -11583,6 +11585,8 @@ the displayed rule, extracted from the corresponding `pred path` entry. )[ The radius-threshold relation between centers and dominating sets @hochbaumshmoys1985 is extended here to all signed source bounds using two mandatory isolated centers. This $O(n+m+1)$ construction preserves the existing endpoint variants. ][ + _Numeric magnitude._ Unit vertex weights and edge lengths give target `max_numeric_magnitude_bits` exactly $1$. + _Construction._ For source graph $G=(V,E)$ with $n$ vertices and integer bound $K$, set $q=max(-1,min(K,n))$. Add isolated vertices $a=n$ and $b=n+1$, leaving every original edge record unchanged. Give all vertices and edges unit weights and lengths, and require exactly $k=q+2$ centers. Then $1<=k<=n+2$ for every input, including an empty graph. _Correctness._ Every finite target placement must select both isolated vertices. If a source dominating set $D$ has $|D|<=K$, then $q>=0$ and $|D|<=q<=n$. Extend $D$ to $q$ original vertices and add $a,b$. This placement has $k$ centers and radius at most $1$, proving the forward direction. Conversely, a target placement of radius at most $1$ selects both isolates and exactly $q$ original vertices. Each original vertex is within one original edge of a selected vertex, so those $q<=K$ vertices dominate $G$. For $K<0$, $k=1$ cannot cover both isolates and the target has no finite placement. For $n=0,K>=0$, the two isolates form a radius-zero placement. Loops and repeated edges preserve this reasoning. @@ -12046,7 +12050,9 @@ the displayed rule, extracted from the corresponding `pred path` entry. Encode $x_i+M_i in [0,2M_i]$ with powers of two and one capped final weight. If $W$ maps the resulting bits to coefficient offsets, $G=B^top B$, $h=B^top bold(t)$, and $bold(ell)=-bold(M)$, then $ norm(B bold(x)-bold(t))_2^2 = bold(z)^top(W^top G W)bold(z) + 2 bold(z)^top W^top(G bold(ell)-h) + "const". $ - The constant is dropped. The exact bit count depends on concrete entries, so its symbolic transform is unavailable. + The constant is dropped. + + _Size bound._ Let $h >= 1$ be the maximum bit length of the absolute entries of $B$ and $bold(t)$. Each cofactor has magnitude at most $(n-1)! 2^(h(n-1))$, and $C_j < (m+1)2^h$. Thus $M_i < n! (m+1)2^(h n)$. Using $log_2(n!) <= n^2$ and $log_2(m+1) <= m$ for $m >= 1$, each coefficient needs at most $n^2+m+n h+3$ bits. The registered bounds are therefore $V=n(n^2+m+n h+3)$ QUBO variables and $V^2$ quadratic terms. A rank-zero basis gives zero variables. The magnitude parameter describes only the source entries, independently of this encoding. _Correctness._ ($arrow.r.double$) Every bit vector decodes inside the derived box and has QUBO value equal to its CVP squared distance minus one common constant, so a QUBO minimizer is best within the box. ($arrow.l.double$) The derived box contains a global CVP minimizer, and every point in the box has an exact-range encoding. Therefore the best encoded point is globally optimal for CVP. @@ -12295,7 +12301,7 @@ where $P$ is a penalty weight large enough that any constraint violation costs m _Solution extraction._ Validate the target configuration once and require squared distance exactly $n$ through the formal aggregate certificate. Return the first $n$ coefficients as Boolean selections, accepting one as true; the remaining coefficients are the specified carries. A larger optimal squared distance proves NO and provides no source witness. - _Representation._ The target has $2n+b$ coordinates and $n+b-1$ basis columns. Since bit length is not a registered Subset Sum parameter, the symbolic relations are marked unavailable with that reason. Dimensions and the total dense basis byte count are checked before allocation. The threshold and squared-distance evaluation use checked `i64` arithmetic; overflow is an error, not a NO certificate. The solver uses exact rational sphere-enumeration bounds; runtime limitations are separate from the mathematical equivalence. + _Representation._ The target has $2n+b$ coordinates and $n+b-1$ basis columns. The existing source parameter `max_numeric_magnitude_bits` is exactly $b$, so both dimensions are predicted exactly. Target basis entries have magnitude at most two and target coordinates are Boolean, giving target `max_numeric_magnitude_bits` at most two; this supplies the numerical parameter needed by the subsequent CVP-to-QUBO rule. Dimensions and the total dense basis byte count are checked before allocation. The threshold and squared-distance evaluation use checked `i64` arithmetic; overflow is an error, not a NO certificate. The solver uses exact rational sphere-enumeration bounds; runtime limitations are separate from the mathematical equivalence. ] ] } @@ -12309,7 +12315,7 @@ where $P$ is a penalty weight large enough that any constraint violation costs m Write the extended rows as $A' z=b$, where the first $n$ bits of $z$ are $x$ and the remaining bits are row-specific slack. Set $P=1+sum_i |d_i|+sum_k |b_k|$, $C=P sum_k b_k^2$, $L=sum_i min(d_i,0)$, and $U=sum_i max(d_i,0)$. The upper-triangular QUBO matrix has diagonal $Q_(i i)=d_i+P sum_k ((a'_(k i))^2-2 b_k a'_(k i))$ and off-diagonal $Q_(i j)=2P sum_k a'_(k i)a'_(k j)$ for $i sum_i v_i$ to the objective. Since all values are nonnegative, every feasible assignment has objective in the range $[-sum_i v_i, 0]$, so that penalty exceeds the entire feasible value range. Among feasible assignments (penalty zero), $f$ reduces to $-sum v_i x_i$, minimized at the knapsack optimum. _Solution extraction._ Discard slack variables: return $bold(z)[0..n]$. @@ -12629,13 +12639,13 @@ where $P$ is a penalty weight large enough that any constraint violation costs m "QUBO", "ILP", source-variant: (weight: "f64"), - target-variant: (coefficient: "f64", variable: "bool"), + target-variant: (bounds: "general", coefficient: "f64", variable: "bool"), ) #let qubo_ilp_sol = qubo_ilp.solutions.at(0) #reduction-rule("QUBO", "ILP", example: true, example-source-variant: (weight: "f64"), - example-target-variant: (coefficient: "f64", variable: "bool"), + example-target-variant: (bounds: "general", coefficient: "f64", variable: "bool"), example-caption: [4-variable QUBO with 3 quadratic terms], extra: [ #pred-commands( @@ -13000,6 +13010,8 @@ where $P$ is a penalty weight large enough that any constraint violation costs m ][ _Construction._ Given a SAT instance $phi$ with $n$ variables and $m$ clauses, introduce a sentinel variable $s$ (variable index $n + 1$). For each clause $C_j = (ell_1 or dots or ell_k)$, construct the NAE clause $C'_j = (ell_1, dots, ell_k, s)$. The target NAE-SAT instance has $n + 1$ variables and $m$ clauses. + _Size bound._ An empty source clause is represented by the contradictory NAE clause $(s,s)$. If the source has $L$ literal occurrences and $m$ clauses, the target has at most $L+2m$ literals. The sum of within-clause literal-pair counts is bounded by $(L+2m)^2$. + _Correctness._ ($arrow.r.double$) Given a satisfying assignment $bold(x)$ for $phi$, set $s = 0$. Each clause $C_j$ has at least one true literal $ell_i$ and the false sentinel $s = 0$, so $C'_j$ has both a true and a false literal, satisfying the NAE constraint. ($arrow.l.double$) Given a satisfying NAE assignment $(bold(x), s)$: if $s = 0$, each clause has at least one true literal (or else all literals in $C'_j$ would be false, including $s$, violating NAE); if $s = 1$, complement the entire assignment --- the complemented sentinel is $0$, and each complemented clause still has at least one true literal because the original NAE clause had at least one false non-sentinel literal. _Solution extraction._ If the sentinel $s = 0$, return the first $n$ variables. If $s = 1$, return the complement of the first $n$ variables. @@ -13040,6 +13052,8 @@ where $P$ is a penalty weight large enough that any constraint violation costs m $ using the 2-clause NOT gadget, the 3-clause AND/OR gadgets, and the 4-clause XOR gadget. If the simplified right-hand side becomes a variable or auxiliary variable $z_e$, add $(overline(o_i) or z_e)$ and $(o_i or overline(z_e))$ for every output $o_i$. If it simplifies to a constant, add the unit clause $o_i$ or $overline(o_i)$ accordingly. Repeat this independently for every assignment in the circuit. + _Size bound._ Let $N$ count all source expression nodes and $O$ all assignment outputs. Replacing multi-input gates by binary gates introduces at most $2N$ gates, each with at most four clauses of three literals. Output equalities add at most $2O$ clauses of two literals. Thus the target has at most $n+2N$ variables, $8N+2O$ clauses, and $24N+4O$ literals, including constant expressions and assignments without outputs. + _Correctness._ ($arrow.r.double$) Let $sigma$ be a satisfying CircuitSAT assignment. Set every auxiliary variable $v_alpha$ to the truth value of the corresponding subexpression $alpha$ under $sigma$. Each Tseitin gadget is then satisfied because its output variable matches the gate semantics, and every output-equivalence or unit clause holds because $sigma$ already makes each circuit assignment $o_1, dots, o_t = e$ true. Hence the CNF is satisfiable. ($arrow.l.double$) Let $tau$ satisfy the constructed CNF. Every Tseitin gadget forces its auxiliary variable to equal the truth value of its subexpression, so the root variable $z_e$ equals the value of $e$. The output-equivalence clauses therefore force every output $o_i$ to equal $e$, and unit clauses force the required constants. Restricting $tau$ to the named circuit variables yields an assignment satisfying every original circuit equation. _Solution extraction._ Return the values of the named circuit variables and discard the auxiliary Tseitin variables. @@ -13132,7 +13146,7 @@ where $P$ is a penalty weight large enough that any constraint violation costs m _Solution extraction._ Evaluate the target certificate once and reject it unless all equations hold. Read off factor bits $p = sum_i p_i 2^(i-1)$ and $q = sum_j q_j 2^(j-1)$, then return $(min(p,q), max(p,q))$. The source requires $m <= n$. Sorting preserves the asymmetric bounds: if inputs are exchanged, the new smaller factor is below the old first factor, while both inputs fit the larger width. - _Size and arithmetic._ Product width and assignment capacity are checked before allocation. At most $6 m n + 2(m+n) + 2$ assignments and $6 m n + 2(m+n) + 1$ variables are generated. Factors and products use exact arbitrary-precision integers; the circuit enforces bit arithmetic without floating-point conversions. The cell and output assignments use only the existing Boolean expression API. + _Size and arithmetic._ Product width and assignment capacity are checked before allocation. At most $6 m n + 2(m+n) + 2$ assignments and $6 m n + 2(m+n) + 1$ variables are generated. Factors and products use exact arbitrary-precision integers; the circuit enforces bit arithmetic without floating-point conversions. Each assignment has one output and at most five expression nodes, so the assignment bound also bounds output count, and five times that bound covers expression nodes. The cell and output assignments use only the existing Boolean expression API. ] @@ -13266,6 +13280,8 @@ The following reductions to Integer Linear Programming are straightforward formu #reduction-rule("MultipleChoiceBranching", "ILP")[ A topological-order formulation makes the branching acyclicity condition linear while retaining the source indegree, partition, and weight inequalities directly. ][ + _Numeric magnitude._ Let $h$ be `max_weight_bits`, covering signed arc weights and the threshold, and $n$ the vertex count. Order constraints introduce magnitudes at most $n$, so target `max_constraint_magnitude_bits` is at most $h+n$. + _Construction._ For every arc $a$ introduce a nonnegative integer $x_a <= 1$, and for every vertex $v$ introduce an integer order $0 <= p_v <= n-1$. Add $sum_(a in A_i)x_a <= 1$ for each partition group, $sum_(a in delta^-(v))x_a <= 1$ for each vertex, and $sum_a w_a x_a >= K$. For every arc $a=(u,v)$ add $p_u-p_v+n x_a <= n-1$. The target has exactly $m+n$ variables and $2m+2n+g+1$ constraints for $g$ partition groups. _Correctness._ A source branching admits a topological order, which satisfies the order rows. Conversely, selecting $(u,v)$ forces $p_v >= p_u+1$, so selected arcs cannot contain a directed cycle. All remaining source conditions are represented verbatim by their corresponding rows. @@ -13464,13 +13480,13 @@ The following reductions to Integer Linear Programming are straightforward formu "MaximumCoKPlex", "ILP", source-variant: (graph: "SimpleGraph", k: "KN", weight: "i64"), - target-variant: (coefficient: "i64", variable: "bool"), + target-variant: (bounds: "general", coefficient: "i64", variable: "bool"), ) #let mckp_ilp_sol = mckp_ilp.solutions.at(0) #reduction-rule("MaximumCoKPlex", "ILP", example: true, example-source-variant: (graph: "SimpleGraph", k: "KN", weight: "i64"), - example-target-variant: (coefficient: "i64", variable: "bool"), + example-target-variant: (bounds: "general", coefficient: "i64", variable: "bool"), example-caption: [Weighted 5-cycle ($n = 5$), $k = 2$], extra: [ #pred-commands( @@ -13499,12 +13515,12 @@ The following reductions to Integer Linear Programming are straightforward formu #let mces_ilp = load-example( "MaximumCommonEdgeSubgraph", "ILP", - target-variant: (coefficient: "i64", variable: "bool"), + target-variant: (bounds: "general", coefficient: "i64", variable: "bool"), ) #let mces_ilp_sol = mces_ilp.solutions.at(0) #reduction-rule("MaximumCommonEdgeSubgraph", "ILP", example: true, - example-target-variant: (coefficient: "i64", variable: "bool"), + example-target-variant: (bounds: "general", coefficient: "i64", variable: "bool"), example-caption: [Two labelled 3-vertex digraphs with 2 arcs each], extra: [ #pred-commands( @@ -13537,12 +13553,12 @@ The following reductions to Integer Linear Programming are straightforward formu #let cmo_ilp = load-example( "MaximumContactMapOverlap", "ILP", - target-variant: (coefficient: "i64", variable: "bool"), + target-variant: (bounds: "general", coefficient: "i64", variable: "bool"), ) #let cmo_ilp_sol = cmo_ilp.solutions.at(0) #reduction-rule("MaximumContactMapOverlap", "ILP", example: true, - example-target-variant: (coefficient: "i64", variable: "bool"), + example-target-variant: (bounds: "general", coefficient: "i64", variable: "bool"), example-caption: [$|V_1| = #cmo_ilp.source.instance.num_vertices_1$, $|E_1| = #cmo_ilp.source.instance.contacts_1.len()$, $|V_2| = #cmo_ilp.source.instance.num_vertices_2$, $|E_2| = #cmo_ilp.source.instance.contacts_2.len()$], extra: [ #pred-commands( @@ -13576,13 +13592,13 @@ The following reductions to Integer Linear Programming are straightforward formu "MaximumEdgeWeightedKClique", "ILP", source-variant: (weight: "i64"), - target-variant: (coefficient: "i64", variable: "bool"), + target-variant: (bounds: "general", coefficient: "i64", variable: "bool"), ) #let mewkc_ilp_sol = mewkc_ilp.solutions.at(0) #reduction-rule("MaximumEdgeWeightedKClique", "ILP", example: true, example-source-variant: (weight: "i64"), - example-target-variant: (coefficient: "i64", variable: "bool"), + example-target-variant: (bounds: "general", coefficient: "i64", variable: "bool"), example-caption: [$n = 4$ vertices, $m = 5$ edges, $k = 3$], extra: [ #pred-commands( @@ -13641,7 +13657,7 @@ The following reductions to Integer Linear Programming are straightforward formu *Uniqueness:* The fixture stores one canonical optimal witness. For this instance the optimum is unique: items $\{#fmt-values(ks_ilp_selected)\}$ are the only feasible choice achieving value #ks_ilp_sel_value. ], )[ - A 0-1 Knapsack instance is already a binary Integer Linear Program @papadimitriou-steiglitz1982: each item-selection bit becomes a binary variable, the capacity condition is a single linear inequality, and the value objective is linear. The reduction preserves the number of decision variables exactly, producing an ILP with $n$ variables and one constraint. + A 0-1 Knapsack instance is already a binary Integer Linear Program @papadimitriou-steiglitz1982: each item-selection bit becomes a binary variable, the capacity condition is a single linear inequality, and the value objective is linear. The reduction preserves the number of decision variables exactly, producing an ILP with $n$ variables and at most $n+1$ constraints after fixing oversized items to zero. ][ _Construction._ Given nonnegative weights $w_0, dots, w_(n-1)$, nonnegative values $v_0, dots, v_(n-1)$, and capacity $C$, introduce binary variables $x_0, dots, x_(n-1) in {0,1}$ where $x_i = 1$ iff item $i$ is selected. The ILP is: $ @@ -13649,7 +13665,7 @@ The following reductions to Integer Linear Programming are straightforward formu "subject to" quad & sum_(i=0)^(n-1) w_i x_i <= C \ & x_i in {0, 1} quad forall i in {0, dots, n - 1}. $ - The target therefore has exactly $n$ variables and one linear constraint. + The implementation omits every weight greater than $C$ from the capacity row and adds $x_i=0$ for that item. This preserves feasibility and gives exactly $n$ variables and at most $n+1$ constraints. Every constraint coefficient and right-hand side has magnitude at most $max(C,1)$, so `capacity_bits` bounds the target `max_constraint_magnitude_bits`. _Correctness._ ($arrow.r.double$) Any feasible knapsack solution $bold(x)$ satisfies $sum_i w_i x_i <= C$, so the same binary vector is feasible for the ILP and attains identical objective value $sum_i v_i x_i$. ($arrow.l.double$) Any feasible binary ILP solution selects exactly the items with $x_i = 1$; the single inequality guarantees the chosen set fits in the knapsack, and the ILP objective equals the knapsack value. Therefore optimal solutions correspond one-to-one and preserve the optimum value. @@ -13710,6 +13726,8 @@ The following reductions to Integer Linear Programming are straightforward formu _Correctness._ ($arrow.r.double$) Any feasible Integer Knapsack multiplicity vector $bold(c)$ already satisfies $sum_i s_i c_i <= B$, and every source multiplicity also satisfies $c_i <= floor.l B / s_i floor.r$, so the same vector is feasible for the ILP and attains exactly the same objective value $sum_i v_i c_i$. ($arrow.l.double$) Any feasible ILP solution satisfies the same capacity inequality and the same per-item multiplicity bounds, so it is a valid Integer Knapsack witness with identical total value. Therefore optimal solutions correspond one-to-one and preserve the optimum value. + _Numeric magnitude._ Items with $s_i>B$ have multiplicity fixed to zero and are omitted from the capacity row. All constraint magnitudes and variable bounds are therefore at most $max(B,1)$. The source `capacity_bits`, defined as the binary digit count of $B$ with a minimum of one, bounds the target `max_constraint_magnitude_bits`. + _Solution extraction._ Identity: return the ILP variable vector $bold(c)$ as the Integer Knapsack multiplicities. ] @@ -13795,6 +13813,8 @@ The following reductions to Integer Linear Programming are straightforward formu _Correctness._ ($arrow.r.double$) Any satisfying bundled flow assigns a non-negative integer to each arc, satisfies every bundle inequality by definition, satisfies every nonterminal conservation equality, and yields sink inflow at least $R$, so it is a feasible ILP solution. ($arrow.l.double$) Any feasible ILP solution gives non-negative integral arc values obeying the same bundle, conservation, and sink-inflow constraints, hence it is a satisfying solution to the original Integral Flow with Bundles instance. _Solution extraction._ Identity: read the ILP vector $(x_0, dots, x_(m-1))$ directly as the arc-flow vector of the source problem. + + _Numeric bounds._ Give each arc the explicit domain $0 <= x_i <= u_i$, where $u_i$ is the minimum capacity of a bundle containing it. With $S = sum_i u_i$, replace $R$ by $min(R, S+1)$: a requirement above $S$ remains infeasible. If $h$ is the maximum bundle-capacity bit length (at least one), all target constraint and domain magnitudes have at most $h + |A| + 1$ bits. No separate requirement parameter is needed. ] #let ola_seqmwct = load-example("OptimalLinearArrangement", "SequencingToMinimizeWeightedCompletionTime") @@ -13814,6 +13834,8 @@ The following reductions to Integer Linear Programming are straightforward formu )[ @lawler1978 This $O(n + m)$ reduction turns each vertex into a unit-length job, each edge into a zero-length job, and uses precedences so that every edge job completes exactly when its later endpoint does. The weighted completion-time objective then equals the linear-arrangement objective plus the fixed shift $d_"max" n (n + 1) / 2$. ][ + _Numeric magnitude._ Processing lengths are zero or one, so target `max_processing_time_bits` is exactly $1$. + _Construction._ Let the source instance be an undirected graph $G = (V, E)$ with $n = |V|$, $m = |E|$, and maximum degree $d_"max" = max_(v in V) deg(v)$. For each vertex $v in V$, create a job $J_v$ of length 1 and weight $d_"max" - deg(v)$. For each edge $e = {u, v} in E$, create a job $J_e$ of length 0 and weight 2. Add the precedence constraints $J_u prec.eq J_e$ and $J_v prec.eq J_e$ for every edge job $J_e$. There are no other precedences, so the target has $n + m$ jobs and $2m$ precedence arcs. _Correctness._ Write the source arrangement as a bijection $pi : V -> {0, dots, n - 1}$. Schedule the vertex jobs in increasing $pi$-order, so $J_v$ completes at time $C_v = pi(v) + 1$. Because $J_e$ has length 0 and must follow both endpoints, edge job $J_{ {u, v} }$ completes at time $max(pi(u), pi(v)) + 1$. The total weighted completion time is @@ -13863,6 +13885,8 @@ The following reductions to Integer Linear Programming are straightforward formu #reduction-rule("SequencingToMinimizeWeightedCompletionTime", "ILP")[ Completion times are natural integer variables, precedence constraints compare those completion times directly, and one binary order variable per task pair enforces that a single machine cannot overlap two jobs. ][ + _Numeric magnitude._ Let $h$ be `max_processing_time_bits` and $n$ the task count. Constraint magnitudes and completion-variable bounds are at most the total processing time (or unit constants), giving target `max_constraint_magnitude_bits` at most $h+n$. Objective weights need no parameter. + _Construction._ For each task $j$, introduce an integer completion-time variable $C_j$. For each unordered pair $i < j$, introduce a binary order variable $y_(i j)$ with $y_(i j) = 1$ meaning task $i$ finishes before task $j$. Let $M = sum_h l_h$. _Bounds._ $l_j <= C_j <= M$ for every task $j$, and $y_(i j) in {0, 1}$. @@ -14093,12 +14117,12 @@ The following reductions to Integer Linear Programming are straightforward formu #let cs_ilp_str = load-example( "ClosestString", "ILP", - target-variant: (coefficient: "i64", variable: "i64"), + target-variant: (bounds: "bounded", coefficient: "i64", variable: "i64"), ) #let cs_ilp_str_sol = cs_ilp_str.solutions.at(0) #reduction-rule("ClosestString", "ILP", example: true, - example-target-variant: (coefficient: "i64", variable: "i64"), + example-target-variant: (bounds: "bounded", coefficient: "i64", variable: "i64"), example-caption: [Binary alphabet, 4 length-3 strings], extra: [ #pred-commands( @@ -14136,12 +14160,12 @@ The following reductions to Integer Linear Programming are straightforward formu #let css_ilp = load-example( "ClosestSubstring", "ILP", - target-variant: (coefficient: "i64", variable: "i64"), + target-variant: (bounds: "bounded", coefficient: "i64", variable: "i64"), ) #let css_ilp_sol = css_ilp.solutions.at(0) #reduction-rule("ClosestSubstring", "ILP", example: true, - example-target-variant: (coefficient: "i64", variable: "i64"), + example-target-variant: (bounds: "bounded", coefficient: "i64", variable: "i64"), example-caption: [Binary alphabet, 3 length-5 strings, length-3 windows], extra: [ #pred-commands( @@ -14431,6 +14455,8 @@ The following reductions to Integer Linear Programming are straightforward formu Create one Betweenness element $a_u$ for each $u in U'$ and one distinguished pole $p$. For every size-2 subset ${u, v} in cal(C)'$, add triple $(a_u, p, a_v)$. For every size-3 subset ${u, v, w} in cal(C)'$, introduce a fresh auxiliary element $d_(u,v,w)$ and add triples $(a_u, d_(u,v,w), a_v)$ and $(d_(u,v,w), p, a_w)$. + _Size bound._ Write $u$ for the source universe size and $s$ for its number of subsets. After deduplication, each subset has at most $u$ elements and undergoes at most $u$ decomposition steps, each adding two elements and two subsets. Hence the normalized universe has at most $u+2s u$ elements and at most $s(2u+1)$ subsets. Adding one pole and at most one auxiliary per normalized subset gives at most $u+1+s(4u+1)$ elements; at most two triples per subset gives $2s(2u+1)$ triples. + _Correctness._ The normalization identity preserves splittability: a coloring splits ${s_1, dots, s_k}$ if and only if it can be extended to fresh elements $y^+, y^-$ so that ${s_1, s_2, y^+}$, ${y^+, y^-}$, and ${y^-, s_3, dots, s_k}$ are all non-monochromatic. Thus it suffices to reason about normalized subsets. ($arrow.r.double$) Let $chi: U' -> {0, 1}$ split every subset of $cal(C)'$. Place all $a_u$ with $chi(u) = 0$ to the left of $p$ and all $a_u$ with $chi(u) = 1$ to the right. For a size-2 subset ${u, v}$, non-monochromaticity gives $chi(u) != chi(v)$, so $p$ lies between $a_u$ and $a_v$, satisfying $(a_u, p, a_v)$. For a size-3 subset ${u, v, w}$, not all three colors are equal. If $u$ and $v$ lie on the same side of $p$, then $w$ lies on the opposite side; place $d_(u,v,w)$ between $a_u$ and $a_v$ on their shared side. If $u$ and $v$ lie on opposite sides of $p$, place $d_(u,v,w)$ between them on the side opposite $a_w$. In both cases $(a_u, d_(u,v,w), a_v)$ and $(d_(u,v,w), p, a_w)$ hold. @@ -14687,6 +14713,8 @@ The following reductions to Integer Linear Programming are straightforward formu #reduction-rule("PartiallyOrderedKnapsack", "ILP")[ Standard knapsack with precedence constraints: item $b$ can only be selected if item $a$ is also selected for each precedence $(a, b)$. ][ + _Numeric magnitude._ Let $h$ be `max_weight_bits`, covering item weights and capacity. The capacity row and unit precedence rows give target `max_constraint_magnitude_bits` exactly $h$; item values occur only in the objective. + _Construction._ Variables: $x_i in {0, 1}$ per item. The ILP is: $ max quad & sum_i v_i x_i \ @@ -14719,6 +14747,8 @@ The following reductions to Integer Linear Programming are straightforward formu #reduction-rule("ShortestWeightConstrainedPath", "ILP")[ Find a minimum-length $s$-$t$ path subject to a weight budget, using directed arc variables with MTZ ordering $o_v - o_u >= 1 - M (1 - a_(u,v))$ on selected arcs to prevent subtours. ][ + _Numeric magnitude._ Let $h$ be `max_weight_bits`, covering edge weights and the weight bound, and $n$ the vertex count. These numeric inputs and the order-variable bounds give target `max_constraint_magnitude_bits` at most $h+n$. Objective edge lengths require no additional parameter. + _Construction._ Let $A$ contain both orientations of every undirected edge and let $M = n$. Variables: binary $a_(u,v) in {0, 1}$ for each directed arc $(u, v) in A$, plus integer $o_v in {0, dots, n-1}$ per vertex. The ILP is: $ "minimize" quad & sum_((u,v) in A) l_(u,v) a_(u,v) \ @@ -14774,6 +14804,8 @@ The following reductions to Integer Linear Programming are straightforward formu #reduction-rule("MinMaxMulticenter", "ILP")[ Select $k$ centers minimizing the maximum weighted distance from any vertex to its assigned center. ][ + _Numeric magnitude._ Let $h$ be `max_numeric_magnitude_bits`, covering vertex weights and edge lengths, and $n$ the vertex count. A shortest simple path uses at most $n-1$ edges. Multiplying its length by a vertex weight gives a magnitude below $n 2^(2h)$, so target `max_constraint_magnitude_bits` is at most $2h+n$. + _Construction._ Same assignment structure as MinimumSumMulticenter (binary $x_j$, $y_(i,j)$), plus an integer variable $z$. The ILP is: $ "minimize" quad & z \ @@ -14802,12 +14834,16 @@ The following reductions to Integer Linear Programming are straightforward formu _Correctness._ One-hot constraints ensure each task is assigned to exactly one processor; load constraints enforce the deadline on every processor. + _Numeric magnitude._ Let $h >= 1$ be the smallest integer such that every task length and the deadline are strictly below $2^h$. At least one processor is present, so the load rows copy all these numbers. All remaining coefficients, right-hand sides, and Boolean endpoints have magnitude at most one. Thus the target's `max_constraint_magnitude_bits` equals the source's `max_numeric_magnitude_bits`, including instances with no tasks. + _Solution extraction._ Task $j$ goes to processor $arg max_p x_(j,p)$. ] #reduction-rule("CapacityAssignment", "ILP")[ Assign a capacity level to each link to minimize total cost subject to a delay budget. ][ + _Numeric magnitude._ Let $h$ be `max_delay_bits`, covering all delays and the delay budget with absolute values strictly below $2^h$. These values, together with unit assignment rows, give target `max_constraint_magnitude_bits` exactly $h$. Costs occur only in the objective. + _Construction._ Variables: binary $x_(l,c)$ (link $l$ gets capacity $c$), one-hot per link. The ILP is: $ "minimize" quad & sum_(l,c) "cost"[l][c] x_(l,c) \ @@ -14895,6 +14931,8 @@ The following reductions to Integer Linear Programming are straightforward formu )[ This $O(n + m)$ parameter-setting reduction (Hadlock, 1974; Garey and Johnson @garey1979[ND10, p.~208]) constructs a Bounded Component Spanning Forest instance on the same graph with unit vertex weights, $K = |V| slash 3$ components, and weight bound $B = 3$. ][ + _Numeric magnitude._ Unit vertex weights and component bound $3$ give target `max_weight_bits` exactly $2$, including the empty graph. + _Construction._ Given a Partition into Paths of Length 2 instance on graph $G = (V, E)$ with $|V| = 3q$: - Graph: use $G$ unchanged. - Vertex weights: $w(v) = 1$ for all $v in V$. @@ -15026,6 +15064,8 @@ The following reductions to Integer Linear Programming are straightforward formu _Correctness._ Direction indicators linearize the capacity-sharing constraint. Per-commodity conservation prevents flow from being created or destroyed at another commodity's terminals, as required by the standard multicommodity-flow formulation @garey1979. _Solution extraction._ Flow variables (first $4|E|$ variables). + + _Numeric bounds._ Bound each flow variable explicitly by its edge capacity. With $S = sum_e "cap"_e$, each sink's net inflow lies in $[-S, S]$, so replace each requirement $R_k$ by $max(-S, min(R_k, S+1))$. This preserves feasibility, including impossible demands. If $h$ is the maximum capacity bit length (at least one), $h + |E| + 1$ bounds the target constraint and domain magnitude bits. ] #reduction-rule("DirectedTwoCommodityIntegralFlow", "ILP")[ @@ -15063,6 +15103,8 @@ The following reductions to Integer Linear Programming are straightforward formu _Correctness._ Direction indicators force flow in one direction per edge; bounds enforce both upper and lower capacity limits. _Solution extraction._ Edge orientations: $z_e$ values. + + _Numeric bounds._ Require $0 <= "lower"_e <= "cap"_e$ and give each directional flow the explicit domain $[0, "cap"_e]$. With $S = sum_e "cap"_e$, replace the positive requirement $R$ by $min(R, S+1)$; demands above $S$ remain infeasible. The capacity bit length $h >= 1$ therefore gives the bound $h + |E| + 1$ on target constraint and domain magnitude bits, without a separate lower-bound or requirement parameter. ] // Flow-based @@ -15100,6 +15142,8 @@ The following reductions to Integer Linear Programming are straightforward formu _Correctness._ ($arrow.r.double$) A valid multiplier flow satisfies these linear equalities and inequalities by definition. ($arrow.l.double$) Any feasible ILP solution gives an integral arc flow whose non-terminal outflow equals the prescribed multiple of its inflow and whose sink inflow meets the requirement. _Solution extraction._ Output the arc-flow vector $(f_a)_(a in A)$. + + _Numeric bounds._ Give each arc the explicit domain $[0, c_a]$ and let $S = sum_a c_a$. Replace $h(v)$ by $min(h(v), S+1)$. If $h(v)>S$, any positive integral inflow would require outflow above $S$, so both the original and replacement equation force zero inflow and outflow. Replace $R$ by $max(-S, min(R, S+1))$. If $b >= 1$ is the maximum capacity bit length, all target constraint and domain magnitudes have at most $b + |A| + 1$ bits. Thus the prediction needs no multiplier or requirement parameter. ] #reduction-rule("PathConstrainedNetworkFlow", "ILP")[ @@ -15268,6 +15312,8 @@ The following reductions to Integer Linear Programming are straightforward formu #reduction-rule("FlowShopScheduling", "ILP")[ Order the jobs with pairwise precedence bits and completion-time variables on every machine; the deadline becomes a makespan bound. ][ + _Numeric magnitude._ Let $h$ be `max_time_bits`, covering processing times and the deadline. The disjunction constant is the deadline plus the largest processing time, giving target `max_constraint_magnitude_bits` at most $h+1$. + _Construction._ Let $q in {1, dots, m}$ index the machines, let $p_(j,q) = ell(t_q [j])$ be the processing time of job $j$ on machine $q$, and let $M = D + max_(j, q) p_(j,q)$. Variables: binary $y_(i,j)$ with $y_(i,j) = 1$ iff job $i$ precedes job $j$, and integer completion times $C_(j,q)$. The ILP is: $ "find" quad & bold(x) \ @@ -15287,6 +15333,8 @@ The following reductions to Integer Linear Programming are straightforward formu #reduction-rule("OpenShopScheduling", "ILP")[ Binary ordering variables and integer start times encode the disjunctive non-overlap constraints for both machines and jobs; the makespan is the minimized objective. ][ + _Numeric magnitude._ The source `schedule_horizon_bits` is the binary digit count of the total processing time $M$, with a minimum of one. Start times and makespan have explicit domains $[0,M]$. All constraint magnitudes are at most $max(M,1)$, so this parameter bounds the target `max_constraint_magnitude_bits`. + _Construction._ Let $M = sum_(j,i) p(j,i)$ be the big-$M$ constant (an upper bound on the makespan). For each pair $j < k$ and each machine $i$, let $x_{j k i} in {0,1}$ with $x_{j k i} = 1$ iff job $j$ precedes job $k$ on machine $i$. For each job $j$ and pair of machines $i < i'$, let $y_{j i i'} in {0,1}$ with $y_{j i i'} = 1$ iff machine $i$ is processed before machine $i'$ for job $j$. Let $s_{j,i} in ZZ_{>=0}$ be the start time of job $j$ on machine $i$, and $C$ be the integer makespan variable. The ILP is: $ min quad & C \ @@ -15320,7 +15368,7 @@ The following reductions to Integer Linear Programming are straightforward formu )[ Impose the decision bound on the open-shop makespan variable. The optimization formulation gains one constraint and no variables. ][ - _Construction._ For bound $B$, use the OpenShopScheduling-to-ILP construction above, add $C <= B$, and replace the objective with zero. + _Construction._ For bound $B$, use the OpenShopScheduling-to-ILP construction above, add $C <= min(M,max(-1,B))$, and replace the objective with zero. Clipping preserves feasibility because $0 <= C <= M$, and keeps the target `max_constraint_magnitude_bits` bounded by the source `schedule_horizon_bits` independently of $B$. _Correctness._ ($arrow.r.double$) A schedule of makespan at most $B$ gives feasible ordering variables and start times, with $C$ equal to its makespan. The existing horizon bounds can be met by removing unnecessary idle time. ($arrow.l.double$) Every feasible target assignment decodes to a schedule whose makespan is at most $C <= B$. Thus target feasibility is equivalent to the source YES answer; no optimum needs to be computed. @@ -15328,36 +15376,46 @@ The following reductions to Integer Linear Programming are straightforward formu ] #reduction-rule("MinimumTardinessSequencing", "ILP")[ - A position-assignment ILP captures the permutation, the precedence constraints, and a binary tardy indicator for each unit-length task. + A position-assignment ILP captures the permutation, the precedence constraints, and a binary tardy indicator for each task. ][ - _Construction._ Variables: binary $x_(j,p)$ placing task $j$ in position $p in {0, dots, n-1}$ and binary tardy indicators $u_j$, where $M = n$. The ILP is: + _Numeric magnitude._ For unit lengths, target `max_constraint_magnitude_bits` is at most $n+1$. For integer lengths let $h$ be `max_processing_time_bits`; then $L= 1$ indicating a compiler switch before position $p$; binary $a_(j,p) = x_(j,p) dot "sw"_p$ (linearised product). Let $M = sum_j ell_j + max_c s(c) dot (n-1)$. The ILP is: $ "find" quad & bold(x) \ @@ -15423,6 +15487,8 @@ The following reductions to Integer Linear Programming are straightforward formu #reduction-rule("SequencingToMinimizeWeightedTardiness", "ILP")[ Encode the single-machine order with pairwise precedence bits and completion times, then linearize the weighted tardiness bound with nonnegative tardiness variables. ][ + _Numeric magnitude._ Let $h$ be `max_numeric_magnitude_bits`, covering lengths, weights, deadlines, and the acceptance bound, and $n$ the task count. Completion and tardiness-variable endpoints are bounded by the total processing time, while the weighted acceptance row copies source weights. Target `max_constraint_magnitude_bits` is at most $h+n$. + _Construction._ Variables: binary $y_(i,j)$ with $y_(i,j) = 1$ iff job $i$ precedes job $j$, integer completion times $C_j$, and nonnegative tardiness variables $T_j$, where $M = sum_j ell_j$ is a valid schedule-horizon bound. The ILP is: $ "find" quad & bold(x) \ @@ -15516,12 +15582,12 @@ The following reductions to Integer Linear Programming are straightforward formu #let hcd_ilp = load-example( "HighlyConnectedDeletion", "ILP", - target-variant: (coefficient: "i64", variable: "bool"), + target-variant: (bounds: "general", coefficient: "i64", variable: "bool"), ) #let hcd_ilp_sol = hcd_ilp.solutions.at(0) #reduction-rule("HighlyConnectedDeletion", "ILP", example: true, - example-target-variant: (coefficient: "i64", variable: "bool"), + example-target-variant: (bounds: "general", coefficient: "i64", variable: "bool"), example-caption: [Triangle plus pendant: $n = 4$ vertices, $m = 4$ edges], extra: [ #pred-commands( @@ -15533,29 +15599,35 @@ The following reductions to Integer Linear Programming are straightforward formu Source deletion witness $(#fmt-values(hcd_ilp_sol.source_config))$, target ILP witness $(#fmt-values(hcd_ilp_sol.target_config))$. ], )[ - Enumerate the family of feasible clusters of $G$ and pick a partition of $V$ into feasible clusters maximizing the kept internal edge count; since $|E|$ is fixed, this is equivalent to minimizing deleted edges @HueffnerKomusiewiczLiebtrauNiedermeier2014. + Encode cluster membership with one binary variable per unordered vertex pair. Transitivity and minimum-degree constraints describe a partition into singletons and highly connected clusters, maximizing the number of retained edges. ][ - _Construction._ Let the source instance be a simple undirected graph $G = (V, E)$. Call a vertex set $S subset.eq V$ a _feasible cluster_ when either $|S| = 1$, or $|S| >= 3$ and the induced subgraph $G[S]$ is _highly connected_, i.e. its edge connectivity satisfies $lambda(G[S]) > |S| / 2$ (strict). Let $cal(C)(G)$ be the family of all feasible clusters. Introduce binary variables $x_S in {0, 1}$ for each $S in cal(C)(G)$, where $x_S = 1$ iff $S$ is chosen as one block of the final partition. The ILP is: - $ - max quad & sum_(S in cal(C)(G)) |E(G[S])| x_S \ - "subject to" quad & sum_(S in cal(C)(G), v in S) x_S = 1 quad forall v in V \ - & x_S in {0, 1}. - $ + _Construction._ Let $G = (V, E)$ have $n$ vertices. Introduce symmetric binary variables $y_(u v) = y_(v u)$ for distinct vertices, meaning that $u$ and $v$ belong to the same cluster, and a binary non-singleton flag $a_v$ for each vertex. For every triple, impose all three inequalities of the form + $ y_(u v) + y_(v w) - y_(u w) <= 1. $ + Together with reflexive membership, these constraints define an equivalence relation. Write + $ s_v = sum_(u != v) y_(u v), quad d_v = sum_(u in N(v)) y_(u v), $ + where $N(v)$ contains distinct neighbors other than $v$. Impose + $ s_v <= (n-1) a_v, quad 2 d_v >= s_v + 2 a_v. $ + For $n = 0$, there are no variables or rows. Maximize $sum_({u,v} in E, u != v) y_(u v)$, counting duplicate edges with multiplicity. Self-loops are always retained and contribute only a constant to the retained-edge count. + + _Degree characterization._ A simple graph on $k >= 2$ vertices is highly connected exactly when its minimum degree $delta$ exceeds $k/2$. Necessity follows from $lambda <= delta$. For sufficiency, consider any cut with smaller side of size $b <= k/2$. At least $b(delta-b+1)$ edges cross it. Since $(b-1)(delta-b) >= 0$, this is at least $delta > k/2$. + + _Correctness._ For a singleton, $s_v = d_v = 0$ and the constraints force $a_v = 0$. In a larger cluster of size $k = s_v+1$, the first inequality forces $a_v = 1$, and the second requires $2 d_v >= k+1$. Thus every non-singleton cluster is highly connected by the degree characterization; clusters of size two are excluded automatically. Conversely, every partition into allowed clusters satisfies the constraints with these membership and flag values. Any feasible source deletion can restore all edges internal to its components without decreasing their connectivity or increasing the deletion cost. Hence some source optimum keeps every internal edge, and maximizing the target objective preserves that optimum. + + _Solution extraction._ Delete precisely the non-loop source edges whose membership variable is zero. Keep every self-loop. The resulting components are the encoded clusters, and their connectivity follows from the constraints. - _Correctness._ ($arrow.r.double$) Any feasible source partition $cal(P) = {B_1, dots, B_k}$ -- where every block $B_i$ is a singleton or a highly connected component on $>= 3$ vertices -- yields the feasible ILP assignment $x_(B_i) = 1$ for $i = 1, dots, k$ and $0$ elsewhere; the partition constraints hold because each vertex belongs to exactly one block, and the objective value is the number of edges kept by the partition. ($arrow.l.double$) Any feasible ILP solution selects a sub-family of $cal(C)(G)$ that, by the equality constraints, partitions $V$ into feasible clusters; the objective equals the number of intra-cluster edges. Since $|E|$ is constant, maximizing intra-cluster edges is equivalent to minimizing $|E| - sum_S |E(G[S])| x_S$, the number of deleted edges. + _Size and running time._ The target has $n(n+1)/2$ binary variables, $3 binom(n,3)+2n$ constraints, and $O(n^3)$ nonzeros. Constraint coefficients have magnitude at most $max(n-1,2)$; objective coefficients count input edge multiplicities. Construction, encoding length, and extraction are polynomial in the source encoding size. All registered parameter bounds use only the source vertex count. - _Solution extraction._ Decode the chosen clusters $C subset.eq cal(C)(G)$ from $x$. The source configuration is the binary edge-deletion vector: edge $e = {u, v}$ is kept (config bit $0$) iff some chosen cluster $S in C$ contains both $u$ and $v$, otherwise deleted (config bit $1$). ] #let ep_ilp = load-example( "EulerianPath", "ILP", - target-variant: (coefficient: "i64", variable: "i64"), + target-variant: (bounds: "bounded", coefficient: "i64", variable: "i64"), ) #let ep_ilp_sol = ep_ilp.solutions.at(0) #reduction-rule("EulerianPath", "ILP", example: true, - example-target-variant: (coefficient: "i64", variable: "i64"), + example-target-variant: (bounds: "bounded", coefficient: "i64", variable: "i64"), example-caption: [3-vertex digraph with 4 arcs (parallel edges)], extra: [ #pred-commands( @@ -15641,6 +15713,8 @@ The following reductions to Integer Linear Programming are straightforward formu )[ @garey1979 This $O(m)$ reduction copies the graph unchanged and assigns unit weight to every edge ($n$ target vertices, $m$ target edges). A Hamiltonian circuit exists iff the optimal circuit length equals $n$. ][ + _Numeric magnitude._ Every edge has length one, so target `max_length_bits` is exactly $1$. + _Construction._ Given a Hamiltonian Circuit instance $G = (V, E)$ with $n = |V|$ and $m = |E|$, construct a DecisionLongestCircuit instance with bound $n$ on the same graph $G' = G$ with edge lengths $l(e) = 1$ for every $e in E$. Its decision condition is circuit length $>= n$. _Correctness._ ($arrow.r.double$) If $G$ has a Hamiltonian circuit $v_0, v_1, dots, v_(n-1), v_0$, then this circuit uses $n$ edges each of length 1, giving total length $n$. Since a simple circuit on $n$ vertices can use at most $n$ edges, this is optimal. ($arrow.l.double$) If the longest circuit in $G'$ has length $n$, it uses $n$ unit-weight edges and therefore visits $n$ distinct vertices, i.e., every vertex exactly once. This circuit is therefore a Hamiltonian circuit in $G$. @@ -15684,9 +15758,11 @@ The following reductions to Integer Linear Programming are straightforward formu )[ Impose the decision bound on the selected circuit length. The optimization formulation gains one constraint and no variables. ][ - _Construction._ For bound $B$, use the LongestCircuit-to-ILP construction above, add $sum_(e in E) l_e y_e >= B$, and replace the objective with zero. + _Numeric magnitude._ Let $h$ be `max_length_bits` and $m$ the edge count. The nonconstant acceptance row has bound at most $S=0$ if $B<=0$, $0>=1$ if $B>S$, and $sum_(e in E) l_e y_e >= B$ otherwise, and replace the objective with zero. - _Correctness._ ($arrow.r.double$) A circuit of length at least $B$ extends to the existing selection and connectivity variables and meets the new constraint. ($arrow.l.double$) Every feasible target assignment selects one simple circuit, and the new constraint guarantees its length is at least $B$. A graph with no circuit remains infeasible regardless of the bound. + _Correctness._ ($arrow.r.double$) A circuit of length at least $B$ extends to the existing selection and connectivity variables and meets the new constraint. ($arrow.l.double$) Every feasible target assignment selects one simple circuit, and the new constraint guarantees its length is at least $B$. Positive edge lengths make the two constant-row cases equivalent to the original threshold comparison. A graph with no circuit remains infeasible regardless of the bound. _Solution extraction._ Check target feasibility, then return the existing edge-selection vector. Construction has the same asymptotic cost as the optimization formulation.#footnote[Complexity follows from the implementation; not independently verified from literature.] ] @@ -15769,6 +15845,8 @@ The following reductions to Integer Linear Programming are straightforward formu #reduction-rule("AcyclicPartition", "ILP")[ Assign every vertex to a topologically numbered partition class and directly require every arc to have nondecreasing class labels, following the upper-triangular formulation of @ozkayaCatalyurek2022. ][ + _Numeric magnitude._ Let $h$ be `max_numeric_magnitude_bits`, covering vertex weights, arc costs, and both bounds, and $n$ the vertex count. Label coefficients and endpoints are at most $n$; normalized product rows can contain coefficient $2$. Target `max_constraint_magnitude_bits` is at most $h+n+1$. + _Construction._ Let $n = |V|$ and let the directed arcs be $A = {a_0, dots, a_(m-1)}$ with $a_t = (u_t -> v_t)$. The source witness already allows every vertex to choose one label in ${0, dots, n - 1}$, so the ILP uses exactly the same label range. Use `ILP` with variable order $(x_(v,c))_(v,c), (s_(t,c))_(t,c), (y_t)_t$. The indices are @@ -15826,7 +15904,7 @@ The following reductions to Integer Linear Programming are straightforward formu #reduction-rule("BiconnectivityAugmentation", "ILP")[ Select candidate edges under the total budget and certify connectivity of both the original augmented graph and every vertex-deleted graph using bounded integral flow witnesses. ][ - _Construction._ Let $n$ be the vertex count, $m$ the number of base edges, and $p$ the number of candidate edges. Candidate $j$ has cost $w_j$, and the budget is $B$. Use `ILP` with an empty minimization objective and every variable bounded to $[0,1]$ through `IntegerVariable::binary()`. + _Construction._ Let $n$ be the vertex count, $m$ the number of base edges, and $p$ the number of candidate edges. Candidate $j$ has cost $w_j$, and the budget is $B$. Use `ILP` with an empty minimization objective and every variable bounded to $[0,1]$ through `IntegerVariable::binary()`. Selection variable $y_j$ has index $j$. Connectivity scenarios are $q in {0, dots, n}$: $q= 1$ be the smallest integer such that $abs(w_j) < 2^h$ for every candidate and $abs(B) < 2^h$. The source parameter `max_numeric_magnitude_bits` equals $h$. The budget row copies these values, and all other constraint and variable-bound magnitudes are at most one. Thus the target's `max_constraint_magnitude_bits` equals $h$, including signed budgets and empty candidate lists. ] #reduction-rule("BoundedComponentSpanningForest", "ILP")[ Assign every vertex to one of at most $K$ components, bound each component's total weight, and certify connectivity inside each used component by a flow witness. ][ + _Numeric magnitude._ Let $h$ be `max_weight_bits`, covering vertex weights and the component bound, and $n$ the vertex count. Weight rows, unit flow rows, and flow-variable endpoints give target `max_constraint_magnitude_bits` at most $h+n$. + _Construction._ Let $n = |V|$, let the graph edges be $E = {e_0, dots, e_(m-1)}$ with $e_i = {u_i, v_i}$, and let the allowed component labels be $c in {0, dots, K - 1}$. Use `ILP` with variables ordered as $(x_(v,c))_(v,c), (u_c)_c, (r_(v,c))_(v,c), (s_c)_c, (b_(v,c))_(v,c), (f_(i,eta,c))_(i,eta,c)$. Their indices are @@ -15916,7 +15998,7 @@ The following reductions to Integer Linear Programming are straightforward formu #reduction-rule("StrongConnectivityAugmentation", "ILP")[ Select candidate arcs under the budget and certify strong connectivity by sending flow both from a root to every vertex and back again. ][ - _Construction._ Let the base arcs be $A = {a_0, dots, a_(m-1)}$ with $a_i = (u_i, v_i)$, let the candidate arcs be $C = {c_0, dots, c_(p-1)}$ with $c_j = (s_j, t_j)$, and, when $n = |V| >= 1$, fix the root to be vertex $r = 0$. If $n <= 1$, return the empty feasible ILP. Use `ILP` with variables ordered as + _Construction._ Let the base arcs be $A = {a_0, dots, a_(m-1)}$ with $a_i = (u_i, v_i)$, let the candidate arcs be $C = {c_0, dots, c_(p-1)}$ with $c_j = (s_j, t_j)$, and, when $n = |V| >= 1$, fix the root to be vertex $r = 0$. Retain the budget constraint even when $n <= 1$. Use `ILP` with variables ordered as $(y_j)_j, (f^t_i)_(t,i), (bar(f)^t_j)_(t,j), (g^t_i)_(t,i), (bar(g)^t_j)_(t,j)$, where $f^t$ is the forward root-to-$t$ flow on base arcs, $bar(f)^t$ is the forward flow on candidate arcs, $g^t$ is the backward $t$-to-root flow on base arcs, and $bar(g)^t$ is the backward flow on candidate arcs. The indices are @@ -15958,6 +16040,8 @@ The following reductions to Integer Linear Programming are straightforward formu _Correctness._ ($arrow.r.double$) A strongly connected augmentation provides both directions of reachability between the root and every other vertex, hence all required flows. ($arrow.l.double$) If those flows exist for every vertex, then every vertex is reachable from the root and can reach the root, so the augmented digraph is strongly connected. _Solution extraction._ Output the binary candidate-arc selection vector $(y_a)$. + + _Numeric magnitude._ The source parameter `max_numeric_magnitude_bits` is the smallest $h >= 1$ for which every candidate weight and the budget are strictly below $2^h$. The budget row copies these values and the remaining constraint and variable-bound magnitudes are at most one, so the target's `max_constraint_magnitude_bits` equals $h$. ] // Matrix/encoding @@ -16933,6 +17017,8 @@ The following table shows concrete target-variable counts for example instances, #reduction-rule("MinimumCapacitatedSpanningTree", "ILP")[ Binary edge selectors $y_e$, directed requirement-flow variables $f$, and directed unit-demand connectivity variables $g$. The first flow enforces subtree capacities; the second connects even zero-requirement vertices. ][ + _Numeric magnitude._ Let $h$ be `max_requirement_bits`, covering requirements and capacity, and $n$ the vertex count. Flow bounds and balance rows involve at most the sum of $n$ requirements; target `max_constraint_magnitude_bits` is at most $h+n$. Edge costs occur only in the objective. + _Construction._ $5m$ variables: $m$ edge selectors and two directed flows of $2m$ variables each. Requirement flow sends $r(v)$ units from each non-root vertex to the root and is bounded by capacity $c$. Connectivity flow sends one unit from every non-root vertex to the root and satisfies $g_(u v)+g_(v u) <= (n-1)y_e$. Also impose $sum_e y_e=n-1$ and minimize $sum_e w(e) dot y_e$. This combines the standard non-unit-demand flow model @gouveiaLopes2000 with the standard unit-demand spanning-tree flow. _Correctness._ ($arrow.r.double$) A feasible capacitated spanning tree induces both flows along its unique root paths. ($arrow.l.double$) Unit-demand flow makes every vertex reachable from the root; together with $n-1$ selected edges this gives a spanning tree. Requirement flow on that tree equals each rooted subtree's total requirement, so its capacity bounds are exactly the source constraints. @@ -16954,13 +17040,13 @@ The following table shows concrete target-variable counts for example instances, // === Non-ILP reduction rules (issue #974) === #reduction-rule("ILP", "ILP")[ - ILP variants convert between binary and bounded integer variable domains and between exact-integer and floating-point coefficients. Binary variables embed directly into integer variables. A finitely bounded integer variable is encoded by binary variables with truncated positional weights. Integer coefficients are embedded only when every stored coefficient and right-hand side has an exact `f64` representation. + ILP variants convert between binary and bounded integer variable domains and between exact-integer and floating-point coefficients. The independent bounds dimension defaults to general; the bounded integer variant requires finite lower and upper endpoints for every variable. Binary variables embed into bounded integer ILP, which embeds into general integer ILP while retaining all stored bounds. A bounded integer variable is encoded by binary variables with truncated positional weights. Integer coefficients are embedded only when every stored coefficient and right-hand side has an exact `f64` representation. ][ - _Construction._ For the binary-to-integer edge, copy the variables, constraints, objective, and optimization direction unchanged. For an integer variable $x_i in [L_i, U_i]$, let $D_i = U_i - L_i$ and choose positive truncated binary weights $w_(i j)$ whose subset sums represent every integer from $0$ through $D_i$; substitute $x_i = L_i + sum_j w_(i j)y_(i j)$ into every constraint and objective term. This edge rejects variables without two finite bounds. For the coefficient edge, copy the variable bounds and optimization direction and convert each entry of the constraint matrix, right-hand side, and objective independently; reject the instance if any integer lies outside the exactly representable `f64` integer range. + _Construction._ For the binary-to-bounded-integer and bounded-to-general edges, copy the variables, constraints, objective, and optimization direction unchanged. For an integer variable $x_i in [L_i, U_i]$, let $D_i = U_i - L_i$ and choose positive truncated binary weights $w_(i j)$ whose subset sums represent every integer from $0$ through $D_i$; substitute $x_i = L_i + sum_j w_(i j)y_(i j)$ into every constraint and objective term. This edge is registered only for the bounded integer variant; its constructor requires two finite bounds for every variable. For the coefficient edge, copy the variable bounds and optimization direction and convert each entry of the constraint matrix, right-hand side, and objective independently; reject the instance if any integer lies outside the exactly representable `f64` integer range. - _Correctness._ The binary-to-integer embedding changes no mathematical expression. For bounded integer variables, every $x_i in [L_i,U_i]$ has a truncated binary representation, and every binary assignment decodes inside that interval; substitution preserves all constraints and objective values. Exact conversion preserves every stored coefficient, so it constructs the same formal linear objective and constraints over the same integer variables. + _Correctness._ The binary-to-bounded-integer and bounded-to-general embeddings change no mathematical expression. For bounded integer variables, every $x_i in [L_i,U_i]$ has a truncated binary representation, and every binary assignment decodes inside that interval; substitution preserves all constraints and changes the stored objective only by the constant lower-bound contribution, so optimal assignments are preserved. Exact conversion preserves every stored coefficient, so it constructs the same formal linear objective and constraints over the same integer variables. - _Solution extraction._ Binary-to-integer and coefficient conversions preserve the assignment; coefficient conversion additionally checks the assignment against the source integer ILP. Binary encoding returns $x_i = L_i + sum_j w_(i j)y_(i j)$. + _Solution extraction._ Binary-to-bounded-integer, bounded-to-general, and coefficient conversions preserve the assignment; coefficient conversion additionally checks the assignment against the source integer ILP. Binary encoding returns $x_i = L_i + sum_j w_(i j)y_(i j)$. ] #let hc_hp = load-example("HamiltonianCircuit", "HamiltonianPath") @@ -17088,6 +17174,8 @@ The following table shows concrete target-variable counts for example instances, ][ _Construction._ Let $A = (a_1, dots, a_n)$ with total sum $S = sum_(i=1)^n a_i$. Set task lengths $ell_i = a_i$, number of processors $m = 2$, and deadline $D = floor(S / 2)$. + _Numeric magnitude._ If each source size is below $2^h$, then $S < n dot 2^h <= 2^(h+n)$. Both the copied lengths and $D$ are therefore below $2^(h+n)$, giving the local upper bound `max_numeric_magnitude_bits + num_elements` on the target's `max_numeric_magnitude_bits`. + _Correctness._ ($arrow.r.double$) If $A' subset.eq A$ has $sum_(i in A') a_i = S/2$, assign tasks in $A'$ to processor 0 and the rest to processor 1; both loads equal $S/2 = D$. ($arrow.l.double$) If a feasible schedule exists with both loads $<= D = floor(S/2)$, since both loads sum to $S$ and each is at most $floor(S/2)$, equality holds, giving a balanced partition. _Solution extraction._ The processor assignment $p_i in {0, 1}$ is the partition assignment directly. @@ -17770,6 +17858,8 @@ The following table shows concrete target-variable counts for example instances, )[ Compose the literal-compatibility clique construction @karp1972 with the incidence construction below. All weights and arc costs are positive integers of polynomial magnitude. This incidence lemma is proved here; it does not use the digit-encoded Subset Sum chain. ][ + _Numeric magnitude._ For $c$ clauses, incidence count $L$ is at most $5(c+1)^2$, the cluster parameter is $c+1$, and capacity is at most $(c+1)^2$. All constructed magnitudes are below $64(c+1)^4<=2^(4c+6)$. Thus target `max_numeric_magnitude_bits` is at most $4c+6$, including $c=0$. + _Construction._ First obtain a clique instance $H=(V,E)$ with threshold $k$ from the formal 3-SAT-to-KClique rule, including its universal vertex and padding. Write $h=|V|$, $e=|E|$, and $L=h+e$. Here $1 <= k <= h$. 1. Create one unit-weight item per vertex and edge of $H$. Set $c=k(k+1)/2$, $M=2L+1$, and $B=2(L+c)+1$. @@ -17822,6 +17912,8 @@ The following table shows concrete target-variable counts for example instances, _Correctness._ For $n < 3$, both instances are infeasible. For $n >= 3$, a source Hamiltonian circuit selects $n$ weight-1 edges forming a biconnected cycle of cost $n$. Conversely, a feasible target is connected and every vertex has degree at least two: a degree-zero vertex contradicts connectivity, and a degree-one vertex would be separated from the other surviving vertices by deleting its neighbor. The degree sum therefore forces at least $n$ selected edges. Positive costs and budget $n$ force exactly $n$ edges, all of cost 1. Every degree is exactly two, so connectivity makes these edges a single spanning cycle of the source. All partial selected-weight sums are at most the final sum for feasible targets; evaluation therefore preserves the budget test with exact integer arithmetic. _Solution extraction._ Validate the target certificate and require its evaluation to be `Or(true)` before decoding. Walk the selected cycle from vertex 0 to recover the circuit order. The negative sentinel has no feasible certificate. The target has at most $n+3$ vertices, no initial edges, and at most $n(n-1)/2$ candidates. + + _Numeric magnitude._ Candidate costs are at most two and the budget is $n$; for $n<3$, the fixed target has budget zero and no candidates. Thus the target's `max_numeric_magnitude_bits` is at most $n+1$, using only the source vertex count. ] #let hc_sca = load-example("HamiltonianCircuit", "StrongConnectivityAugmentation") @@ -17853,11 +17945,13 @@ The following table shows concrete target-variable counts for example instances, )[ Start with the empty digraph on $n$ vertices. Weight-1 candidate arcs correspond to edges of $G$; weight-2 arcs for non-edges. A budget-$n$ augmentation that achieves strong connectivity must select exactly $n$ weight-1 arcs forming a directed Hamiltonian cycle. ][ - _Construction._ Given $G = (V, E)$ with $n = |V|$. Build $D = (V, emptyset)$. For every ordered pair $(u, v)$ with $u != v$: candidate arc with weight 1 if ${u,v} in E$, else weight 2. Budget $B = n$. + _Construction._ Given $G = (V, E)$ with $n = |V|$. If $n<3$, output two isolated vertices, no candidate arcs, and budget zero; both source and target are infeasible. Otherwise build $D = (V, emptyset)$. For every ordered pair $(u, v)$ with $u != v$: candidate arc with weight 1 if ${u,v} in E$, else weight 2. Budget $B = n$. _Correctness._ ($arrow.r.double$) A Hamiltonian circuit gives $n$ directed arcs of weight 1 forming a strongly-connected cycle. ($arrow.l.double$) Strong connectivity needs $>= n$ arcs; budget $n$ forces all weight 1, hence all from $E$, forming a single $n$-cycle. _Solution extraction._ Follow unique successors from vertex 0 to recover the Hamiltonian permutation. + + _Numeric magnitude._ Candidate costs are at most two and the budget is $n$; the fixed target for $n<3$ has budget zero and no candidates. The target's `max_numeric_magnitude_bits` is therefore at most $n+1$. ] #let hc_sc = load-example("HamiltonianCircuit", "DecisionStackerCrane") @@ -18249,6 +18343,8 @@ The following table shows concrete target-variable counts for example instances, $ Set the knapsack capacity to $B$. The target therefore has the same number of items as the source has elements. + _Numeric magnitude._ The source `max_numeric_magnitude_bits` includes the target sum $B$ and therefore bounds the Integer Knapsack `capacity_bits`. Raw `capacity` remains unavailable in the symbolic contract; its bit-length bound suffices for the downstream ILP size predictions. + _Correctness._ ($arrow.r.double$) If $I subset.eq {1, dots, n}$ satisfies $sum_(i in I) a_i = B$, define multiplicities $c_i = 1$ for $i in I$ and $c_i = 0$ otherwise. Then $ sum_i c_i s_i = sum_(i in I) a_i = B <= B @@ -18826,9 +18922,11 @@ The following table shows concrete target-variable counts for example instances, *Multiplicity:* The fixture stores one canonical witness. ], )[ - This $O(n^2 + m)$ reduction @stockmeyer1973 assigns each variable $x_i$ a distinct prime $p_i >= 5$, encoding TRUE as residue 1 and FALSE as residue 2 modulo $p_i$. All other residues are forbidden. Each clause is encoded via CRT as a single forbidden residue class modulo the product of its variables' primes. A satisfying assignment exists iff some integer avoids all forbidden classes. + This polynomial-time reduction @stockmeyer1973 assigns each variable $x_i$ a distinct prime $p_i >= 3$, encoding TRUE as residue 1 and FALSE as residue 2 modulo $p_i$. All other residues are forbidden. Each clause is encoded via CRT as a single forbidden residue class modulo the product of its variables' primes. A satisfying assignment exists iff some integer avoids all forbidden classes. ][ - _Construction._ Given 3-SAT with $n$ variables and $m$ clauses, assign primes $p_1, dots, p_n >= 5$. For each variable $x_i$, forbid residues ${0, 3, 4, dots, p_i - 1}$ modulo $p_i$, leaving only ${1, 2}$. For each clause $C_j$ over variables $x_(i_1), x_(i_2), x_(i_3)$, compute the falsifying residue $r_k in {1, 2}$ for each literal and use CRT to find $R_j$ with $R_j equiv r_k mod p_(i_k)$ for $k = 1,2,3$. Forbid $R_j$ modulo $M_j = p_(i_1) p_(i_2) p_(i_3)$. + _Construction._ Given 3-SAT with $n$ variables and $m$ clauses, assign primes $p_1, dots, p_n >= 3$. For each variable $x_i$, forbid residues ${0, 3, 4, dots, p_i - 1}$ modulo $p_i$, leaving only ${1, 2}$. For each clause $C_j$ over variables $x_(i_1), x_(i_2), x_(i_3)$, compute the falsifying residue $r_k in {1, 2}$ for each literal and use CRT to find $R_j$ with $R_j equiv r_k mod p_(i_k)$ for $k = 1,2,3$. Forbid $R_j$ modulo $M_j = p_(i_1) p_(i_2) p_(i_3)$. + + _Size bound._ There are $sum_(i=1)^n (p_i-2)+m$ forbidden pairs, where $p_i$ is the $i$th odd prime. For the ordinary $k$th prime $q_k$, the standard estimate $q_k < k(ln k+ln ln k)$ for $k >= 6$ @axler2019, together with the first five primes, implies $q_k <= 2k^2$ for every $k >= 1$. Consequently $p_i=q_(i+1) <= 2(n+1)^2$ and $2n(n+1)^2+m$ bounds the pair count. _Correctness._ ($arrow.r.double$) A satisfying assignment $tau$ defines residues $r_i in {1,2}$ per variable. By CRT, some integer $x$ has these residues. It avoids all variable-forbidden classes and all clause-forbidden classes (since at least one literal is true, the residue triple differs from the falsifying triple). ($arrow.l.double$) Any feasible $x$ has $x mod p_i in {1,2}$ for all $i$. Define $tau(x_i) = "TRUE"$ if residue 1, FALSE if 2. If a clause were false, $x$ would match its forbidden CRT class -- contradiction. @@ -18906,6 +19004,8 @@ The following table shows concrete target-variable counts for example instances, )[ This $O(n)$ specialization of Karp's common-deadline sequencing construction @karp1972 maps each element $a_i$ to a task with processing time and tardy weight $a_i$. For total $S$, use common deadline $B = floor(S / 2)$. A balanced partition exists exactly when the minimum tardy weight is $B$; other optimum values map to false through the formal aggregate reduction. ][ + _Numeric magnitude._ Task lengths copy the source integers, so target `max_processing_time_bits` equals source `max_numeric_magnitude_bits`. + _Construction._ The source has $n >= 1$ positive sizes with checked total $S$. Create $n$ tasks in source order, each with length and weight $a_i$, and deadline $B = floor(S / 2)$. The construction is identical for odd and even totals. Karp's original paper gives Knapsack to Job Sequencing (p. 100), with equal processing times and penalties and a common deadline; the Partition specialization and its optimization certificate are proved here. _Correctness._ Let $E$ be the total size of tasks completing by $B$ in any valid permutation. Positive processing times make these tasks a prefix, so $E <= B$. Because weights equal processing times, tardy weight is $W = S - E >= S - B >= B$. @@ -18962,6 +19062,8 @@ The following table shows concrete target-variable counts for example instances, ][ _Construction._ The source is a nonempty list of positive integers $a_1,...,a_k$, with $S=sum_j a_j$. Set $Q=floor(S/2)$. Use three machines, one job $(a_j,a_j,a_j)$ per element, and one special job $(Q,Q,Q)$. Let $D=3Q$. + _Numeric magnitude._ Let $h$ be the source `max_numeric_magnitude_bits`. The sum $S$ needs at most $h+k$ bits, and the target schedule horizon is $3S+3Q <= 6S$. Thus `schedule_horizon_bits` is at most $h+k+3$. Raw `schedule_horizon` remains unavailable; its bit-length bound is sufficient to compose polynomial size bounds through bounded ILP and QUBO. + _Forward correctness._ Given a balanced partition into groups $A,B$, each group has total size $Q$ and $S=2Q$. Divide time into three phases $[r Q,(r+1)Q)$, $r=0,1,2$. In phase $r$, run the special job on machine $r$, all jobs of $A$ consecutively on machine $(r+1) mod 3$, and all jobs of $B$ consecutively on machine $(r+2) mod 3$. Each group exactly fills its phase, each element job has one operation per phase, and the machine rotation processes it once on every machine. This is a feasible nonpreemptive schedule of makespan $D$. _Backward correctness._ Every schedule has makespan at least $3Q$ by the special job's total processing time, and at least $S+Q$ by each machine's load. If a feasible schedule attains $D$, these bounds give $S+Q<=3Q$; together with $S>=2Q$ this forces $S=2Q$. Since the source sizes are positive, $Q>0$. The special job must run without gaps and start its three operations at $0,Q,2Q$, in some machine order. Select the unique machine on which it starts at $Q$. Element operations on that machine fit before $Q$ or after $2Q$, each interval having capacity $Q$. Their total load is $2Q$, so the jobs completing by $Q$ have total size exactly $Q$ and define a balanced partition. It is the middle machine that supplies these two separate intervals, not an arbitrary fixed machine. @@ -19066,6 +19168,8 @@ The following table shows concrete target-variable counts for example instances, )[ This $O(t^2)$ reduction @garey1979 first checks whether every coordinate of $W$, $X$, and $Y$ appears in some triple; uncovered coordinates yield a fixed infeasible 3-Partition instance. Otherwise it composes the classical 3DM $arrow.r$ ABCD-Partition, ABCD-Partition $arrow.r$ 4-Partition, and 4-Partition $arrow.r$ 3-Partition constructions, producing $24 t^2 - 3 t$ integers arranged into $8 t^2 - t$ triples. ][ + _Numeric magnitude._ For universe size $q>=1$, the nonconstant gadget has partition bound $42949672960 q^4+964<2^36 q^4$. Since $q<=2^q$, target `max_numeric_magnitude_bits` is at most $4q+36$. The fixed feasible and infeasible outputs also satisfy this bound, including $q=0$. + _Construction._ Let the source instance have universe size $q$ and triples $m_l = (w_(a_l), x_(b_l), y_(c_l))$ for $l = 0, dots, t - 1$. If $q=0$, the empty matching is a solution: return sizes $(1,1,1)$ with bound $3$, and recover the empty matching. Otherwise, if some coordinate of $W union X union Y$ is absent from all triples (including $t=0$), return the fixed infeasible instance $(6,6,6,6,7,9)$ with bound $20$. Including these constant cases, $24t^2-3t+6$ elements and $8t^2-t+2$ groups are upper bounds, not exact counts. Otherwise set $r = 32 q$ and $T_1 = 40 r^4$. For each triple create @@ -19213,6 +19317,8 @@ The following table shows concrete target-variable counts for example instances, )[ Each element becomes a unit-length task requiring $a_i$ units of a shared resource with bound $B$. With 3 processors and deadline $m$, every slot receives exactly 3 tasks summing to $B$. ][ + _Numeric magnitude._ Resource requirements copy the element sizes and the resource capacity copies the partition bound. Target `max_resource_bits` therefore equals source `max_numeric_magnitude_bits`. + _Construction._ Given $(S, B)$ with $|S| = 3m$ and $B/4 < a_i < B/2$. Create $3m$ unit-length tasks with resource requirement $r_i = a_i$, $p = 3$ processors, resource bound $B$, deadline $D = m$. _Correctness._ ($arrow.r.double$) A valid 3-partition assigns each triple to a time slot; each slot uses exactly $B$ resource units. ($arrow.l.double$) $3m$ tasks in $m$ slots with $p = 3$: every slot has exactly 3 tasks. Resource bound $B$ with total $m B$: each slot sums to exactly $B$. Size constraints prevent fewer or more than 3 elements per slot. diff --git a/docs/paper/references.bib b/docs/paper/references.bib index 369d309f0..a65104787 100644 --- a/docs/paper/references.bib +++ b/docs/paper/references.bib @@ -2321,3 +2321,14 @@ @techreport{mandersAdleman1976 month = {November}, url = {https://digicoll.lib.berkeley.edu/record/134974/files/ERL-m-615.pdf} } + +@article{axler2019, + author = {Axler, Christian}, + title = {New Estimates for the nth Prime Number}, + journal = {Journal of Integer Sequences}, + volume = {22}, + number = {4}, + pages = {Article 19.4.2}, + year = {2019}, + url = {https://www.maths.tcd.ie/EMIS/journals/JIS/VOL22/Axler/axler17.pdf} +} diff --git a/docs/src/design.md b/docs/src/design.md index cf7a7aa82..5b58f73ff 100644 --- a/docs/src/design.md +++ b/docs/src/design.md @@ -489,6 +489,18 @@ nonzeros. If a rule predicts those dimensions by source expressions `f` and `g`, explicitly declare `num_nonzeros <= f * g`. Such structural bounds remain valid when coefficients cancel; exact sparsity can still require additional source information. +Prefer coarse, sound bounds that use existing source parameters. Before adding a +parameter, check whether an equivalent normalization can remove irrelevant input +magnitudes. New parameters must describe intrinsic source data independently of +any reduction, and their propagation must be audited on incoming rules. Keep +model-specific definitions and rule-specific formulas beside their implementations. + +Avoid registering synonymous aliases. Arithmetic dependence alone does not make a +parameter redundant: keep a meaningful derived quantity when its name makes +formulas clearer or enables useful, sound predictions. Substitute existing +parameters when doing so preserves clarity. Lack of a current formula consumer +alone is not a reason to remove a parameter. + `ReductionParameterDeclarations::fields` stores `(name, relation, expression)` triples. Use `ParameterTransform::relation(field)` to inspect a formula's accuracy and `unavailable(field)` for a composition failure and its upstream cause. The uniform @@ -536,7 +548,7 @@ proved infeasibility, and `Err` reports an operational failure. | Solver | Description | |--------|-------------| | **BruteForce** | Enumerates a registered finite search space and returns an optimal or satisfying solution. Used for testing and verification. | -| **ILPSolver** | Executes a problem's registered ILP pipeline, terminating at the native `ILP` with `bool`/`i64` variables and `i64`/`f64` coefficients. `HighsAdapter` owns numerical conversion, backend settings, termination status, and returned-assignment validation. Integer terminals go directly to the adapter; the explicit integer-to-float reduction remains available but is not part of solver pipelines. Optimality and infeasibility follow HiGHS numerical tolerances; the adapter does not provide exact proofs. | +| **ILPSolver** | Executes a problem's registered ILP pipeline, terminating at the native `ILP` with `bool`/`i64` variables and `i64`/`f64` coefficients. `HighsAdapter` owns numerical conversion, backend settings, termination status, and returned-assignment validation. Integer terminals go directly to the adapter; the explicit integer-to-float reduction remains available but is not part of solver pipelines. Optimality and infeasibility follow HiGHS numerical tolerances; the adapter does not provide exact proofs. | ILP results are optimal or infeasible according to HiGHS numerical tolerances; zero MIP gaps do not imply mathematical exactness. Integer extraction rounds diff --git a/examples/chained_reduction_factoring_to_spinglass.rs b/examples/chained_reduction_factoring_to_spinglass.rs index c07f0faa8..d3a2a6969 100644 --- a/examples/chained_reduction_factoring_to_spinglass.rs +++ b/examples/chained_reduction_factoring_to_spinglass.rs @@ -1,3 +1,4 @@ +use problemreductions::models::algebraic::Bounded; // # Chained Reduction: Factoring -> SpinGlass // // Mirrors Julia's examples/Ising.jl — reduces a Factoring problem @@ -43,9 +44,10 @@ pub fn run() -> std::result::Result<(), Box> { // ANCHOR_END: step2 // ANCHOR: step3 - // Factoring reduces to ILP, so we manually reduce, solve, and extract + // Factoring reduces to ILP, so we manually reduce, solve, and extract let solver = ILPSolver::new(); - let reduction = ReduceTo::>::reduce_to(&factoring).expect("reduction should succeed"); + let reduction = ReduceTo::>::reduce_to(&factoring) + .expect("reduction should succeed"); let ilp_solution = solver.solve(reduction.target_problem()).unwrap(); let solution = reduction.extract_solution(&ilp_solution).unwrap(); // ANCHOR_END: step3 diff --git a/problemreductions-cli/src/create_args.rs b/problemreductions-cli/src/create_args.rs index 0b559465f..27c446fcd 100644 --- a/problemreductions-cli/src/create_args.rs +++ b/problemreductions-cli/src/create_args.rs @@ -238,7 +238,7 @@ fn invalid_problem_spec(command: &Command, message: String) -> Error { } fn canonical_problem_spec(problem: &ProblemType, variant: &BTreeMap) -> String { - let values = problem + let mut values = problem .dimensions .iter() .filter_map(|dimension| { @@ -246,6 +246,20 @@ fn canonical_problem_spec(problem: &ProblemType, variant: &BTreeMap>(); + if values.iter().any(|value| { + problem + .dimensions + .iter() + .filter(|dimension| dimension.allowed_values.contains(value)) + .count() + > 1 + }) { + values = problem + .dimensions + .iter() + .map(|dimension| dimension_value(variant, dimension.key, dimension.default_value)) + .collect(); + } join_spec(problem.canonical_name, &values) } @@ -284,6 +298,19 @@ fn add_value_parser(arg: Arg, kind: crate::commands::create::InputValueKind) -> #[cfg(test)] mod tests { + #[test] + fn canonical_create_specs_resolve_to_the_original_variant() { + let graph = problemreductions::rules::ReductionGraph::new(); + for entry in problemreductions::registry::variant_entries() { + let problem = problemreductions::registry::find_problem_type(entry.name).unwrap(); + let variant = entry.variant_map(); + let spec = super::canonical_problem_spec(&problem, &variant); + let resolved = crate::problem_name::resolve_problem_ref(&spec, &graph) + .unwrap_or_else(|error| panic!("{spec}: {error}")); + assert_eq!(resolved.variant, variant, "{spec}"); + } + } + #[test] fn decision_create_help_includes_field_descriptions_and_bound_direction() { for (spec, direction) in [("DecisionMaxCut", ">="), ("DecisionQUBO", "<=")] { diff --git a/problemreductions-cli/src/dispatch.rs b/problemreductions-cli/src/dispatch.rs index 883ebc1ca..cebb9591b 100644 --- a/problemreductions-cli/src/dispatch.rs +++ b/problemreductions-cli/src/dispatch.rs @@ -853,7 +853,7 @@ mod tests { let route = crate::commands::reduce::parse_path_json( r#"{"path":[{ "from":{"name":"ExpectedRetrievalCost","variant":{}}, - "to":{"name":"ILP","variant":{"coefficient":"f64","variable":"bool"}} + "to":{"name":"ILP","variant":{"coefficient":"f64","variable":"bool","bounds":"general"}} }]}"#, ) .unwrap(); diff --git a/problemreductions-cli/src/problem_name.rs b/problemreductions-cli/src/problem_name.rs index 1c3057466..96035a9eb 100644 --- a/problemreductions-cli/src/problem_name.rs +++ b/problemreductions-cli/src/problem_name.rs @@ -156,19 +156,21 @@ fn resolve_variant_updates( let problem = problemreductions::registry::find_problem_type(&spec.name) .expect("registered problem has a schema"); - if spec.variant_values.len() == problem.dimensions.len() + if spec.variant_values.len() <= problem.dimensions.len() && problem .dimensions .iter() .zip(&spec.variant_values) .all(|(dimension, value)| dimension.allowed_values.contains(&value.as_str())) { - let resolved = problem - .dimensions - .iter() - .zip(&spec.variant_values) - .map(|(dimension, value)| (dimension.key.to_string(), value.clone())) - .collect(); + let mut resolved = default_variant.clone(); + resolved.extend( + problem + .dimensions + .iter() + .zip(&spec.variant_values) + .map(|(dimension, value)| (dimension.key.to_string(), value.clone())), + ); anyhow::ensure!( known_variants.contains(&resolved), "Resolved variant {} is not declared for {}", @@ -406,10 +408,29 @@ mod tests { let resolved = resolve_problem_ref("ILP/bool/i64", &graph).unwrap(); assert_eq!(resolved.variant["variable"], "bool"); assert_eq!(resolved.variant["coefficient"], "i64"); + assert_eq!(resolved.variant["bounds"], "general"); assert_eq!( crate::commands::graph::variant_to_full_slash("ILP", &resolved.variant), - "/bool/i64" + "/bool/i64/general" + ); + } + + #[test] + fn ilp_bounds_default_to_general_without_registering_unused_combinations() { + let graph = problemreductions::rules::ReductionGraph::new(); + for spec in ["ILP/i64", "ILP/i64/i64", "ILP/variable=i64"] { + let resolved = resolve_problem_ref(spec, &graph).unwrap(); + assert_eq!(resolved.variant["variable"], "i64"); + assert_eq!(resolved.variant["bounds"], "general"); + } + assert_eq!( + resolve_problem_ref("ILP/i64/i64/bounded", &graph) + .unwrap() + .variant["bounds"], + "bounded" ); + assert!(resolve_problem_ref("ILP/i64/f64/bounded", &graph).is_err()); + assert!(resolve_problem_ref("ILP/bool/i64/bounded", &graph).is_err()); } #[test] diff --git a/problemreductions-cli/src/test_support.rs b/problemreductions-cli/src/test_support.rs index a955d2442..88c3c941d 100644 --- a/problemreductions-cli/src/test_support.rs +++ b/problemreductions-cli/src/test_support.rs @@ -431,6 +431,10 @@ problemreductions::inventory::submit! { parameter_declarations_fn: || ReductionParameterDeclarations { fields: vec![], unavailable: vec![ + problemreductions::rules::registry::UnavailableParameterField { + field: "max_constraint_magnitude_bits", + reason: "the synthetic aggregate-to-ILP reduction has no parameter model", + }, problemreductions::rules::registry::UnavailableParameterField { field: "num_vars", reason: "the synthetic aggregate-to-ILP reduction has no parameter model", diff --git a/problemreductions-cli/tests/cli_tests.rs b/problemreductions-cli/tests/cli_tests.rs index e5641a0a5..899189d80 100644 --- a/problemreductions-cli/tests/cli_tests.rs +++ b/problemreductions-cli/tests/cli_tests.rs @@ -3588,7 +3588,7 @@ fn test_solve_bundle_ilp() { } #[test] -fn test_solve_direct_ilp_i64_problem() { +fn test_solve_direct_bounded_integer_ilp_problem() { let problem_file = std::env::temp_dir().join("pred_test_solve_ilp_i64_problem.json"); let create_out = pred() @@ -3599,7 +3599,7 @@ fn test_solve_direct_ilp_i64_problem() { "--example", "SequencingToMinimizeWeightedCompletionTime", "--to", - "ILP/variable=i64", + "ILP/variable=i64/bounds=bounded", "--example-side", "target", ]) @@ -5536,12 +5536,31 @@ fn test_path_set_has_explicit_parameter_information() { #[test] fn test_path_overall_unavailable_is_reported_per_field_without_internal_modes() { let output = pred() - .args(["path", "Factoring", "SpinGlass", "--json"]) + .args([ + "path", + "ThreePartition", + "QUBO/i64", + "--limit", + "2", + "--json", + ]) .output() .unwrap(); assert!(output.status.success()); let envelope: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap(); - let overall = &envelope["paths"][0]["overall_parameters"]; + let path = envelope["paths"] + .as_array() + .unwrap() + .iter() + .find(|path| { + path["path"] + .as_array() + .unwrap() + .iter() + .any(|step| step["from"]["name"] == "SequencingWithReleaseTimesAndDeadlines") + }) + .expect("time-indexed scheduling path exists"); + let overall = &path["overall_parameters"]; let fields = overall["fields"].as_array().unwrap(); assert!(!fields.is_empty()); assert!(fields.iter().all(|field| { @@ -5601,14 +5620,7 @@ fn test_path_preserves_exact_variables_and_bounded_quadratic_terms() { #[test] fn test_path_overall_preserves_unavailable_fields_alongside_exact_fields() { let output = pred() - .args([ - "path", - "HighlyConnectedDeletion", - "ILP/bool", - "--limit", - "1", - "--json", - ]) + .args(["path", "Partition", "Knapsack", "--limit", "1", "--json"]) .output() .unwrap(); assert!(output.status.success()); @@ -5625,12 +5637,17 @@ fn test_path_overall_preserves_unavailable_fields_alongside_exact_fields() { ) }) .collect::>(); - assert_eq!(relations["num_constraints"], "exact"); - assert_eq!(relations["num_vars"], "unavailable"); + assert_eq!(relations["num_items"], "exact"); + assert_eq!(relations["capacity"], "unavailable"); + let unavailable = fields + .iter() + .find(|field| field["relation"] == "unavailable") + .unwrap(); + assert!(!unavailable["reason"].as_str().unwrap().is_empty()); } #[test] -fn test_path_overall_unavailable_reason_explains_unsupported_bound() { +fn test_path_highly_connected_deletion_has_complete_polynomial_predictions() { let output = pred() .args(["path", "HighlyConnectedDeletion", "ILP/bool", "--json"]) .output() @@ -5644,11 +5661,15 @@ fn test_path_overall_unavailable_reason_explains_unsupported_bound() { .map(|field| (field["field"].as_str().unwrap(), field)) .collect::>(); - assert_eq!(fields["num_vars"]["relation"], "unavailable"); - assert!(fields["num_vars"]["reason"] - .as_str() - .unwrap() - .contains("variable exponent unsupported")); + assert_eq!(fields.len(), 4); + assert_eq!(fields["num_vars"]["relation"], "exact"); + for field in [ + "num_constraints", + "num_nonzeros", + "max_constraint_magnitude_bits", + ] { + assert_eq!(fields[field]["relation"], "upper_bound"); + } } #[test] @@ -7008,7 +7029,8 @@ fn test_inspect_integral_flow_with_multipliers_reports_parameters() { assert!(parameters.contains(&"num_vertices")); assert!(parameters.contains(&"num_arcs")); assert!(parameters.contains(&"max_capacity")); - assert!(parameters.contains(&"requirement")); + assert!(parameters.contains(&"max_capacity_bits")); + assert_eq!(json["parameter_values"]["max_capacity_bits"], 3); std::fs::remove_file(&problem_file).ok(); std::fs::remove_file(&result_file).ok(); @@ -9750,13 +9772,17 @@ fn test_extract_rejects_infeasible_target_even_when_decoded_source_is_feasible() use serde_json::json; let source = OpenShopScheduling::new(1, vec![vec![1]]); - let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + let reduction = + ReduceTo::>::reduce_to( + &source, + ) + .unwrap(); let bundle = std::env::temp_dir().join(format!( "pred-extract-target-feasibility-{}.json", std::process::id() )); let source_key = json!({"name":"OpenShopScheduling","variant":{}}); - let target_variant = json!({"variable":"i64","coefficient":"i64"}); + let target_variant = json!({"variable":"i64","coefficient":"i64","bounds":"bounded"}); std::fs::write( &bundle, json!({ diff --git a/src/example_db/specs.rs b/src/example_db/specs.rs index 75f2210f4..39d9dd8e8 100644 --- a/src/example_db/specs.rs +++ b/src/example_db/specs.rs @@ -4,6 +4,9 @@ //! that can be validated against the catalog and reduction registry. use crate::export::{ProblemSide, RuleExample, SolutionPair}; +use crate::models::algebraic::{ + Bounded, BoundsPolicy, General, ILPCoefficient, VariableDomain, ILP, +}; use crate::prelude::{Problem, ReduceTo, ReductionResult}; use crate::registry::DynProblem; use serde::Serialize; @@ -70,34 +73,43 @@ where /// the double `reduce_to()` that would occur with `rule_example_with_witness`. pub fn rule_example_via_ilp(source: S) -> RuleExample where - S: Problem + Serialize + ReduceTo>, - V: crate::models::algebraic::VariableDomain, - >>::Result: - ReductionResult>, + S: Problem + Serialize + ReduceTo>, + V: VariableDomain, + >>::Result: ReductionResult>, S::Solution: Serialize, { - rule_example_via_typed_ilp::(source) + rule_example_via_typed_ilp::(source) +} + +/// Integer ILP example with explicit finite variable domains. +pub fn rule_example_via_bounded_ilp(source: S) -> RuleExample +where + S: Problem + Serialize + ReduceTo>, + >>::Result: + ReductionResult>, + S::Solution: Serialize, +{ + rule_example_via_typed_ilp::(source) } /// Float-coefficient counterpart of [`rule_example_via_ilp`]. pub fn rule_example_via_float_ilp(source: S) -> RuleExample where - S: Problem + Serialize + ReduceTo>, - V: crate::models::algebraic::VariableDomain, - >>::Result: - ReductionResult>, + S: Problem + Serialize + ReduceTo>, + V: VariableDomain, + >>::Result: ReductionResult>, S::Solution: Serialize, { - rule_example_via_typed_ilp::(source) + rule_example_via_typed_ilp::(source) } -fn rule_example_via_typed_ilp(source: S) -> RuleExample +fn rule_example_via_typed_ilp(source: S) -> RuleExample where - S: Problem + Serialize + ReduceTo>, - V: crate::models::algebraic::VariableDomain, - C: crate::models::algebraic::ILPCoefficient + Serialize, - >>::Result: - ReductionResult>, + S: Problem + Serialize + ReduceTo>, + V: VariableDomain, + C: ILPCoefficient + Serialize, + B: BoundsPolicy, + >>::Result: ReductionResult>, S::Solution: Serialize, { use crate::export::SolutionPair; diff --git a/src/lib.rs b/src/lib.rs index afeac7aa1..1c70132cb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -159,3 +159,7 @@ mod test_reduction_graph; #[cfg(test)] #[path = "unit_tests/unitdiskmapping_algorithms/mod.rs"] mod test_unitdiskmapping_algorithms; + +#[cfg(test)] +#[path = "unit_tests/ilp_overhead.rs"] +mod ilp_overhead; diff --git a/src/models/algebraic/closest_vector_problem.rs b/src/models/algebraic/closest_vector_problem.rs index bce15f48f..2153e3a3f 100644 --- a/src/models/algebraic/closest_vector_problem.rs +++ b/src/models/algebraic/closest_vector_problem.rs @@ -86,6 +86,13 @@ impl ClosestVectorProblem { self.target.len() } + /// Maximum bit length of an absolute basis or target entry, at least one. + pub fn max_numeric_magnitude_bits(&self) -> u64 { + crate::types::max_numeric_magnitude_bits( + self.basis.iter().flatten().chain(&self.target).copied(), + ) + } + /// Integer basis columns. pub fn basis(&self) -> &[Vec] { &self.basis @@ -167,6 +174,7 @@ impl Problem for ClosestVectorProblem { crate::problem_parameters![ ("ambient_dimension", ambient_dimension), ("num_basis_vectors", num_basis_vectors), + ("max_numeric_magnitude_bits", max_numeric_magnitude_bits), ]; fn evaluate(&self, solution: &Self::Solution) -> Result, EvaluationError> { diff --git a/src/models/algebraic/ilp.rs b/src/models/algebraic/ilp.rs index 7f4111e35..13c5a64c7 100644 --- a/src/models/algebraic/ilp.rs +++ b/src/models/algebraic/ilp.rs @@ -21,6 +21,7 @@ inventory::submit! { dimensions: &[ VariantDimension::new("variable", "bool", &["bool", "i64"]), VariantDimension::new("coefficient", "i64", &["i64", "f64"]), + VariantDimension::new("bounds", "general", &["general", "bounded"]), ], category: crate::registry::ProblemCategory::Algebraic, module_path: module_path!(), @@ -46,6 +47,46 @@ pub trait VariableDomain: 'static + Clone + Debug + Send + Sync { fn validate_variables(variables: &[IntegerVariable]) -> Result<(), ConstructionError>; } +/// Type-level requirement on the explicitly stored variable intervals. +pub trait BoundsPolicy: 'static + Clone + Debug + Send + Sync { + /// Registered bounds dimension value. + const NAME: &'static str; + /// Validate the interval requirement independently of the variable domain. + fn validate_variables(variables: &[IntegerVariable]) -> Result<(), ConstructionError>; +} + +/// Variable intervals may have infinite endpoints. +#[derive(Debug, Clone, Copy)] +pub struct General; + +impl BoundsPolicy for General { + const NAME: &'static str = "general"; + + fn validate_variables(_variables: &[IntegerVariable]) -> Result<(), ConstructionError> { + Ok(()) + } +} + +/// Every variable has explicit finite lower and upper bounds. +#[derive(Debug, Clone, Copy)] +pub struct Bounded; + +impl BoundsPolicy for Bounded { + const NAME: &'static str = "bounded"; + + fn validate_variables(variables: &[IntegerVariable]) -> Result<(), ConstructionError> { + if variables + .iter() + .any(|variable| variable.lower_bound().is_none() || variable.upper_bound().is_none()) + { + return Err(ConstructionError::Conversion( + "bounded ILP requires finite lower and upper bounds for every variable".into(), + )); + } + Ok(()) + } +} + /// Numeric domain shared by an ILP's constraints, right-hand sides, and objective. pub trait ILPCoefficient: NumericSize + WeightElement + Copy + Debug + Send + Sync @@ -318,13 +359,13 @@ pub enum ObjectiveSense { /// Integer Linear Programming model. #[derive(Debug, Clone, Serialize)] -pub struct ILP { +pub struct ILP { variables: Vec, constraints: Vec>, objective: Vec<(usize, C)>, sense: ObjectiveSense, #[serde(skip)] - marker: PhantomData, + marker: PhantomData<(V, B)>, } #[derive(Deserialize)] @@ -335,10 +376,11 @@ struct ILPData { sense: ObjectiveSense, } -impl<'de, V, C> Deserialize<'de> for ILP +impl<'de, V, C, B> Deserialize<'de> for ILP where V: VariableDomain, C: ILPCoefficient + Deserialize<'de>, + B: BoundsPolicy, { fn deserialize(deserializer: D) -> Result where @@ -350,9 +392,10 @@ where } } -impl ILP { +impl ILP { /// Construct a homogeneous model using the domain certificate's standard /// variable interval: binary `[0, 1]` or integer `[0, +∞)`. + /// Bounded integer models with variables must use [`Self::with_variables`]. pub fn new( num_variables: usize, constraints: Vec>, @@ -375,6 +418,7 @@ impl ILP { sense: ObjectiveSense, ) -> Result { V::validate_variables(&variables)?; + B::validate_variables(&variables)?; let num_variables = variables.len(); let constraints = constraints .into_iter() @@ -440,6 +484,28 @@ impl ILP { .sum() } + /// Smallest `h >= 1` for which every constraint coefficient, RHS and finite + /// variable endpoint has magnitude strictly below `2^h`. + /// + /// Measures normalized feasible-set data, excluding the objective. Infinite + /// endpoints are not numbers in this maximum; this parameter does not + /// certify boundedness. For floating coefficients it measures magnitude, + /// not mantissa precision. + pub fn max_constraint_magnitude_bits(&self) -> u64 { + let row_bits = + crate::types::max_numeric_magnitude_bits(self.constraints.iter().flat_map(|row| { + std::iter::once(row.rhs) + .chain(row.terms.iter().map(|&(_, coefficient)| coefficient)) + })); + let endpoint_bits = crate::types::max_numeric_magnitude_bits( + self.variables + .iter() + .flat_map(|variable| [variable.lower_bound, variable.upper_bound]) + .flatten(), + ); + row_bits.max(endpoint_bits) + } + /// Evaluate the objective in the coefficient domain. pub fn evaluate_objective(&self, values: &[i64]) -> Result { self.objective @@ -578,12 +644,16 @@ fn normalize_objective( Ok(normalized) } -impl Problem for ILP { +impl Problem for ILP { const NAME: &'static str = "ILP"; type Solution = Vec; type Value = Extremum; crate::problem_parameters![ + ( + "max_constraint_magnitude_bits", + max_constraint_magnitude_bits + ), ("num_constraints", num_constraints), ("num_nonzeros", num_nonzeros), ("num_vars", num_vars), @@ -604,39 +674,60 @@ impl Problem for ILP { } fn variant() -> Vec<(&'static str, &'static str)> { - vec![("variable", V::NAME), ("coefficient", C::NAME)] + vec![ + ("variable", V::NAME), + ("coefficient", C::NAME), + ("bounds", B::NAME), + ] } } crate::declare_variants! { default ILP => "2^num_vars", ILP => "num_vars^num_vars", + ILP => "num_vars^num_vars", ILP => "2^num_vars", ILP => "num_vars^num_vars", } #[cfg(feature = "example-db")] pub(crate) fn canonical_model_example_specs() -> Vec { - vec![crate::example_db::specs::ModelExampleSpec { - id: "ilp", - instance: Box::new( - ILP::::new( - 2, - vec![ - LinearConstraint::le(vec![(0, 1), (1, 1)], 5), - LinearConstraint::le(vec![(0, 4), (1, 7)], 28), - ], - vec![(0, -5), (1, -6)], - ObjectiveSense::Minimize, - ) - .expect("canonical ILP construction must succeed"), - ), - optimal_config: serde_json::json!(vec![3, 2]), - optimal_value: serde_json::json!({ - "sense": "Minimize", - "value": -27, - }), - }] + vec![ + crate::example_db::specs::ModelExampleSpec { + id: "ilp", + instance: Box::new( + ILP::::new( + 2, + vec![ + LinearConstraint::le(vec![(0, 1), (1, 1)], 5), + LinearConstraint::le(vec![(0, 4), (1, 7)], 28), + ], + vec![(0, -5), (1, -6)], + ObjectiveSense::Minimize, + ) + .expect("canonical ILP construction must succeed"), + ), + optimal_config: serde_json::json!(vec![3, 2]), + optimal_value: serde_json::json!({ + "sense": "Minimize", + "value": -27, + }), + }, + crate::example_db::specs::ModelExampleSpec { + id: "bounded_ilp", + instance: Box::new( + ILP::::with_variables( + vec![IntegerVariable::new(Some(-2), Some(3)).unwrap()], + vec![LinearConstraint::le(vec![(0, 1)], 1)], + vec![(0, 3)], + ObjectiveSense::Maximize, + ) + .unwrap(), + ), + optimal_config: serde_json::json!([1]), + optimal_value: serde_json::json!({"sense": "Maximize", "value": 3}), + }, + ] } #[cfg(test)] diff --git a/src/models/algebraic/mod.rs b/src/models/algebraic/mod.rs index 9568a0a8e..c4b7393c9 100644 --- a/src/models/algebraic/mod.rs +++ b/src/models/algebraic/mod.rs @@ -47,8 +47,8 @@ pub use consecutive_ones_submatrix::ConsecutiveOnesSubmatrix; pub use equilibrium_point::EquilibriumPoint; pub use feasible_basis_extension::FeasibleBasisExtension; pub use ilp::{ - Comparison, ILPCoefficient, IntegerVariable, LinearConstraint, ObjectiveSense, VariableDomain, - ILP, + Bounded, BoundsPolicy, Comparison, General, ILPCoefficient, IntegerVariable, LinearConstraint, + ObjectiveSense, VariableDomain, ILP, }; pub use minimum_matrix_cover::MinimumMatrixCover; pub use minimum_matrix_domination::MinimumMatrixDomination; diff --git a/src/models/graph/acyclic_partition.rs b/src/models/graph/acyclic_partition.rs index b56ec16c6..219eed6d4 100644 --- a/src/models/graph/acyclic_partition.rs +++ b/src/models/graph/acyclic_partition.rs @@ -219,6 +219,17 @@ impl AcyclicPartition { !W::IS_UNIT } + /// Smallest h >= 1 bounding vertex weights, arc costs, and both budgets in magnitude by 2^h. + pub fn max_numeric_magnitude_bits(&self) -> u64 { + crate::types::max_numeric_magnitude_bits( + self.vertex_weights + .iter() + .chain(&self.arc_costs) + .map(|value| value.to_sum()) + .chain([self.weight_bound.clone(), self.cost_bound.clone()]), + ) + } + /// Get the number of vertices. pub fn num_vertices(&self) -> usize { self.graph.num_vertices() @@ -253,7 +264,11 @@ where type Solution = Vec; type Value = crate::types::Or; - crate::problem_parameters![("num_arcs", num_arcs), ("num_vertices", num_vertices),]; + crate::problem_parameters![ + ("max_numeric_magnitude_bits", max_numeric_magnitude_bits), + ("num_arcs", num_arcs), + ("num_vertices", num_vertices), + ]; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![W] diff --git a/src/models/graph/biconnectivity_augmentation.rs b/src/models/graph/biconnectivity_augmentation.rs index ce58f773f..391240d3b 100644 --- a/src/models/graph/biconnectivity_augmentation.rs +++ b/src/models/graph/biconnectivity_augmentation.rs @@ -181,6 +181,16 @@ impl BiconnectivityAugmentation { &self.budget } + /// Smallest h >= 1 bounding candidate-weight and budget magnitudes strictly by 2^h. + pub fn max_numeric_magnitude_bits(&self) -> u64 { + crate::types::max_numeric_magnitude_bits( + self.potential_weights + .iter() + .map(|(_, _, weight)| weight.to_sum()) + .chain(std::iter::once(self.budget.clone())), + ) + } + /// Get the number of vertices in the underlying graph. pub fn num_vertices(&self) -> usize { self.graph.num_vertices() @@ -251,6 +261,7 @@ where type Value = crate::types::Or; crate::problem_parameters![ + ("max_numeric_magnitude_bits", max_numeric_magnitude_bits), ("num_edges", num_edges), ("num_potential_edges", num_potential_edges), ("num_vertices", num_vertices), diff --git a/src/models/graph/bounded_component_spanning_forest.rs b/src/models/graph/bounded_component_spanning_forest.rs index 4b0f3a30b..4671ac06d 100644 --- a/src/models/graph/bounded_component_spanning_forest.rs +++ b/src/models/graph/bounded_component_spanning_forest.rs @@ -161,6 +161,16 @@ impl BoundedComponentSpanningForest { &self.max_weight } + /// Smallest h >= 1 with every vertex weight and the component weight limit below 2^h. + pub fn max_weight_bits(&self) -> u64 { + crate::types::max_numeric_magnitude_bits( + self.weights + .iter() + .map(|weight| weight.to_sum()) + .chain(std::iter::once(self.max_weight.clone())), + ) + } + /// Get the number of vertices in the underlying graph. pub fn num_vertices(&self) -> usize { self.graph.num_vertices() @@ -263,6 +273,7 @@ where type Value = crate::types::Or; crate::problem_parameters![ + ("max_weight_bits", max_weight_bits), ("max_components", max_components), ("num_edges", num_edges), ("num_vertices", num_vertices), diff --git a/src/models/graph/highly_connected_deletion.rs b/src/models/graph/highly_connected_deletion.rs index d1f7fca64..436eb6f7c 100644 --- a/src/models/graph/highly_connected_deletion.rs +++ b/src/models/graph/highly_connected_deletion.rs @@ -345,71 +345,6 @@ pub(crate) fn canonical_model_example_specs() -> Vec(graph: &G, vertices: &[usize]) -> bool { - let size = vertices.len(); - if size == 0 { - return false; - } - if size == 1 { - return true; - } - if size == 2 { - return false; - } - - // Build induced-subgraph adjacency restricted to `vertices`. - let n = graph.num_vertices(); - let in_subset: HashSet = vertices.iter().copied().collect(); - let mut adj: Vec> = vec![Vec::new(); n]; - for (u, v) in graph.edges() { - if in_subset.contains(&u) && in_subset.contains(&v) { - adj[u].push(v); - adj[v].push(u); - } - } - - // The induced subgraph must itself be connected (a single component). - let mut visited: HashSet = HashSet::new(); - let start = vertices[0]; - let mut queue: VecDeque = VecDeque::new(); - queue.push_back(start); - visited.insert(start); - while let Some(u) = queue.pop_front() { - for &w in &adj[u] { - if !visited.contains(&w) { - visited.insert(w); - queue.push_back(w); - } - } - } - if visited.len() != size { - return false; - } - - // Strict inequality: λ(G[S]) > |S| / 2, equivalently 2 * λ > |S|. - let lambda = edge_connectivity(vertices, &adj); - 2 * lambda > size -} - -/// Count the number of induced edges of `graph` whose endpoints both lie -/// inside `vertices`. -pub(crate) fn induced_edge_count(graph: &G, vertices: &[usize]) -> usize { - let in_subset: HashSet = vertices.iter().copied().collect(); - graph - .edges() - .into_iter() - .filter(|(u, v)| in_subset.contains(u) && in_subset.contains(v)) - .count() -} - #[cfg(test)] #[path = "../../unit_tests/models/graph/highly_connected_deletion.rs"] mod tests; diff --git a/src/models/graph/integral_flow_bundles.rs b/src/models/graph/integral_flow_bundles.rs index 8e76d423c..61fe71d5c 100644 --- a/src/models/graph/integral_flow_bundles.rs +++ b/src/models/graph/integral_flow_bundles.rs @@ -224,6 +224,11 @@ impl IntegralFlowBundles { &self.bundle_capacities } + /// Maximum bit length of the bundle capacities, with a minimum of one. + pub fn max_capacity_bits(&self) -> u64 { + crate::types::max_numeric_magnitude_bits(self.bundle_capacities.iter().copied()) + } + /// Get the required net inflow at the sink. pub fn requirement(&self) -> i64 { self.requirement @@ -350,6 +355,7 @@ impl Problem for IntegralFlowBundles { type Value = crate::types::Or; crate::problem_parameters![ + ("max_capacity_bits", max_capacity_bits), ("num_arcs", num_arcs), ("num_bundles", num_bundles), ("num_vertices", num_vertices), diff --git a/src/models/graph/integral_flow_with_multipliers.rs b/src/models/graph/integral_flow_with_multipliers.rs index bcb3bc0bd..42fc7169d 100644 --- a/src/models/graph/integral_flow_with_multipliers.rs +++ b/src/models/graph/integral_flow_with_multipliers.rs @@ -206,6 +206,11 @@ impl IntegralFlowWithMultipliers { self.requirement } + /// Maximum bit length of the arc capacities, with a minimum of one. + pub fn max_capacity_bits(&self) -> u64 { + crate::types::max_numeric_magnitude_bits(self.capacities.iter().copied()) + } + pub fn num_vertices(&self) -> usize { self.graph.num_vertices() } @@ -296,9 +301,9 @@ impl Problem for IntegralFlowWithMultipliers { crate::problem_parameters![ ("max_capacity", max_capacity), + ("max_capacity_bits", max_capacity_bits), ("num_arcs", num_arcs), ("num_vertices", num_vertices), - ("requirement", requirement), ]; fn evaluate( diff --git a/src/models/graph/longest_circuit.rs b/src/models/graph/longest_circuit.rs index e98895bb7..83d6d0ac7 100644 --- a/src/models/graph/longest_circuit.rs +++ b/src/models/graph/longest_circuit.rs @@ -181,6 +181,13 @@ impl LongestCircuit { self.edge_lengths.clone() } + /// Smallest h >= 1 with every edge length strictly below 2^h. + pub fn max_length_bits(&self) -> u64 { + crate::types::max_numeric_magnitude_bits( + self.edge_lengths.iter().map(|length| length.to_sum()), + ) + } + /// Get the number of vertices in the graph. pub fn num_vertices(&self) -> usize { self.graph.num_vertices() @@ -211,7 +218,11 @@ where type Solution = Vec; type Value = Max; - crate::problem_parameters![("num_edges", num_edges), ("num_vertices", num_vertices),]; + crate::problem_parameters![ + ("max_length_bits", max_length_bits), + ("num_edges", num_edges), + ("num_vertices", num_vertices), + ]; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![G, W] diff --git a/src/models/graph/min_max_multicenter.rs b/src/models/graph/min_max_multicenter.rs index 661fa6c51..880c598cd 100644 --- a/src/models/graph/min_max_multicenter.rs +++ b/src/models/graph/min_max_multicenter.rs @@ -236,6 +236,16 @@ impl MinMaxMulticenter { self.k } + /// Smallest h >= 1 with every vertex weight and edge length strictly below 2^h. + pub fn max_numeric_magnitude_bits(&self) -> u64 { + crate::types::max_numeric_magnitude_bits( + self.vertex_weights + .iter() + .chain(&self.edge_lengths) + .map(|value| value.to_sum()), + ) + } + /// Get the number of vertices in the underlying graph. pub fn num_vertices(&self) -> usize { self.graph().num_vertices() @@ -258,11 +268,11 @@ impl MinMaxMulticenter { /// Correct because all edge lengths are non-negative. /// /// Returns `None` if any vertex is unreachable from all centers. - fn shortest_distances(&self, config: &[bool]) -> Option> { + fn shortest_distances( + &self, + config: &[bool], + ) -> Result>, crate::traits::EvaluationError> { let n = self.graph.num_vertices(); - if config.len() != n { - return None; - } let edges = self.graph.edges(); let mut adj: Vec> = vec![Vec::new(); n]; @@ -312,7 +322,11 @@ impl MinMaxMulticenter { if visited[next] { continue; } - let new_dist = du.clone() + len.clone(); + let new_dist = W::checked_add_to_sum( + du.clone(), + len.clone(), + "adding min-max multicenter path lengths", + )?; let update = match &dist[next] { None => true, Some(d) => new_dist < *d, @@ -323,7 +337,7 @@ impl MinMaxMulticenter { } } - dist.into_iter().collect() + Ok(dist.into_iter().collect()) } } @@ -336,7 +350,11 @@ where type Solution = Vec; type Value = Min; - crate::problem_parameters![("num_edges", num_edges), ("num_vertices", num_vertices),]; + crate::problem_parameters![ + ("max_numeric_magnitude_bits", max_numeric_magnitude_bits), + ("num_edges", num_edges), + ("num_vertices", num_vertices), + ]; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![G, W] @@ -360,7 +378,7 @@ where } // Compute shortest distances to nearest center - let distances = match self.shortest_distances(config) { + let distances = match self.shortest_distances(config)? { Some(d) => d, None => { return Ok(Min(None)); diff --git a/src/models/graph/minimum_capacitated_spanning_tree.rs b/src/models/graph/minimum_capacitated_spanning_tree.rs index 8ce48e7d5..ad014ba6d 100644 --- a/src/models/graph/minimum_capacitated_spanning_tree.rs +++ b/src/models/graph/minimum_capacitated_spanning_tree.rs @@ -227,6 +227,16 @@ impl MinimumCapacitatedSpanningTree { &self.capacity } + /// Smallest h >= 1 bounding vertex requirements and capacity in magnitude strictly by 2^h. + pub fn max_requirement_bits(&self) -> u64 { + crate::types::max_numeric_magnitude_bits( + self.requirements + .iter() + .map(|value| value.to_sum()) + .chain(std::iter::once(self.capacity.clone())), + ) + } + /// Get the number of vertices in the underlying graph. pub fn num_vertices(&self) -> usize { self.graph.num_vertices() @@ -381,7 +391,11 @@ where type Solution = Vec; type Value = Min; - crate::problem_parameters![("num_edges", num_edges), ("num_vertices", num_vertices),]; + crate::problem_parameters![ + ("max_requirement_bits", max_requirement_bits), + ("num_edges", num_edges), + ("num_vertices", num_vertices), + ]; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![G, W] diff --git a/src/models/graph/multiple_choice_branching.rs b/src/models/graph/multiple_choice_branching.rs index b04671ee2..e10c858d4 100644 --- a/src/models/graph/multiple_choice_branching.rs +++ b/src/models/graph/multiple_choice_branching.rs @@ -193,6 +193,16 @@ impl MultipleChoiceBranching { &self.threshold } + /// Smallest h >= 1 bounding arc weights and the threshold in magnitude strictly by 2^h. + pub fn max_weight_bits(&self) -> u64 { + crate::types::max_numeric_magnitude_bits( + self.weights + .iter() + .map(|weight| weight.to_sum()) + .chain(std::iter::once(self.threshold.clone())), + ) + } + /// Get the number of vertices. pub fn num_vertices(&self) -> usize { self.graph.num_vertices() @@ -232,6 +242,7 @@ where type Value = crate::types::Or; crate::problem_parameters![ + ("max_weight_bits", max_weight_bits), ("num_vertices", num_vertices), ("num_arcs", num_arcs), ("num_partition_groups", num_partition_groups), diff --git a/src/models/graph/shortest_weight_constrained_path.rs b/src/models/graph/shortest_weight_constrained_path.rs index b95ce34d8..7715caa35 100644 --- a/src/models/graph/shortest_weight_constrained_path.rs +++ b/src/models/graph/shortest_weight_constrained_path.rs @@ -267,6 +267,16 @@ impl ShortestWeightConstrainedPath { !N::IS_UNIT } + /// Smallest h >= 1 with every edge weight and the weight limit strictly below 2^h. + pub fn max_weight_bits(&self) -> u64 { + crate::types::max_numeric_magnitude_bits( + self.edge_weights + .iter() + .map(|weight| weight.to_sum()) + .chain(std::iter::once(self.weight_bound.clone())), + ) + } + /// Get the number of vertices in the graph. pub fn num_vertices(&self) -> usize { self.graph.num_vertices() @@ -341,7 +351,11 @@ where type Solution = Vec; type Value = Min; - crate::problem_parameters![("num_edges", num_edges), ("num_vertices", num_vertices),]; + crate::problem_parameters![ + ("max_weight_bits", max_weight_bits), + ("num_edges", num_edges), + ("num_vertices", num_vertices), + ]; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![G, N] diff --git a/src/models/graph/strong_connectivity_augmentation.rs b/src/models/graph/strong_connectivity_augmentation.rs index 83c2c6cdf..5fbe8e943 100644 --- a/src/models/graph/strong_connectivity_augmentation.rs +++ b/src/models/graph/strong_connectivity_augmentation.rs @@ -124,6 +124,16 @@ impl StrongConnectivityAugmentation { &self.bound } + /// Smallest h >= 1 bounding candidate-weight and budget magnitudes strictly by 2^h. + pub fn max_numeric_magnitude_bits(&self) -> u64 { + crate::types::max_numeric_magnitude_bits( + self.candidate_arcs + .iter() + .map(|(_, _, weight)| weight.to_sum()) + .chain(std::iter::once(self.bound.clone())), + ) + } + /// Get the number of vertices in the base graph. pub fn num_vertices(&self) -> usize { self.graph.num_vertices() @@ -187,6 +197,7 @@ where type Value = crate::types::Or; crate::problem_parameters![ + ("max_numeric_magnitude_bits", max_numeric_magnitude_bits), ("num_arcs", num_arcs), ("num_potential_arcs", num_potential_arcs), ("num_vertices", num_vertices), diff --git a/src/models/graph/undirected_flow_lower_bounds.rs b/src/models/graph/undirected_flow_lower_bounds.rs index 8cd64d366..1c7472ef8 100644 --- a/src/models/graph/undirected_flow_lower_bounds.rs +++ b/src/models/graph/undirected_flow_lower_bounds.rs @@ -126,9 +126,9 @@ impl UndirectedFlowLowerBounds { } for (edge_index, (&lower, &upper)) in lower_bounds.iter().zip(&capacities).enumerate() { - if lower > upper { + if lower < 0 || lower > upper { return Err(format!( - "lower bound at edge {edge_index} must be at most its capacity" + "lower bound at edge {edge_index} must be nonnegative and at most its capacity" ) .into()); } @@ -168,6 +168,11 @@ impl UndirectedFlowLowerBounds { self.requirement } + /// Maximum bit length of the edge capacities, with a minimum of one. + pub fn max_capacity_bits(&self) -> u64 { + crate::types::max_numeric_magnitude_bits(self.capacities.iter().copied()) + } + pub fn num_vertices(&self) -> usize { self.graph.num_vertices() } @@ -278,7 +283,11 @@ impl Problem for UndirectedFlowLowerBounds { type Solution = Vec; type Value = crate::types::Or; - crate::problem_parameters![("num_edges", num_edges), ("num_vertices", num_vertices),]; + crate::problem_parameters![ + ("max_capacity_bits", max_capacity_bits), + ("num_edges", num_edges), + ("num_vertices", num_vertices), + ]; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] diff --git a/src/models/graph/undirected_two_commodity_integral_flow.rs b/src/models/graph/undirected_two_commodity_integral_flow.rs index 72161a497..0f50526c6 100644 --- a/src/models/graph/undirected_two_commodity_integral_flow.rs +++ b/src/models/graph/undirected_two_commodity_integral_flow.rs @@ -212,6 +212,11 @@ impl UndirectedTwoCommodityIntegralFlow { &self.capacities } + /// Maximum bit length of the edge capacities, with a minimum of one. + pub fn max_capacity_bits(&self) -> u64 { + crate::types::max_numeric_magnitude_bits(self.capacities.iter().copied()) + } + pub fn source_1(&self) -> usize { self.source_1 } @@ -423,6 +428,7 @@ impl Problem for UndirectedTwoCommodityIntegralFlow { type Value = crate::types::Or; crate::problem_parameters![ + ("max_capacity_bits", max_capacity_bits), ("num_edges", num_edges), ("num_conservation_constraints", num_conservation_constraints), ("num_vertices", num_vertices), diff --git a/src/models/misc/bin_packing.rs b/src/models/misc/bin_packing.rs index 0f952f549..994c064b8 100644 --- a/src/models/misc/bin_packing.rs +++ b/src/models/misc/bin_packing.rs @@ -104,6 +104,17 @@ impl BinPacking { pub fn num_items(&self) -> usize { self.sizes.len() } + + /// Smallest h >= 1 bounding the magnitudes of item sizes and capacity by 2^h + /// (strictly). For floating weights, this measures magnitude, not precision. + pub fn max_numeric_magnitude_bits(&self) -> u64 { + crate::types::max_numeric_magnitude_bits( + self.sizes + .iter() + .chain(std::iter::once(&self.capacity)) + .map(W::to_sum), + ) + } } impl Problem for BinPacking @@ -115,7 +126,10 @@ where type Solution = Vec; type Value = Min; - crate::problem_parameters![("num_items", num_items),]; + crate::problem_parameters![ + ("max_numeric_magnitude_bits", max_numeric_magnitude_bits), + ("num_items", num_items), + ]; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![W] diff --git a/src/models/misc/capacity_assignment.rs b/src/models/misc/capacity_assignment.rs index aa18cea41..e76d250d2 100644 --- a/src/models/misc/capacity_assignment.rs +++ b/src/models/misc/capacity_assignment.rs @@ -118,6 +118,17 @@ impl CapacityAssignment { }) } + /// Smallest h >= 1 bounding every delay and the delay budget in magnitude strictly by 2^h. + pub fn max_delay_bits(&self) -> u64 { + crate::types::max_numeric_magnitude_bits( + self.delay + .iter() + .flatten() + .copied() + .chain(std::iter::once(self.delay_budget)), + ) + } + /// Number of communication links. pub fn num_links(&self) -> usize { self.cost.len() @@ -189,7 +200,11 @@ impl Problem for CapacityAssignment { type Solution = Vec; type Value = crate::types::Min; - crate::problem_parameters![("num_capacities", num_capacities), ("num_links", num_links),]; + crate::problem_parameters![ + ("max_delay_bits", max_delay_bits), + ("num_capacities", num_capacities), + ("num_links", num_links), + ]; fn evaluate( &self, diff --git a/src/models/misc/flow_shop_scheduling.rs b/src/models/misc/flow_shop_scheduling.rs index 1ef72562f..a33e1b30a 100644 --- a/src/models/misc/flow_shop_scheduling.rs +++ b/src/models/misc/flow_shop_scheduling.rs @@ -143,6 +143,17 @@ impl FlowShopScheduling { self.deadline } + /// Smallest h >= 1 with every processing time and the deadline strictly below 2^h. + pub fn max_time_bits(&self) -> u64 { + crate::types::max_numeric_magnitude_bits( + self.task_lengths + .iter() + .flatten() + .copied() + .chain(std::iter::once(self.deadline)), + ) + } + /// Get the number of jobs. pub fn num_jobs(&self) -> usize { self.task_lengths.len() @@ -206,7 +217,11 @@ impl Problem for FlowShopScheduling { type Solution = Vec; type Value = crate::types::Or; - crate::problem_parameters![("num_jobs", num_jobs), ("num_processors", num_processors),]; + crate::problem_parameters![ + ("max_time_bits", max_time_bits), + ("num_jobs", num_jobs), + ("num_processors", num_processors), + ]; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] diff --git a/src/models/misc/knapsack.rs b/src/models/misc/knapsack.rs index 91eca16a6..8b7d37150 100644 --- a/src/models/misc/knapsack.rs +++ b/src/models/misc/knapsack.rs @@ -132,6 +132,11 @@ impl Knapsack { self.capacity } + /// Binary digit count of the capacity, with a minimum of one for zero. + pub fn capacity_bits(&self) -> u64 { + crate::types::max_numeric_magnitude_bits([self.capacity]) + } + /// Returns the number of items. pub fn num_items(&self) -> usize { self.weights.len() @@ -142,11 +147,7 @@ impl Knapsack { /// For positive capacity this is `floor(log2(C)) + 1`; for zero capacity we /// keep one slack bit so the encoding shape remains uniform. pub fn num_slack_bits(&self) -> usize { - if self.capacity == 0 { - 1 - } else { - self.capacity.ilog2() as usize + 1 - } + usize::try_from(self.capacity_bits()).expect("capacity bit length fits usize") } } @@ -155,7 +156,11 @@ impl Problem for Knapsack { type Solution = Vec; type Value = Max; - crate::problem_parameters![("capacity", capacity), ("num_items", num_items),]; + crate::problem_parameters![ + ("capacity", capacity), + ("capacity_bits", capacity_bits), + ("num_items", num_items), + ]; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] diff --git a/src/models/misc/minimum_tardiness_sequencing.rs b/src/models/misc/minimum_tardiness_sequencing.rs index a17af7951..0b7b2784a 100644 --- a/src/models/misc/minimum_tardiness_sequencing.rs +++ b/src/models/misc/minimum_tardiness_sequencing.rs @@ -201,6 +201,11 @@ fn validate_task_data( } impl MinimumTardinessSequencing { + /// Smallest h >= 1 with every processing time strictly below 2^h. + pub fn max_processing_time_bits(&self) -> u64 { + crate::types::max_numeric_magnitude_bits(self.lengths.iter().map(|length| length.to_sum())) + } + /// Returns the number of tasks. pub fn num_tasks(&self) -> usize { self.deadlines.len() @@ -253,6 +258,7 @@ impl Problem for MinimumTardinessSequencing { type Value = Min; crate::problem_parameters![ + ("max_processing_time_bits", max_processing_time_bits), ("num_precedences", num_precedences), ("num_tasks", num_tasks), ]; @@ -318,6 +324,7 @@ impl Problem for MinimumTardinessSequencing { type Value = Min; crate::problem_parameters![ + ("max_processing_time_bits", max_processing_time_bits), ("num_precedences", num_precedences), ("num_tasks", num_tasks), ]; diff --git a/src/models/misc/multiprocessor_scheduling.rs b/src/models/misc/multiprocessor_scheduling.rs index 8eb7c7f0c..6abcd7301 100644 --- a/src/models/misc/multiprocessor_scheduling.rs +++ b/src/models/misc/multiprocessor_scheduling.rs @@ -126,6 +126,16 @@ impl MultiprocessorScheduling { self.lengths.len() } + /// Smallest h >= 1 such that every task length and the deadline are below 2^h. + pub fn max_numeric_magnitude_bits(&self) -> u64 { + crate::types::max_numeric_magnitude_bits( + self.lengths + .iter() + .copied() + .chain(std::iter::once(self.deadline)), + ) + } + /// Returns the total processing time of all tasks. pub fn total_length(&self) -> i64 { self.lengths.iter().sum() @@ -137,7 +147,11 @@ impl Problem for MultiprocessorScheduling { type Solution = Vec; type Value = crate::types::Or; - crate::problem_parameters![("num_processors", num_processors), ("num_tasks", num_tasks),]; + crate::problem_parameters![ + ("max_numeric_magnitude_bits", max_numeric_magnitude_bits), + ("num_processors", num_processors), + ("num_tasks", num_tasks), + ]; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] diff --git a/src/models/misc/open_shop_scheduling.rs b/src/models/misc/open_shop_scheduling.rs index a08efa1a7..d3b5062c0 100644 --- a/src/models/misc/open_shop_scheduling.rs +++ b/src/models/misc/open_shop_scheduling.rs @@ -189,6 +189,11 @@ impl OpenShopScheduling { .expect("processing times must fit the brute-force schedule horizon") } + /// Binary digit count of the schedule horizon, with a minimum of one for zero. + pub fn schedule_horizon_bits(&self) -> u64 { + crate::types::max_numeric_magnitude_bits([self.schedule_horizon()]) + } + fn finish_time( &self, config: &[usize], @@ -238,6 +243,7 @@ impl Problem for OpenShopScheduling { ("num_jobs", num_jobs), ("num_machines", num_machines), ("schedule_horizon", schedule_horizon), + ("schedule_horizon_bits", schedule_horizon_bits), ]; fn variant() -> Vec<(&'static str, &'static str)> { diff --git a/src/models/misc/partially_ordered_knapsack.rs b/src/models/misc/partially_ordered_knapsack.rs index b0445099e..3d0b24072 100644 --- a/src/models/misc/partially_ordered_knapsack.rs +++ b/src/models/misc/partially_ordered_knapsack.rs @@ -223,6 +223,16 @@ impl PartiallyOrderedKnapsack { self.capacity } + /// Smallest h >= 1 with every item weight and the capacity strictly below 2^h. + pub fn max_weight_bits(&self) -> u64 { + crate::types::max_numeric_magnitude_bits( + self.weights + .iter() + .copied() + .chain(std::iter::once(self.capacity)), + ) + } + /// Returns the number of items. pub fn num_items(&self) -> usize { self.weights.len() @@ -257,6 +267,7 @@ impl Problem for PartiallyOrderedKnapsack { type Value = Max; crate::problem_parameters![ + ("max_weight_bits", max_weight_bits), ("num_items", num_items), ("num_precedences", num_precedences), ]; diff --git a/src/models/misc/partition.rs b/src/models/misc/partition.rs index 032f4eccc..5e5a36470 100644 --- a/src/models/misc/partition.rs +++ b/src/models/misc/partition.rs @@ -81,6 +81,11 @@ impl Partition { self.sizes.len() } + /// Smallest h >= 1 such that every input size is strictly below 2^h. + pub fn max_numeric_magnitude_bits(&self) -> u64 { + crate::types::max_numeric_magnitude_bits(self.sizes.iter().copied()) + } + /// Returns the total sum of all sizes. pub fn total_sum(&self) -> i64 { self.sizes.iter().sum() @@ -107,7 +112,10 @@ impl Problem for Partition { type Solution = Vec; type Value = crate::types::Or; - crate::problem_parameters![("num_elements", num_elements),]; + crate::problem_parameters![ + ("max_numeric_magnitude_bits", max_numeric_magnitude_bits), + ("num_elements", num_elements), + ]; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] diff --git a/src/models/misc/resource_constrained_scheduling.rs b/src/models/misc/resource_constrained_scheduling.rs index c43d4bdca..c186a6f0a 100644 --- a/src/models/misc/resource_constrained_scheduling.rs +++ b/src/models/misc/resource_constrained_scheduling.rs @@ -118,6 +118,17 @@ impl ResourceConstrainedScheduling { }) } + /// Smallest h >= 1 with every resource requirement and bound strictly below 2^h. + pub fn max_resource_bits(&self) -> u64 { + crate::types::max_numeric_magnitude_bits( + self.resource_requirements + .iter() + .flatten() + .copied() + .chain(self.resource_bounds.iter().copied()), + ) + } + /// Get the number of tasks. pub fn num_tasks(&self) -> usize { self.resource_requirements.len() @@ -179,6 +190,7 @@ impl Problem for ResourceConstrainedScheduling { type Value = crate::types::Or; crate::problem_parameters![ + ("max_resource_bits", max_resource_bits), ("deadline", deadline), ("num_resources", num_resources), ("num_tasks", num_tasks), @@ -211,8 +223,12 @@ impl Problem for ResourceConstrainedScheduling { )); } - // Check processor capacity and resource constraints at each time slot - for u in 0..d { + // Empty slots consume no resources. Keep ascending slot order and + // task order within each slot for checked accumulation. + let mut occupied = config.clone(); + occupied.sort_unstable(); + occupied.dedup(); + for u in occupied { // Collect tasks scheduled at time slot u let mut task_count = 0usize; let mut resource_usage = vec![0i64; r]; diff --git a/src/models/misc/scheduling_to_minimize_weighted_completion_time.rs b/src/models/misc/scheduling_to_minimize_weighted_completion_time.rs index 68459fc3f..bd478f199 100644 --- a/src/models/misc/scheduling_to_minimize_weighted_completion_time.rs +++ b/src/models/misc/scheduling_to_minimize_weighted_completion_time.rs @@ -142,6 +142,11 @@ impl SchedulingToMinimizeWeightedCompletionTime { } } + /// Smallest h >= 1 bounding every processing time in magnitude strictly by 2^h. + pub fn max_processing_time_bits(&self) -> u64 { + crate::types::max_numeric_magnitude_bits(self.lengths.iter().copied()) + } + /// Returns the number of tasks. pub fn num_tasks(&self) -> usize { self.lengths.len() @@ -275,7 +280,11 @@ impl Problem for SchedulingToMinimizeWeightedCompletionTime { type Solution = Vec; type Value = Min; - crate::problem_parameters![("num_processors", num_processors), ("num_tasks", num_tasks),]; + crate::problem_parameters![ + ("max_processing_time_bits", max_processing_time_bits), + ("num_processors", num_processors), + ("num_tasks", num_tasks), + ]; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] diff --git a/src/models/misc/sequencing_to_minimize_maximum_cumulative_cost.rs b/src/models/misc/sequencing_to_minimize_maximum_cumulative_cost.rs index 95b9cc819..b226e1914 100644 --- a/src/models/misc/sequencing_to_minimize_maximum_cumulative_cost.rs +++ b/src/models/misc/sequencing_to_minimize_maximum_cumulative_cost.rs @@ -89,6 +89,11 @@ impl SequencingToMinimizeMaximumCumulativeCost { &self.precedences } + /// Smallest h >= 1 bounding every task cost in magnitude strictly by 2^h. + pub fn max_cost_bits(&self) -> u64 { + crate::types::max_numeric_magnitude_bits(self.costs.iter().copied()) + } + /// Return the number of tasks. pub fn num_tasks(&self) -> usize { self.costs.len() @@ -153,6 +158,7 @@ impl Problem for SequencingToMinimizeMaximumCumulativeCost { type Value = crate::types::Min; crate::problem_parameters![ + ("max_cost_bits", max_cost_bits), ("num_precedences", num_precedences), ("num_tasks", num_tasks), ]; diff --git a/src/models/misc/sequencing_to_minimize_tardy_task_weight.rs b/src/models/misc/sequencing_to_minimize_tardy_task_weight.rs index cae8f9a6d..edf3b7916 100644 --- a/src/models/misc/sequencing_to_minimize_tardy_task_weight.rs +++ b/src/models/misc/sequencing_to_minimize_tardy_task_weight.rs @@ -118,6 +118,11 @@ impl SequencingToMinimizeTardyTaskWeight { } } + /// Smallest h >= 1 bounding every processing time in magnitude strictly by 2^h. + pub fn max_processing_time_bits(&self) -> u64 { + crate::types::max_numeric_magnitude_bits(self.lengths.iter().copied()) + } + /// Returns the number of tasks. pub fn num_tasks(&self) -> usize { self.lengths.len() @@ -190,7 +195,10 @@ impl Problem for SequencingToMinimizeTardyTaskWeight { type Solution = Vec; type Value = Min; - crate::problem_parameters![("num_tasks", num_tasks),]; + crate::problem_parameters![ + ("max_processing_time_bits", max_processing_time_bits), + ("num_tasks", num_tasks), + ]; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] diff --git a/src/models/misc/sequencing_to_minimize_weighted_completion_time.rs b/src/models/misc/sequencing_to_minimize_weighted_completion_time.rs index 05cd17d62..a0004949c 100644 --- a/src/models/misc/sequencing_to_minimize_weighted_completion_time.rs +++ b/src/models/misc/sequencing_to_minimize_weighted_completion_time.rs @@ -120,6 +120,11 @@ impl SequencingToMinimizeWeightedCompletionTime { } } + /// Smallest h >= 1 bounding every processing time in magnitude strictly by 2^h. + pub fn max_processing_time_bits(&self) -> u64 { + crate::types::max_numeric_magnitude_bits(self.lengths.iter().copied()) + } + /// Returns the number of tasks. pub fn num_tasks(&self) -> usize { self.lengths.len() @@ -223,6 +228,7 @@ impl Problem for SequencingToMinimizeWeightedCompletionTime { type Value = Min; crate::problem_parameters![ + ("max_processing_time_bits", max_processing_time_bits), ("num_precedences", num_precedences), ("num_tasks", num_tasks), ]; diff --git a/src/models/misc/sequencing_to_minimize_weighted_tardiness.rs b/src/models/misc/sequencing_to_minimize_weighted_tardiness.rs index ebcba6231..dc729a1e8 100644 --- a/src/models/misc/sequencing_to_minimize_weighted_tardiness.rs +++ b/src/models/misc/sequencing_to_minimize_weighted_tardiness.rs @@ -144,6 +144,18 @@ impl SequencingToMinimizeWeightedTardiness { self.bound } + /// Smallest h >= 1 with all lengths, weights, deadlines, and the cost bound below 2^h. + pub fn max_numeric_magnitude_bits(&self) -> u64 { + crate::types::max_numeric_magnitude_bits( + self.lengths + .iter() + .chain(&self.weights) + .chain(&self.deadlines) + .copied() + .chain(std::iter::once(self.bound)), + ) + } + /// Returns the number of jobs. pub fn num_tasks(&self) -> usize { self.lengths.len() @@ -208,7 +220,10 @@ impl Problem for SequencingToMinimizeWeightedTardiness { type Solution = Vec; type Value = crate::types::Or; - crate::problem_parameters![("num_tasks", num_tasks),]; + crate::problem_parameters![ + ("max_numeric_magnitude_bits", max_numeric_magnitude_bits), + ("num_tasks", num_tasks), + ]; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] diff --git a/src/models/misc/sequencing_with_deadlines_and_set_up_times.rs b/src/models/misc/sequencing_with_deadlines_and_set_up_times.rs index 568c4f3df..0f5ddbebb 100644 --- a/src/models/misc/sequencing_with_deadlines_and_set_up_times.rs +++ b/src/models/misc/sequencing_with_deadlines_and_set_up_times.rs @@ -111,6 +111,17 @@ impl SequencingWithDeadlinesAndSetUpTimes { } } + /// Smallest h >= 1 bounding lengths, deadlines, and setup times in magnitude by 2^h. + pub fn max_time_bits(&self) -> u64 { + crate::types::max_numeric_magnitude_bits( + self.lengths + .iter() + .chain(&self.deadlines) + .chain(&self.setup_times) + .copied(), + ) + } + /// Returns the number of tasks. pub fn num_tasks(&self) -> usize { self.lengths.len() @@ -211,7 +222,7 @@ impl Problem for SequencingWithDeadlinesAndSetUpTimes { type Solution = Vec; type Value = Or; - crate::problem_parameters![("num_tasks", num_tasks),]; + crate::problem_parameters![("max_time_bits", max_time_bits), ("num_tasks", num_tasks),]; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] diff --git a/src/models/misc/subset_sum.rs b/src/models/misc/subset_sum.rs index deead9112..c2f300359 100644 --- a/src/models/misc/subset_sum.rs +++ b/src/models/misc/subset_sum.rs @@ -131,6 +131,14 @@ impl SubsetSum { pub fn num_elements(&self) -> usize { self.sizes.len() } + + /// Smallest h >= 1 such that every input size and the target are below 2^h. + pub fn max_numeric_magnitude_bits(&self) -> u64 { + self.sizes + .iter() + .chain(std::iter::once(&self.target)) + .fold(1, |bits, value| bits.max(value.bits())) + } } impl Problem for SubsetSum { @@ -138,7 +146,10 @@ impl Problem for SubsetSum { type Solution = Vec; type Value = crate::types::Or; - crate::problem_parameters![("num_elements", num_elements),]; + crate::problem_parameters![ + ("max_numeric_magnitude_bits", max_numeric_magnitude_bits), + ("num_elements", num_elements), + ]; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] diff --git a/src/models/misc/three_partition.rs b/src/models/misc/three_partition.rs index 9a57d336c..2220198c8 100644 --- a/src/models/misc/three_partition.rs +++ b/src/models/misc/three_partition.rs @@ -88,6 +88,16 @@ impl ThreePartition { Ok(Self { sizes, bound }) } + /// Smallest h >= 1 with every element size and the target sum strictly below 2^h. + pub fn max_numeric_magnitude_bits(&self) -> u64 { + crate::types::max_numeric_magnitude_bits( + self.sizes + .iter() + .copied() + .chain(std::iter::once(self.bound)), + ) + } + /// Create a new 3-Partition instance. /// /// # Panics @@ -183,7 +193,11 @@ impl Problem for ThreePartition { type Solution = Vec; type Value = Or; - crate::problem_parameters![("num_elements", num_elements), ("num_groups", num_groups),]; + crate::problem_parameters![ + ("max_numeric_magnitude_bits", max_numeric_magnitude_bits), + ("num_elements", num_elements), + ("num_groups", num_groups), + ]; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] diff --git a/src/models/set/exact_cover_by_3_sets.rs b/src/models/set/exact_cover_by_3_sets.rs index 43cce38b0..1fc915b31 100644 --- a/src/models/set/exact_cover_by_3_sets.rs +++ b/src/models/set/exact_cover_by_3_sets.rs @@ -202,7 +202,6 @@ impl Problem for ExactCoverBy3Sets { type Value = crate::types::Or; crate::problem_parameters![ - ("num_sets", num_sets), ("num_subsets", num_subsets), ("universe_size", universe_size), ]; diff --git a/src/models/set/integer_knapsack.rs b/src/models/set/integer_knapsack.rs index 6080cbdff..04be21fc8 100644 --- a/src/models/set/integer_knapsack.rs +++ b/src/models/set/integer_knapsack.rs @@ -88,6 +88,11 @@ impl IntegerKnapsack { self.capacity } + /// Binary digit count of the capacity, with a minimum of one for zero. + pub fn capacity_bits(&self) -> u64 { + crate::types::max_numeric_magnitude_bits([self.capacity]) + } + /// Returns the number of items. pub fn num_items(&self) -> usize { self.sizes.len() @@ -99,7 +104,11 @@ impl Problem for IntegerKnapsack { type Solution = Vec; type Value = Max; - crate::problem_parameters![("capacity", capacity), ("num_items", num_items),]; + crate::problem_parameters![ + ("capacity", capacity), + ("capacity_bits", capacity_bits), + ("num_items", num_items), + ]; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] diff --git a/src/rules/acyclicpartition_ilp.rs b/src/rules/acyclicpartition_ilp.rs index f042f7b37..a409c6fe1 100644 --- a/src/rules/acyclicpartition_ilp.rs +++ b/src/rules/acyclicpartition_ilp.rs @@ -1,4 +1,4 @@ -//! Reduction from AcyclicPartition to `ILP`. +//! Reduction from AcyclicPartition to `ILP`. //! //! One-hot assignment x_{v,c}, McCormick same-class indicators s_{t,c}, //! crossing flags y_t, and partition labels used directly as a topological order. @@ -12,15 +12,15 @@ use crate::rules::traits::{ReduceTo, ReductionResult}; #[derive(Debug, Clone)] pub struct ReductionAcyclicPartitionToILP { - target: ILP, + target: ILP, n: usize, } impl ReductionResult for ReductionAcyclicPartitionToILP { type Source = AcyclicPartition; - type Target = ILP; + type Target = ILP; - fn target_problem(&self) -> &ILP { + fn target_problem(&self) -> &ILP { &self.target } @@ -49,10 +49,11 @@ impl crate::rules::AggregateReductionResult for ReductionAcyclicPartitionToILP { num_constraints = "num_vertices^2 + 4 * num_vertices + 3 * num_arcs * num_vertices + 2 * num_arcs + 1", }, upper_bound { + max_constraint_magnitude_bits = "max_numeric_magnitude_bits + num_vertices + 1", num_nonzeros = "(num_vertices * num_vertices + num_arcs * num_vertices + num_arcs + num_vertices) * (num_vertices^2 + 4 * num_vertices + 3 * num_arcs * num_vertices + 2 * num_arcs + 1)", }, })] -impl ReduceTo> for AcyclicPartition { +impl ReduceTo> for AcyclicPartition { type Result = ReductionAcyclicPartitionToILP; fn reduce_to(&self) -> Result { @@ -101,7 +102,7 @@ impl ReduceTo> for AcyclicPartition { terms.push(( used_idx(c), weight_bound.checked_neg().ok_or_else(|| { - crate::rules::ReductionError::integer_overflow::>( + crate::rules::ReductionError::integer_overflow::>( "negating the partition weight bound", ) })?, @@ -170,13 +171,13 @@ pub(crate) fn canonical_rule_example_specs() -> Vec>::reduce_to(&source) + crate::rules::ReduceTo::>::reduce_to(&source) .expect("reduction should succeed"); let ilp_sol = crate::solvers::ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be solvable"); let extracted = reduction.extract_solution(&ilp_sol).unwrap(); - crate::example_db::specs::rule_example_with_witness::<_, ILP>( + crate::example_db::specs::rule_example_with_witness::<_, ILP>( source, SolutionPair { source_config: serde_json::json!(extracted), diff --git a/src/rules/balancedcompletebipartitesubgraph_ilp.rs b/src/rules/balancedcompletebipartitesubgraph_ilp.rs index a14d79e83..bd1ffb04c 100644 --- a/src/rules/balancedcompletebipartitesubgraph_ilp.rs +++ b/src/rules/balancedcompletebipartitesubgraph_ilp.rs @@ -46,6 +46,7 @@ impl ReductionResult for ReductionBCBSToILP { impl crate::rules::AggregateReductionResult for ReductionBCBSToILP {} #[reduction(transform = upper_bound { + max_constraint_magnitude_bits = "k + 1", num_vars = "num_vertices", num_constraints = "num_vertices^2 + 2", num_nonzeros = "num_vertices * (num_vertices^2 + 2)", diff --git a/src/rules/biconnectivityaugmentation_ilp.rs b/src/rules/biconnectivityaugmentation_ilp.rs index af0513832..09f9ef368 100644 --- a/src/rules/biconnectivityaugmentation_ilp.rs +++ b/src/rules/biconnectivityaugmentation_ilp.rs @@ -1,4 +1,4 @@ -//! Reduction from BiconnectivityAugmentation to `ILP`. +//! Reduction from BiconnectivityAugmentation to `ILP`. //! //! Select candidate edges under budget and, both before deletion and for every deleted vertex q, //! certify that the remaining augmented graph stays connected via unit-flow @@ -12,7 +12,7 @@ use crate::topology::{Graph, SimpleGraph}; #[derive(Debug, Clone)] pub struct ReductionBiconnAugToILP { - target: ILP, + target: ILP, num_candidates: usize, } @@ -25,7 +25,7 @@ impl ReductionBiconnAugToILP { let overflow = || { crate::rules::ReductionError::integer_overflow::< BiconnectivityAugmentation, - ILP, + ILP, >("computing connectivity flow variable counts") }; let commodities = n @@ -48,9 +48,9 @@ impl ReductionBiconnAugToILP { impl ReductionResult for ReductionBiconnAugToILP { type Source = BiconnectivityAugmentation; - type Target = ILP; + type Target = ILP; - fn target_problem(&self) -> &ILP { + fn target_problem(&self) -> &ILP { &self.target } @@ -75,12 +75,17 @@ impl ReductionResult for ReductionBiconnAugToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionBiconnAugToILP {} -#[reduction(transform = 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 + 2 * num_vertices * (num_vertices + 1) * (num_edges + num_potential_edges)) * (1 + num_vertices * (num_vertices + 1) * (2 * num_edges + 4 * num_potential_edges + num_vertices))", +#[reduction(transform = { + exact { + max_constraint_magnitude_bits = "max_numeric_magnitude_bits", + }, + 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 + 2 * num_vertices * (num_vertices + 1) * (num_edges + num_potential_edges)) * (1 + num_vertices * (num_vertices + 1) * (2 * num_edges + 4 * num_potential_edges + num_vertices))", + }, })] -impl ReduceTo> for BiconnectivityAugmentation { +impl ReduceTo> for BiconnectivityAugmentation { type Result = ReductionBiconnAugToILP; fn reduce_to(&self) -> Result { @@ -242,13 +247,13 @@ pub(crate) fn canonical_rule_example_specs() -> Vec>::reduce_to(&source) + crate::rules::ReduceTo::>::reduce_to(&source) .expect("reduction should succeed"); let ilp_sol = crate::solvers::ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be solvable"); let extracted = reduction.extract_solution(&ilp_sol).unwrap(); - crate::example_db::specs::rule_example_with_witness::<_, ILP>( + crate::example_db::specs::rule_example_with_witness::<_, ILP>( source, SolutionPair { source_config: serde_json::json!(extracted), diff --git a/src/rules/binpacking_ilp.rs b/src/rules/binpacking_ilp.rs index bef93355a..37657bc44 100644 --- a/src/rules/binpacking_ilp.rs +++ b/src/rules/binpacking_ilp.rs @@ -47,12 +47,14 @@ impl ReductionResult for ReductionBPToILP { } } +// Rows copy item sizes and capacity (up to sign). Binary endpoints require only one bit. #[reduction(transform = { exact { num_vars = "num_items * num_items + num_items", num_constraints = "2 * num_items", }, upper_bound { + max_constraint_magnitude_bits = "max_numeric_magnitude_bits", num_nonzeros = "(num_items * num_items + num_items) * (2 * num_items)", }, })] diff --git a/src/rules/bmf_bicliquecover.rs b/src/rules/bmf_bicliquecover.rs index 867926684..57908da05 100644 --- a/src/rules/bmf_bicliquecover.rs +++ b/src/rules/bmf_bicliquecover.rs @@ -92,17 +92,17 @@ impl ReductionResult for ReductionBMFToBicliqueCover { } } -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vertices = "rows + cols", left_size = "rows", right_size = "cols", rank = "rank", }, - unavailable = { - num_edges = "the number of true matrix entries is not a registered BMF parameter", - } -)] + upper_bound { + num_edges = "rows * cols", + }, +})] impl ReduceTo for BMF { type Result = ReductionBMFToBicliqueCover; diff --git a/src/rules/bmf_ilp.rs b/src/rules/bmf_ilp.rs index c69887bb6..2d36b0180 100644 --- a/src/rules/bmf_ilp.rs +++ b/src/rules/bmf_ilp.rs @@ -52,6 +52,7 @@ impl ReductionResult for ReductionBMFToILP { #[reduction( transform = exact { + max_constraint_magnitude_bits = "1", num_vars = "rows * rank + rank * cols + rows * rank * cols + rows * cols", num_constraints = "3 * rows * rank * cols + rank * rows * cols + rows * cols + rows * cols", num_nonzeros = "10 * rows * rank * cols + 2 * rows * cols", diff --git a/src/rules/bottlenecktravelingsalesman_ilp.rs b/src/rules/bottlenecktravelingsalesman_ilp.rs index 5c36ad7d8..9845c359f 100644 --- a/src/rules/bottlenecktravelingsalesman_ilp.rs +++ b/src/rules/bottlenecktravelingsalesman_ilp.rs @@ -10,16 +10,16 @@ use crate::topology::Graph; /// One selected maximum-weight edge carries the exact objective coefficient. #[derive(Debug, Clone)] pub struct ReductionBTSPToILP { - target: ILP, + target: ILP, num_vertices: usize, num_edges: usize, } impl ReductionResult for ReductionBTSPToILP { type Source = BottleneckTravelingSalesman; - type Target = ILP; + type Target = ILP; - fn target_problem(&self) -> &ILP { + fn target_problem(&self) -> &ILP { &self.target } @@ -48,7 +48,7 @@ impl ReductionBTSPToILP { m: usize, ) -> Result<(usize, usize, usize, usize), crate::rules::ReductionError> { let overflow = || { - crate::rules::ReductionError::integer_overflow::>( + crate::rules::ReductionError::integer_overflow::>( "sizing the cyclic edge-selection formulation", ) }; @@ -71,7 +71,7 @@ impl ReductionBTSPToILP { }) .and_then(|v| v.checked_add(1)) .ok_or_else(overflow)?; - >>::exact_i64( + >>::exact_i64( vars, "bounding binary constraint accumulation", )?; @@ -85,10 +85,11 @@ impl ReductionBTSPToILP { num_constraints = "num_vertices^2 + 6 * num_edges * num_vertices + 4 * num_edges + 3 * num_vertices + 1", }, upper_bound { + max_constraint_magnitude_bits = "2", num_nonzeros = "(num_vertices^2 + 2 * num_edges * num_vertices + num_edges) * (num_vertices^2 + 6 * num_edges * num_vertices + 4 * num_edges + 3 * num_vertices + 1)", }, })] -impl ReduceTo> for BottleneckTravelingSalesman { +impl ReduceTo> for BottleneckTravelingSalesman { type Result = ReductionBTSPToILP; fn reduce_to(&self) -> Result { @@ -108,7 +109,7 @@ impl ReduceTo> for BottleneckTravelingSalesman { .collect::>() }; let mut constraints = Vec::with_capacity(num_constraints); - // ILP variables are nonnegative. Check binary bounds before sums. + // ILP variables are nonnegative. Check binary bounds before sums. for variable in 0..num_vars { constraints.push(LinearConstraint::le(vec![(variable, 1)], 1)); } @@ -197,7 +198,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) + crate::example_db::specs::rule_example_via_ilp::<_, bool>(source) }, }] } diff --git a/src/rules/boundedcomponentspanningforest_ilp.rs b/src/rules/boundedcomponentspanningforest_ilp.rs index 093f35112..02018bba1 100644 --- a/src/rules/boundedcomponentspanningforest_ilp.rs +++ b/src/rules/boundedcomponentspanningforest_ilp.rs @@ -1,10 +1,10 @@ -//! Reduction from BoundedComponentSpanningForest to `ILP`. +//! Reduction from BoundedComponentSpanningForest to `ILP`. //! //! Assign every vertex to one of K components, bound weight, certify //! connectivity inside each used component via flow. //! See the paper entry for the full formulation. -use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{Bounded, IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::BoundedComponentSpanningForest; use crate::reduction; use crate::rules::ilp_helpers::one_hot_decode_rows; @@ -13,16 +13,16 @@ use crate::topology::{Graph, SimpleGraph}; #[derive(Debug, Clone)] pub struct ReductionBCSFToILP { - target: ILP, + target: ILP, n: usize, k: usize, } impl ReductionResult for ReductionBCSFToILP { type Source = BoundedComponentSpanningForest; - type Target = ILP; + type Target = ILP; - fn target_problem(&self) -> &ILP { + fn target_problem(&self) -> &ILP { &self.target } @@ -51,10 +51,11 @@ impl crate::rules::AggregateReductionResult for ReductionBCSFToILP {} num_constraints = "num_vertices + 5 * max_components + 6 * num_vertices * max_components + 6 * num_edges * max_components", }, upper_bound { + max_constraint_magnitude_bits = "max_weight_bits + num_vertices", num_nonzeros = "(3 * num_vertices * max_components + 2 * max_components + 2 * num_edges * max_components) * (num_vertices + 5 * max_components + 6 * num_vertices * max_components + 6 * num_edges * max_components)", }, })] -impl ReduceTo> for BoundedComponentSpanningForest { +impl ReduceTo> for BoundedComponentSpanningForest { type Result = ReductionBCSFToILP; fn reduce_to(&self) -> Result { @@ -214,13 +215,13 @@ pub(crate) fn canonical_rule_example_specs() -> Vec>::reduce_to(&source) + crate::rules::ReduceTo::>::reduce_to(&source) .expect("reduction should succeed"); let ilp_sol = crate::solvers::ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be solvable"); let extracted = reduction.extract_solution(&ilp_sol).unwrap(); - crate::example_db::specs::rule_example_with_witness::<_, ILP>( + crate::example_db::specs::rule_example_with_witness::<_, ILP>( source, SolutionPair { source_config: serde_json::json!(extracted), diff --git a/src/rules/capacityassignment_ilp.rs b/src/rules/capacityassignment_ilp.rs index f38073a82..af1ca2817 100644 --- a/src/rules/capacityassignment_ilp.rs +++ b/src/rules/capacityassignment_ilp.rs @@ -51,6 +51,7 @@ impl ReductionResult for ReductionCAToILP { #[reduction(transform = { exact { + max_constraint_magnitude_bits = "max_delay_bits", num_vars = "num_links * num_capacities", num_constraints = "num_links + 1", }, diff --git a/src/rules/circuit_ilp.rs b/src/rules/circuit_ilp.rs index aa7169b5a..31fcb461f 100644 --- a/src/rules/circuit_ilp.rs +++ b/src/rules/circuit_ilp.rs @@ -195,6 +195,7 @@ impl ILPBuilder { impl crate::rules::AggregateReductionResult for ReductionCircuitToILP {} #[reduction(transform = upper_bound { + max_constraint_magnitude_bits = "num_expression_nodes + num_assignment_outputs + 2", num_vars = "num_variables + 2 * num_expression_nodes", num_constraints = "5 * num_expression_nodes + num_assignment_outputs", num_nonzeros = "(num_variables + 2 * num_expression_nodes) * (5 * num_expression_nodes + num_assignment_outputs)", diff --git a/src/rules/circuit_sat.rs b/src/rules/circuit_sat.rs index 4030b23ca..0bc5657ac 100644 --- a/src/rules/circuit_sat.rs +++ b/src/rules/circuit_sat.rs @@ -307,13 +307,11 @@ impl ReductionResult for ReductionCircuitSATToSAT { #[crate::aggregate_reduction(identity)] impl crate::rules::AggregateReductionResult for ReductionCircuitSATToSAT {} -#[reduction( - transform = unavailable { - num_vars = "the exact Tseitin variable count is specific to this reduction and is not a CircuitSAT parameter", - num_clauses = "the exact Tseitin clause count is specific to this reduction and is not a CircuitSAT parameter", - num_literals = "the exact target parameter is not represented by this reduction's symbolic transform", -} -)] +#[reduction(transform = upper_bound { + num_vars = "num_variables + 2 * num_expression_nodes", + num_clauses = "8 * num_expression_nodes + 2 * num_assignment_outputs", + num_literals = "24 * num_expression_nodes + 4 * num_assignment_outputs", +})] impl ReduceTo for CircuitSAT { type Result = ReductionCircuitSATToSAT; diff --git a/src/rules/closeststring_ilp.rs b/src/rules/closeststring_ilp.rs index 852f45288..ad007d5c9 100644 --- a/src/rules/closeststring_ilp.rs +++ b/src/rules/closeststring_ilp.rs @@ -21,29 +21,29 @@ //! substring problems," Journal of the ACM 49(2):157-171, 2002. //! -use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{Bounded, IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::ClosestString; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; /// Result of reducing ClosestString to ILP. /// -/// Variable layout (`ILP`, all non-negative): +/// Variable layout (`ILP`, all non-negative): /// - `x_{j, a}` at index `j * alphabet_size + a` for `j in [0, m)` and /// `a in [0, q)`, bounded to `{0, 1}`. /// - `R` (radius) at index `m * q`, an integer in `[0, m]`. #[derive(Debug, Clone)] pub struct ReductionClosestStringToILP { - target: ILP, + target: ILP, alphabet_size: usize, string_length: usize, } impl ReductionResult for ReductionClosestStringToILP { type Source = ClosestString; - type Target = ILP; + type Target = ILP; - fn target_problem(&self) -> &ILP { + fn target_problem(&self) -> &ILP { &self.target } @@ -78,14 +78,17 @@ impl ReductionResult for ReductionClosestStringToILP { } } -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "alphabet_size * string_length + 1", num_constraints = "string_length + num_strings", num_nonzeros = "alphabet_size * string_length + num_strings * (string_length + 1)", - } -)] -impl ReduceTo> for ClosestString { + }, + upper_bound { + max_constraint_magnitude_bits = "string_length + 1", + }, +})] +impl ReduceTo> for ClosestString { type Result = ReductionClosestStringToILP; fn reduce_to(&self) -> Result { @@ -101,7 +104,7 @@ impl ReduceTo> for ClosestString { let mut constraints: Vec = Vec::with_capacity(m + n); // Assignment constraints: exactly one symbol per center position. - // Together with the non-negativity built into `ILP`, this also + // Together with the non-negativity built into `ILP`, this also // forces every x_{j, a} to lie in {0, 1}. for j in 0..m { let terms: Vec<(usize, i64)> = (0..q).map(|a| (x_idx(j, a), 1)).collect(); @@ -157,7 +160,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) + crate::example_db::specs::rule_example_via_bounded_ilp::<_>(source) }, }] } diff --git a/src/rules/closestsubstring_ilp.rs b/src/rules/closestsubstring_ilp.rs index 8bb7589b9..c6c3353cf 100644 --- a/src/rules/closestsubstring_ilp.rs +++ b/src/rules/closestsubstring_ilp.rs @@ -29,14 +29,14 @@ //! substring problems," Journal of the ACM 49(2):157-171, 2002. //! -use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{Bounded, IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::ClosestSubstring; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; /// Result of reducing ClosestSubstring to ILP. /// -/// Variable layout (`ILP`, all non-negative): +/// Variable layout (`ILP`, all non-negative): /// - `x_{r, a}` at index `r * alphabet_size + a` for `r in [0, ell)` and /// `a in [0, q)`, forced into `{0, 1}` by the assignment constraints. /// - `y_{i, p}` at index `q * ell + window_offsets[i] + p` for input string @@ -46,7 +46,7 @@ use crate::rules::traits::{ReduceTo, ReductionResult}; /// integer in `[0, ell]`. #[derive(Debug, Clone)] pub struct ReductionClosestSubstringToILP { - target: ILP, + target: ILP, alphabet_size: usize, substring_length: usize, /// Prefix sums of per-string window counts: `window_offsets[i]` is the @@ -58,9 +58,9 @@ pub struct ReductionClosestSubstringToILP { impl ReductionResult for ReductionClosestSubstringToILP { type Source = ClosestSubstring; - type Target = ILP; + type Target = ILP; - fn target_problem(&self) -> &ILP { + fn target_problem(&self) -> &ILP { &self.target } @@ -124,10 +124,11 @@ fn decode_one_hot( num_constraints = "substring_length + num_strings + total_num_windows + 1", }, upper_bound { + max_constraint_magnitude_bits = "substring_length + 1", num_nonzeros = "(alphabet_size * substring_length + total_num_windows + 1) * (substring_length + num_strings + total_num_windows + 1)", }, })] -impl ReduceTo> for ClosestSubstring { +impl ReduceTo> for ClosestSubstring { type Result = ReductionClosestSubstringToILP; fn reduce_to(&self) -> Result { @@ -158,7 +159,7 @@ impl ReduceTo> for ClosestSubstring { Vec::with_capacity(ell + n + total_windows + 1); // Assignment constraints: exactly one symbol per center position. - // Together with the non-negativity built into `ILP`, this also + // Together with the non-negativity built into `ILP`, this also // forces every x_{r, a} to lie in {0, 1}. for r in 0..ell { let terms: Vec<(usize, i64)> = (0..q).map(|a| (x_idx(r, a), 1)).collect(); @@ -237,7 +238,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) + crate::example_db::specs::rule_example_via_bounded_ilp::<_>(source) }, }] } diff --git a/src/rules/closestvectorproblem_qubo.rs b/src/rules/closestvectorproblem_qubo.rs index 6d8986da3..f29dde9fa 100644 --- a/src/rules/closestvectorproblem_qubo.rs +++ b/src/rules/closestvectorproblem_qubo.rs @@ -231,9 +231,11 @@ fn dot(left: &[i64], right: &[i64], operation: &str) -> Result> for ClosestVectorProblem { type Result = ReductionCVPToQUBO; diff --git a/src/rules/clustering_ilp.rs b/src/rules/clustering_ilp.rs index 95a457064..0a10f254e 100644 --- a/src/rules/clustering_ilp.rs +++ b/src/rules/clustering_ilp.rs @@ -50,6 +50,7 @@ impl ReductionResult for ReductionClusteringToILP { 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 * (num_elements - 1) / 2 * num_clusters)", diff --git a/src/rules/coloring_ilp.rs b/src/rules/coloring_ilp.rs index 6dfcf29cc..7383ec766 100644 --- a/src/rules/coloring_ilp.rs +++ b/src/rules/coloring_ilp.rs @@ -113,13 +113,16 @@ impl crate::rules::Aggregate } // Register only the KN variant in the reduction graph -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_vertices * num_colors", num_constraints = "num_vertices + num_edges * num_colors", num_nonzeros = "num_colors * (num_vertices + 2 * num_edges)", - } -)] + }, + upper_bound { + max_constraint_magnitude_bits = "2", + }, +})] impl ReduceTo> for KColoring { type Result = ReductionKColoringToILP; diff --git a/src/rules/consecutiveblockminimization_ilp.rs b/src/rules/consecutiveblockminimization_ilp.rs index 08eb9974e..14d1ca199 100644 --- a/src/rules/consecutiveblockminimization_ilp.rs +++ b/src/rules/consecutiveblockminimization_ilp.rs @@ -42,11 +42,14 @@ impl ReductionResult for ReductionCBMToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionCBMToILP {} -#[reduction(transform = upper_bound { - 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 = "(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)", -})] +#[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 = "(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)", + }, +)] impl ReduceTo> for ConsecutiveBlockMinimization { type Result = ReductionCBMToILP; @@ -115,7 +118,13 @@ impl ReduceTo> for ConsecutiveBlockMinimization { bound_terms.push((b_offset + r * n + p, 1)); } } - constraints.push(LinearConstraint::le(bound_terms, self.bound())); + // The sum is nonnegative and at most the number of Boolean indicators. + // All negative thresholds are equivalent to -1 (infeasible). + let max_blocks = Self::exact_i64(bound_terms.len(), "encoding the block-count bound")?; + constraints.push(LinearConstraint::le( + bound_terms, + self.bound().clamp(-1, max_blocks), + )); let target = ILP::new(num_vars, constraints, vec![], ObjectiveSense::Minimize) .map_err(Self::target_construction)?; diff --git a/src/rules/consecutiveonesmatrixaugmentation_ilp.rs b/src/rules/consecutiveonesmatrixaugmentation_ilp.rs index 18dbd6535..da906ea83 100644 --- a/src/rules/consecutiveonesmatrixaugmentation_ilp.rs +++ b/src/rules/consecutiveonesmatrixaugmentation_ilp.rs @@ -49,6 +49,7 @@ impl crate::rules::AggregateReductionResult for ReductionCOMAToILP {} num_constraints = "num_cols + num_cols + num_rows * num_cols + 2 * num_rows + num_rows + 3 * num_rows * num_cols + 4 * num_rows * num_cols + 1", }, upper_bound { + max_constraint_magnitude_bits = "num_rows * num_cols + num_cols + 1", num_nonzeros = "(num_cols * num_cols + 5 * num_rows * num_cols) * (num_cols + num_cols + num_rows * num_cols + 2 * num_rows + num_rows + 3 * num_rows * num_cols + 4 * num_rows * num_cols + 1)", }, })] @@ -181,7 +182,12 @@ impl ReduceTo> for ConsecutiveOnesMatrixAugmentation { budget_terms.push((f_off + r * n + p, 1)); } } - constraints.push(LinearConstraint::le(budget_terms, self.bound())); + let max_augmentations = + Self::exact_i64(budget_terms.len(), "encoding the augmentation budget")?; + constraints.push(LinearConstraint::le( + budget_terms, + self.bound().min(max_augmentations), + )); let target = ILP::new(num_vars, constraints, vec![], ObjectiveSense::Minimize) .map_err(Self::target_construction)?; diff --git a/src/rules/consecutiveonessubmatrix_ilp.rs b/src/rules/consecutiveonessubmatrix_ilp.rs index 04b40ce82..62b4c3517 100644 --- a/src/rules/consecutiveonessubmatrix_ilp.rs +++ b/src/rules/consecutiveonessubmatrix_ilp.rs @@ -47,6 +47,7 @@ impl ReductionResult for ReductionCOSToILP { 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 = "(num_cols + num_cols * bound + 5 * num_rows * bound) * (2 + num_cols + bound + 3 * num_rows + 8 * num_rows * bound)", diff --git a/src/rules/consistencyofdatabasefrequencytables_ilp.rs b/src/rules/consistencyofdatabasefrequencytables_ilp.rs index 0e1853f84..f53094e6a 100644 --- a/src/rules/consistencyofdatabasefrequencytables_ilp.rs +++ b/src/rules/consistencyofdatabasefrequencytables_ilp.rs @@ -135,13 +135,16 @@ impl ReductionResult for ReductionCDFTToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionCDFTToILP {} -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_objects * total_domain_size + num_objects * num_frequency_cells", num_constraints = "num_objects * num_attributes + num_known_values + num_frequency_cells + 3 * num_objects * num_frequency_cells", num_nonzeros = "num_objects * total_domain_size + num_known_values + 8 * num_objects * num_frequency_cells", - } -)] + }, + upper_bound { + max_constraint_magnitude_bits = "num_objects + 1", + }, +})] impl ReduceTo> for ConsistencyOfDatabaseFrequencyTables { type Result = ReductionCDFTToILP; diff --git a/src/rules/decisionmaximumindependentset_integralflowbundles.rs b/src/rules/decisionmaximumindependentset_integralflowbundles.rs index 3f367ec5d..fe3b6d246 100644 --- a/src/rules/decisionmaximumindependentset_integralflowbundles.rs +++ b/src/rules/decisionmaximumindependentset_integralflowbundles.rs @@ -80,6 +80,7 @@ impl crate::rules::AggregateReductionResult for ReductionDecisionMISToIFB {} #[reduction( transform = exact { + max_capacity_bits = "2", num_vertices = "num_vertices + 3", num_arcs = "2 * num_vertices + 2", num_bundles = "num_edges + num_vertices + 1", diff --git a/src/rules/decisionminimumdominatingset_minmaxmulticenter.rs b/src/rules/decisionminimumdominatingset_minmaxmulticenter.rs index ac4b58d98..757c4223f 100644 --- a/src/rules/decisionminimumdominatingset_minmaxmulticenter.rs +++ b/src/rules/decisionminimumdominatingset_minmaxmulticenter.rs @@ -48,6 +48,7 @@ impl crate::rules::AggregateReductionResult #[reduction( transform = exact { + max_numeric_magnitude_bits = "1", num_vertices = "num_vertices + 2", num_edges = "num_edges", } diff --git a/src/rules/decisionminimumvertexcover_hamiltoniancircuit.rs b/src/rules/decisionminimumvertexcover_hamiltoniancircuit.rs index e2cea1103..2c98d97d4 100644 --- a/src/rules/decisionminimumvertexcover_hamiltoniancircuit.rs +++ b/src/rules/decisionminimumvertexcover_hamiltoniancircuit.rs @@ -300,12 +300,10 @@ impl crate::rules::AggregateReductionResult { } -#[reduction( - transform = unavailable { - num_vertices = "the construction size depends on the decision threshold, which is not a problem parameter", - num_edges = "the construction size depends on the decision threshold, which is not a problem parameter", - } -)] +#[reduction(transform = upper_bound { + num_vertices = "num_vertices + 12 * num_edges + 3", + num_edges = "(num_vertices + 12 * num_edges + 3)^2", +})] impl ReduceTo> for Decision> { type Result = ReductionDecisionMinimumVertexCoverToHamiltonianCircuit; diff --git a/src/rules/directedhamiltonianpath_ilp.rs b/src/rules/directedhamiltonianpath_ilp.rs index fb4415956..9aa92ae52 100644 --- a/src/rules/directedhamiltonianpath_ilp.rs +++ b/src/rules/directedhamiltonianpath_ilp.rs @@ -54,6 +54,7 @@ impl ReductionResult for ReductionDirectedHamiltonianPathToILP { 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) * (3 * num_vertices + num_vertices^3)", diff --git a/src/rules/directedtwocommodityintegralflow_ilp.rs b/src/rules/directedtwocommodityintegralflow_ilp.rs index de6b9e0ef..f281c8e22 100644 --- a/src/rules/directedtwocommodityintegralflow_ilp.rs +++ b/src/rules/directedtwocommodityintegralflow_ilp.rs @@ -1,4 +1,4 @@ -//! Reduction from DirectedTwoCommodityIntegralFlow to `ILP`. +//! Reduction from DirectedTwoCommodityIntegralFlow to `ILP`. //! //! One non-negative integer variable per (commodity, arc): //! f1_a = a for a in 0..num_arcs (commodity 1 flow on arc a) @@ -12,27 +12,27 @@ //! Objective: Minimize 0 (feasibility). //! Extraction: Direct 2*|A| variables. -use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{Bounded, IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::DirectedTwoCommodityIntegralFlow; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; -/// Result of reducing DirectedTwoCommodityIntegralFlow to `ILP`. +/// Result of reducing DirectedTwoCommodityIntegralFlow to `ILP`. /// /// Variable layout: /// - `f1_a` at index a for a in 0..num_arcs (commodity 1) /// - `f2_a` at index num_arcs + a for a in 0..num_arcs (commodity 2) #[derive(Debug, Clone)] pub struct ReductionD2CIFToILP { - target: ILP, + target: ILP, num_arcs: usize, } impl ReductionResult for ReductionD2CIFToILP { type Source = DirectedTwoCommodityIntegralFlow; - type Target = ILP; + type Target = ILP; - fn target_problem(&self) -> &ILP { + fn target_problem(&self) -> &ILP { &self.target } @@ -55,12 +55,15 @@ impl ReductionResult for ReductionD2CIFToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionD2CIFToILP {} -#[reduction(transform = upper_bound { - num_vars = "2 * num_arcs", - num_constraints = "num_arcs + 2 * num_vertices + 2", - num_nonzeros = "(2 * num_arcs) * (num_arcs + 2 * num_vertices + 2)", -})] -impl ReduceTo> for DirectedTwoCommodityIntegralFlow { +#[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)", + }, +)] +impl ReduceTo> for DirectedTwoCommodityIntegralFlow { type Result = ReductionD2CIFToILP; fn reduce_to(&self) -> Result { @@ -136,7 +139,13 @@ impl ReduceTo> for DirectedTwoCommodityIntegralFlow { sink1_terms.push((f1(a), -1)); } } - constraints.push(LinearConstraint::ge(sink1_terms, self.requirement_1())); + constraints.push(LinearConstraint::ge( + sink1_terms, + crate::rules::ilp_helpers::bounded_flow_requirement( + self.requirement_1(), + self.capacities().iter().copied(), + ), + )); // Net flow into sink_2 ≥ requirement_2 let sink_2 = self.sink_2(); @@ -149,7 +158,13 @@ impl ReduceTo> for DirectedTwoCommodityIntegralFlow { sink2_terms.push((f2(a), -1)); } } - constraints.push(LinearConstraint::ge(sink2_terms, self.requirement_2())); + constraints.push(LinearConstraint::ge( + sink2_terms, + crate::rules::ilp_helpers::bounded_flow_requirement( + self.requirement_2(), + self.capacities().iter().copied(), + ), + )); let variables = self .capacities() @@ -201,7 +216,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) + crate::example_db::specs::rule_example_via_bounded_ilp::<_>(source) }, }] } diff --git a/src/rules/disjointconnectingpaths_ilp.rs b/src/rules/disjointconnectingpaths_ilp.rs index 674db108b..39ddfbfc5 100644 --- a/src/rules/disjointconnectingpaths_ilp.rs +++ b/src/rules/disjointconnectingpaths_ilp.rs @@ -99,6 +99,7 @@ impl crate::rules::AggregateReductionResult for ReductionDCPToILP {} num_constraints = "num_pairs * num_vertices + num_vertices", }, upper_bound { + max_constraint_magnitude_bits = "1", num_nonzeros = "(num_pairs * 2 * num_edges) * (num_pairs * num_vertices + num_vertices)", }, })] diff --git a/src/rules/ensemblecomputation_ilp.rs b/src/rules/ensemblecomputation_ilp.rs index edd086456..ef8a079df 100644 --- a/src/rules/ensemblecomputation_ilp.rs +++ b/src/rules/ensemblecomputation_ilp.rs @@ -1,4 +1,4 @@ -//! Polynomial-size circuit-slot reduction from EnsembleComputation to `ILP`. +//! Polynomial-size circuit-slot reduction from EnsembleComputation to `ILP`. use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::EnsembleComputation; @@ -7,7 +7,7 @@ use crate::rules::traits::{ReduceTo, ReductionResult}; #[derive(Debug, Clone)] pub struct ReductionEnsembleComputationToILP { - target: ILP, + target: ILP, universe_size: usize, budget: usize, activity_base: usize, @@ -32,7 +32,7 @@ impl ReductionEnsembleComputationToILP { impl ReductionResult for ReductionEnsembleComputationToILP { type Source = EnsembleComputation; - type Target = ILP; + type Target = ILP; fn target_problem(&self) -> &Self::Target { &self.target @@ -86,10 +86,11 @@ impl ReductionResult for ReductionEnsembleComputationToILP { num_constraints = "5 * budget - 1 + budget * (budget - 1) * (1 + 3 * universe_size) + 2 * budget * universe_size + num_subsets * budget * (universe_size + 2) + num_subsets", }, upper_bound { + max_constraint_magnitude_bits = "universe_size + budget + 1", num_nonzeros = "(3 * budget * universe_size + budget * (budget - 1) * (universe_size + 1) + num_subsets * budget + budget) * (5 * budget - 1 + budget * (budget - 1) * (1 + 3 * universe_size) + 2 * budget * universe_size + num_subsets * budget * (universe_size + 2) + num_subsets)", }, })] -impl ReduceTo> for EnsembleComputation { +impl ReduceTo> for EnsembleComputation { type Result = ReductionEnsembleComputationToILP; fn reduce_to(&self) -> Result { @@ -97,7 +98,7 @@ impl ReduceTo> for EnsembleComputation { let budget = self.budget(); let t = self.num_subsets(); let overflow = |operation| { - crate::rules::ReductionError::integer_overflow::>( + crate::rules::ReductionError::integer_overflow::>( operation, ) }; @@ -301,7 +302,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) + crate::example_db::specs::rule_example_via_ilp::<_, bool>(source) }, }] } diff --git a/src/rules/eulerianpath_ilp.rs b/src/rules/eulerianpath_ilp.rs index 64741308a..367b7efba 100644 --- a/src/rules/eulerianpath_ilp.rs +++ b/src/rules/eulerianpath_ilp.rs @@ -23,12 +23,12 @@ //! Bang-Jensen and Gutin, *Digraphs: Theory, Algorithms and Applications*, //! 2nd ed., Springer (2009). -use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{Bounded, IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::EulerianPath; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; -/// Result of reducing EulerianPath to `ILP`. +/// Result of reducing EulerianPath to `ILP`. /// /// Variable layout (all in the non-negative integer domain, with explicit /// upper bounds enforcing the intended `0/1` and `0..m-1` ranges): @@ -42,7 +42,7 @@ use crate::rules::traits::{ReduceTo, ReductionResult}; /// where `p = pairs.len()` is the number of compatible ordered pairs. #[derive(Debug, Clone)] pub struct ReductionEulerianPathToILP { - target: ILP, + target: ILP, /// Compatible ordered pairs `(a, b)` in the order their `y_{a,b}` variables /// appear in the ILP, for `m > 0`. Empty when `m = 0`. pairs: Vec<(usize, usize)>, @@ -58,9 +58,9 @@ impl ReductionEulerianPathToILP { impl ReductionResult for ReductionEulerianPathToILP { type Source = EulerianPath; - type Target = ILP; + type Target = ILP; - fn target_problem(&self) -> &ILP { + fn target_problem(&self) -> &ILP { &self.target } @@ -148,11 +148,12 @@ fn compatible_pairs(arcs: &[(usize, usize)]) -> Vec<(usize, usize)> { impl crate::rules::AggregateReductionResult for ReductionEulerianPathToILP {} #[reduction(transform = upper_bound { + max_constraint_magnitude_bits = "num_arcs + 1", num_vars = "3 * num_arcs + num_arcs * num_arcs", num_constraints = "5 * num_arcs + 2 * num_arcs * num_arcs + 2", num_nonzeros = "(3 * num_arcs + num_arcs * num_arcs) * (5 * num_arcs + 2 * num_arcs * num_arcs + 2)", })] -impl ReduceTo> for EulerianPath { +impl ReduceTo> for EulerianPath { type Result = ReductionEulerianPathToILP; fn reduce_to(&self) -> Result { @@ -263,7 +264,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec1->2->0->1. let source = EulerianPath::new(DirectedGraph::new(3, vec![(0, 1), (0, 1), (1, 2), (2, 0)])); - crate::example_db::specs::rule_example_via_ilp::<_, i64>(source) + crate::example_db::specs::rule_example_via_bounded_ilp::<_>(source) }, }] } diff --git a/src/rules/exactcoverby3sets_algebraicequationsovergf2.rs b/src/rules/exactcoverby3sets_algebraicequationsovergf2.rs index b177223cf..a397b158f 100644 --- a/src/rules/exactcoverby3sets_algebraicequationsovergf2.rs +++ b/src/rules/exactcoverby3sets_algebraicequationsovergf2.rs @@ -38,8 +38,8 @@ impl crate::rules::AggregateReductionResult for ReductionX3CToAlgebraicEquations #[reduction( transform = upper_bound { - num_variables = "num_sets", - num_equations = "universe_size + 9 * num_sets^2", + num_variables = "num_subsets", + num_equations = "universe_size + 9 * num_subsets^2", })] impl ReduceTo for ExactCoverBy3Sets { type Result = ReductionX3CToAlgebraicEquationsOverGF2; diff --git a/src/rules/exactcoverby3sets_ilp.rs b/src/rules/exactcoverby3sets_ilp.rs index 446243f8e..3072e4ea9 100644 --- a/src/rules/exactcoverby3sets_ilp.rs +++ b/src/rules/exactcoverby3sets_ilp.rs @@ -39,13 +39,16 @@ impl ReductionResult for ReductionX3CToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionX3CToILP {} -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_subsets", num_constraints = "universe_size + 1", num_nonzeros = "4 * num_subsets", - } -)] + }, + upper_bound { + max_constraint_magnitude_bits = "universe_size + 1", + }, +})] impl ReduceTo> for ExactCoverBy3Sets { type Result = ReductionX3CToILP; diff --git a/src/rules/exactcoverby3sets_maximumsetpacking.rs b/src/rules/exactcoverby3sets_maximumsetpacking.rs index 8aa93e9c5..d1ac3445f 100644 --- a/src/rules/exactcoverby3sets_maximumsetpacking.rs +++ b/src/rules/exactcoverby3sets_maximumsetpacking.rs @@ -63,14 +63,14 @@ impl crate::rules::AggregateReductionResult for ReductionXC3SToMaximumSetPacking } } -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_sets = "num_subsets", }, - unavailable = { - universe_size = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + universe_size = "universe_size", + }, +})] impl ReduceTo> for ExactCoverBy3Sets { type Result = ReductionXC3SToMaximumSetPacking; diff --git a/src/rules/exactcoverby3sets_subsetproduct.rs b/src/rules/exactcoverby3sets_subsetproduct.rs index 1e902948b..b184b5942 100644 --- a/src/rules/exactcoverby3sets_subsetproduct.rs +++ b/src/rules/exactcoverby3sets_subsetproduct.rs @@ -68,7 +68,7 @@ impl crate::rules::AggregateReductionResult for ReductionX3CToSubsetProduct {} #[reduction( transform = exact { - num_elements = "num_sets", + num_elements = "num_subsets", })] impl ReduceTo for ExactCoverBy3Sets { type Result = ReductionX3CToSubsetProduct; diff --git a/src/rules/expectedretrievalcost_ilp.rs b/src/rules/expectedretrievalcost_ilp.rs index 0d321f552..5f0d49bad 100644 --- a/src/rules/expectedretrievalcost_ilp.rs +++ b/src/rules/expectedretrievalcost_ilp.rs @@ -76,13 +76,16 @@ impl ReductionResult for ReductionERCToILP { } } -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_records * num_sectors + num_records^2 * num_sectors^2", num_constraints = "num_records + 3 * num_records^2 * num_sectors^2", num_nonzeros = "7 * num_records^2 * num_sectors^2", - } -)] + }, + upper_bound { + max_constraint_magnitude_bits = "2", + }, +})] impl ReduceTo> for ExpectedRetrievalCost { type Result = ReductionERCToILP; diff --git a/src/rules/factoring_circuit.rs b/src/rules/factoring_circuit.rs index ddbe91027..c130d1fe3 100644 --- a/src/rules/factoring_circuit.rs +++ b/src/rules/factoring_circuit.rs @@ -214,16 +214,12 @@ fn build_multiplier_cell( #[crate::aggregate_reduction(identity)] impl crate::rules::AggregateReductionResult for ReductionFactoringToCircuit {} -#[reduction( - transform = upper_bound { - num_variables = "6 * num_bits_first * num_bits_second + 2 * (num_bits_first + num_bits_second) + 1", - num_assignments = "6 * num_bits_first * num_bits_second + 2 * (num_bits_first + num_bits_second) + 2", - }, - unavailable = { - num_assignment_outputs = "the exact target parameter is not represented by this reduction's symbolic transform", - num_expression_nodes = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] +#[reduction(transform = upper_bound { + num_variables = "6 * num_bits_first * num_bits_second + 2 * (num_bits_first + num_bits_second) + 1", + num_assignments = "6 * num_bits_first * num_bits_second + 2 * (num_bits_first + num_bits_second) + 2", + num_assignment_outputs = "6 * num_bits_first * num_bits_second + 2 * (num_bits_first + num_bits_second) + 2", + num_expression_nodes = "5 * (6 * num_bits_first * num_bits_second + 2 * (num_bits_first + num_bits_second) + 2)", +})] impl ReduceTo for Factoring { type Result = ReductionFactoringToCircuit; diff --git a/src/rules/factoring_ilp.rs b/src/rules/factoring_ilp.rs index a9ee5b30f..d3cabd998 100644 --- a/src/rules/factoring_ilp.rs +++ b/src/rules/factoring_ilp.rs @@ -19,7 +19,7 @@ //! 4. Binary bounds: p_i ≤ 1, q_j ≤ 1 //! 5. Carry bounds: 0 ≤ c_k ≤ min(m, n) -use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{Bounded, IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::Factoring; use crate::reduction; use crate::rules::ilp_helpers::mccormick_product; @@ -34,7 +34,7 @@ use std::cmp::min; /// - Constraints enforce the multiplication equals the target #[derive(Debug, Clone)] pub struct ReductionFactoringToILP { - target: ILP, + target: ILP, m: usize, // bits for first factor n: usize, // bits for second factor } @@ -65,9 +65,9 @@ impl ReductionFactoringToILP { impl ReductionResult for ReductionFactoringToILP { type Source = Factoring; - type Target = ILP; + type Target = ILP; - fn target_problem(&self) -> &ILP { + fn target_problem(&self) -> &ILP { &self.target } @@ -114,11 +114,12 @@ impl ReductionResult for ReductionFactoringToILP { impl crate::rules::AggregateReductionResult for ReductionFactoringToILP {} #[reduction(transform = upper_bound { + max_constraint_magnitude_bits = "num_bits_first + num_bits_second + 2", num_vars = "num_bits_first * num_bits_second + 2 * num_bits_first + 2 * num_bits_second + target_bits", num_constraints = "3 * num_bits_first * num_bits_second + 4 * num_bits_first + 4 * num_bits_second + 3 * target_bits + 1", num_nonzeros = "(num_bits_first * num_bits_second + 2 * num_bits_first + 2 * num_bits_second + target_bits) * (3 * num_bits_first * num_bits_second + 4 * num_bits_first + 4 * num_bits_second + 3 * target_bits + 1)", })] -impl ReduceTo> for Factoring { +impl ReduceTo> for Factoring { type Result = ReductionFactoringToILP; fn reduce_to(&self) -> Result { @@ -208,8 +209,10 @@ impl ReduceTo> for Factoring { } // Constraint 5: Carry bounds (0 ≤ c_k ≤ min(m, n)) - let carry_upper = - >>::exact_i64(min(m, n), "encoding a carry bound")?; + let carry_upper = >>::exact_i64( + min(m, n), + "encoding a carry bound", + )?; for k in 0..num_carries { let cv = carry_var(k); constraints.push(LinearConstraint::ge(vec![(cv, 1)], 0)); @@ -222,12 +225,16 @@ impl ReduceTo> for Factoring { let mut variables = vec![IntegerVariable::binary(); num_vars]; variables[num_p + num_q + num_z..].fill( IntegerVariable::new(Some(0), Some(carry_upper)) - .map_err(>>::target_construction)?, + .map_err(>>::target_construction)?, ); - let ilp = - ILP::::with_variables(variables, constraints, objective, ObjectiveSense::Minimize) - .map_err(>>::target_construction)?; + let ilp = ILP::::with_variables( + variables, + constraints, + objective, + ObjectiveSense::Minimize, + ) + .map_err(>>::target_construction)?; Ok(ReductionFactoringToILP { target: ilp, m, n }) } @@ -239,7 +246,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) + crate::example_db::specs::rule_example_via_bounded_ilp::<_>(source) }, }] } diff --git a/src/rules/feasibleregisterassignment_ilp.rs b/src/rules/feasibleregisterassignment_ilp.rs index d56068386..d5815f763 100644 --- a/src/rules/feasibleregisterassignment_ilp.rs +++ b/src/rules/feasibleregisterassignment_ilp.rs @@ -10,22 +10,22 @@ //! interval non-overlap: if `u` is before `v`, then `v` must be scheduled no //! earlier than the latest dependent of `u`. -use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{Bounded, IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::FeasibleRegisterAssignment; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; #[derive(Debug, Clone)] pub struct ReductionFeasibleRegisterAssignmentToILP { - target: ILP, + target: ILP, num_vertices: usize, } impl ReductionResult for ReductionFeasibleRegisterAssignmentToILP { type Source = FeasibleRegisterAssignment; - type Target = ILP; + type Target = ILP; - fn target_problem(&self) -> &ILP { + fn target_problem(&self) -> &ILP { &self.target } @@ -47,14 +47,17 @@ impl ReductionResult for ReductionFeasibleRegisterAssignmentToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionFeasibleRegisterAssignmentToILP {} -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "2 * num_vertices + num_vertices * (num_vertices - 1) / 2", num_constraints = "3 * num_vertices * (num_vertices - 1) / 2 + 3 * num_vertices + 2 * num_arcs + 2 * num_same_register_pairs", num_nonzeros = "4 * num_vertices + 4 * num_arcs + 7 * num_vertices * (num_vertices - 1) / 2 + 6 * num_same_register_pairs", - } -)] -impl ReduceTo> for FeasibleRegisterAssignment { + }, + upper_bound { + max_constraint_magnitude_bits = "num_vertices + 1", + }, +})] +impl ReduceTo> for FeasibleRegisterAssignment { type Result = ReductionFeasibleRegisterAssignmentToILP; fn reduce_to(&self) -> Result { @@ -160,7 +163,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) + crate::example_db::specs::rule_example_via_bounded_ilp::<_>(source) }, }] } diff --git a/src/rules/flowshopscheduling_ilp.rs b/src/rules/flowshopscheduling_ilp.rs index fd91dcb2d..712610d30 100644 --- a/src/rules/flowshopscheduling_ilp.rs +++ b/src/rules/flowshopscheduling_ilp.rs @@ -1,16 +1,16 @@ -//! Reduction from FlowShopScheduling to `ILP`. +//! Reduction from FlowShopScheduling to `ILP`. //! //! Binary order variables y_{i,j} with y_{i,j}=1 iff job i precedes job j, //! integer completion-time variables C_{j,q} for each job j and machine q. //! Machine-chain and big-M disjunctive constraints enforce a valid flow-shop //! schedule; the deadline becomes a makespan bound. -use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{Bounded, IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::FlowShopScheduling; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; -/// Result of reducing FlowShopScheduling to `ILP`. +/// Result of reducing FlowShopScheduling to `ILP`. /// /// Variable layout: /// - `y_{i,j}` for each ordered pair (i,j) with i, + target: ILP, num_jobs: usize, num_machines: usize, num_order_vars: usize, @@ -28,9 +28,9 @@ pub struct ReductionFSSToILP { impl ReductionResult for ReductionFSSToILP { type Source = FlowShopScheduling; - type Target = ILP; + type Target = ILP; - fn target_problem(&self) -> &ILP { + fn target_problem(&self) -> &ILP { &self.target } @@ -71,12 +71,15 @@ impl ReductionResult for ReductionFSSToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionFSSToILP {} -#[reduction(transform = upper_bound { - 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) / 2 + num_jobs * num_processors) * (num_jobs * (num_jobs - 1) + num_jobs + num_jobs * (num_processors - 1) + num_jobs * (num_jobs - 1) * num_processors + num_jobs)", -})] -impl ReduceTo> for FlowShopScheduling { +#[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) / 2 + num_jobs * num_processors) * (num_jobs * (num_jobs - 1) + num_jobs + num_jobs * (num_processors - 1) + num_jobs * (num_jobs - 1) * num_processors + num_jobs)", + }, +)] +impl ReduceTo> for FlowShopScheduling { type Result = ReductionFSSToILP; fn reduce_to(&self) -> Result { @@ -107,9 +110,10 @@ impl ReduceTo> for FlowShopScheduling { .max() .unwrap_or(0); let big_m = d.checked_add(max_p).ok_or_else(|| { - crate::rules::ReductionError::integer_overflow::>( - "computing the flow-shop big-M bound", - ) + crate::rules::ReductionError::integer_overflow::< + FlowShopScheduling, + ILP, + >("computing the flow-shop big-M bound") })?; let mut constraints = Vec::new(); @@ -209,7 +213,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) + crate::example_db::specs::rule_example_via_bounded_ilp::<_>(source) }, }] } diff --git a/src/rules/graphpartitioning_ilp.rs b/src/rules/graphpartitioning_ilp.rs index d6cceea7a..4965ccedd 100644 --- a/src/rules/graphpartitioning_ilp.rs +++ b/src/rules/graphpartitioning_ilp.rs @@ -49,6 +49,7 @@ impl ReductionResult for ReductionGraphPartitioningToILP { num_constraints = "2 * num_edges + 1", }, upper_bound { + max_constraint_magnitude_bits = "num_vertices + 2", num_nonzeros = "(num_vertices + num_edges) * (2 * num_edges + 1)", }, })] diff --git a/src/rules/hamiltoniancircuit_biconnectivityaugmentation.rs b/src/rules/hamiltoniancircuit_biconnectivityaugmentation.rs index 654ee26f5..8a5b16a46 100644 --- a/src/rules/hamiltoniancircuit_biconnectivityaugmentation.rs +++ b/src/rules/hamiltoniancircuit_biconnectivityaugmentation.rs @@ -124,6 +124,7 @@ impl crate::rules::AggregateReductionResult #[reduction( transform = upper_bound { + max_numeric_magnitude_bits = "num_vertices + 1", num_vertices = "num_vertices + 3", num_edges = "0", num_potential_edges = "num_vertices * (num_vertices - 1) / 2", diff --git a/src/rules/hamiltoniancircuit_longestcircuit.rs b/src/rules/hamiltoniancircuit_longestcircuit.rs index 03ab5871c..0eaf83041 100644 --- a/src/rules/hamiltoniancircuit_longestcircuit.rs +++ b/src/rules/hamiltoniancircuit_longestcircuit.rs @@ -47,6 +47,7 @@ impl crate::rules::AggregateReductionResult for ReductionHamiltonianCircuitToLon #[reduction( transform = exact { + max_length_bits = "1", num_vertices = "num_vertices", num_edges = "num_edges", } diff --git a/src/rules/hamiltoniancircuit_strongconnectivityaugmentation.rs b/src/rules/hamiltoniancircuit_strongconnectivityaugmentation.rs index 089159a34..cc621ea80 100644 --- a/src/rules/hamiltoniancircuit_strongconnectivityaugmentation.rs +++ b/src/rules/hamiltoniancircuit_strongconnectivityaugmentation.rs @@ -87,6 +87,7 @@ impl crate::rules::AggregateReductionResult #[reduction( transform = upper_bound { + max_numeric_magnitude_bits = "num_vertices + 1", num_vertices = "num_vertices + 2", num_arcs = "0", num_potential_arcs = "num_vertices * (num_vertices - 1)", diff --git a/src/rules/hamiltonianpath_ilp.rs b/src/rules/hamiltonianpath_ilp.rs index c5087553c..d6ba3a77c 100644 --- a/src/rules/hamiltonianpath_ilp.rs +++ b/src/rules/hamiltonianpath_ilp.rs @@ -55,6 +55,7 @@ impl crate::rules::AggregateReductionResult for ReductionHamiltonianPathToILP {} #[reduction( transform = exact { + max_constraint_magnitude_bits = "1", num_vars = "num_vertices^2 + 2 * num_edges * num_consecutive_positions", num_constraints = "2 * num_vertices + 6 * num_edges * num_consecutive_positions + num_consecutive_positions", num_nonzeros = "2 * num_vertices^2 + 16 * num_edges * num_consecutive_positions", diff --git a/src/rules/highlyconnecteddeletion_ilp.rs b/src/rules/highlyconnecteddeletion_ilp.rs index 7d5d9f556..8fd2c2cf2 100644 --- a/src/rules/highlyconnecteddeletion_ilp.rs +++ b/src/rules/highlyconnecteddeletion_ilp.rs @@ -1,50 +1,34 @@ -//! Reduction from HighlyConnectedDeletion to ILP (Integer Linear Programming). +//! Polynomial reduction from HighlyConnectedDeletion to binary ILP. //! -//! Encodes the set-partitioning ILP of Hüffner, Komusiewicz, Liebtrau, and -//! Niedermeier (IEEE/ACM TCBB 2014). Given a simple undirected graph -//! `G = (V, E)`: +//! One variable per unordered vertex pair records membership in the same cluster. +//! Triangle inequalities make membership transitive. A binary flag per vertex +//! distinguishes singleton clusters, and linear degree constraints enforce +//! minimum degree strictly greater than half the cluster size otherwise. //! -//! - Enumerate the family `C(G)` of *feasible clusters*: every singleton plus -//! every subset `S` with `|S| >= 3` whose induced subgraph `G[S]` is highly -//! connected (edge connectivity strictly greater than `|S| / 2`). -//! - Introduce a binary variable `x_S` per feasible cluster (1 iff `S` is one -//! block of the chosen partition). -//! - Partition constraints: for every vertex `v`, -//! `sum_{S in C(G), v in S} x_S = 1`. -//! - Maximize the number of kept (intra-cluster) edges: -//! `max sum_{S in C(G)} |E(G[S])| * x_S`. +//! This degree condition is equivalent to high edge connectivity: for minimum +//! degree d > k/2, a cut with smaller side a <= k/2 has at least +//! a*(d-a+1) >= d edges. Conversely, edge connectivity never exceeds minimum +//! degree. See . //! -//! Because `|E|` is fixed, maximizing kept internal edges minimizes deleted -//! edges; the source value is recovered as -//! `deleted_edges = |E| - ilp_objective`. -//! -//! Reference: Falk Hüffner, Christian Komusiewicz, Adrian Liebtrau, and Rolf -//! Niedermeier, "Partitioning Biological Networks into Highly Connected -//! Clusters with Maximum Edge Coverage," IEEE/ACM Transactions on -//! Computational Biology and Bioinformatics 11(3):455–467, 2014. -//! +//! Maximize the number of non-loop edges kept inside clusters. Any feasible +//! deletion can restore all internal edges without losing connectivity, so an +//! optimal source solution is represented. Self-loops are always kept. use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; -use crate::models::graph::highly_connected_deletion::{induced_edge_count, is_feasible_cluster}; use crate::models::graph::HighlyConnectedDeletion; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; use crate::topology::{Graph, SimpleGraph}; -/// Result of reducing HighlyConnectedDeletion to ILP. +/// Result of reducing HighlyConnectedDeletion to binary ILP. /// -/// Variable layout (all binary): -/// - `x_S` at index `c` is the indicator for the `c`-th feasible cluster -/// stored in `clusters`. Indices follow the enumeration order produced by -/// [`enumerate_feasible_clusters`], which always lists every singleton first -/// followed by larger feasible clusters in subset-id order. +/// Variables are unordered pairs in lexicographic order, followed by one +/// non-singleton flag per vertex. #[derive(Debug, Clone)] pub struct ReductionHighlyConnectedDeletionToILP { target: ILP, - /// Feasible clusters in variable order; `clusters[c]` is sorted ascending. - clusters: Vec>, - /// Source graph edges in the same order as `source.graph().edges()`. - edges: Vec<(usize, usize)>, + /// Pair variable for each source edge; self-loops have no variable. + edge_variables: Vec>, } impl ReductionResult for ReductionHighlyConnectedDeletionToILP { @@ -55,165 +39,110 @@ impl ReductionResult for ReductionHighlyConnectedDeletionToILP { &self.target } - /// Decode a binary ILP assignment into the source's edge-deletion config. - /// - /// For every source edge `(u, v)`, the edge is *kept* iff some chosen - /// cluster `S` (i.e. with `x_S = 1`) contains both `u` and `v`; otherwise - /// it is deleted (`config[e] = 1`). fn extract_solution( &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - - let mut cluster_of: Vec> = vec![None; vertex_count(&self.clusters)]; - for (c, cluster) in self.clusters.iter().enumerate() { - if target_solution[c] == 1 { - for &v in cluster { - if cluster_of[v].is_some() { - return Err(crate::rules::ExtractionError::invalid(format!( - "vertex {v} belongs to multiple selected clusters" - ))); - } - cluster_of[v] = Some(c); - } - } else if target_solution[c] != 0 { - return Err(crate::rules::ExtractionError::invalid(format!( - "cluster selection {c} is not binary" - ))); - } - } - - if let Some(vertex) = cluster_of.iter().position(Option::is_none) { - return Err(crate::rules::ExtractionError::invalid(format!( - "vertex {vertex} has no selected cluster" - ))); - } - + crate::rules::traits::validate_target_witness( + self.target_problem(), + target_solution, + |value| value.value.is_some(), + "target ILP assignment is infeasible", + )?; Ok(self - .edges + .edge_variables .iter() - .map(|&(u, v)| cluster_of[u] != cluster_of[v]) + .map(|variable| variable.is_some_and(|index| target_solution[index] == 0)) .collect()) } } -/// Number of source vertices, recovered from the clusters list. -/// -/// The reduction always enumerates the `n` singletons first, so any vertex id -/// occurring anywhere in `clusters` is `< n`. We read `n` off the singletons -/// for clarity and robustness. -fn vertex_count(clusters: &[Vec]) -> usize { - clusters - .iter() - .filter(|c| c.len() == 1) - .map(|c| c[0] + 1) - .max() - .unwrap_or(0) -} - -/// Enumerate every feasible cluster of `graph` in deterministic order. -/// -/// Order: all `n` singletons first (subset ids `1, 2, 4, ...`), then larger -/// feasible clusters listed by ascending bitmask of their vertex set. This -/// gives a stable variable layout; tests pin the singleton prefix. -fn enumerate_feasible_clusters( - graph: &SimpleGraph, -) -> Result>, crate::rules::ReductionError> { - let n = graph.num_vertices(); - if n >= u64::BITS as usize { - return Err(crate::rules::ReductionError::integer_overflow::< - HighlyConnectedDeletion, - ILP, - >("enumerating vertex subsets with a u64 mask")); - } - let mut clusters: Vec> = Vec::new(); - - // Singletons first. - for v in 0..n { - clusters.push(vec![v]); - } - - if n < 3 { - return Ok(clusters); - } - - // Larger feasible clusters by ascending subset bitmask. - for mask in 1u64..(1u64 << n) { - let popcount = mask.count_ones() as usize; - if popcount < 3 { - continue; - } - let subset: Vec = (0..n).filter(|v| (mask >> v) & 1 == 1).collect(); - if is_feasible_cluster(graph, &subset) { - clusters.push(subset); - } - } - - Ok(clusters) -} - -#[reduction( - transform = exact { - num_constraints = "num_vertices", +// Three rows per vertex triple and two per vertex. Triple rows have three +// nonzeros; each vertex row has at most n. All row magnitudes are <= max(n-1, 2). +#[reduction(transform = { + exact { + num_vars = "num_vertices * (num_vertices + 1) / 2", }, - unavailable = { - num_vars = "the feasible-cluster count depends on graph structure, and its 2^num_vertices upper bound requires a variable exponent unsupported by the size-transform evaluator", - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", -} -)] + upper_bound { + num_constraints = "num_vertices^3 + 2 * num_vertices", + num_nonzeros = "3 * num_vertices^3 + 2 * num_vertices^2", + max_constraint_magnitude_bits = "num_vertices + 2", + }, +})] impl ReduceTo> for HighlyConnectedDeletion { type Result = ReductionHighlyConnectedDeletionToILP; fn reduce_to(&self) -> Result { let graph = self.graph(); let n = graph.num_vertices(); - let clusters = enumerate_feasible_clusters(graph)?; - let num_vars = clusters.len(); - - // Partition constraints: for every vertex v, sum_{S : v in S} x_S = 1. - // Each constraint is built by scanning the cluster list once per - // vertex; total work is O(n * sum |S|) which stays tractable for the - // small graphs we use in tests. - let mut constraints: Vec = Vec::with_capacity(n); - for v in 0..n { - let terms: Vec<(usize, i64)> = clusters - .iter() - .enumerate() - .filter_map(|(c, cluster)| { - if cluster.binary_search(&v).is_ok() { - Some((c, 1)) - } else { - None + let num_pairs = n.checked_mul(n.saturating_sub(1)).map(|value| value / 2); + let num_vars = num_pairs + .and_then(|pairs| pairs.checked_add(n)) + .ok_or_else(|| { + crate::rules::ReductionError::integer_overflow::>( + "counting pair and non-singleton variables", + ) + })?; + let flag_offset = num_vars - n; + let max_companions = + Self::exact_i64(n.saturating_sub(1), "encoding the cluster-size bound")?; + // Lexicographic pair index: preceding row lengths, then the column offset. + // The checked n*(n-1) above also bounds these products for u < v < n. + let pair = |u: usize, v: usize| { + let (u, v) = (u.min(v), u.max(v)); + u * n - u * (u + 1) / 2 + (v - u - 1) + }; + let mut constraints = Vec::new(); + for u in 0..n { + for v in u + 1..n { + for w in v + 1..n { + let (a, b, c) = (pair(u, v), pair(u, w), pair(v, w)); + for (first, second, third) in [(a, b, c), (a, c, b), (b, c, a)] { + constraints.push(LinearConstraint::le( + vec![(first, 1), (second, 1), (third, -1)], + 1, + )); } - }) - .collect(); - constraints.push(LinearConstraint::eq(terms, 1)); + } + } } - - // Objective: maximize sum_S |E(G[S])| * x_S. - let objective: Vec<(usize, i64)> = clusters + for v in 0..n { + // s_v is the number of other vertices in v's cluster. The flag + // a_v is forced on for s_v > 0 by s_v <= (n-1)*a_v. + let mut size_terms = Vec::new(); + let mut degree_terms = Vec::new(); + for u in 0..n { + if u != v { + let variable = pair(u, v); + size_terms.push((variable, 1)); + // 2*d_v - s_v: count each distinct, non-loop neighbor once. + degree_terms.push((variable, if graph.has_edge(u, v) { 1 } else { -1 })); + } + } + size_terms.push((flag_offset + v, -max_companions)); + constraints.push(LinearConstraint::le(size_terms, 0)); + // 2*d_v >= s_v + 2*a_v: singleton clusters pass, while other + // clusters require 2*d_v > s_v+1 and automatically exclude pairs. + degree_terms.push((flag_offset + v, -2)); + constraints.push(LinearConstraint::ge(degree_terms, 0)); + } + let edge_variables: Vec<_> = graph + .edges() .iter() - .enumerate() - .map(|(c, cluster)| { - i64::try_from(induced_edge_count(graph, cluster)) - .map(|count| (c, count)) - .map_err(|_| { - crate::rules::ReductionError::integer_overflow::< - HighlyConnectedDeletion, - ILP, - >("encoding an induced edge count") - }) - }) - .collect::>()?; - + .map(|&(u, v)| (u != v).then(|| pair(u, v))) + .collect(); + // Duplicate edges contribute their multiplicity to the objective; + // self-loops are constant and need no objective term. + let objective = edge_variables + .iter() + .flatten() + .map(|&variable| (variable, 1)) + .collect(); let target = ILP::new(num_vars, constraints, objective, ObjectiveSense::Maximize) .map_err(Self::target_construction)?; - Ok(ReductionHighlyConnectedDeletionToILP { target, - clusters, - edges: graph.edges(), + edge_variables, }) } } @@ -223,7 +152,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec, + target: ILP, } impl ReductionResult for ReductionBinaryILPToIntILP { type Source = ILP; - type Target = ILP; + type Target = ILP; - fn target_problem(&self) -> &ILP { + fn target_problem(&self) -> &ILP { &self.target } @@ -33,22 +34,23 @@ impl ReductionResult for ReductionBinaryILPToIntILP { #[reduction( transform = exact { + max_constraint_magnitude_bits = "max_constraint_magnitude_bits", num_vars = "num_vars", num_constraints = "num_constraints", num_nonzeros = "num_nonzeros", },)] -impl ReduceTo> for ILP { +impl ReduceTo> for ILP { type Result = ReductionBinaryILPToIntILP; fn reduce_to(&self) -> Result { Ok(ReductionBinaryILPToIntILP { - target: ILP::::with_variables( + target: ILP::::with_variables( self.variables().to_vec(), self.constraints().to_vec(), self.objective().to_vec(), self.sense(), ) - .map_err(>>::target_construction)?, + .map_err(>>::target_construction)?, }) } } diff --git a/src/rules/ilp_bounded_ilp.rs b/src/rules/ilp_bounded_ilp.rs new file mode 100644 index 000000000..26d57103f --- /dev/null +++ b/src/rules/ilp_bounded_ilp.rs @@ -0,0 +1,59 @@ +//! Forget the bounded-interval certificate while retaining the entire ILP. + +use crate::models::algebraic::{Bounded, ILP}; +use crate::reduction; +use crate::rules::{ReduceTo, ReductionResult}; + +/// Identity embedding of bounded integer ILP into general integer ILP. +#[derive(Debug, Clone)] +pub struct ReductionBoundedILPToILP { + target: ILP, +} + +impl ReductionResult for ReductionBoundedILPToILP { + type Source = ILP; + type Target = ILP; + + fn target_problem(&self) -> &Self::Target { + &self.target + } + + fn extract_solution(&self, solution: &Vec) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_witness( + ReductionResult::target_problem(self), + solution, + |value| value.value.is_some(), + "target ILP assignment is infeasible", + )?; + Ok(solution.clone()) + } +} + +#[crate::aggregate_reduction(identity)] +impl crate::rules::AggregateReductionResult for ReductionBoundedILPToILP {} + +#[reduction(transform = exact { + max_constraint_magnitude_bits = "max_constraint_magnitude_bits", + num_vars = "num_vars", + num_constraints = "num_constraints", + num_nonzeros = "num_nonzeros", +})] +impl ReduceTo> for ILP { + type Result = ReductionBoundedILPToILP; + + fn reduce_to(&self) -> Result { + Ok(ReductionBoundedILPToILP { + target: ILP::with_variables( + self.variables().to_vec(), + self.constraints().to_vec(), + self.objective().to_vec(), + self.sense(), + ) + .map_err(>>::target_construction)?, + }) + } +} + +#[cfg(test)] +#[path = "../unit_tests/rules/ilp_bounded_ilp.rs"] +mod tests; diff --git a/src/rules/ilp_casts.rs b/src/rules/ilp_casts.rs index 586fe6605..088ce6dca 100644 --- a/src/rules/ilp_casts.rs +++ b/src/rules/ilp_casts.rs @@ -77,6 +77,7 @@ impl ReductionResult for ReductionILPToFloat { #[reduction( transform = exact { + max_constraint_magnitude_bits = "max_constraint_magnitude_bits", num_vars = "num_vars", num_constraints = "num_constraints", num_nonzeros = "num_nonzeros", @@ -92,6 +93,7 @@ impl ReduceTo> for ILP { #[reduction( transform = exact { + max_constraint_magnitude_bits = "max_constraint_magnitude_bits", num_vars = "num_vars", num_constraints = "num_constraints", num_nonzeros = "num_nonzeros", diff --git a/src/rules/ilp_helpers.rs b/src/rules/ilp_helpers.rs index 4f982bdc5..22bf6e298 100644 --- a/src/rules/ilp_helpers.rs +++ b/src/rules/ilp_helpers.rs @@ -2,6 +2,18 @@ use crate::models::algebraic::LinearConstraint; +/// Normalize a lower threshold for flow in `[-sum(capacities), sum(capacities)]`. +/// Capacities must be nonnegative. Requests above the range remain infeasible; +/// those below it remain redundant. Saturation is safe because the input +/// threshold is i64, so it cannot exceed a larger mathematical range. +pub(crate) fn bounded_flow_requirement( + requirement: i64, + capacities: impl IntoIterator, +) -> i64 { + let magnitude = capacities.into_iter().fold(0_i64, i64::saturating_add); + requirement.clamp(-magnitude, magnitude.saturating_add(1)) +} + /// Convert exact ILP integer values into a source model's `usize` representation. pub fn decode_usize_values(values: &[i64]) -> crate::rules::ExtractionResult> { values diff --git a/src/rules/ilp_i64_ilp_bool.rs b/src/rules/ilp_i64_ilp_bool.rs index 4a6944ea2..4d357920d 100644 --- a/src/rules/ilp_i64_ilp_bool.rs +++ b/src/rules/ilp_i64_ilp_bool.rs @@ -1,6 +1,6 @@ //! Encode finitely bounded integer ILP variables as binary variables. -use crate::models::algebraic::{Comparison, LinearConstraint, ILP}; +use crate::models::algebraic::{Bounded, Comparison, LinearConstraint, ILP}; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; use crate::rules::ReductionError; @@ -13,7 +13,7 @@ struct VarEncoding { } fn overflow(operation: impl Into) -> ReductionError { - ReductionError::integer_overflow::, ILP>(operation) + ReductionError::integer_overflow::, ILP>(operation) } fn binary_weights(width: i64) -> Vec { @@ -73,7 +73,7 @@ pub struct ReductionIntILPToBinaryILP { } impl ReductionResult for ReductionIntILPToBinaryILP { - type Source = ILP; + type Source = ILP; type Target = ILP; fn target_problem(&self) -> &ILP { @@ -110,32 +110,30 @@ impl ReductionResult for ReductionIntILPToBinaryILP { } } +// If all finite endpoints and row entries have magnitude below 2^h, widths +// need at most h+1 bits. Encoded coefficients are below 2^(2h+1), and the +// lower-bound shift gives |b'| < 2^h + n*2^(2h) < 2^(2h+n+1). #[reduction( - transform = exact { - num_constraints = "num_constraints", - }, - unavailable = { - num_vars = "the binary width depends on concrete variable bounds, not registered problem parameters", - num_nonzeros = "binary expansion depends on concrete variable bounds and row sparsity", + transform = { + exact { + num_constraints = "num_constraints", + }, + upper_bound { + num_vars = "num_vars * (max_constraint_magnitude_bits + 1)", + num_nonzeros = "num_nonzeros * (max_constraint_magnitude_bits + 1)", + max_constraint_magnitude_bits = "2 * max_constraint_magnitude_bits + num_vars + 1", + }, }, )] -impl ReduceTo> for ILP { +impl ReduceTo> for ILP { type Result = ReductionIntILPToBinaryILP; fn reduce_to(&self) -> Result { let mut encodings = Vec::with_capacity(self.num_vars()); let mut num_binary_variables = 0_usize; for variable in self.variables() { - let lower_bound = variable.lower_bound().ok_or_else(|| { - ReductionError::invalid_target::, ILP>( - "binary encoding requires a finite lower bound for every integer variable", - ) - })?; - let upper_bound = variable.upper_bound().ok_or_else(|| { - ReductionError::invalid_target::, ILP>( - "binary encoding requires a finite upper bound for every integer variable", - ) - })?; + let lower_bound = variable.lower_bound().expect("bounded ILP lower bound"); + let upper_bound = variable.upper_bound().expect("bounded ILP upper bound"); let width = upper_bound .checked_sub(lower_bound) .ok_or_else(|| overflow("computing an integer variable interval width"))?; diff --git a/src/rules/ilp_qubo.rs b/src/rules/ilp_qubo.rs index 32408abe6..68d37f8c9 100644 --- a/src/rules/ilp_qubo.rs +++ b/src/rules/ilp_qubo.rs @@ -78,12 +78,12 @@ impl crate::rules::AggregateReductionResult for ReductionILPToQUBO { } } -// Each successfully computed positive i64 slack range needs at most 63 bits. -// Distinct off-diagonal pairs bound the resulting quadratic terms, including -// when penalty contributions cancel. Both bounds use only source parameters. +// With n variables and magnitude bits h, each positive slack range is less +// than (n+1)*2^h <= 2^(n+h), so each row adds at most n+h bits. Squaring the +// resulting variable bound covers every off-diagonal pair, even with cancellation. #[reduction(transform = upper_bound { - num_vars = "num_vars + 63 * num_constraints", - num_quadratic_terms = "(num_vars + 63 * num_constraints) * (num_vars + 63 * num_constraints - 1) / 2", + num_vars = "num_vars + num_constraints * (num_vars + max_constraint_magnitude_bits)", + num_quadratic_terms = "(num_vars + num_constraints * (num_vars + max_constraint_magnitude_bits))^2", })] impl ReduceTo> for ILP { type Result = ReductionILPToQUBO; diff --git a/src/rules/integerknapsack_ilp.rs b/src/rules/integerknapsack_ilp.rs index 42f56afa1..5c6918b9a 100644 --- a/src/rules/integerknapsack_ilp.rs +++ b/src/rules/integerknapsack_ilp.rs @@ -1,24 +1,24 @@ -//! Reduction from IntegerKnapsack to `ILP`. +//! Reduction from IntegerKnapsack to `ILP`. //! //! Each item multiplicity becomes a non-negative integer ILP variable. The -//! capacity inequality is kept directly, and explicit upper bounds +//! capacity inequality omits oversized items (whose multiplicities are zero), and explicit upper bounds //! `c_i <= floor(B / s_i)` preserve the exact witness domain of the source. -use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{Bounded, IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::set::IntegerKnapsack; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; #[derive(Debug, Clone)] pub struct ReductionIntegerKnapsackToILP { - target: ILP, + target: ILP, } impl ReductionResult for ReductionIntegerKnapsackToILP { type Source = IntegerKnapsack; - type Target = ILP; + type Target = ILP; - fn target_problem(&self) -> &ILP { + fn target_problem(&self) -> &ILP { &self.target } @@ -32,14 +32,17 @@ impl ReductionResult for ReductionIntegerKnapsackToILP { } } -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_items", num_constraints = "num_items + 1", + }, + upper_bound { + max_constraint_magnitude_bits = "capacity_bits", num_nonzeros = "2 * num_items", - } -)] -impl ReduceTo> for IntegerKnapsack { + }, +})] +impl ReduceTo> for IntegerKnapsack { type Result = ReductionIntegerKnapsackToILP; fn reduce_to(&self) -> Result { @@ -52,6 +55,8 @@ impl ReduceTo> for IntegerKnapsack { sizes .iter() .enumerate() + // Oversized items already have multiplicity fixed to zero by their bounds. + .filter(|&(_, &size)| size <= self.capacity()) .map(|(item, &size)| (item, size)) .collect(), self.capacity(), @@ -89,7 +94,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) + crate::example_db::specs::rule_example_via_bounded_ilp::<_>(source) }, }] } diff --git a/src/rules/integralflowbundles_ilp.rs b/src/rules/integralflowbundles_ilp.rs index 98038f27c..8edf06a77 100644 --- a/src/rules/integralflowbundles_ilp.rs +++ b/src/rules/integralflowbundles_ilp.rs @@ -4,22 +4,23 @@ //! the bundle-capacity inequalities, flow-conservation equalities at //! nonterminals, and the sink inflow lower bound from the source problem. -use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{Bounded, IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::IntegralFlowBundles; use crate::reduction; +use crate::rules::ilp_helpers::bounded_flow_requirement; use crate::rules::traits::{ReduceTo, ReductionResult}; /// Result of reducing IntegralFlowBundles to ILP. #[derive(Debug, Clone)] pub struct ReductionIFBToILP { - target: ILP, + target: ILP, } impl ReductionResult for ReductionIFBToILP { type Source = IntegralFlowBundles; - type Target = ILP; + type Target = ILP; - fn target_problem(&self) -> &ILP { + fn target_problem(&self) -> &ILP { &self.target } @@ -47,10 +48,11 @@ impl crate::rules::AggregateReductionResult for ReductionIFBToILP {} num_constraints = "num_bundles + num_vertices - 1", }, upper_bound { + max_constraint_magnitude_bits = "max_capacity_bits + num_arcs + 1", num_nonzeros = "num_arcs * (num_bundles + num_vertices - 1)", }, })] -impl ReduceTo> for IntegralFlowBundles { +impl ReduceTo> for IntegralFlowBundles { type Result = ReductionIFBToILP; fn reduce_to(&self) -> Result { @@ -88,10 +90,12 @@ impl ReduceTo> for IntegralFlowBundles { sink_terms.push((arc_index, 1)); } } - constraints.push(LinearConstraint::ge(sink_terms, self.requirement())); + let upper_bounds = self.arc_upper_bounds(); + let requirement = + bounded_flow_requirement(self.requirement(), upper_bounds.iter().copied()); + constraints.push(LinearConstraint::ge(sink_terms, requirement)); - let variables = self - .arc_upper_bounds() + let variables = upper_bounds .into_iter() .map(|capacity| IntegerVariable::new(Some(0), Some(capacity))) .collect::, _>>() @@ -119,7 +123,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) + crate::example_db::specs::rule_example_via_bounded_ilp::<_>(source) }, }] } diff --git a/src/rules/integralflowhomologousarcs_ilp.rs b/src/rules/integralflowhomologousarcs_ilp.rs index 2ec627726..c6c8cf8b4 100644 --- a/src/rules/integralflowhomologousarcs_ilp.rs +++ b/src/rules/integralflowhomologousarcs_ilp.rs @@ -3,7 +3,7 @@ //! One integer flow variable per arc. Capacity bounds, conservation at //! non-terminals, homologous-pair equality, and sink inflow requirement. -use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{Bounded, IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::IntegralFlowHomologousArcs; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -11,14 +11,14 @@ use crate::rules::traits::{ReduceTo, ReductionResult}; /// Result of reducing IntegralFlowHomologousArcs to ILP. #[derive(Debug, Clone)] pub struct ReductionIFHAToILP { - target: ILP, + target: ILP, } impl ReductionResult for ReductionIFHAToILP { type Source = IntegralFlowHomologousArcs; - type Target = ILP; + type Target = ILP; - fn target_problem(&self) -> &ILP { + fn target_problem(&self) -> &ILP { &self.target } @@ -40,12 +40,15 @@ impl ReductionResult for ReductionIFHAToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionIFHAToILP {} -#[reduction(transform = upper_bound { - num_vars = "num_arcs", - num_constraints = "num_arcs^2 + num_arcs + num_vertices + 1", - num_nonzeros = "num_arcs * (num_arcs^2 + num_arcs + num_vertices + 1)", -})] -impl ReduceTo> for IntegralFlowHomologousArcs { +#[reduction( + transform = upper_bound { + max_constraint_magnitude_bits = "max_capacity * (num_arcs + 1) + 2", + num_vars = "num_arcs", + num_constraints = "num_arcs^2 + num_arcs + num_vertices + 1", + num_nonzeros = "num_arcs * (num_arcs^2 + num_arcs + num_vertices + 1)", + }, +)] +impl ReduceTo> for IntegralFlowHomologousArcs { type Result = ReductionIFHAToILP; fn reduce_to(&self) -> Result { @@ -91,7 +94,13 @@ impl ReduceTo> for IntegralFlowHomologousArcs { sink_terms.push((arc_idx, -1)); // outgoing } } - constraints.push(LinearConstraint::ge(sink_terms, self.requirement())); + constraints.push(LinearConstraint::ge( + sink_terms, + crate::rules::ilp_helpers::bounded_flow_requirement( + self.requirement(), + self.capacities().iter().copied(), + ), + )); let variables = self .capacities() @@ -123,7 +132,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) + crate::example_db::specs::rule_example_via_bounded_ilp::<_>(source) }, }] } diff --git a/src/rules/integralflowwithmultipliers_ilp.rs b/src/rules/integralflowwithmultipliers_ilp.rs index dc41b9fcf..94619829a 100644 --- a/src/rules/integralflowwithmultipliers_ilp.rs +++ b/src/rules/integralflowwithmultipliers_ilp.rs @@ -3,22 +3,23 @@ //! One integer flow variable per arc. Capacity bounds, multiplier-scaled //! conservation at non-terminals, and sink inflow requirement. -use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{Bounded, IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::IntegralFlowWithMultipliers; use crate::reduction; +use crate::rules::ilp_helpers::bounded_flow_requirement; use crate::rules::traits::{ReduceTo, ReductionResult}; /// Result of reducing IntegralFlowWithMultipliers to ILP. #[derive(Debug, Clone)] pub struct ReductionIFWMToILP { - target: ILP, + target: ILP, } impl ReductionResult for ReductionIFWMToILP { type Source = IntegralFlowWithMultipliers; - type Target = ILP; + type Target = ILP; - fn target_problem(&self) -> &ILP { + fn target_problem(&self) -> &ILP { &self.target } @@ -46,16 +47,22 @@ impl crate::rules::AggregateReductionResult for ReductionIFWMToILP {} num_constraints = "num_arcs + num_vertices - 1", }, upper_bound { + max_constraint_magnitude_bits = "max_capacity_bits + num_arcs + 1", num_nonzeros = "num_arcs * (num_arcs + num_vertices - 1)", }, })] -impl ReduceTo> for IntegralFlowWithMultipliers { +impl ReduceTo> for IntegralFlowWithMultipliers { type Result = ReductionIFWMToILP; fn reduce_to(&self) -> Result { let arcs = self.graph().arcs(); let num_vertices = self.num_vertices(); let mut constraints = Vec::new(); + let total_capacity = self + .capacities() + .iter() + .copied() + .fold(0_i64, i64::saturating_add); // Capacity: f_a <= c_a for each arc for (arc_idx, &capacity) in self.capacities().iter().enumerate() { @@ -70,7 +77,9 @@ impl ReduceTo> for IntegralFlowWithMultipliers { if vertex == self.source() || vertex == self.sink() { continue; } - let multiplier = self.multipliers()[vertex]; + // Outflow cannot exceed the total capacity S. A multiplier above + // S forces both integral inflow and outflow to zero, as does S+1. + let multiplier = self.multipliers()[vertex].min(total_capacity.saturating_add(1)); let mut terms = Vec::new(); for (arc_idx, &(u, v)) in arcs.iter().enumerate() { if u == vertex { @@ -93,7 +102,8 @@ impl ReduceTo> for IntegralFlowWithMultipliers { sink_terms.push((arc_idx, -1)); // outgoing } } - constraints.push(LinearConstraint::ge(sink_terms, self.requirement())); + let requirement = bounded_flow_requirement(self.requirement(), [total_capacity]); + constraints.push(LinearConstraint::ge(sink_terms, requirement)); let variables = self .capacities() @@ -126,7 +136,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) + crate::example_db::specs::rule_example_via_bounded_ilp::<_>(source) }, }] } diff --git a/src/rules/isomorphicspanningtree_ilp.rs b/src/rules/isomorphicspanningtree_ilp.rs index fadee1715..66f7db1fc 100644 --- a/src/rules/isomorphicspanningtree_ilp.rs +++ b/src/rules/isomorphicspanningtree_ilp.rs @@ -43,6 +43,7 @@ impl ReductionResult for ReductionISTToILP { impl crate::rules::AggregateReductionResult for ReductionISTToILP {} #[reduction(transform = upper_bound { + max_constraint_magnitude_bits = "2", num_vars = "num_vertices * num_vertices", num_constraints = "2 * num_vertices + 2 * (num_vertices - 1) * num_vertices * num_vertices", num_nonzeros = "(num_vertices * num_vertices) * (2 * num_vertices + 2 * (num_vertices - 1) * num_vertices * num_vertices)", diff --git a/src/rules/kclique_balancedcompletebipartitesubgraph.rs b/src/rules/kclique_balancedcompletebipartitesubgraph.rs index af52a2233..b3cb09b81 100644 --- a/src/rules/kclique_balancedcompletebipartitesubgraph.rs +++ b/src/rules/kclique_balancedcompletebipartitesubgraph.rs @@ -56,16 +56,12 @@ impl ReductionResult for ReductionKCliqueToBCBS { #[crate::aggregate_reduction(identity)] impl crate::rules::AggregateReductionResult for ReductionKCliqueToBCBS {} -#[reduction( - transform = upper_bound { - left_size = "num_vertices + k * (k - 1) / 2", - right_size = "num_edges + num_vertices - k", - k = "num_vertices + k * (k - 1) / 2 - k", - }, - unavailable = { - num_vertices = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] +#[reduction(transform = upper_bound { + left_size = "num_vertices + k * (k - 1) / 2", + right_size = "num_edges + num_vertices - k", + k = "num_vertices + k * (k - 1) / 2 - k", + num_vertices = "2 * num_vertices + num_edges + k * (k - 1) / 2 - k", +})] impl ReduceTo for KClique { type Result = ReductionKCliqueToBCBS; diff --git a/src/rules/kclique_ilp.rs b/src/rules/kclique_ilp.rs index 0ed5ae82e..34af3cb66 100644 --- a/src/rules/kclique_ilp.rs +++ b/src/rules/kclique_ilp.rs @@ -58,6 +58,7 @@ impl ReductionResult for ReductionKCliqueToILP { impl crate::rules::AggregateReductionResult for ReductionKCliqueToILP {} #[reduction(transform = upper_bound { + max_constraint_magnitude_bits = "k + 1", num_vars = "num_vertices", num_constraints = "num_vertices^2 + 1", num_nonzeros = "num_vertices * (num_vertices^2 + 1)", diff --git a/src/rules/knapsack_ilp.rs b/src/rules/knapsack_ilp.rs index 1d3e57852..e26e2f8be 100644 --- a/src/rules/knapsack_ilp.rs +++ b/src/rules/knapsack_ilp.rs @@ -4,6 +4,8 @@ //! - Variables: one binary variable per item //! - Constraint: the total selected weight must not exceed capacity //! - Objective: maximize the total selected value +//! +//! Items exceeding capacity are fixed to zero and omitted from the capacity row. use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::Knapsack; @@ -37,9 +39,10 @@ impl ReductionResult for ReductionKnapsackToILP { #[reduction(transform = { exact { num_vars = "num_items", - num_constraints = "1", }, upper_bound { + max_constraint_magnitude_bits = "capacity_bits", + num_constraints = "num_items + 1", num_nonzeros = "num_items", }, })] @@ -51,14 +54,21 @@ impl ReduceTo> for Knapsack { let weights = self.weights(); let values = self.values(); let capacity = self.capacity(); - let constraints = vec![LinearConstraint::le( + let mut constraints = vec![LinearConstraint::le( weights .iter() .enumerate() + .filter(|&(_, &weight)| weight <= capacity) .map(|(item, &weight)| (item, weight)) .collect(), capacity, )]; + // Oversized items are impossible choices; their magnitudes need not enter the ILP. + for (item, &weight) in weights.iter().enumerate() { + if weight > capacity { + constraints.push(LinearConstraint::eq(vec![(item, 1)], 0)); + } + } let objective = values.iter().copied().enumerate().collect(); let target = ILP::new(num_vars, constraints, objective, ObjectiveSense::Maximize) .map_err(>>::target_construction)?; diff --git a/src/rules/knapsack_qubo.rs b/src/rules/knapsack_qubo.rs index 39d844644..8ecc8cdda 100644 --- a/src/rules/knapsack_qubo.rs +++ b/src/rules/knapsack_qubo.rs @@ -44,9 +44,13 @@ impl ReductionResult for ReductionKnapsackToQUBO { } } -#[reduction(transform = unavailable { - num_vars = "the exact piecewise slack-bit count is not representable in the parameter-expression language", - num_quadratic_terms = "the nonzero products depend on item sizes and values", +#[reduction(transform = { + exact { + num_vars = "num_items + capacity_bits", + }, + upper_bound { + num_quadratic_terms = "(num_items + capacity_bits)^2", + }, })] impl ReduceTo> for Knapsack { type Result = ReductionKnapsackToQUBO; diff --git a/src/rules/ksatisfiability_acyclicpartition.rs b/src/rules/ksatisfiability_acyclicpartition.rs index d88ce5861..f1b404d0b 100644 --- a/src/rules/ksatisfiability_acyclicpartition.rs +++ b/src/rules/ksatisfiability_acyclicpartition.rs @@ -53,6 +53,7 @@ impl crate::rules::AggregateReductionResult for Reduction3SATToAcyclicPartition #[reduction( transform = upper_bound { + max_numeric_magnitude_bits = "4 * num_clauses + 6", num_vertices = "(9 * num_clauses^2 + 3 * num_clauses + 6) / 2", num_arcs = "18 * num_clauses^2 + 2", } diff --git a/src/rules/ksatisfiability_directedtwocommodityintegralflow.rs b/src/rules/ksatisfiability_directedtwocommodityintegralflow.rs index 655396bd3..cbc842d3f 100644 --- a/src/rules/ksatisfiability_directedtwocommodityintegralflow.rs +++ b/src/rules/ksatisfiability_directedtwocommodityintegralflow.rs @@ -189,15 +189,11 @@ impl ReductionResult for Reduction3SATToDirectedTwoCommodityIntegralFlow { #[crate::aggregate_reduction(identity)] impl crate::rules::AggregateReductionResult for Reduction3SATToDirectedTwoCommodityIntegralFlow {} -#[reduction( - transform = exact { - num_vertices = "6 * num_vars + 2 * num_literals + num_clauses + 4", - num_arcs = "7 * num_vars + 4 * num_literals + num_clauses + 1", - }, - unavailable = { - max_capacity = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] +#[reduction(transform = exact { + num_vertices = "6 * num_vars + 2 * num_literals + num_clauses + 4", + num_arcs = "7 * num_vars + 4 * num_literals + num_clauses + 1", + max_capacity = "1", +})] impl ReduceTo for KSatisfiability { type Result = Reduction3SATToDirectedTwoCommodityIntegralFlow; diff --git a/src/rules/ksatisfiability_feasibleregisterassignment.rs b/src/rules/ksatisfiability_feasibleregisterassignment.rs index 0d2e15fa4..c7e20a609 100644 --- a/src/rules/ksatisfiability_feasibleregisterassignment.rs +++ b/src/rules/ksatisfiability_feasibleregisterassignment.rs @@ -231,7 +231,7 @@ impl ReduceTo for KSatisfiability { #[cfg(feature = "example-db")] pub(crate) fn canonical_rule_example_specs() -> Vec { use crate::export::SolutionPair; - use crate::models::algebraic::ILP; + use crate::models::algebraic::{Bounded, ILP}; use crate::models::formula::CNFClause; use crate::solvers::ILPSolver; @@ -248,10 +248,11 @@ pub(crate) fn canonical_rule_example_specs() -> Vec as ReduceTo>::reduce_to(&source) .expect("reduction should succeed"); - let to_ilp = >>::reduce_to( - to_fra.target_problem(), - ) - .expect("reduction should succeed"); + let to_ilp = + >>::reduce_to( + to_fra.target_problem(), + ) + .expect("reduction should succeed"); let ilp_solution = ILPSolver::new() .solve(to_ilp.target_problem()) .expect("canonical FRA example must reduce to a feasible ILP"); diff --git a/src/rules/ksatisfiability_preemptivescheduling.rs b/src/rules/ksatisfiability_preemptivescheduling.rs index 2d651e5f1..1a61efea7 100644 --- a/src/rules/ksatisfiability_preemptivescheduling.rs +++ b/src/rules/ksatisfiability_preemptivescheduling.rs @@ -368,16 +368,12 @@ impl crate::rules::AggregateReductionResult for Reduction3SATToPreemptiveSchedul } } -#[reduction( - transform = upper_bound { - num_tasks = "(2 * num_vars + 3 + 6 * num_clauses) * (num_vars + 3)", - num_processors = "2 * num_vars + 3 + 6 * num_clauses", - d_max = "(2 * num_vars + 3 + 6 * num_clauses) * (num_vars + 3)", - }, - unavailable = { - num_precedences = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] +#[reduction(transform = upper_bound { + num_tasks = "(2 * num_vars + 3 + 6 * num_clauses) * (num_vars + 3)", + num_processors = "2 * num_vars + 3 + 6 * num_clauses", + d_max = "(2 * num_vars + 3 + 6 * num_clauses) * (num_vars + 3)", + num_precedences = "((2 * num_vars + 3 + 6 * num_clauses) * (num_vars + 3))^2", +})] impl ReduceTo for KSatisfiability { type Result = Reduction3SATToPreemptiveScheduling; diff --git a/src/rules/ksatisfiability_simultaneousincongruences.rs b/src/rules/ksatisfiability_simultaneousincongruences.rs index 7fa0c5dc5..693b5805f 100644 --- a/src/rules/ksatisfiability_simultaneousincongruences.rs +++ b/src/rules/ksatisfiability_simultaneousincongruences.rs @@ -181,8 +181,8 @@ fn ensure_prime_product_fits_target( impl crate::rules::AggregateReductionResult for Reduction3SATToSimultaneousIncongruences {} #[reduction( - transform = unavailable { - num_pairs = "the number of residue pairs depends on the first num_vars odd primes and is not expressible in the size-expression language", + transform = upper_bound { + num_pairs = "2 * num_vars * (num_vars + 1)^2 + num_clauses", } )] impl ReduceTo for KSatisfiability { diff --git a/src/rules/ksatisfiability_subsetsum.rs b/src/rules/ksatisfiability_subsetsum.rs index 7079a3db8..3efa0a862 100644 --- a/src/rules/ksatisfiability_subsetsum.rs +++ b/src/rules/ksatisfiability_subsetsum.rs @@ -72,9 +72,12 @@ fn digits_to_integer(digits: &[u8]) -> BigUint { #[crate::aggregate_reduction(identity)] impl crate::rules::AggregateReductionResult for Reduction3SATToSubsetSum {} -#[reduction( - transform = upper_bound { num_elements = "2 * num_vars + 2 * num_clauses" } -)] +// Every integer has at most n+m decimal digits, and 10 < 2^4. The extra 1 +// also covers the empty formula's zero target (whose magnitude parameter is 1). +#[reduction(transform = upper_bound { + num_elements = "2 * num_vars + 2 * num_clauses", + max_numeric_magnitude_bits = "4 * (num_vars + num_clauses) + 1", +})] impl ReduceTo for KSatisfiability { type Result = Reduction3SATToSubsetSum; diff --git a/src/rules/lengthboundeddisjointpaths_ilp.rs b/src/rules/lengthboundeddisjointpaths_ilp.rs index 505c7e3b3..4f3dc6432 100644 --- a/src/rules/lengthboundeddisjointpaths_ilp.rs +++ b/src/rules/lengthboundeddisjointpaths_ilp.rs @@ -105,6 +105,7 @@ impl ReductionResult for ReductionLBDPToILP { num_vars = "max_paths * 2 * num_edges + max_paths", }, upper_bound { + max_constraint_magnitude_bits = "num_vertices + 1", num_constraints = "max_paths * num_vertices + max_paths * num_edges + max_paths + num_edges + num_vertices + max_paths", num_nonzeros = "(max_paths * 2 * num_edges + max_paths) * (max_paths * num_vertices + max_paths * num_edges + max_paths + num_edges + num_vertices + max_paths)", }, @@ -119,7 +120,11 @@ impl ReduceTo> for LengthBoundedDisjointPaths { let m = edges.len(); let n = self.num_vertices(); let j = self.max_paths(); - let max_len = Self::exact_i64(self.max_length(), "encoding the path-length bound")?; + // A simple path uses at most n-1 edges; cycles never improve the path packing. + let max_len = Self::exact_i64( + self.max_length().min(n.saturating_sub(1)), + "encoding the path-length bound", + )?; let s = self.source(); let t = self.sink(); diff --git a/src/rules/longestcircuit_ilp.rs b/src/rules/longestcircuit_ilp.rs index cbac8605f..dbf34f385 100644 --- a/src/rules/longestcircuit_ilp.rs +++ b/src/rules/longestcircuit_ilp.rs @@ -62,6 +62,7 @@ impl ReductionResult for ReductionLongestCircuitToILP { num_constraints = "2 + num_vertices + 2 * num_vertices^2 + 2 * num_edges * num_vertices", }, upper_bound { + max_constraint_magnitude_bits = "2", num_nonzeros = "(num_edges + 2 * num_vertices + 2 * num_edges * num_vertices) * (2 + num_vertices + 2 * num_vertices^2 + 2 * num_edges * num_vertices)", }, })] @@ -215,6 +216,7 @@ impl crate::rules::AggregateReductionResult for ReductionDecisionLongestCircuitT num_constraints = "3 + num_vertices + 2 * num_vertices^2 + 2 * num_edges * num_vertices", }, upper_bound { + max_constraint_magnitude_bits = "max_length_bits + num_edges + 1", num_nonzeros = "(num_edges + 2 * num_vertices + 2 * num_edges * num_vertices) * (3 + num_vertices + 2 * num_vertices^2 + 2 * num_edges * num_vertices)", }, })] @@ -224,10 +226,22 @@ impl ReduceTo> for Decision> { fn reduce_to(&self) -> Result { let mut inner = ReduceTo::>::reduce_to(self.inner())?; let mut constraints = inner.target.constraints().to_vec(); - constraints.push(LinearConstraint::ge( - inner.target.objective().to_vec(), - *self.bound(), - )); + // Positive edge lengths bound every circuit by their total. Normalize + // thresholds outside that range while retaining one acceptance row. + let total_length: i128 = self + .inner() + .edge_lengths() + .iter() + .map(|&length| i128::from(length)) + .sum(); + let acceptance = if *self.bound() <= 0 { + LinearConstraint::ge(vec![], 0) + } else if i128::from(*self.bound()) > total_length { + LinearConstraint::ge(vec![], 1) + } else { + LinearConstraint::ge(inner.target.objective().to_vec(), *self.bound()) + }; + constraints.push(acceptance); inner.target = ILP::with_variables( inner.target.variables().to_vec(), constraints, diff --git a/src/rules/longestcommonsubsequence_ilp.rs b/src/rules/longestcommonsubsequence_ilp.rs index d156ed261..a35ba3a48 100644 --- a/src/rules/longestcommonsubsequence_ilp.rs +++ b/src/rules/longestcommonsubsequence_ilp.rs @@ -51,6 +51,7 @@ impl ReductionResult for ReductionLCSToILP { #[reduction( transform = exact { + max_constraint_magnitude_bits = "1", num_vars = "max_length * (alphabet_size + 1) + max_length * total_length", num_constraints = "max_length + num_transitions + max_length * num_strings + max_length * total_length + num_transitions * sum_triangular_lengths", num_nonzeros = "max_length * (alphabet_size + 1 + num_strings + 3 * total_length) + 2 * num_transitions * (1 + sum_triangular_lengths)", diff --git a/src/rules/longestpath_ilp.rs b/src/rules/longestpath_ilp.rs index e2e99c0da..1fc8e7159 100644 --- a/src/rules/longestpath_ilp.rs +++ b/src/rules/longestpath_ilp.rs @@ -5,7 +5,7 @@ //! path positions. Flow-balance constraints force a single directed `s-t` path, //! while MTZ-style ordering constraints eliminate detached cycles. -use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{Bounded, IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::LongestPath; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -13,7 +13,7 @@ use crate::topology::{Graph, SimpleGraph}; #[derive(Debug, Clone)] pub struct ReductionLongestPathToILP { - target: ILP, + target: ILP, num_edges: usize, } @@ -25,9 +25,9 @@ impl ReductionLongestPathToILP { impl ReductionResult for ReductionLongestPathToILP { type Source = LongestPath; - type Target = ILP; + type Target = ILP; - fn target_problem(&self) -> &ILP { + fn target_problem(&self) -> &ILP { &self.target } @@ -54,10 +54,11 @@ impl ReductionResult for ReductionLongestPathToILP { num_constraints = "5 * num_edges + 4 * num_vertices + 1", }, upper_bound { + max_constraint_magnitude_bits = "num_vertices + 1", num_nonzeros = "(2 * num_edges + num_vertices) * (5 * num_edges + 4 * num_vertices + 1)", }, })] -impl ReduceTo> for LongestPath { +impl ReduceTo> for LongestPath { type Result = ReductionLongestPathToILP; fn reduce_to(&self) -> Result { @@ -89,7 +90,7 @@ impl ReduceTo> for LongestPath { let mut constraints = Vec::new(); - // Directed arc variables are binary within `ILP`. + // Directed arc variables are binary within `ILP`. for edge_idx in 0..num_edges { constraints.push(LinearConstraint::le( vec![(ReductionLongestPathToILP::arc_var(edge_idx, 0), 1)], @@ -197,7 +198,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) + crate::example_db::specs::rule_example_via_bounded_ilp::<_>(source) }, }] } diff --git a/src/rules/maximalis_ilp.rs b/src/rules/maximalis_ilp.rs index 7be51dd64..1f8ebb2fb 100644 --- a/src/rules/maximalis_ilp.rs +++ b/src/rules/maximalis_ilp.rs @@ -39,6 +39,7 @@ impl ReductionResult for ReductionMxISToILP { num_vars = "num_vertices", }, upper_bound { + max_constraint_magnitude_bits = "2 * num_edges + 2", num_constraints = "num_edges + num_vertices", num_nonzeros = "4 * num_edges + num_vertices", }, diff --git a/src/rules/maximum2satisfiability_ilp.rs b/src/rules/maximum2satisfiability_ilp.rs index a9445762b..8dd0d60df 100644 --- a/src/rules/maximum2satisfiability_ilp.rs +++ b/src/rules/maximum2satisfiability_ilp.rs @@ -46,6 +46,7 @@ impl ReductionResult for ReductionMaximum2SatisfiabilityToILP { num_constraints = "num_clauses", }, upper_bound { + max_constraint_magnitude_bits = "2", num_nonzeros = "(num_vars + num_clauses) * num_clauses", }, })] diff --git a/src/rules/maximumclique_ilp.rs b/src/rules/maximumclique_ilp.rs index f4922efd5..752c767fd 100644 --- a/src/rules/maximumclique_ilp.rs +++ b/src/rules/maximumclique_ilp.rs @@ -46,6 +46,7 @@ impl ReductionResult for ReductionCliqueToILP { } #[reduction(transform = upper_bound { + max_constraint_magnitude_bits = "1", num_vars = "num_vertices", num_constraints = "num_vertices^2", num_nonzeros = "num_vertices * (num_vertices^2)", diff --git a/src/rules/maximumcokplex_ilp.rs b/src/rules/maximumcokplex_ilp.rs index d9e6067e1..2b1ff1161 100644 --- a/src/rules/maximumcokplex_ilp.rs +++ b/src/rules/maximumcokplex_ilp.rs @@ -44,17 +44,16 @@ where fn build_constraints(graph: &SimpleGraph, bound_k: usize) -> Result, ()> { (0..graph.num_vertices()) .map(|v| { - let degree = i64::try_from(graph.degree(v)).map_err(|_| ())?; - let bound_k = i64::try_from(bound_k).map_err(|_| ())?; + let degree = graph.degree(v); + let allowed_neighbors = + i64::try_from(bound_k.checked_sub(1).ok_or(())?.min(degree)).map_err(|_| ())?; + let degree = i64::try_from(degree).map_err(|_| ())?; let mut terms: Vec<(usize, i64)> = graph.neighbors(v).into_iter().map(|u| (u, 1)).collect(); if degree > 0 { terms.push((v, degree)); } - let rhs = degree - .checked_add(bound_k) - .and_then(|value| value.checked_sub(1)) - .ok_or(())?; + let rhs = degree.checked_add(allowed_neighbors).ok_or(())?; Ok(LinearConstraint::le(terms, rhs)) }) .collect() @@ -86,6 +85,7 @@ where num_constraints = "num_vertices", }, upper_bound { + max_constraint_magnitude_bits = "4 * num_edges + 2", num_nonzeros = "num_vertices * num_vertices", }, })] @@ -115,6 +115,7 @@ impl ReduceTo> for MaximumCoKPlex { num_constraints = "num_vertices", }, upper_bound { + max_constraint_magnitude_bits = "4 * num_edges + 2", num_nonzeros = "num_vertices * num_vertices", }, })] diff --git a/src/rules/maximumcommonedgesubgraph_ilp.rs b/src/rules/maximumcommonedgesubgraph_ilp.rs index 64001e434..4ed3f6e90 100644 --- a/src/rules/maximumcommonedgesubgraph_ilp.rs +++ b/src/rules/maximumcommonedgesubgraph_ilp.rs @@ -67,6 +67,7 @@ impl ReductionResult for ReductionMCESToILP { } #[reduction(transform = upper_bound { + max_constraint_magnitude_bits = "2", num_vars = "num_vertices_1 * num_vertices_2 + num_arcs_1 * num_arcs_2", num_constraints = "num_vertices_1 + num_vertices_2 + 3 * num_arcs_1 * num_arcs_2", num_nonzeros = "(num_vertices_1 * num_vertices_2 + num_arcs_1 * num_arcs_2) * (num_vertices_1 + num_vertices_2 + 3 * num_arcs_1 * num_arcs_2)", diff --git a/src/rules/maximumcontactmapoverlap_ilp.rs b/src/rules/maximumcontactmapoverlap_ilp.rs index 5970556b6..d134b77e9 100644 --- a/src/rules/maximumcontactmapoverlap_ilp.rs +++ b/src/rules/maximumcontactmapoverlap_ilp.rs @@ -69,13 +69,16 @@ impl ReductionResult for ReductionCMOToILP { } } -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_vertices_1 * num_vertices_2 + num_contacts_1 * num_contacts_2", num_constraints = "num_vertices_1 + num_vertices_2 + num_vertices_1 * (num_vertices_1 - 1) / 2 * num_vertices_2 * (num_vertices_2 + 1) / 2 + 2 * num_contacts_1 * num_contacts_2", num_nonzeros = "2 * num_vertices_1 * num_vertices_2 + num_vertices_1 * (num_vertices_1 - 1) * num_vertices_2 * (num_vertices_2 + 1) / 2 + 4 * num_contacts_1 * num_contacts_2", - } -)] + }, + upper_bound { + max_constraint_magnitude_bits = "2", + }, +})] impl ReduceTo> for MaximumContactMapOverlap { type Result = ReductionCMOToILP; diff --git a/src/rules/maximumdomaticnumber_ilp.rs b/src/rules/maximumdomaticnumber_ilp.rs index acba50a38..312c616ca 100644 --- a/src/rules/maximumdomaticnumber_ilp.rs +++ b/src/rules/maximumdomaticnumber_ilp.rs @@ -60,6 +60,7 @@ impl ReductionResult for ReductionDomaticNumberToILP { #[reduction(transform = { exact { + max_constraint_magnitude_bits = "1", num_vars = "num_vertices * num_vertices + num_vertices", num_constraints = "num_vertices + num_vertices * num_vertices + num_vertices * num_vertices", }, @@ -84,12 +85,13 @@ impl ReduceTo> for MaximumDomaticNumber { // Domination constraints: for each v, i: x_{v,i} + Σ_{u ∈ N(v)} x_{u,i} >= y_i // Rewritten as: x_{v,i} + Σ_{u ∈ N(v)} x_{u,i} - y_i >= 0 for v in 0..n { - let neighbors = self.graph().neighbors(v); + let mut neighborhood = self.graph().neighbors(v); + neighborhood.push(v); + neighborhood.sort_unstable(); + neighborhood.dedup(); for i in 0..n { - let mut terms: Vec<(usize, i64)> = vec![(v * n + i, 1)]; - for &u in &neighbors { - terms.push((u * n + i, 1)); - } + let mut terms: Vec<(usize, i64)> = + neighborhood.iter().map(|&u| (u * n + i, 1)).collect(); // -y_i terms.push((n * n + i, -1)); constraints.push(LinearConstraint::ge(terms, 0)); diff --git a/src/rules/maximumedgeweightedkclique_ilp.rs b/src/rules/maximumedgeweightedkclique_ilp.rs index 58f4cf93d..06ad62928 100644 --- a/src/rules/maximumedgeweightedkclique_ilp.rs +++ b/src/rules/maximumedgeweightedkclique_ilp.rs @@ -144,6 +144,7 @@ where num_constraints = "1 + num_vertices * (num_vertices - 1) / 2 + 2 * num_edges", }, upper_bound { + max_constraint_magnitude_bits = "num_vertices + 1", num_nonzeros = "(num_vertices + num_edges) * (1 + num_vertices * (num_vertices - 1) / 2 + 2 * num_edges)", }, })] @@ -161,6 +162,7 @@ impl ReduceTo> for MaximumEdgeWeightedKClique { num_constraints = "1 + num_vertices * (num_vertices - 1) / 2 + 2 * num_edges", }, upper_bound { + max_constraint_magnitude_bits = "num_vertices + 1", num_nonzeros = "(num_vertices + num_edges) * (1 + num_vertices * (num_vertices - 1) / 2 + 2 * num_edges)", }, })] diff --git a/src/rules/maximumleafspanningtree_ilp.rs b/src/rules/maximumleafspanningtree_ilp.rs index 1fac86ae3..e0398c6a9 100644 --- a/src/rules/maximumleafspanningtree_ilp.rs +++ b/src/rules/maximumleafspanningtree_ilp.rs @@ -18,7 +18,7 @@ //! //! Objective: maximize sum(z_v) -use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{Bounded, IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::MaximumLeafSpanningTree; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -27,15 +27,15 @@ use crate::topology::{Graph, SimpleGraph}; /// Result of reducing MaximumLeafSpanningTree to ILP. #[derive(Debug, Clone)] pub struct ReductionMaximumLeafSpanningTreeToILP { - target: ILP, + target: ILP, num_edges: usize, } impl ReductionResult for ReductionMaximumLeafSpanningTreeToILP { type Source = MaximumLeafSpanningTree; - type Target = ILP; + type Target = ILP; - fn target_problem(&self) -> &ILP { + fn target_problem(&self) -> &ILP { &self.target } @@ -61,10 +61,11 @@ impl ReductionResult for ReductionMaximumLeafSpanningTreeToILP { num_constraints = "3 * num_vertices + 2 * num_edges + 1", }, upper_bound { + max_constraint_magnitude_bits = "num_vertices + 2", num_nonzeros = "(3 * num_edges + num_vertices) * (3 * num_vertices + 2 * num_edges + 1)", }, })] -impl ReduceTo> for MaximumLeafSpanningTree { +impl ReduceTo> for MaximumLeafSpanningTree { type Result = ReductionMaximumLeafSpanningTreeToILP; fn reduce_to(&self) -> Result { @@ -182,7 +183,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) + crate::example_db::specs::rule_example_via_bounded_ilp::<_>(source) }, }] } diff --git a/src/rules/maximumlikelihoodranking_ilp.rs b/src/rules/maximumlikelihoodranking_ilp.rs index 0afd9524a..14d009bf7 100644 --- a/src/rules/maximumlikelihoodranking_ilp.rs +++ b/src/rules/maximumlikelihoodranking_ilp.rs @@ -74,6 +74,7 @@ impl ReductionResult for ReductionMaximumLikelihoodRankingToILP { #[reduction( transform = exact { + max_constraint_magnitude_bits = "1", num_vars = "num_items * (num_items - 1) / 2", num_constraints = "num_items * (num_items - 1) * (num_items - 2) / 3", num_nonzeros = "num_items * (num_items - 1) * (num_items - 2)", diff --git a/src/rules/maximummatching_ilp.rs b/src/rules/maximummatching_ilp.rs index 84f01cdb4..42f1a3799 100644 --- a/src/rules/maximummatching_ilp.rs +++ b/src/rules/maximummatching_ilp.rs @@ -50,6 +50,7 @@ impl ReductionResult for ReductionMatchingToILP { num_vars = "num_edges", }, upper_bound { + max_constraint_magnitude_bits = "2", num_constraints = "num_vertices", num_nonzeros = "2 * num_edges", }, diff --git a/src/rules/maximumsetpacking_ilp.rs b/src/rules/maximumsetpacking_ilp.rs index 2a6d290f0..1a40c0185 100644 --- a/src/rules/maximumsetpacking_ilp.rs +++ b/src/rules/maximumsetpacking_ilp.rs @@ -44,6 +44,7 @@ impl ReductionResult for ReductionSPToILP { num_vars = "num_sets", }, upper_bound { + max_constraint_magnitude_bits = "1", num_constraints = "universe_size", num_nonzeros = "num_sets * universe_size", }, @@ -62,6 +63,10 @@ impl ReduceTo> for MaximumSetPacking { elem_to_sets[e].push(i); } } + // Each set contributes once per element, irrespective of repeated input entries. + for sets in &mut elem_to_sets { + sets.dedup(); + } let constraints: Vec = elem_to_sets .into_iter() diff --git a/src/rules/minimumcapacitatedspanningtree_ilp.rs b/src/rules/minimumcapacitatedspanningtree_ilp.rs index a3362f65a..3008c04ce 100644 --- a/src/rules/minimumcapacitatedspanningtree_ilp.rs +++ b/src/rules/minimumcapacitatedspanningtree_ilp.rs @@ -5,7 +5,7 @@ //! - Flow on each edge is bounded by the capacity constraint //! - Flow-edge linking ensures flow only travels on selected edges //! -//! Variable layout (all non-negative integers, `ILP`): +//! Variable layout (all non-negative integers, `ILP`): //! - `y_e` for each undirected edge `e` (indices `0..m`): edge selector (binary) //! - `f_{2e}`, `f_{2e+1}` for each edge `e=(u,v)` (indices `m..3m`): //! directed requirement flow from u to v and v to u respectively @@ -24,7 +24,7 @@ //! //! Objective: minimize sum(w_e * y_e) -use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{Bounded, IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::MinimumCapacitatedSpanningTree; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -34,15 +34,15 @@ use crate::types::WeightElement; /// Result of reducing MinimumCapacitatedSpanningTree to ILP. #[derive(Debug, Clone)] pub struct ReductionMinimumCapacitatedSpanningTreeToILP { - target: ILP, + target: ILP, num_edges: usize, } impl ReductionResult for ReductionMinimumCapacitatedSpanningTreeToILP { type Source = MinimumCapacitatedSpanningTree; - type Target = ILP; + type Target = ILP; - fn target_problem(&self) -> &ILP { + fn target_problem(&self) -> &ILP { &self.target } @@ -70,10 +70,11 @@ impl ReductionResult for ReductionMinimumCapacitatedSpanningTreeToILP { num_constraints = "5 * num_edges + 2 * num_vertices + 1", }, upper_bound { + max_constraint_magnitude_bits = "max_requirement_bits + num_vertices", num_nonzeros = "18 * num_edges", }, })] -impl ReduceTo> for MinimumCapacitatedSpanningTree { +impl ReduceTo> for MinimumCapacitatedSpanningTree { type Result = ReductionMinimumCapacitatedSpanningTreeToILP; fn reduce_to(&self) -> Result { @@ -100,7 +101,7 @@ impl ReduceTo> for MinimumCapacitatedSpanningTree { total.checked_add(requirement.to_sum()).ok_or_else(|| { crate::rules::ReductionError::integer_overflow::< MinimumCapacitatedSpanningTree, - ILP, + ILP, >("summing vertex requirements") }) })?; @@ -243,7 +244,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) + crate::example_db::specs::rule_example_via_bounded_ilp::<_>(source) }, }] } diff --git a/src/rules/minimumcoveringbycliques_ilp.rs b/src/rules/minimumcoveringbycliques_ilp.rs index 63278b39d..37bc8a8ff 100644 --- a/src/rules/minimumcoveringbycliques_ilp.rs +++ b/src/rules/minimumcoveringbycliques_ilp.rs @@ -67,6 +67,7 @@ impl ReductionResult for ReductionMinimumCoveringByCliquesToILP { num_constraints = "num_vertices * num_edges + (num_vertices * (num_vertices - 1) / 2 - num_edges) * num_edges + 3 * num_edges * num_edges + num_edges", }, upper_bound { + max_constraint_magnitude_bits = "2", num_nonzeros = "(num_vertices * num_edges + num_edges + num_edges * num_edges) * (num_vertices * num_edges + (num_vertices * (num_vertices - 1) / 2 - num_edges) * num_edges + 3 * num_edges * num_edges + num_edges)", }, })] diff --git a/src/rules/minimumcutintoboundedsets_ilp.rs b/src/rules/minimumcutintoboundedsets_ilp.rs index 0386d9f6a..9b609d32d 100644 --- a/src/rules/minimumcutintoboundedsets_ilp.rs +++ b/src/rules/minimumcutintoboundedsets_ilp.rs @@ -45,6 +45,7 @@ impl ReductionResult for ReductionMinCutBSToILP { num_constraints = "2 + 2 + 2 * num_edges", }, upper_bound { + max_constraint_magnitude_bits = "num_vertices + 1", num_nonzeros = "(num_vertices + num_edges) * (2 + 2 + 2 * num_edges)", }, })] @@ -57,7 +58,8 @@ impl ReduceTo> for MinimumCutIntoBoundedSets { let m = edges.len(); let num_vars = n + m; let n_i64 = Self::exact_i64(n, "encoding the partition size")?; - let size_bound = Self::exact_i64(self.size_bound(), "encoding the set-size bound")?; + // Each side contains at most all n vertices. + let size_bound = Self::exact_i64(self.size_bound().min(n), "encoding the set-size bound")?; let mut constraints = Vec::new(); // x_s = 0 diff --git a/src/rules/minimumdominatingset_ilp.rs b/src/rules/minimumdominatingset_ilp.rs index f1bda8fa6..7f3007d82 100644 --- a/src/rules/minimumdominatingset_ilp.rs +++ b/src/rules/minimumdominatingset_ilp.rs @@ -54,6 +54,7 @@ impl ReductionResult for ReductionDSToILP { num_constraints = "num_vertices", }, upper_bound { + max_constraint_magnitude_bits = "2 * num_edges + 2", num_nonzeros = "2 * num_edges + num_vertices", }, })] diff --git a/src/rules/minimumedgecostflow_ilp.rs b/src/rules/minimumedgecostflow_ilp.rs index efd85e521..b5a83c814 100644 --- a/src/rules/minimumedgecostflow_ilp.rs +++ b/src/rules/minimumedgecostflow_ilp.rs @@ -1,4 +1,4 @@ -//! Reduction from MinimumEdgeCostFlow to `ILP`. +//! Reduction from MinimumEdgeCostFlow to `ILP`. //! //! Variables (2m total): //! f_a (a = 0..m-1) — integer flow on arc a, domain {0, ..., c(a)} @@ -15,27 +15,27 @@ //! Objective: minimize Σ p(a) · y_a. //! Extraction: first m variables are the flow values. -use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{Bounded, IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::MinimumEdgeCostFlow; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; -/// Result of reducing MinimumEdgeCostFlow to `ILP`. +/// Result of reducing MinimumEdgeCostFlow to `ILP`. /// /// Variable layout: /// - `f_a` at index a for a in 0..num_edges (flow on arc a) /// - `y_a` at index num_edges + a for a in 0..num_edges (binary indicator) #[derive(Debug, Clone)] pub struct ReductionMECFToILP { - target: ILP, + target: ILP, num_edges: usize, } impl ReductionResult for ReductionMECFToILP { type Source = MinimumEdgeCostFlow; - type Target = ILP; + type Target = ILP; - fn target_problem(&self) -> &ILP { + fn target_problem(&self) -> &ILP { &self.target } @@ -57,11 +57,12 @@ impl ReductionResult for ReductionMECFToILP { num_vars = "2 * num_edges", }, upper_bound { + max_constraint_magnitude_bits = "max_capacity * (num_edges + 1) + 2", num_constraints = "2 * num_edges + num_vertices - 1", num_nonzeros = "5 * num_edges", }, })] -impl ReduceTo> for MinimumEdgeCostFlow { +impl ReduceTo> for MinimumEdgeCostFlow { type Result = ReductionMECFToILP; fn reduce_to(&self) -> Result { @@ -118,7 +119,13 @@ impl ReduceTo> for MinimumEdgeCostFlow { sink_terms.push((f(a), -1)); } } - constraints.push(LinearConstraint::ge(sink_terms, self.required_flow())); + constraints.push(LinearConstraint::ge( + sink_terms, + crate::rules::ilp_helpers::bounded_flow_requirement( + self.required_flow(), + self.capacities().iter().copied(), + ), + )); // Objective: minimize Σ p(a) · y_a let objective: Vec<(usize, i64)> = (0..m).map(|a| (y(a), self.prices()[a])).collect(); @@ -159,7 +166,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) + crate::example_db::specs::rule_example_via_bounded_ilp::<_>(source) }, }] } diff --git a/src/rules/minimumexternalmacrodatacompression_ilp.rs b/src/rules/minimumexternalmacrodatacompression_ilp.rs index 5f6acb6c8..09e460598 100644 --- a/src/rules/minimumexternalmacrodatacompression_ilp.rs +++ b/src/rules/minimumexternalmacrodatacompression_ilp.rs @@ -214,6 +214,7 @@ fn encode_pointer(n: usize, start: usize, len: usize) -> usize { } #[reduction(transform = upper_bound { + max_constraint_magnitude_bits = "1", num_vars = "string_length * alphabet_size + 2 * string_length + string_length ^ 3", num_constraints = "string_length + string_length * alphabet_size + string_length + string_length + 1 + string_length ^ 3 * string_length", num_nonzeros = "(string_length * alphabet_size + 2 * string_length + string_length ^ 3) * (string_length + string_length * alphabet_size + string_length + string_length + 1 + string_length ^ 3 * string_length)", diff --git a/src/rules/minimumfaultdetectiontestset_ilp.rs b/src/rules/minimumfaultdetectiontestset_ilp.rs index c41c5919d..ecf7fab3e 100644 --- a/src/rules/minimumfaultdetectiontestset_ilp.rs +++ b/src/rules/minimumfaultdetectiontestset_ilp.rs @@ -52,6 +52,7 @@ impl ReductionResult for ReductionMFDTSToILP { num_vars = "num_inputs * num_outputs", }, upper_bound { + max_constraint_magnitude_bits = "1", num_constraints = "num_vertices - 1", num_nonzeros = "num_inputs * num_outputs * (num_vertices - 1)", }, diff --git a/src/rules/minimumfeedbackarcset_ilp.rs b/src/rules/minimumfeedbackarcset_ilp.rs index b2f236d5b..2cafd52ec 100644 --- a/src/rules/minimumfeedbackarcset_ilp.rs +++ b/src/rules/minimumfeedbackarcset_ilp.rs @@ -9,14 +9,14 @@ //! - Objective: Minimize Σ w_a * y_a //! - Variable layout: first |A| are y_a, next |V| are o_v -use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{Bounded, IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::MinimumFeedbackArcSet; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; /// Result of reducing MinimumFeedbackArcSet to ILP. /// -/// The ILP uses integer variables (`ILP`) because it needs both +/// The ILP uses integer variables (`ILP`) because it needs both /// binary arc-removal variables (y_a) and integer ordering variables (o_v). /// /// Variable layout: @@ -24,16 +24,16 @@ use crate::rules::traits::{ReduceTo, ReductionResult}; /// - `o_v` at index `m + v` for `v in 0..n`: integer in {0, ..., n-1}, topological order #[derive(Debug, Clone)] pub struct ReductionFASToILP { - target: ILP, + target: ILP, /// Number of arcs in the source graph (needed for solution extraction). num_arcs: usize, } impl ReductionResult for ReductionFASToILP { type Source = MinimumFeedbackArcSet; - type Target = ILP; + type Target = ILP; - fn target_problem(&self) -> &ILP { + fn target_problem(&self) -> &ILP { &self.target } @@ -60,10 +60,11 @@ impl ReductionResult for ReductionFASToILP { num_constraints = "num_arcs + num_arcs + num_vertices", }, upper_bound { + max_constraint_magnitude_bits = "num_vertices + 1", num_nonzeros = "(num_arcs + num_vertices) * (num_arcs + num_arcs + num_vertices)", }, })] -impl ReduceTo> for MinimumFeedbackArcSet { +impl ReduceTo> for MinimumFeedbackArcSet { type Result = ReductionFASToILP; fn reduce_to(&self) -> Result { @@ -139,7 +140,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) + crate::example_db::specs::rule_example_via_bounded_ilp::<_>(source) }, }] } diff --git a/src/rules/minimumfeedbackvertexset_ilp.rs b/src/rules/minimumfeedbackvertexset_ilp.rs index b6e0b88e7..554de5e28 100644 --- a/src/rules/minimumfeedbackvertexset_ilp.rs +++ b/src/rules/minimumfeedbackvertexset_ilp.rs @@ -6,14 +6,14 @@ //! Plus binary bounds (x_i <= 1) and order bounds (o_i <= n-1) //! - Objective: Minimize the weighted sum of removed vertices -use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{Bounded, IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::MinimumFeedbackVertexSet; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; /// Result of reducing MinimumFeedbackVertexSet to ILP. /// -/// The ILP uses integer variables (`ILP`) because it needs both +/// The ILP uses integer variables (`ILP`) because it needs both /// binary selection variables (x_i) and integer ordering variables (o_i). /// /// Variable layout: @@ -21,16 +21,16 @@ use crate::rules::traits::{ReduceTo, ReductionResult}; /// - `o_i` at index `n + i` for `i in 0..n`: integer in {0, ..., n-1}, topological order #[derive(Debug, Clone)] pub struct ReductionMFVSToILP { - target: ILP, + target: ILP, /// Number of vertices in the source graph (needed for solution extraction). num_vertices: usize, } impl ReductionResult for ReductionMFVSToILP { type Source = MinimumFeedbackVertexSet; - type Target = ILP; + type Target = ILP; - fn target_problem(&self) -> &ILP { + fn target_problem(&self) -> &ILP { &self.target } @@ -57,10 +57,11 @@ impl ReductionResult for ReductionMFVSToILP { num_constraints = "num_arcs + 2 * num_vertices", }, upper_bound { + max_constraint_magnitude_bits = "2 * num_vertices + 1", num_nonzeros = "(2 * num_vertices) * (num_arcs + 2 * num_vertices)", }, })] -impl ReduceTo> for MinimumFeedbackVertexSet { +impl ReduceTo> for MinimumFeedbackVertexSet { type Result = ReductionMFVSToILP; fn reduce_to(&self) -> Result { @@ -73,7 +74,10 @@ impl ReduceTo> for MinimumFeedbackVertexSet { // o_i = n + i (integer: topological order of vertex i) let mut constraints = Vec::new(); - let n_i64 = >>::exact_i64(n, "encoding the topological order")?; + let n_i64 = >>::exact_i64( + n, + "encoding the topological order", + )?; // Binary bounds: x_i <= 1 for i in 0..n for i in 0..n { @@ -109,12 +113,12 @@ impl ReduceTo> for MinimumFeedbackVertexSet { let mut variables = vec![IntegerVariable::binary(); num_vars]; variables[n..].fill( IntegerVariable::new(Some(0), Some((n_i64 - 1).max(0))) - .map_err(>>::target_construction)?, + .map_err(>>::target_construction)?, ); let target = ILP::with_variables(variables, constraints, objective, ObjectiveSense::Minimize) - .map_err(>>::target_construction)?; + .map_err(>>::target_construction)?; Ok(ReductionMFVSToILP { target, @@ -133,7 +137,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec 1 -> 2 -> 0 (FVS = 1 vertex) let graph = DirectedGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]); let source = MinimumFeedbackVertexSet::new(graph, vec![1i64; 3]); - crate::example_db::specs::rule_example_via_ilp::<_, i64>(source) + crate::example_db::specs::rule_example_via_bounded_ilp::<_>(source) }, }] } diff --git a/src/rules/minimumgraphbandwidth_ilp.rs b/src/rules/minimumgraphbandwidth_ilp.rs index 9dc8f2914..7504bfa1a 100644 --- a/src/rules/minimumgraphbandwidth_ilp.rs +++ b/src/rules/minimumgraphbandwidth_ilp.rs @@ -7,7 +7,7 @@ //! - For each edge (u,v): pos_u - pos_v <= B, pos_v - pos_u <= B //! - Objective: minimize B -use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{Bounded, IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::MinimumGraphBandwidth; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -15,21 +15,21 @@ use crate::topology::{Graph, SimpleGraph}; /// Result of reducing MinimumGraphBandwidth to ILP. /// -/// Variable layout (`ILP`, non-negative integers): +/// Variable layout (`ILP`, non-negative integers): /// - `x_{v,p}` at index `v * n + p`, bounded to {0,1} /// - `pos_v` at index `n^2 + v`, integer position in {0, ..., n-1} /// - `B` (bandwidth) at index `n^2 + n` #[derive(Debug, Clone)] pub struct ReductionMGBToILP { - target: ILP, + target: ILP, num_vertices: usize, } impl ReductionResult for ReductionMGBToILP { type Source = MinimumGraphBandwidth; - type Target = ILP; + type Target = ILP; - fn target_problem(&self) -> &ILP { + fn target_problem(&self) -> &ILP { &self.target } @@ -55,10 +55,11 @@ impl ReductionResult for ReductionMGBToILP { num_constraints = "2 * num_vertices + num_vertices^2 + num_vertices + num_vertices + 1 + 2 * num_edges", }, upper_bound { + max_constraint_magnitude_bits = "num_vertices + 1", num_nonzeros = "(num_vertices^2 + num_vertices + 1) * (2 * num_vertices + num_vertices^2 + num_vertices + num_vertices + 1 + 2 * num_edges)", }, })] -impl ReduceTo> for MinimumGraphBandwidth { +impl ReduceTo> for MinimumGraphBandwidth { type Result = ReductionMGBToILP; fn reduce_to(&self) -> Result { @@ -88,7 +89,7 @@ impl ReduceTo> for MinimumGraphBandwidth { constraints.push(LinearConstraint::eq(terms, 1)); } - // Binary bounds for x variables (`ILP`) + // Binary bounds for x variables (`ILP`) for v in 0..n { for p in 0..n { constraints.push(LinearConstraint::le(vec![(x_idx(v, p), 1)], 1)); @@ -156,7 +157,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) + crate::example_db::specs::rule_example_via_bounded_ilp::<_>(source) }, }] } diff --git a/src/rules/minimumhittingset_ilp.rs b/src/rules/minimumhittingset_ilp.rs index e9771539d..c6eeb8c77 100644 --- a/src/rules/minimumhittingset_ilp.rs +++ b/src/rules/minimumhittingset_ilp.rs @@ -37,6 +37,7 @@ impl ReductionResult for ReductionHSToILP { num_constraints = "num_sets", }, upper_bound { + max_constraint_magnitude_bits = "1", num_nonzeros = "universe_size * num_sets", }, })] diff --git a/src/rules/minimuminternalmacrodatacompression_ilp.rs b/src/rules/minimuminternalmacrodatacompression_ilp.rs index 1c58d96b1..e418a0417 100644 --- a/src/rules/minimuminternalmacrodatacompression_ilp.rs +++ b/src/rules/minimuminternalmacrodatacompression_ilp.rs @@ -160,6 +160,7 @@ impl ReductionResult for ReductionIMDCToILP { } #[reduction(transform = upper_bound { + max_constraint_magnitude_bits = "1", num_vars = "string_len + string_len ^ 3", num_constraints = "string_len + 1", num_nonzeros = "(string_len + string_len ^ 3) * (string_len + 1)", diff --git a/src/rules/minimummatrixcover_ilp.rs b/src/rules/minimummatrixcover_ilp.rs index 8de7cad1d..84e7afe6f 100644 --- a/src/rules/minimummatrixcover_ilp.rs +++ b/src/rules/minimummatrixcover_ilp.rs @@ -54,6 +54,7 @@ fn y_index(n: usize, i: usize, j: usize) -> usize { #[reduction( transform = exact { + max_constraint_magnitude_bits = "1", num_vars = "num_rows + num_rows * (num_rows - 1) / 2", num_constraints = "3 * num_rows * (num_rows - 1) / 2", num_nonzeros = "7 * num_rows * (num_rows - 1) / 2", diff --git a/src/rules/minimummaximalmatching_ilp.rs b/src/rules/minimummaximalmatching_ilp.rs index 54d77ae65..771237219 100644 --- a/src/rules/minimummaximalmatching_ilp.rs +++ b/src/rules/minimummaximalmatching_ilp.rs @@ -56,6 +56,7 @@ impl ReductionResult for ReductionMMMToILP { num_vars = "num_edges", }, upper_bound { + max_constraint_magnitude_bits = "2", num_constraints = "num_vertices + num_edges", num_nonzeros = "2 * num_edges + num_edges^2", }, diff --git a/src/rules/minimummetricdimension_ilp.rs b/src/rules/minimummetricdimension_ilp.rs index 12fef2119..1a71ac3ce 100644 --- a/src/rules/minimummetricdimension_ilp.rs +++ b/src/rules/minimummetricdimension_ilp.rs @@ -54,6 +54,7 @@ impl ReductionResult for ReductionMDToILP { num_constraints = "num_vertices * (num_vertices - 1) / 2", }, upper_bound { + max_constraint_magnitude_bits = "1", num_nonzeros = "num_vertices * (num_vertices * (num_vertices - 1) / 2)", }, })] diff --git a/src/rules/minimummultiwaycut_ilp.rs b/src/rules/minimummultiwaycut_ilp.rs index 4600bf0ef..1986a3586 100644 --- a/src/rules/minimummultiwaycut_ilp.rs +++ b/src/rules/minimummultiwaycut_ilp.rs @@ -63,6 +63,7 @@ impl ReductionResult for ReductionMMCToILP { num_constraints = "num_vertices + 2 * num_terminals * num_edges + num_terminals * num_terminals", }, upper_bound { + max_constraint_magnitude_bits = "1", num_nonzeros = "(num_terminals * num_vertices + num_edges) * (num_vertices + 2 * num_terminals * num_edges + num_terminals * num_terminals)", }, })] diff --git a/src/rules/minimumsetcovering_ilp.rs b/src/rules/minimumsetcovering_ilp.rs index 7b830ac62..2805a7407 100644 --- a/src/rules/minimumsetcovering_ilp.rs +++ b/src/rules/minimumsetcovering_ilp.rs @@ -49,6 +49,7 @@ impl ReductionResult for ReductionSCToILP { num_constraints = "universe_size", }, upper_bound { + max_constraint_magnitude_bits = "1", num_nonzeros = "num_sets * universe_size", }, })] diff --git a/src/rules/minimumsummulticenter_ilp.rs b/src/rules/minimumsummulticenter_ilp.rs index edfa101cd..d2e33f01b 100644 --- a/src/rules/minimumsummulticenter_ilp.rs +++ b/src/rules/minimumsummulticenter_ilp.rs @@ -118,6 +118,7 @@ fn weighted_distances_msmc( } #[reduction(transform = upper_bound { + max_constraint_magnitude_bits = "num_vertices + 1", num_vars = "num_vertices + num_vertices^2", num_constraints = "num_vertices^2 + 2 * num_vertices + 1", num_nonzeros = "(num_vertices + num_vertices^2) * (num_vertices^2 + 2 * num_vertices + 1)", diff --git a/src/rules/minimumtardinesssequencing_ilp.rs b/src/rules/minimumtardinesssequencing_ilp.rs index bbf61f745..1955399c7 100644 --- a/src/rules/minimumtardinesssequencing_ilp.rs +++ b/src/rules/minimumtardinesssequencing_ilp.rs @@ -110,6 +110,7 @@ fn build_common_constraints( num_constraints = "2 * num_tasks + num_precedences + num_tasks", }, upper_bound { + max_constraint_magnitude_bits = "num_tasks + 1", num_nonzeros = "(num_tasks * num_tasks + num_tasks) * (2 * num_tasks + num_precedences + num_tasks)", }, })] @@ -135,7 +136,8 @@ impl ReduceTo> for MinimumTardinessSequencing { let mut terms: Vec<(usize, i64)> = (0..n).map(|p| (x_var(j, p), positions[p] + 1)).collect(); terms.push((u_var(j), -big_m)); - let deadline = self.deadlines()[j]; + // Completion times lie in [1, n]; outside deadlines have the same tardy status. + let deadline = self.deadlines()[j].clamp(0, big_m); constraints.push(LinearConstraint::le(terms, deadline)); } @@ -156,6 +158,7 @@ impl ReduceTo> for MinimumTardinessSequencing { num_constraints = "2 * num_tasks + num_precedences + num_tasks * num_tasks", }, upper_bound { + max_constraint_magnitude_bits = "max_processing_time_bits + num_tasks + 1", num_nonzeros = "(num_tasks * num_tasks + num_tasks) * (2 * num_tasks + num_precedences + num_tasks * num_tasks)", }, })] @@ -187,6 +190,17 @@ impl ReduceTo> for MinimumTardinessSequencing { // Tardy indicator for arbitrary lengths. let lengths = self.lengths(); for j in 0..n { + // Positive lengths bound all completion times by total_length. + let rhs = self.deadlines()[j] + .clamp(0, total_length) + .checked_sub(lengths[j]) + .and_then(|value| value.checked_add(total_length)) + .ok_or_else(|| { + crate::rules::ReductionError::integer_overflow::< + MinimumTardinessSequencing, + ILP, + >("computing a tardiness constraint bound") + })?; for p in 0..n { let mut terms: Vec<(usize, i64)> = Vec::new(); terms.push((x_var(j, p), big_m)); @@ -196,15 +210,6 @@ impl ReduceTo> for MinimumTardinessSequencing { } } terms.push((u_var(j), -big_m)); - let rhs = self.deadlines()[j] - .checked_sub(lengths[j]) - .and_then(|value| value.checked_add(total_length)) - .ok_or_else(|| { - crate::rules::ReductionError::integer_overflow::< - MinimumTardinessSequencing, - ILP, - >("computing a tardiness constraint bound") - })?; constraints.push(LinearConstraint::le(terms, rhs)); } } diff --git a/src/rules/minimumvertexcover_longestcommonsubsequence.rs b/src/rules/minimumvertexcover_longestcommonsubsequence.rs index 2cdc94188..12b8bc604 100644 --- a/src/rules/minimumvertexcover_longestcommonsubsequence.rs +++ b/src/rules/minimumvertexcover_longestcommonsubsequence.rs @@ -39,16 +39,20 @@ impl ReductionResult for ReductionVCToLCS { } #[reduction( - transform = exact { - alphabet_size = "num_vertices", - num_strings = "num_edges + 1", - max_length = "num_vertices", - total_length = "num_vertices + 2 * num_edges * num_vertices - 2 * num_edges", - sum_triangular_lengths = "num_vertices * (num_vertices + 1) / 2 + num_edges * (2 * num_vertices - 2) * (2 * num_vertices - 1) / 2", + transform = { + exact { + alphabet_size = "num_vertices", + num_strings = "num_edges + 1", + max_length = "num_vertices", + total_length = "num_vertices + 2 * num_edges * num_vertices - 2 * num_edges", + sum_triangular_lengths = "num_vertices * (num_vertices + 1) / 2 + num_edges * (2 * num_vertices - 2) * (2 * num_vertices - 1) / 2", + }, + upper_bound { + num_transitions = "num_vertices", + }, }, unavailable = { cross_frequency_product = "the exact target parameter is not represented by this reduction's symbolic transform", - num_transitions = "the exact target parameter is not represented by this reduction's symbolic transform", } )] impl ReduceTo for MinimumVertexCover { diff --git a/src/rules/minimumweightdecoding_ilp.rs b/src/rules/minimumweightdecoding_ilp.rs index 606fe757d..879784922 100644 --- a/src/rules/minimumweightdecoding_ilp.rs +++ b/src/rules/minimumweightdecoding_ilp.rs @@ -1,4 +1,4 @@ -//! Reduction from MinimumWeightDecoding to `ILP`. +//! Reduction from MinimumWeightDecoding to `ILP`. //! //! The GF(2) constraint Hx ≡ s (mod 2) is linearized by introducing integer //! slack variables k_i for each row: @@ -16,26 +16,26 @@ //! Objective: minimize Σ x_j (Hamming weight). use crate::models::algebraic::MinimumWeightDecoding; -use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{Bounded, IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; -/// Result of reducing MinimumWeightDecoding to `ILP`. +/// Result of reducing MinimumWeightDecoding to `ILP`. /// /// Variable layout: /// - x_j at index j for j in 0..num_cols (binary codeword bits) /// - k_i at index num_cols + i for i in 0..num_rows (integer slack) #[derive(Debug, Clone)] pub struct ReductionMinimumWeightDecodingToILP { - target: ILP, + target: ILP, num_cols: usize, } impl ReductionResult for ReductionMinimumWeightDecodingToILP { type Source = MinimumWeightDecoding; - type Target = ILP; + type Target = ILP; - fn target_problem(&self) -> &ILP { + fn target_problem(&self) -> &ILP { &self.target } @@ -59,10 +59,11 @@ impl ReductionResult for ReductionMinimumWeightDecodingToILP { num_constraints = "num_rows + num_cols", }, upper_bound { + max_constraint_magnitude_bits = "num_cols + 2", num_nonzeros = "(num_cols + num_rows) * (num_rows + num_cols)", }, })] -impl ReduceTo> for MinimumWeightDecoding { +impl ReduceTo> for MinimumWeightDecoding { type Result = ReductionMinimumWeightDecodingToILP; fn reduce_to(&self) -> Result { @@ -131,7 +132,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) + crate::example_db::specs::rule_example_via_bounded_ilp::<_>(source) }, }] } diff --git a/src/rules/minmaxmulticenter_ilp.rs b/src/rules/minmaxmulticenter_ilp.rs index 82b521f56..ed410c8b0 100644 --- a/src/rules/minmaxmulticenter_ilp.rs +++ b/src/rules/minmaxmulticenter_ilp.rs @@ -1,7 +1,7 @@ //! Reduction from MinMaxMulticenter to ILP (Integer Linear Programming). //! //! The vertex p-center optimization problem is formulated as a mixed ILP -//! using `ILP` to accommodate both binary and integer variables. +//! using `ILP` to accommodate both binary and integer variables. //! //! Variable layout: //! - `x_j` for each vertex j (binary: 1 if vertex j is selected as a center), indices `0..n` @@ -14,7 +14,7 @@ //! - Assignment: ∀i: Σ_j y_{i,j} = 1 (each vertex assigned to exactly one center) //! - Assignment link: ∀i,j: if j is reachable from i then y_{i,j} ≤ x_j, //! otherwise y_{i,j} = 0 -//! - Binary bounds: x_j ≤ 1, y_{i,j} ≤ 1 (enforce binary within `ILP`) +//! - Binary bounds: x_j ≤ 1, y_{i,j} ≤ 1 (enforce binary within `ILP`) //! - Minimax: ∀i: Σ_j w_i · d(i,j) · y_{i,j} ≤ z //! //! Objective: minimize z. @@ -24,7 +24,7 @@ //! Note: All-pairs shortest-path distances are computed using weighted shortest //! paths over `edge_lengths`. Unreachable assignment variables are forced to 0. -use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{Bounded, IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::MinMaxMulticenter; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -33,15 +33,15 @@ use crate::topology::{Graph, SimpleGraph}; /// Result of reducing MinMaxMulticenter to ILP. #[derive(Debug, Clone)] pub struct ReductionMMCToILP { - target: ILP, + target: ILP, num_vertices: usize, } impl ReductionResult for ReductionMMCToILP { type Source = MinMaxMulticenter; - type Target = ILP; + type Target = ILP; - fn target_problem(&self) -> &ILP { + fn target_problem(&self) -> &ILP { &self.target } @@ -66,7 +66,7 @@ fn weighted_distances_mmc( edge_lengths: &[i64], source: usize, n: usize, -) -> Vec> { +) -> Result>, crate::rules::ReductionError> { let mut adj: Vec> = vec![Vec::new(); n]; for (idx, &(u, v)) in graph.edges().iter().enumerate() { let len = edge_lengths[idx]; @@ -74,7 +74,7 @@ fn weighted_distances_mmc( adj[v].push((u, len)); } - let mut dist = vec![None; n]; + let mut dist = vec![None::; n]; let mut visited = vec![false; n]; dist[source] = Some(0); @@ -107,7 +107,12 @@ fn weighted_distances_mmc( if visited[v] { continue; } - let candidate = du + len; + let candidate = du.checked_add(len).ok_or_else(|| { + crate::rules::ReductionError::integer_overflow::< + MinMaxMulticenter, + ILP, + >("adding shortest-path lengths") + })?; let should_update = match dist[v] { None => true, Some(current) => candidate < current, @@ -118,7 +123,7 @@ fn weighted_distances_mmc( } } - dist + Ok(dist) } #[reduction(transform = { @@ -127,10 +132,11 @@ fn weighted_distances_mmc( num_constraints = "2 * num_vertices^2 + 3 * num_vertices + 2", }, upper_bound { + max_constraint_magnitude_bits = "2 * max_numeric_magnitude_bits + num_vertices", num_nonzeros = "(num_vertices + num_vertices^2 + 1) * (2 * num_vertices^2 + 3 * num_vertices + 2)", }, })] -impl ReduceTo> for MinMaxMulticenter { +impl ReduceTo> for MinMaxMulticenter { type Result = ReductionMMCToILP; fn reduce_to(&self) -> Result { @@ -142,7 +148,7 @@ impl ReduceTo> for MinMaxMulticenter { // Precompute all-pairs weighted shortest-path distances. let all_dist: Vec>> = (0..n) .map(|s| weighted_distances_mmc(self.graph(), edge_lengths, s, n)) - .collect(); + .collect::>()?; // Index helpers. let x_var = |j: usize| j; @@ -177,7 +183,7 @@ impl ReduceTo> for MinMaxMulticenter { } } - // Binary bounds for x_j and y_{i,j} (enforce binary within `ILP`) + // Binary bounds for x_j and y_{i,j} (enforce binary within `ILP`) for j in 0..n { constraints.push(LinearConstraint::le(vec![(x_var(j), 1)], 1)); } @@ -200,7 +206,7 @@ impl ReduceTo> for MinMaxMulticenter { vertex_weights[i].checked_mul(distance).ok_or_else(|| { crate::rules::ReductionError::integer_overflow::< MinMaxMulticenter, - ILP, + ILP, >( "multiplying a vertex weight by a shortest-path distance" ) @@ -259,7 +265,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) + crate::example_db::specs::rule_example_via_bounded_ilp::<_>(source) }, }] } diff --git a/src/rules/mixedchinesepostman_ilp.rs b/src/rules/mixedchinesepostman_ilp.rs index a12819f2d..0cecfd58d 100644 --- a/src/rules/mixedchinesepostman_ilp.rs +++ b/src/rules/mixedchinesepostman_ilp.rs @@ -5,7 +5,7 @@ //! within the length bound. Uses connectivity flow constraints on both //! forward and reverse directions. -use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{Bounded, IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::MixedChinesePostman; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -14,15 +14,15 @@ use crate::types::WeightElement; /// Result of reducing MixedChinesePostman to ILP. #[derive(Debug, Clone)] pub struct ReductionMCPToILP { - target: ILP, + target: ILP, num_undirected_edges: usize, } impl ReductionResult for ReductionMCPToILP { type Source = MixedChinesePostman; - type Target = ILP; + type Target = ILP; - fn target_problem(&self) -> &ILP { + fn target_problem(&self) -> &ILP { &self.target } @@ -43,11 +43,12 @@ impl ReductionResult for ReductionMCPToILP { } #[reduction(transform = upper_bound { + max_constraint_magnitude_bits = "(num_arcs + num_edges + 1) * (num_vertices + 1) + 1", num_vars = "num_edges + 4 * (num_arcs + 2 * num_edges) + 3 * num_vertices + 1", num_constraints = "num_edges + 8 * (num_arcs + 2 * num_edges) + 10 * num_vertices + 2", num_nonzeros = "(num_edges + 4 * (num_arcs + 2 * num_edges) + 3 * num_vertices + 1) * (num_edges + 8 * (num_arcs + 2 * num_edges) + 10 * num_vertices + 2)", })] -impl ReduceTo> for MixedChinesePostman { +impl ReduceTo> for MixedChinesePostman { type Result = ReductionMCPToILP; #[allow(clippy::needless_range_loop)] @@ -115,14 +116,16 @@ impl ReduceTo> for MixedChinesePostman { let n_i64 = Self::exact_i64(n, "encoding the active-vertex count")?; let r_count_i64 = Self::exact_i64(r_count, "encoding the required-arc count")?; let big_g = r_count_i64.checked_mul(n_i64 - 1).ok_or_else(|| { - crate::rules::ReductionError::integer_overflow::, ILP>( - "computing the extra-traversal bound", - ) + crate::rules::ReductionError::integer_overflow::< + MixedChinesePostman, + ILP, + >("computing the extra-traversal bound") })?; let m_use = big_g.checked_add(1).ok_or_else(|| { - crate::rules::ReductionError::integer_overflow::, ILP>( - "computing the arc-use bound", - ) + crate::rules::ReductionError::integer_overflow::< + MixedChinesePostman, + ILP, + >("computing the arc-use bound") })?; let mut constraints = Vec::new(); @@ -293,7 +296,7 @@ impl ReduceTo> for MixedChinesePostman { vec![(b_idx(v), 1), (s_idx, -1), (rho_idx(v), -n_i64)], -n_i64, )); - // b_v >= 0 is implied by `ILP` non-negativity + // b_v >= 0 is implied by `ILP` non-negativity } // Flow bounds: 0 <= f_j, h_j <= (n-1) * y_j @@ -400,7 +403,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) + crate::example_db::specs::rule_example_via_bounded_ilp::<_>(source) }, }] } diff --git a/src/rules/mod.rs b/src/rules/mod.rs index a55e411e4..2251492c5 100644 --- a/src/rules/mod.rs +++ b/src/rules/mod.rs @@ -191,6 +191,7 @@ pub(crate) mod graphpartitioning_ilp; pub(crate) mod hamiltonianpath_ilp; pub(crate) mod highlyconnecteddeletion_ilp; mod ilp_bool_ilp_i64; +mod ilp_bounded_ilp; pub(crate) mod ilp_helpers; pub(crate) mod ilp_qubo; pub(crate) mod integralflowbundles_ilp; diff --git a/src/rules/monochromatictriangle_ilp.rs b/src/rules/monochromatictriangle_ilp.rs index 5bdcc3449..ddbc7dacd 100644 --- a/src/rules/monochromatictriangle_ilp.rs +++ b/src/rules/monochromatictriangle_ilp.rs @@ -44,6 +44,7 @@ impl ReductionResult for ReductionMonochromaticTriangleToILP { impl crate::rules::AggregateReductionResult for ReductionMonochromaticTriangleToILP {} #[reduction(transform = upper_bound { + max_constraint_magnitude_bits = "2", num_vars = "num_edges", num_constraints = "2 * num_triangles + num_vertices^5 / 8", num_nonzeros = "num_edges * (2 * num_triangles + num_vertices^5 / 8)", diff --git a/src/rules/multiplechoicebranching_ilp.rs b/src/rules/multiplechoicebranching_ilp.rs index 63e8b1121..5103abecc 100644 --- a/src/rules/multiplechoicebranching_ilp.rs +++ b/src/rules/multiplechoicebranching_ilp.rs @@ -1,19 +1,19 @@ //! Reduction from MultipleChoiceBranching with integer weights to integer ILP. -use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{Bounded, IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::MultipleChoiceBranching; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; #[derive(Debug, Clone)] pub struct ReductionMultipleChoiceBranchingToILP { - target: ILP, + target: ILP, num_arcs: usize, } impl ReductionResult for ReductionMultipleChoiceBranchingToILP { type Source = MultipleChoiceBranching; - type Target = ILP; + type Target = ILP; fn target_problem(&self) -> &Self::Target { &self.target @@ -45,10 +45,11 @@ impl crate::rules::AggregateReductionResult for ReductionMultipleChoiceBranching num_constraints = "2 * num_arcs + 2 * num_vertices + num_partition_groups + 1", }, upper_bound { + max_constraint_magnitude_bits = "max_weight_bits + num_vertices", num_nonzeros = "(num_arcs + num_vertices) * (2 * num_arcs + 2 * num_vertices + num_partition_groups + 1)", }, })] -impl ReduceTo> for MultipleChoiceBranching { +impl ReduceTo> for MultipleChoiceBranching { type Result = ReductionMultipleChoiceBranchingToILP; fn reduce_to(&self) -> Result { @@ -61,7 +62,7 @@ impl ReduceTo> for MultipleChoiceBranching { constraints.push(LinearConstraint::le(vec![(arc, 1)], 1)); } if num_vertices > 0 { - let big_m = >>::exact_i64( + let big_m = >>::exact_i64( num_vertices, "encoding topological-order constraints", )?; @@ -114,7 +115,7 @@ impl ReduceTo> for MultipleChoiceBranching { ); let target = ILP::with_variables(variables, constraints, vec![], ObjectiveSense::Minimize) - .map_err(>>::target_construction)?; + .map_err(>>::target_construction)?; Ok(ReductionMultipleChoiceBranchingToILP { target, num_arcs }) } } @@ -132,7 +133,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) + crate::example_db::specs::rule_example_via_bounded_ilp::<_>(source) }, }] } diff --git a/src/rules/multiplecopyfileallocation_ilp.rs b/src/rules/multiplecopyfileallocation_ilp.rs index 9b294b615..4a67d6a92 100644 --- a/src/rules/multiplecopyfileallocation_ilp.rs +++ b/src/rules/multiplecopyfileallocation_ilp.rs @@ -74,6 +74,7 @@ fn bfs_distances(graph: &SimpleGraph, source: usize, n: usize) -> Vec { num_constraints = "num_vertices^2 + num_vertices", }, upper_bound { + max_constraint_magnitude_bits = "1", num_nonzeros = "(num_vertices + num_vertices^2) * (num_vertices^2 + num_vertices)", }, })] diff --git a/src/rules/multiprocessorscheduling_ilp.rs b/src/rules/multiprocessorscheduling_ilp.rs index be832401b..f1e19d945 100644 --- a/src/rules/multiprocessorscheduling_ilp.rs +++ b/src/rules/multiprocessorscheduling_ilp.rs @@ -58,6 +58,7 @@ impl crate::rules::AggregateReductionResult for ReductionMSToILP {} #[reduction(transform = { exact { + max_constraint_magnitude_bits = "max_numeric_magnitude_bits", num_vars = "num_tasks * num_processors", num_constraints = "num_tasks + num_processors", }, diff --git a/src/rules/naesatisfiability_ilp.rs b/src/rules/naesatisfiability_ilp.rs index 03d6a3b72..b2218bee2 100644 --- a/src/rules/naesatisfiability_ilp.rs +++ b/src/rules/naesatisfiability_ilp.rs @@ -50,6 +50,7 @@ impl crate::rules::AggregateReductionResult for ReductionNAESATToILP {} num_constraints = "2 * num_clauses", }, upper_bound { + max_constraint_magnitude_bits = "num_literals + 1", num_nonzeros = "num_vars * (2 * num_clauses)", }, })] diff --git a/src/rules/numericalmatchingwithtargetsums_ilp.rs b/src/rules/numericalmatchingwithtargetsums_ilp.rs index 670905866..a063d074a 100644 --- a/src/rules/numericalmatchingwithtargetsums_ilp.rs +++ b/src/rules/numericalmatchingwithtargetsums_ilp.rs @@ -71,6 +71,7 @@ impl ReductionResult for ReductionNMTSToILP { impl crate::rules::AggregateReductionResult for ReductionNMTSToILP {} #[reduction(transform = upper_bound { + max_constraint_magnitude_bits = "1", num_vars = "num_pairs * num_pairs * num_pairs", num_constraints = "3 * num_pairs", num_nonzeros = "(num_pairs * num_pairs * num_pairs) * (3 * num_pairs)", diff --git a/src/rules/openshopscheduling_ilp.rs b/src/rules/openshopscheduling_ilp.rs index 514ff218a..41a20ffe0 100644 --- a/src/rules/openshopscheduling_ilp.rs +++ b/src/rules/openshopscheduling_ilp.rs @@ -1,4 +1,4 @@ -//! Reduction from OpenShopScheduling to `ILP`. +//! Reduction from OpenShopScheduling to `ILP`. //! //! Disjunctive formulation with binary ordering variables and integer start times: //! @@ -26,13 +26,13 @@ //! //! **Objective:** Minimize C. -use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{Bounded, IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::OpenShopScheduling; use crate::models::Decision; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; -/// Result of reducing OpenShopScheduling to `ILP`. +/// Result of reducing OpenShopScheduling to `ILP`. /// /// Variable layout: /// - `x_{j,k,i}` at index `pair_idx(j,k) * m + i` (num_pairs * m vars) @@ -42,7 +42,7 @@ use crate::rules::traits::{ReduceTo, ReductionResult}; /// - `C`: at index `num_order_vars + n * m + n * m*(m-1)/2` (1 var) #[derive(Debug, Clone)] pub struct ReductionOSSToILP { - target: ILP, + target: ILP, num_jobs: usize, num_machines: usize, /// n*(n-1)/2 * m — start index of s_{j,i} variables @@ -87,9 +87,9 @@ impl ReductionOSSToILP { impl ReductionResult for ReductionOSSToILP { type Source = OpenShopScheduling; - type Target = ILP; + type Target = ILP; - fn target_problem(&self) -> &ILP { + fn target_problem(&self) -> &ILP { &self.target } @@ -109,10 +109,11 @@ impl ReductionResult for ReductionOSSToILP { num_constraints = "num_jobs * (num_jobs - 1) / 2 * num_machines + num_jobs * num_machines + 1 + 2 * num_jobs * (num_jobs - 1) / 2 * num_machines + num_jobs * num_machines * (num_machines - 1) / 2 + 2 * num_jobs * num_machines * (num_machines - 1) / 2 + num_jobs * num_machines", }, upper_bound { + max_constraint_magnitude_bits = "schedule_horizon_bits", num_nonzeros = "(num_jobs * (num_jobs - 1) / 2 * num_machines + num_jobs * num_machines + num_jobs * num_machines * (num_machines - 1) / 2 + 1) * (num_jobs * (num_jobs - 1) / 2 * num_machines + num_jobs * num_machines + 1 + 2 * num_jobs * (num_jobs - 1) / 2 * num_machines + num_jobs * num_machines * (num_machines - 1) / 2 + 2 * num_jobs * num_machines * (num_machines - 1) / 2 + num_jobs * num_machines)", }, })] -impl ReduceTo> for OpenShopScheduling { +impl ReduceTo> for OpenShopScheduling { type Result = ReductionOSSToILP; fn reduce_to(&self) -> Result { @@ -143,9 +144,10 @@ impl ReduceTo> for OpenShopScheduling { .flat_map(|row| row.iter()) .try_fold(0_i64, |total, &time| total.checked_add(time)) .ok_or_else(|| { - crate::rules::ReductionError::integer_overflow::>( - "summing open-shop processing times", - ) + crate::rules::ReductionError::integer_overflow::< + OpenShopScheduling, + ILP, + >("summing open-shop processing times") })?; let big_m = total_p; let processing_times = p; @@ -296,7 +298,7 @@ pub struct ReductionDecisionOpenShopSchedulingToILP { impl ReductionResult for ReductionDecisionOpenShopSchedulingToILP { type Source = Decision; - type Target = ILP; + type Target = ILP; fn target_problem(&self) -> &Self::Target { self.inner.target_problem() @@ -322,18 +324,24 @@ impl crate::rules::AggregateReductionResult for ReductionDecisionOpenShopSchedul num_constraints = "3 * num_jobs * (num_jobs - 1) / 2 * num_machines + 2 * num_jobs * num_machines + 3 * num_jobs * num_machines * (num_machines - 1) / 2 + 2", }, upper_bound { + max_constraint_magnitude_bits = "schedule_horizon_bits", num_nonzeros = "(num_jobs * (num_jobs - 1) / 2 * num_machines + num_jobs * num_machines + num_jobs * num_machines * (num_machines - 1) / 2 + 1) * (3 * num_jobs * (num_jobs - 1) / 2 * num_machines + 2 * num_jobs * num_machines + 3 * num_jobs * num_machines * (num_machines - 1) / 2 + 2)", }, })] -impl ReduceTo> for Decision { +impl ReduceTo> for Decision { type Result = ReductionDecisionOpenShopSchedulingToILP; fn reduce_to(&self) -> Result { - let mut inner = ReduceTo::>::reduce_to(self.inner())?; + let mut inner = ReduceTo::>::reduce_to(self.inner())?; let mut constraints = inner.target.constraints().to_vec(); + // The makespan variable is already bounded by the total processing time. + let horizon = >>::exact_i64( + self.inner().schedule_horizon(), + "encoding the makespan bound", + )?; constraints.push(LinearConstraint::le( inner.target.objective().to_vec(), - *self.bound(), + (*self.bound()).clamp(-1, horizon), )); inner.target = ILP::with_variables( inner.target.variables().to_vec(), @@ -341,7 +349,7 @@ impl ReduceTo> for Decision { vec![], ObjectiveSense::Minimize, ) - .map_err(>>::target_construction)?; + .map_err(>>::target_construction)?; Ok(ReductionDecisionOpenShopSchedulingToILP { inner }) } } @@ -354,7 +362,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) + crate::example_db::specs::rule_example_via_bounded_ilp::<_>(source) }, }, crate::example_db::specs::RuleExampleSpec { @@ -362,7 +370,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) + crate::example_db::specs::rule_example_via_bounded_ilp::<_>(source) }, }, ] diff --git a/src/rules/optimallineararrangement_ilp.rs b/src/rules/optimallineararrangement_ilp.rs index e37b18c83..de1ca2add 100644 --- a/src/rules/optimallineararrangement_ilp.rs +++ b/src/rules/optimallineararrangement_ilp.rs @@ -7,7 +7,7 @@ //! - abs_diff_le constraints: z_{u,v} >= p_u - p_v, z_{u,v} >= p_v - p_u //! - Minimize: sum z_{u,v} -use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{Bounded, IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::OptimalLinearArrangement; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -15,21 +15,21 @@ use crate::topology::{Graph, SimpleGraph}; /// Result of reducing OptimalLinearArrangement to ILP. /// -/// Variable layout (`ILP`, non-negative integers): +/// Variable layout (`ILP`, non-negative integers): /// - `x_{v,p}` at index `v * n + p`, bounded to {0,1} /// - `p_v` at index `n^2 + v`, integer position in {0, ..., n-1} /// - `z_e` at index `n^2 + n + e`, non-negative integer for edge length #[derive(Debug, Clone)] pub struct ReductionOLAToILP { - target: ILP, + target: ILP, num_vertices: usize, } impl ReductionResult for ReductionOLAToILP { type Source = OptimalLinearArrangement; - type Target = ILP; + type Target = ILP; - fn target_problem(&self) -> &ILP { + fn target_problem(&self) -> &ILP { &self.target } @@ -55,10 +55,11 @@ impl ReductionResult for ReductionOLAToILP { num_constraints = "2 * num_vertices + num_vertices^2 + num_vertices + num_vertices + 3 * num_edges", }, upper_bound { + max_constraint_magnitude_bits = "num_vertices + 1", num_nonzeros = "(num_vertices^2 + num_vertices + num_edges) * (2 * num_vertices + num_vertices^2 + num_vertices + num_vertices + 3 * num_edges)", }, })] -impl ReduceTo> for OptimalLinearArrangement { +impl ReduceTo> for OptimalLinearArrangement { type Result = ReductionOLAToILP; fn reduce_to(&self) -> Result { @@ -75,7 +76,8 @@ impl ReduceTo> for OptimalLinearArrangement { let z_idx = |e: usize| -> usize { num_x + n + e }; let mut constraints = Vec::new(); - let n_i64 = >>::exact_i64(n, "encoding a vertex position")?; + let n_i64 = + >>::exact_i64(n, "encoding a vertex position")?; // Assignment: each vertex in exactly one position for v in 0..n { @@ -89,7 +91,7 @@ impl ReduceTo> for OptimalLinearArrangement { constraints.push(LinearConstraint::eq(terms, 1)); } - // Binary bounds for x variables (`ILP`) + // Binary bounds for x variables (`ILP`) for v in 0..n { for p in 0..n { constraints.push(LinearConstraint::le(vec![(x_idx(v, p), 1)], 1)); @@ -103,7 +105,10 @@ impl ReduceTo> for OptimalLinearArrangement { for p in 0..n { terms.push(( x_idx(v, p), - ->>::exact_i64(p, "encoding a vertex position")?, + ->>::exact_i64( + p, + "encoding a vertex position", + )?, )); } constraints.push(LinearConstraint::eq(terms, 0)); @@ -135,12 +140,12 @@ impl ReduceTo> for OptimalLinearArrangement { let mut variables = vec![IntegerVariable::binary(); num_vars]; variables[num_x..].fill( IntegerVariable::new(Some(0), Some((n_i64 - 1).max(0))) - .map_err(>>::target_construction)?, + .map_err(>>::target_construction)?, ); let target = ILP::with_variables(variables, constraints, objective, ObjectiveSense::Minimize) - .map_err(>>::target_construction)?; + .map_err(>>::target_construction)?; Ok(ReductionOLAToILP { target, @@ -157,7 +162,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) + crate::example_db::specs::rule_example_via_bounded_ilp::<_>(source) }, }] } diff --git a/src/rules/optimallineararrangement_sequencingtominimizeweightedcompletiontime.rs b/src/rules/optimallineararrangement_sequencingtominimizeweightedcompletiontime.rs index a83378b83..a4ef67f52 100644 --- a/src/rules/optimallineararrangement_sequencingtominimizeweightedcompletiontime.rs +++ b/src/rules/optimallineararrangement_sequencingtominimizeweightedcompletiontime.rs @@ -56,6 +56,7 @@ impl ReductionResult for ReductionOLAToSequencingToMinimizeWeightedCompletionTim #[reduction( transform = exact { + max_processing_time_bits = "1", num_tasks = "num_vertices + num_edges", num_precedences = "2 * num_edges", } diff --git a/src/rules/optimumcommunicationspanningtree_ilp.rs b/src/rules/optimumcommunicationspanningtree_ilp.rs index b586d3211..54fc448ec 100644 --- a/src/rules/optimumcommunicationspanningtree_ilp.rs +++ b/src/rules/optimumcommunicationspanningtree_ilp.rs @@ -52,6 +52,7 @@ impl ReductionResult for ReductionOptimumCommunicationSpanningTreeToILP { num_constraints = "1 + num_vertices * num_vertices * (num_vertices - 1) / 2 + 2 * num_edges * num_vertices * (num_vertices - 1) / 2", }, upper_bound { + max_constraint_magnitude_bits = "num_vertices + 1", num_nonzeros = "(num_edges + 2 * num_edges * num_vertices * (num_vertices - 1) / 2) * (1 + num_vertices * num_vertices * (num_vertices - 1) / 2 + 2 * num_edges * num_vertices * (num_vertices - 1) / 2)", }, })] diff --git a/src/rules/paintshop_ilp.rs b/src/rules/paintshop_ilp.rs index cc01ab912..fed682174 100644 --- a/src/rules/paintshop_ilp.rs +++ b/src/rules/paintshop_ilp.rs @@ -44,6 +44,7 @@ impl ReductionResult for ReductionPaintShopToILP { num_vars = "num_cars + 2 * num_sequence", }, upper_bound { + max_constraint_magnitude_bits = "1", num_constraints = "3 * num_sequence", num_nonzeros = "8 * num_sequence", }, diff --git a/src/rules/partiallyorderedknapsack_ilp.rs b/src/rules/partiallyorderedknapsack_ilp.rs index 546d20027..16af0e786 100644 --- a/src/rules/partiallyorderedknapsack_ilp.rs +++ b/src/rules/partiallyorderedknapsack_ilp.rs @@ -33,6 +33,7 @@ impl ReductionResult for ReductionPOKToILP { #[reduction(transform = { exact { + max_constraint_magnitude_bits = "max_weight_bits", num_vars = "num_items", num_constraints = "num_precedences + 1", }, diff --git a/src/rules/partition_binpacking.rs b/src/rules/partition_binpacking.rs index a5e2872aa..57411e031 100644 --- a/src/rules/partition_binpacking.rs +++ b/src/rules/partition_binpacking.rs @@ -66,10 +66,15 @@ impl crate::rules::AggregateReductionResult for ReductionPartitionToBinPacking { } } -#[reduction( - transform = exact { +// Capacity is at most max(1, sum(sizes)/2); summing n entries adds at most n bits. +#[reduction(transform = { + exact { num_items = "num_elements", - })] + }, + upper_bound { + max_numeric_magnitude_bits = "max_numeric_magnitude_bits + num_elements", + }, +})] impl ReduceTo> for Partition { type Result = ReductionPartitionToBinPacking; diff --git a/src/rules/partition_integralflowwithmultipliers.rs b/src/rules/partition_integralflowwithmultipliers.rs index f72063df2..2cabe4659 100644 --- a/src/rules/partition_integralflowwithmultipliers.rs +++ b/src/rules/partition_integralflowwithmultipliers.rs @@ -56,12 +56,12 @@ impl crate::rules::AggregateReductionResult for ReductionPartitionToIntegralFlow #[reduction( transform = upper_bound { + max_capacity_bits = "max_numeric_magnitude_bits + num_elements", num_vertices = "num_elements + 3", num_arcs = "2 * num_elements + 1", }, unavailable = { - max_capacity = "the target capacity depends on source numeric values not represented by Partition parameters", - requirement = "the target requirement depends on source numeric values not represented by Partition parameters", + max_capacity = "bounding raw capacities from source magnitude bits requires a variable exponent; downstream ILP predictions use max_capacity_bits", } )] impl ReduceTo for Partition { diff --git a/src/rules/partition_knapsack.rs b/src/rules/partition_knapsack.rs index bb0584ff8..3d088f0d1 100644 --- a/src/rules/partition_knapsack.rs +++ b/src/rules/partition_knapsack.rs @@ -49,9 +49,12 @@ impl crate::rules::AggregateReductionResult for ReductionPartitionToKnapsack { } #[reduction( - transform = exact { num_items = "num_elements" }, + transform = { + exact { num_items = "num_elements" }, + upper_bound { capacity_bits = "max_numeric_magnitude_bits + num_elements" }, + }, unavailable = { - capacity = "the exact target parameter is not represented by this reduction's symbolic transform", + capacity = "raw capacity requires numeric magnitude values; downstream predictions use capacity_bits", } )] impl ReduceTo for Partition { diff --git a/src/rules/partition_multiprocessorscheduling.rs b/src/rules/partition_multiprocessorscheduling.rs index 6ecdc103c..5ff8a17f1 100644 --- a/src/rules/partition_multiprocessorscheduling.rs +++ b/src/rules/partition_multiprocessorscheduling.rs @@ -54,9 +54,14 @@ impl ReductionResult for ReductionPartitionToMPS { impl crate::rules::AggregateReductionResult for ReductionPartitionToMPS {} #[reduction( - transform = exact { - num_tasks = "num_elements", - num_processors = "2", + transform = { + exact { + num_tasks = "num_elements", + num_processors = "2", + }, + upper_bound { + max_numeric_magnitude_bits = "max_numeric_magnitude_bits + num_elements", + }, } )] impl ReduceTo for Partition { diff --git a/src/rules/partition_openshopscheduling.rs b/src/rules/partition_openshopscheduling.rs index 7fb5d0ac2..fe467b900 100644 --- a/src/rules/partition_openshopscheduling.rs +++ b/src/rules/partition_openshopscheduling.rs @@ -81,12 +81,17 @@ impl ReductionResult for ReductionPartitionToOpenShopScheduling { impl crate::rules::AggregateReductionResult for ReductionPartitionToOpenShopScheduling {} #[reduction( - transform = exact { - num_jobs = "num_elements + 1", - num_machines = "3", + transform = { + exact { + num_jobs = "num_elements + 1", + num_machines = "3", + }, + upper_bound { + schedule_horizon_bits = "max_numeric_magnitude_bits + num_elements + 3", + }, }, unavailable = { - schedule_horizon = "depends on the numeric partition sizes, which are not represented by source size parameters", + schedule_horizon = "raw horizon requires numeric magnitude values; downstream predictions use schedule_horizon_bits", } )] impl ReduceTo> for Partition { diff --git a/src/rules/partition_sequencingtominimizetardytaskweight.rs b/src/rules/partition_sequencingtominimizetardytaskweight.rs index 0fca17064..74eaa761e 100644 --- a/src/rules/partition_sequencingtominimizetardytaskweight.rs +++ b/src/rules/partition_sequencingtominimizetardytaskweight.rs @@ -60,6 +60,7 @@ impl crate::rules::AggregateReductionResult #[reduction( transform = exact { + max_processing_time_bits = "max_numeric_magnitude_bits", num_tasks = "num_elements", })] impl ReduceTo> for Partition { diff --git a/src/rules/partition_subsetsum.rs b/src/rules/partition_subsetsum.rs index ceaca59ee..afcac632f 100644 --- a/src/rules/partition_subsetsum.rs +++ b/src/rules/partition_subsetsum.rs @@ -51,8 +51,10 @@ impl ReductionResult for ReductionPartitionToSubsetSum { #[crate::aggregate_reduction(identity)] impl crate::rules::AggregateReductionResult for ReductionPartitionToSubsetSum {} +// The target is half the sum, bounded by n*2^h; odd sums use the constant NO instance. #[reduction( transform = upper_bound { + max_numeric_magnitude_bits = "max_numeric_magnitude_bits + num_elements", num_elements = "num_elements", })] impl ReduceTo for Partition { diff --git a/src/rules/partitionintocliques_ilp.rs b/src/rules/partitionintocliques_ilp.rs index 7ef0c820d..b530b6fbf 100644 --- a/src/rules/partitionintocliques_ilp.rs +++ b/src/rules/partitionintocliques_ilp.rs @@ -50,6 +50,7 @@ impl ReductionResult for ReductionPartitionIntoCliquesToILP { impl crate::rules::AggregateReductionResult for ReductionPartitionIntoCliquesToILP {} #[reduction(transform = upper_bound { + max_constraint_magnitude_bits = "1", num_vars = "num_vertices^2", num_constraints = "num_vertices + num_vertices^3", num_nonzeros = "(num_vertices^2) * (num_vertices + num_vertices^3)", diff --git a/src/rules/partitionintopathsoflength2_boundedcomponentspanningforest.rs b/src/rules/partitionintopathsoflength2_boundedcomponentspanningforest.rs index e393cd3c1..eb7778cf8 100644 --- a/src/rules/partitionintopathsoflength2_boundedcomponentspanningforest.rs +++ b/src/rules/partitionintopathsoflength2_boundedcomponentspanningforest.rs @@ -52,10 +52,13 @@ impl ReductionResult for ReductionPPL2ToBCSF { impl crate::rules::AggregateReductionResult for ReductionPPL2ToBCSF {} #[reduction( - transform = upper_bound { - num_vertices = "num_vertices", - num_edges = "num_edges", - max_components = "num_vertices / 3 + 1", + transform = { + exact { max_weight_bits = "2", }, + upper_bound { + num_vertices = "num_vertices", + num_edges = "num_edges", + max_components = "num_vertices / 3 + 1", + }, } )] impl ReduceTo> diff --git a/src/rules/partitionintopathsoflength2_ilp.rs b/src/rules/partitionintopathsoflength2_ilp.rs index 30b32901d..8dea37fd6 100644 --- a/src/rules/partitionintopathsoflength2_ilp.rs +++ b/src/rules/partitionintopathsoflength2_ilp.rs @@ -68,6 +68,7 @@ impl ReductionResult for ReductionPIPL2ToILP { impl crate::rules::AggregateReductionResult for ReductionPIPL2ToILP {} #[reduction(transform = upper_bound { + max_constraint_magnitude_bits = "2", num_vars = "num_vertices^2 + num_edges * num_vertices", num_constraints = "num_vertices^2 + num_edges * num_vertices + num_vertices", num_nonzeros = "(num_vertices^2 + num_edges * num_vertices) * (num_vertices^2 + num_edges * num_vertices + num_vertices)", diff --git a/src/rules/partitionintotriangles_ilp.rs b/src/rules/partitionintotriangles_ilp.rs index 3ec37775c..45dc8be3e 100644 --- a/src/rules/partitionintotriangles_ilp.rs +++ b/src/rules/partitionintotriangles_ilp.rs @@ -61,6 +61,7 @@ impl ReductionResult for ReductionPITToILP { impl crate::rules::AggregateReductionResult for ReductionPITToILP {} #[reduction(transform = upper_bound { + max_constraint_magnitude_bits = "2", num_vars = "num_vertices^2", num_constraints = "num_vertices^2 * num_vertices", num_nonzeros = "(num_vertices^2) * (num_vertices^2 * num_vertices)", diff --git a/src/rules/pathconstrainednetworkflow_ilp.rs b/src/rules/pathconstrainednetworkflow_ilp.rs index 635030112..7b7c59494 100644 --- a/src/rules/pathconstrainednetworkflow_ilp.rs +++ b/src/rules/pathconstrainednetworkflow_ilp.rs @@ -3,7 +3,7 @@ //! One integer variable per prescribed path. Arc capacity aggregation //! across paths and total flow requirement. -use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{Bounded, IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::PathConstrainedNetworkFlow; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -11,14 +11,14 @@ use crate::rules::traits::{ReduceTo, ReductionResult}; /// Result of reducing PathConstrainedNetworkFlow to ILP. #[derive(Debug, Clone)] pub struct ReductionPCNFToILP { - target: ILP, + target: ILP, } impl ReductionResult for ReductionPCNFToILP { type Source = PathConstrainedNetworkFlow; - type Target = ILP; + type Target = ILP; - fn target_problem(&self) -> &ILP { + fn target_problem(&self) -> &ILP { &self.target } @@ -40,12 +40,15 @@ impl ReductionResult for ReductionPCNFToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionPCNFToILP {} -#[reduction(transform = upper_bound { - num_vars = "num_paths", - num_constraints = "num_arcs + 1", - num_nonzeros = "num_paths * (num_arcs + 1)", -})] -impl ReduceTo> for PathConstrainedNetworkFlow { +#[reduction( + transform = upper_bound { + max_constraint_magnitude_bits = "max_capacity * (num_paths + 1) + 2", + num_vars = "num_paths", + num_constraints = "num_arcs + 1", + num_nonzeros = "num_paths * (num_arcs + 1)", + }, +)] +impl ReduceTo> for PathConstrainedNetworkFlow { type Result = ReductionPCNFToILP; fn reduce_to(&self) -> Result { @@ -69,7 +72,13 @@ impl ReduceTo> for PathConstrainedNetworkFlow { // Total flow requirement: sum_i f_i >= R let total_terms: Vec<(usize, i64)> = (0..num_paths).map(|i| (i, 1)).collect(); - constraints.push(LinearConstraint::ge(total_terms, self.requirement())); + constraints.push(LinearConstraint::ge( + total_terms, + crate::rules::ilp_helpers::bounded_flow_requirement( + self.requirement(), + self.paths().iter().map(|path| self.path_bottleneck(path)), + ), + )); let variables = self .paths() @@ -102,7 +111,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) + crate::example_db::specs::rule_example_via_bounded_ilp::<_>(source) }, }] } diff --git a/src/rules/precedenceconstrainedscheduling_ilp.rs b/src/rules/precedenceconstrainedscheduling_ilp.rs index 5416b8e6d..10cd0e7c3 100644 --- a/src/rules/precedenceconstrainedscheduling_ilp.rs +++ b/src/rules/precedenceconstrainedscheduling_ilp.rs @@ -67,6 +67,7 @@ impl crate::rules::AggregateReductionResult for ReductionPCSToILP {} num_constraints = "num_tasks + deadline + num_precedences", }, upper_bound { + max_constraint_magnitude_bits = "num_tasks + deadline + 1", num_nonzeros = "(num_tasks * deadline) * (num_tasks + deadline + num_precedences)", }, })] @@ -85,8 +86,11 @@ impl ReduceTo> for PrecedenceConstrainedScheduling { // x_{j,t} variable index let var = |j: usize, t: usize| j * d + t; - let processor_count = - Self::exact_i64(self.num_processors(), "encoding the processor capacity")?; + // More processors than tasks cannot admit any additional schedules. + let processor_count = Self::exact_i64( + self.num_processors().min(n), + "encoding the processor capacity", + )?; let mut constraints = Vec::new(); diff --git a/src/rules/preemptivescheduling_ilp.rs b/src/rules/preemptivescheduling_ilp.rs index badad6f82..f6e9e438f 100644 --- a/src/rules/preemptivescheduling_ilp.rs +++ b/src/rules/preemptivescheduling_ilp.rs @@ -1,4 +1,4 @@ -//! Reduction from PreemptiveScheduling to `ILP`. +//! Reduction from PreemptiveScheduling to `ILP`. //! //! Time-indexed formulation with an auxiliary integer makespan variable: //! - Variables: binary x_{t,u} for t in 0..n, u in 0..D_max (task t processed at slot u), @@ -15,18 +15,18 @@ //! 4. Makespan lower bound: M ≥ (u+1) when x_{t,u}=1: //! `M - (u+1)*x_{t,u} ≥ 0` for all t,u //! 5. Binary bounds: x_{t,u} ≤ 1 for each t,u -//! (since `ILP` uses non-negative integer domain) +//! (since `ILP` uses non-negative integer domain) //! - Objective: Minimize M. //! -//! Note: `ILP` treats all variables as non-negative integers. Binary constraints +//! Note: `ILP` treats all variables as non-negative integers. Binary constraints //! on x_{t,u} are enforced by x_{t,u} ≤ 1. -use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{Bounded, IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::PreemptiveScheduling; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; -/// Result of reducing PreemptiveScheduling to `ILP`. +/// Result of reducing PreemptiveScheduling to `ILP`. /// /// Variable layout: /// - x_{t,u} at index t * D_max + u for t in 0..n, u in 0..D_max (n*D_max vars) @@ -35,16 +35,16 @@ use crate::rules::traits::{ReduceTo, ReductionResult}; /// Total: n * D_max + 1 variables. #[derive(Debug, Clone)] pub struct ReductionPSToILP { - target: ILP, + target: ILP, num_tasks: usize, d_max: usize, } impl ReductionResult for ReductionPSToILP { type Source = PreemptiveScheduling; - type Target = ILP; + type Target = ILP; - fn target_problem(&self) -> &ILP { + fn target_problem(&self) -> &ILP { &self.target } @@ -73,10 +73,11 @@ impl ReductionResult for ReductionPSToILP { num_constraints = "num_tasks + d_max + num_precedences * d_max + 2 * num_tasks * d_max", }, upper_bound { + max_constraint_magnitude_bits = "d_max + num_processors + 1", num_nonzeros = "(num_tasks * d_max + 1) * (num_tasks + d_max + num_precedences * d_max + 2 * num_tasks * d_max)", }, })] -impl ReduceTo> for PreemptiveScheduling { +impl ReduceTo> for PreemptiveScheduling { type Result = ReductionPSToILP; fn reduce_to(&self) -> Result { @@ -178,7 +179,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) + crate::example_db::specs::rule_example_via_bounded_ilp::<_>(source) }, }] } diff --git a/src/rules/quadraticassignment_ilp.rs b/src/rules/quadraticassignment_ilp.rs index f1585eb7d..157bf6937 100644 --- a/src/rules/quadraticassignment_ilp.rs +++ b/src/rules/quadraticassignment_ilp.rs @@ -49,13 +49,16 @@ impl ReductionResult for ReductionQAPToILP { } } -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_facilities * num_locations + num_facilities * (num_facilities - 1) * num_locations^2", num_constraints = "num_facilities + num_locations + 3 * num_facilities * (num_facilities - 1) * num_locations^2", num_nonzeros = "2 * num_facilities * num_locations + 7 * num_facilities * (num_facilities - 1) * num_locations^2", - } -)] + }, + upper_bound { + max_constraint_magnitude_bits = "2", + }, +})] impl ReduceTo> for QuadraticAssignment { type Result = ReductionQAPToILP; diff --git a/src/rules/qubo_ilp.rs b/src/rules/qubo_ilp.rs index 9ed5218dc..fd67288ee 100644 --- a/src/rules/qubo_ilp.rs +++ b/src/rules/qubo_ilp.rs @@ -95,6 +95,7 @@ macro_rules! impl_qubo_to_ilp { ($coefficient:ty) => { #[reduction( transform = exact { + max_constraint_magnitude_bits = "1", num_vars = "num_vars + num_quadratic_terms", num_constraints = "3 * num_quadratic_terms", num_nonzeros = "7 * num_quadratic_terms", diff --git a/src/rules/rectilinearpicturecompression_ilp.rs b/src/rules/rectilinearpicturecompression_ilp.rs index 751aee09e..632fea4a8 100644 --- a/src/rules/rectilinearpicturecompression_ilp.rs +++ b/src/rules/rectilinearpicturecompression_ilp.rs @@ -39,11 +39,14 @@ impl ReductionResult for ReductionRPCToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionRPCToILP {} -#[reduction(transform = upper_bound { - num_vars = "num_rows^2 * num_cols^2", - num_constraints = "num_rows * num_cols + 1", - num_nonzeros = "(num_rows^2 * num_cols^2) * (num_rows * num_cols + 1)", -})] +#[reduction( + transform = upper_bound { + max_constraint_magnitude_bits = "num_rows^2 * num_cols^2 + 1", + num_vars = "num_rows^2 * num_cols^2", + num_constraints = "num_rows * num_cols + 1", + num_nonzeros = "(num_rows^2 * num_cols^2) * (num_rows * num_cols + 1)", + }, +)] impl ReduceTo> for RectilinearPictureCompression { type Result = ReductionRPCToILP; @@ -69,7 +72,11 @@ impl ReduceTo> for RectilinearPictureCompression { // Bound constraint: Σ x_r ≤ bound let bound_terms: Vec<(usize, i64)> = (0..num_vars).map(|i| (i, 1)).collect(); - constraints.push(LinearConstraint::le(bound_terms, self.bound())); + let max_rectangles = Self::exact_i64(num_vars, "encoding the rectangle-count bound")?; + constraints.push(LinearConstraint::le( + bound_terms, + self.bound().clamp(-1, max_rectangles), + )); let target = ILP::new(num_vars, constraints, vec![], ObjectiveSense::Minimize) .map_err(Self::target_construction)?; diff --git a/src/rules/registersufficiency_ilp.rs b/src/rules/registersufficiency_ilp.rs index c5595f662..c126fe801 100644 --- a/src/rules/registersufficiency_ilp.rs +++ b/src/rules/registersufficiency_ilp.rs @@ -1,4 +1,4 @@ -//! Reduction from RegisterSufficiency to `ILP`. +//! Reduction from RegisterSufficiency to `ILP`. //! //! The formulation uses: //! - integer `t_v` variables for evaluation positions @@ -7,22 +7,22 @@ //! - binary threshold/live indicators to count how many values are live after //! each evaluation step -use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{Bounded, IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::RegisterSufficiency; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; #[derive(Debug, Clone)] pub struct ReductionRegisterSufficiencyToILP { - target: ILP, + target: ILP, num_vertices: usize, } impl ReductionResult for ReductionRegisterSufficiencyToILP { type Source = RegisterSufficiency; - type Target = ILP; + type Target = ILP; - fn target_problem(&self) -> &ILP { + fn target_problem(&self) -> &ILP { &self.target } @@ -44,14 +44,17 @@ impl ReductionResult for ReductionRegisterSufficiencyToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionRegisterSufficiencyToILP {} -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "3 * num_vertices^2 + num_vertices * (num_vertices - 1) / 2 + 2 * num_vertices", num_constraints = "9 * num_vertices^2 + 3 * num_vertices * (num_vertices - 1) / 2 + 3 * num_vertices + 2 * num_arcs + num_sinks", num_nonzeros = "18 * num_vertices^2 + 2 * num_vertices + 7 * num_vertices * (num_vertices - 1) / 2 + 4 * num_arcs + num_sinks", }, -)] -impl ReduceTo> for RegisterSufficiency { + upper_bound { + max_constraint_magnitude_bits = "2 * num_vertices + bound + 1", + }, +})] +impl ReduceTo> for RegisterSufficiency { type Result = ReductionRegisterSufficiencyToILP; fn reduce_to(&self) -> Result { @@ -219,7 +222,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) + crate::example_db::specs::rule_example_via_bounded_ilp::<_>(source) }, }] } diff --git a/src/rules/resourceconstrainedscheduling_ilp.rs b/src/rules/resourceconstrainedscheduling_ilp.rs index 37ef1f54f..c4a30fb7f 100644 --- a/src/rules/resourceconstrainedscheduling_ilp.rs +++ b/src/rules/resourceconstrainedscheduling_ilp.rs @@ -52,35 +52,37 @@ impl ReductionResult for ReductionRCSToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionRCSToILP {} -#[reduction(transform = { - exact { - num_vars = "num_tasks * deadline", - num_constraints = "num_tasks + deadline + num_resources * deadline", - }, - upper_bound { - num_nonzeros = "(num_tasks * deadline) * (num_tasks + deadline + num_resources * deadline)", - }, +#[reduction(transform = upper_bound { + max_constraint_magnitude_bits = "max_resource_bits + num_tasks", + num_vars = "num_tasks^2", + num_constraints = "num_tasks * (num_resources + 2)", + num_nonzeros = "num_tasks^3 * (num_resources + 2)", })] impl ReduceTo> for ResourceConstrainedScheduling { type Result = ReductionRCSToILP; fn reduce_to(&self) -> Result { let n = self.num_tasks(); - let d = - usize::try_from(self.deadline()).map_err(|_| { + // Unit tasks can pack occupied slots without gaps; at most n slots + // and n processors are needed, regardless of the numeric deadline. + let d = usize::try_from(self.deadline()) + .map_err(|_| { crate::rules::ReductionError::invalid_target::< ResourceConstrainedScheduling, ILP, >("deadline does not fit the structural usize domain") - })?; + })? + .min(n); let r = self.num_resources(); let resource_requirements = self.resource_requirements(); let resource_bounds = self.resource_bounds(); let num_vars = n * d; let var = |j: usize, t: usize| -> usize { j * d + t }; - let processor_count = - Self::exact_i64(self.num_processors(), "encoding the processor capacity")?; + let processor_count = Self::exact_i64( + self.num_processors().min(n), + "encoding the processor capacity", + )?; let mut constraints = Vec::new(); diff --git a/src/rules/rootedtreestorageassignment_ilp.rs b/src/rules/rootedtreestorageassignment_ilp.rs index dce49eff9..27386196a 100644 --- a/src/rules/rootedtreestorageassignment_ilp.rs +++ b/src/rules/rootedtreestorageassignment_ilp.rs @@ -4,7 +4,7 @@ //! a_{u,v}, transitive-closure helpers h_{u,v,w}, and per-subset gadgets //! (top/bottom selectors, pair selectors, endpoint depths, extension costs). -use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{Bounded, IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::set::RootedTreeStorageAssignment; use crate::reduction; use crate::rules::ilp_helpers::{mccormick_product, one_hot_decode_rows}; @@ -59,15 +59,15 @@ fn total_vars(n: usize, r: usize) -> usize { #[derive(Debug, Clone)] pub struct ReductionRTSAToILP { - target: ILP, + target: ILP, n: usize, } impl ReductionResult for ReductionRTSAToILP { type Source = RootedTreeStorageAssignment; - type Target = ILP; + type Target = ILP; - fn target_problem(&self) -> &ILP { + fn target_problem(&self) -> &ILP { &self.target } @@ -90,24 +90,32 @@ impl ReductionResult for ReductionRTSAToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionRTSAToILP {} -#[reduction(transform = upper_bound { - num_vars = "universe_size * universe_size * universe_size + 2 * universe_size * universe_size + universe_size + num_subsets * (universe_size * universe_size + 2 * universe_size + 3)", - num_constraints = "4 * universe_size^3 + 6 * universe_size^2 + 5 * universe_size + 2 + num_subsets * (2 * universe_size^3 + 5 * universe_size^2 + 8 * universe_size + 8)", - num_nonzeros = "(universe_size * universe_size * universe_size + 2 * universe_size * universe_size + universe_size + num_subsets * (universe_size * universe_size + 2 * universe_size + 3)) * (4 * universe_size^3 + 6 * universe_size^2 + 5 * universe_size + 2 + num_subsets * (2 * universe_size^3 + 5 * universe_size^2 + 8 * universe_size + 8))", -})] -impl ReduceTo> for RootedTreeStorageAssignment { +#[reduction( + transform = upper_bound { + max_constraint_magnitude_bits = "universe_size * (num_subsets + 1) + 2", + num_vars = "universe_size * universe_size * universe_size + 2 * universe_size * universe_size + universe_size + num_subsets * (universe_size * universe_size + 2 * universe_size + 3)", + num_constraints = "4 * universe_size^3 + 6 * universe_size^2 + 5 * universe_size + 2 + num_subsets * (2 * universe_size^3 + 5 * universe_size^2 + 8 * universe_size + 8)", + num_nonzeros = "(universe_size * universe_size * universe_size + 2 * universe_size * universe_size + universe_size + num_subsets * (universe_size * universe_size + 2 * universe_size + 3)) * (4 * universe_size^3 + 6 * universe_size^2 + 5 * universe_size + 2 + num_subsets * (2 * universe_size^3 + 5 * universe_size^2 + 8 * universe_size + 8))", + }, +)] +impl ReduceTo> for RootedTreeStorageAssignment { type Result = ReductionRTSAToILP; fn reduce_to(&self) -> Result { let n = self.universe_size(); let subsets = self.subsets(); - let bound = self.bound(); // Nontrivial subsets (size >= 2) let nontrivial: Vec = (0..subsets.len()) .filter(|&k| subsets[k].len() >= 2) .collect(); let r = nontrivial.len(); + // Each extension cost is between 0 and n-1; negative budgets are infeasible. + // Saturation is safe: the original i64 budget cannot exceed i64::MAX. + let max_cost = Self::exact_i64(r, "encoding the subset count")?.saturating_mul( + Self::exact_i64(n.saturating_sub(1), "encoding the maximum extension cost")?, + ); + let bound = self.bound().clamp(-1, max_cost); if n == 0 { return Ok(ReductionRTSAToILP { @@ -407,7 +415,8 @@ pub(crate) fn canonical_rule_example_specs() -> Vec>::reduce_to(&source).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&source) + .expect("reduction should succeed"); let target_config = { let ilp_solver = crate::solvers::ILPSolver::new(); ilp_solver @@ -415,7 +424,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec>( + crate::example_db::specs::rule_example_with_witness::<_, ILP>( source, SolutionPair { source_config: serde_json::to_value(source_config) diff --git a/src/rules/ruralpostman_ilp.rs b/src/rules/ruralpostman_ilp.rs index ef1fe5355..74225b9c4 100644 --- a/src/rules/ruralpostman_ilp.rs +++ b/src/rules/ruralpostman_ilp.rs @@ -4,7 +4,7 @@ //! connectivity flow constraints to encode an Eulerian connected subgraph //! covering all required edges within the length bound. -use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{Bounded, IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::RuralPostman; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -14,15 +14,15 @@ use crate::types::WeightElement; /// Result of reducing RuralPostman to ILP. #[derive(Debug, Clone)] pub struct ReductionRPToILP { - target: ILP, + target: ILP, num_edges: usize, } impl ReductionResult for ReductionRPToILP { type Source = RuralPostman; - type Target = ILP; + type Target = ILP; - fn target_problem(&self) -> &ILP { + fn target_problem(&self) -> &ILP { &self.target } @@ -45,11 +45,12 @@ impl ReductionResult for ReductionRPToILP { // linking 4m, required r, parity 2m+n, edge activation 4m, vertex activation // 2m+n, flow capacity 4m, conservation 4m+2n, and upper bounds 2m+n. #[reduction(transform = upper_bound { + max_constraint_magnitude_bits = "num_vertices + num_edges + 2", num_vars = "4 * num_edges + 2 * num_vertices", num_constraints = "8 * num_edges + 4 * num_vertices + num_required_edges", num_nonzeros = "22 * num_edges + 5 * num_vertices + num_required_edges", })] -impl ReduceTo> for RuralPostman { +impl ReduceTo> for RuralPostman { type Result = ReductionRPToILP; fn reduce_to(&self) -> Result { @@ -239,7 +240,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) + crate::example_db::specs::rule_example_via_bounded_ilp::<_>(source) }, }] } diff --git a/src/rules/sat_circuitsat.rs b/src/rules/sat_circuitsat.rs index 3f79a7a1e..9675dd5a0 100644 --- a/src/rules/sat_circuitsat.rs +++ b/src/rules/sat_circuitsat.rs @@ -49,16 +49,12 @@ impl ReductionResult for ReductionSATToCircuit { #[crate::aggregate_reduction(identity)] impl crate::rules::AggregateReductionResult for ReductionSATToCircuit {} -#[reduction( - transform = upper_bound { - num_variables = "2 * num_vars + num_clauses + 1", - num_assignments = "num_vars + num_clauses + 2", - }, - unavailable = { - num_assignment_outputs = "the exact target parameter is not represented by this reduction's symbolic transform", - num_expression_nodes = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] +#[reduction(transform = upper_bound { + num_variables = "2 * num_vars + num_clauses + 1", + num_assignments = "num_vars + num_clauses + 2", + num_assignment_outputs = "num_vars + num_clauses + 2", + num_expression_nodes = "num_vars + 2 * num_literals + 2 * num_clauses + 2", +})] impl ReduceTo for Satisfiability { type Result = ReductionSATToCircuit; diff --git a/src/rules/sat_ksat.rs b/src/rules/sat_ksat.rs index b7aef7264..fd5607f92 100644 --- a/src/rules/sat_ksat.rs +++ b/src/rules/sat_ksat.rs @@ -129,15 +129,11 @@ fn add_clause_to_ksat( macro_rules! impl_sat_to_ksat { ($ktype:ty, $k:expr) => { #[rustfmt::skip] - #[reduction( - transform = upper_bound { - num_clauses = "8 * num_clauses + num_literals", - num_vars = "num_vars + 7 * num_clauses + num_literals", - }, - unavailable = { - num_literals = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + #[reduction(transform = upper_bound { + num_clauses = "8 * num_clauses + num_literals", + num_vars = "num_vars + 7 * num_clauses + num_literals", + num_literals = "3 * (8 * num_clauses + num_literals)", + })] impl ReduceTo> for Satisfiability { type Result = ReductionSATToKSAT<$ktype>; diff --git a/src/rules/satisfiability_integralflowhomologousarcs.rs b/src/rules/satisfiability_integralflowhomologousarcs.rs index 17b963efc..042b3bed1 100644 --- a/src/rules/satisfiability_integralflowhomologousarcs.rs +++ b/src/rules/satisfiability_integralflowhomologousarcs.rs @@ -125,15 +125,11 @@ impl ReductionResult for ReductionSATToIntegralFlowHomologousArcs { #[crate::aggregate_reduction(identity)] impl crate::rules::AggregateReductionResult for ReductionSATToIntegralFlowHomologousArcs {} -#[reduction( - transform = upper_bound { - num_vertices = "2 * num_vars * num_clauses + 3 * num_vars + 2 * num_clauses + 2", - num_arcs = "2 * num_vars * num_clauses + 5 * num_vars + num_clauses + num_literals", - }, - unavailable = { - max_capacity = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] +#[reduction(transform = upper_bound { + num_vertices = "2 * num_vars * num_clauses + 3 * num_vars + 2 * num_clauses + 2", + num_arcs = "2 * num_vars * num_clauses + 5 * num_vars + num_clauses + num_literals", + max_capacity = "num_literals + 1", +})] impl ReduceTo for Satisfiability { type Result = ReductionSATToIntegralFlowHomologousArcs; diff --git a/src/rules/satisfiability_naesatisfiability.rs b/src/rules/satisfiability_naesatisfiability.rs index e0efdf532..a02a28321 100644 --- a/src/rules/satisfiability_naesatisfiability.rs +++ b/src/rules/satisfiability_naesatisfiability.rs @@ -52,16 +52,16 @@ impl ReductionResult for ReductionSATToNAESAT { #[crate::aggregate_reduction(identity)] impl crate::rules::AggregateReductionResult for ReductionSATToNAESAT {} -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_vars + 1", num_clauses = "num_clauses", - num_literals = "num_literals + num_clauses", }, - unavailable = { - num_literal_pairs = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] + upper_bound { + num_literals = "num_literals + 2 * num_clauses", + num_literal_pairs = "(num_literals + 2 * num_clauses)^2", + }, +})] impl ReduceTo for Satisfiability { type Result = ReductionSATToNAESAT; diff --git a/src/rules/schedulingtominimizeweightedcompletiontime_ilp.rs b/src/rules/schedulingtominimizeweightedcompletiontime_ilp.rs index 9b5aaf957..ab3a5e2b7 100644 --- a/src/rules/schedulingtominimizeweightedcompletiontime_ilp.rs +++ b/src/rules/schedulingtominimizeweightedcompletiontime_ilp.rs @@ -5,7 +5,7 @@ //! ordering variables `y_{i,j}` for each task pair. Big-M constraints //! enforce that tasks sharing a processor do not overlap. -use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{Bounded, IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::SchedulingToMinimizeWeightedCompletionTime; use crate::reduction; use crate::rules::ilp_helpers::one_hot_decode_rows; @@ -22,7 +22,7 @@ use crate::rules::traits::{ReduceTo, ReductionResult}; /// Total variables: n*m + n + n*(n-1)/2 #[derive(Debug, Clone)] pub struct ReductionSMWCTToILP { - target: ILP, + target: ILP, num_tasks: usize, num_processors: usize, } @@ -45,9 +45,9 @@ impl ReductionSMWCTToILP { impl ReductionResult for ReductionSMWCTToILP { type Source = SchedulingToMinimizeWeightedCompletionTime; - type Target = ILP; + type Target = ILP; - fn target_problem(&self) -> &ILP { + fn target_problem(&self) -> &ILP { &self.target } @@ -68,10 +68,11 @@ impl ReductionResult for ReductionSMWCTToILP { num_constraints = "num_tasks + num_tasks * num_processors + 2 * num_tasks + 2 * num_tasks * (num_tasks - 1) / 2 * num_processors + num_tasks * (num_tasks - 1) / 2", }, upper_bound { + max_constraint_magnitude_bits = "max_processing_time_bits + num_tasks + 2", num_nonzeros = "(num_tasks * num_processors + num_tasks + num_tasks * (num_tasks - 1) / 2) * (num_tasks + num_tasks * num_processors + 2 * num_tasks + 2 * num_tasks * (num_tasks - 1) / 2 * num_processors + num_tasks * (num_tasks - 1) / 2)", }, })] -impl ReduceTo> for SchedulingToMinimizeWeightedCompletionTime { +impl ReduceTo> for SchedulingToMinimizeWeightedCompletionTime { type Result = ReductionSMWCTToILP; fn reduce_to(&self) -> Result { @@ -85,7 +86,7 @@ impl ReduceTo> for SchedulingToMinimizeWeightedCompletionTime { .ok_or_else(|| { crate::rules::ReductionError::integer_overflow::< SchedulingToMinimizeWeightedCompletionTime, - ILP, + ILP, >("summing task processing times") })?; let lengths = self.lengths(); @@ -94,13 +95,13 @@ impl ReduceTo> for SchedulingToMinimizeWeightedCompletionTime { let two_big_m = big_m.checked_mul(2).ok_or_else(|| { crate::rules::ReductionError::integer_overflow::< SchedulingToMinimizeWeightedCompletionTime, - ILP, + ILP, >("doubling the disjunctive scheduling bound") })?; let three_big_m = big_m.checked_mul(3).ok_or_else(|| { crate::rules::ReductionError::integer_overflow::< SchedulingToMinimizeWeightedCompletionTime, - ILP, + ILP, >("tripling the disjunctive scheduling bound") })?; @@ -218,7 +219,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) + crate::example_db::specs::rule_example_via_bounded_ilp::<_>(source) }, }] } diff --git a/src/rules/schedulingwithindividualdeadlines_ilp.rs b/src/rules/schedulingwithindividualdeadlines_ilp.rs index 034bc4c89..93df3a70c 100644 --- a/src/rules/schedulingwithindividualdeadlines_ilp.rs +++ b/src/rules/schedulingwithindividualdeadlines_ilp.rs @@ -63,6 +63,7 @@ impl crate::rules::AggregateReductionResult for ReductionSWIDToILP {} num_constraints = "num_tasks + max_deadline + num_precedences + 1", }, upper_bound { + max_constraint_magnitude_bits = "num_tasks + max_deadline + 1", num_nonzeros = "(num_tasks * max_deadline) * (num_tasks + max_deadline + num_precedences + 1)", }, })] @@ -80,8 +81,10 @@ impl ReduceTo> for SchedulingWithIndividualDeadlines { let num_vars = n * max_d; let var = |j: usize, t: usize| j * max_d + t; - let processor_count = - Self::exact_i64(self.num_processors(), "encoding the processor capacity")?; + let processor_count = Self::exact_i64( + self.num_processors().min(n), + "encoding the processor capacity", + )?; let mut constraints = Vec::new(); diff --git a/src/rules/sequencingtominimizemaximumcumulativecost_ilp.rs b/src/rules/sequencingtominimizemaximumcumulativecost_ilp.rs index 2e5bc5947..ee560f5dc 100644 --- a/src/rules/sequencingtominimizemaximumcumulativecost_ilp.rs +++ b/src/rules/sequencingtominimizemaximumcumulativecost_ilp.rs @@ -1,16 +1,16 @@ -//! Reduction from SequencingToMinimizeMaximumCumulativeCost to `ILP`. +//! Reduction from SequencingToMinimizeMaximumCumulativeCost to `ILP`. //! //! Position-assignment ILP: binary x_{j,p} placing task j in position p. //! Permutation constraints, precedence constraints, and prefix cumulative-cost //! bounds at every position. -use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{Bounded, IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::SequencingToMinimizeMaximumCumulativeCost; use crate::reduction; use crate::rules::ilp_helpers::one_hot_decode; use crate::rules::traits::{ReduceTo, ReductionResult}; -/// Result of reducing SequencingToMinimizeMaximumCumulativeCost to `ILP`. +/// Result of reducing SequencingToMinimizeMaximumCumulativeCost to `ILP`. /// /// Variable layout: /// - x_{j,p} for j in 0..n, p in 0..n: index `j*n + p` @@ -18,15 +18,15 @@ use crate::rules::traits::{ReduceTo, ReductionResult}; /// Total: n^2 variables. #[derive(Debug, Clone)] pub struct ReductionSTMMCCToILP { - target: ILP, + target: ILP, num_tasks: usize, } impl ReductionResult for ReductionSTMMCCToILP { type Source = SequencingToMinimizeMaximumCumulativeCost; - type Target = ILP; + type Target = ILP; - fn target_problem(&self) -> &ILP { + fn target_problem(&self) -> &ILP { &self.target } @@ -51,10 +51,11 @@ impl ReductionResult for ReductionSTMMCCToILP { num_constraints = "num_tasks^2 + 3 * num_tasks + num_precedences + 1", }, upper_bound { + max_constraint_magnitude_bits = "max_cost_bits + num_tasks", num_nonzeros = "(num_tasks^2 + 1) * (num_tasks^2 + 3 * num_tasks + num_precedences + 1)", }, })] -impl ReduceTo> for SequencingToMinimizeMaximumCumulativeCost { +impl ReduceTo> for SequencingToMinimizeMaximumCumulativeCost { type Result = ReductionSTMMCCToILP; fn reduce_to(&self) -> Result { @@ -90,7 +91,7 @@ impl ReduceTo> for SequencingToMinimizeMaximumCumulativeCost { constraints.push(LinearConstraint::ge(terms, 1)); } - // Binary bounds for x variables (`ILP` allows any non-negative integer) + // Binary bounds for x variables (`ILP` allows any non-negative integer) for j in 0..n { for p in 0..n { constraints.push(LinearConstraint::le(vec![(x_var(j, p), 1)], 1)); @@ -116,13 +117,13 @@ impl ReduceTo> for SequencingToMinimizeMaximumCumulativeCost { let magnitude = cost.checked_abs().ok_or_else(|| { crate::rules::ReductionError::integer_overflow::< SequencingToMinimizeMaximumCumulativeCost, - ILP, + ILP, >("taking the absolute value of a task cost") })?; total.checked_add(magnitude).ok_or_else(|| { crate::rules::ReductionError::integer_overflow::< SequencingToMinimizeMaximumCumulativeCost, - ILP, + ILP, >("summing absolute task costs") }) })?; @@ -155,7 +156,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) + crate::example_db::specs::rule_example_via_bounded_ilp::<_>(source) }, }] } diff --git a/src/rules/sequencingtominimizetardytaskweight_ilp.rs b/src/rules/sequencingtominimizetardytaskweight_ilp.rs index b2f3c8a0a..e1f05f4bd 100644 --- a/src/rules/sequencingtominimizetardytaskweight_ilp.rs +++ b/src/rules/sequencingtominimizetardytaskweight_ilp.rs @@ -52,6 +52,7 @@ impl ReductionResult for ReductionSTMTTWToILP { num_constraints = "2 * num_tasks + 2 * num_tasks * num_tasks", }, upper_bound { + max_constraint_magnitude_bits = "max_processing_time_bits + num_tasks + 2", num_nonzeros = "(num_tasks * num_tasks + num_tasks) * (2 * num_tasks + 2 * num_tasks * num_tasks)", }, })] diff --git a/src/rules/sequencingtominimizeweightedcompletiontime_ilp.rs b/src/rules/sequencingtominimizeweightedcompletiontime_ilp.rs index d0e1f4a4e..2df4c39af 100644 --- a/src/rules/sequencingtominimizeweightedcompletiontime_ilp.rs +++ b/src/rules/sequencingtominimizeweightedcompletiontime_ilp.rs @@ -1,18 +1,18 @@ //! Reduction from SequencingToMinimizeWeightedCompletionTime to ILP. //! //! The reduction uses integer completion-time variables `C_j` and integer -//! order variables `y_{i,j}` constrained to `{0, 1}` within `ILP`. +//! order variables `y_{i,j}` constrained to `{0, 1}` within `ILP`. //! For each unordered pair `{i, j}`, a pair of big-M constraints forces one //! task to finish before the other starts. -use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{Bounded, IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::SequencingToMinimizeWeightedCompletionTime; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; #[derive(Debug, Clone)] pub struct ReductionSTMWCTToILP { - target: ILP, + target: ILP, num_tasks: usize, } @@ -31,9 +31,9 @@ impl ReductionSTMWCTToILP { impl ReductionResult for ReductionSTMWCTToILP { type Source = SequencingToMinimizeWeightedCompletionTime; - type Target = ILP; + type Target = ILP; - fn target_problem(&self) -> &ILP { + fn target_problem(&self) -> &ILP { &self.target } @@ -57,10 +57,11 @@ impl ReductionResult for ReductionSTMWCTToILP { num_constraints = "2 * num_tasks + 3 * num_tasks * (num_tasks - 1) / 2 + num_precedences", }, upper_bound { + max_constraint_magnitude_bits = "max_processing_time_bits + num_tasks", num_nonzeros = "(num_tasks + num_tasks * (num_tasks - 1) / 2) * (2 * num_tasks + 3 * num_tasks * (num_tasks - 1) / 2 + num_precedences)", }, })] -impl ReduceTo> for SequencingToMinimizeWeightedCompletionTime { +impl ReduceTo> for SequencingToMinimizeWeightedCompletionTime { type Result = ReductionSTMWCTToILP; fn reduce_to(&self) -> Result { @@ -70,7 +71,7 @@ impl ReduceTo> for SequencingToMinimizeWeightedCompletionTime { total.checked_add(length).ok_or_else(|| { crate::rules::ReductionError::integer_overflow::< SequencingToMinimizeWeightedCompletionTime, - ILP, + ILP, >("summing task processing times") }) })?; @@ -158,7 +159,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) + crate::example_db::specs::rule_example_via_bounded_ilp::<_>(source) }, }] } diff --git a/src/rules/sequencingtominimizeweightedtardiness_ilp.rs b/src/rules/sequencingtominimizeweightedtardiness_ilp.rs index de1d41b38..62471feb0 100644 --- a/src/rules/sequencingtominimizeweightedtardiness_ilp.rs +++ b/src/rules/sequencingtominimizeweightedtardiness_ilp.rs @@ -1,15 +1,15 @@ -//! Reduction from SequencingToMinimizeWeightedTardiness to `ILP`. +//! Reduction from SequencingToMinimizeWeightedTardiness to `ILP`. //! //! Pairwise order variables y_{i,j}, integer completion times C_j, //! and nonnegative tardiness variables T_j. Big-M disjunctive constraints //! force a single-machine order; the weighted tardiness sum is bounded by K. -use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{Bounded, IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::SequencingToMinimizeWeightedTardiness; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; -/// Result of reducing SequencingToMinimizeWeightedTardiness to `ILP`. +/// Result of reducing SequencingToMinimizeWeightedTardiness to `ILP`. /// /// Variable layout: /// - `y_{i,j}` for i < j: pairwise order bits (n*(n-1)/2 vars) @@ -19,16 +19,16 @@ use crate::rules::traits::{ReduceTo, ReductionResult}; /// Total: n*(n-1)/2 + 2*n variables. #[derive(Debug, Clone)] pub struct ReductionSTMWTToILP { - target: ILP, + target: ILP, num_tasks: usize, num_order_vars: usize, } impl ReductionResult for ReductionSTMWTToILP { type Source = SequencingToMinimizeWeightedTardiness; - type Target = ILP; + type Target = ILP; - fn target_problem(&self) -> &ILP { + fn target_problem(&self) -> &ILP { &self.target } @@ -57,12 +57,15 @@ impl ReductionResult for ReductionSTMWTToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionSTMWTToILP {} -#[reduction(transform = upper_bound { - num_vars = "num_tasks^2 + 2 * num_tasks", - num_constraints = "2 * num_tasks^2 + 3 * num_tasks + 1", - num_nonzeros = "(num_tasks^2 + 2 * num_tasks) * (2 * num_tasks^2 + 3 * num_tasks + 1)", -})] -impl ReduceTo> for SequencingToMinimizeWeightedTardiness { +#[reduction( + transform = upper_bound { + max_constraint_magnitude_bits = "max_numeric_magnitude_bits + num_tasks", + num_vars = "num_tasks^2 + 2 * num_tasks", + num_constraints = "2 * num_tasks^2 + 3 * num_tasks + 1", + num_nonzeros = "(num_tasks^2 + 2 * num_tasks) * (2 * num_tasks^2 + 3 * num_tasks + 1)", + }, +)] +impl ReduceTo> for SequencingToMinimizeWeightedTardiness { type Result = ReductionSTMWTToILP; fn reduce_to(&self) -> Result { @@ -89,7 +92,7 @@ impl ReduceTo> for SequencingToMinimizeWeightedTardiness { .ok_or_else(|| { crate::rules::ReductionError::integer_overflow::< SequencingToMinimizeWeightedTardiness, - ILP, + ILP, >("summing task processing times") })?; let big_m = horizon; @@ -177,7 +180,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) + crate::example_db::specs::rule_example_via_bounded_ilp::<_>(source) }, }] } diff --git a/src/rules/sequencingwithdeadlinesandsetuptimes_ilp.rs b/src/rules/sequencingwithdeadlinesandsetuptimes_ilp.rs index 2829801bf..3168f0f40 100644 --- a/src/rules/sequencingwithdeadlinesandsetuptimes_ilp.rs +++ b/src/rules/sequencingwithdeadlinesandsetuptimes_ilp.rs @@ -58,11 +58,14 @@ impl ReductionResult for ReductionSWDSTToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionSWDSTToILP {} -#[reduction(transform = upper_bound { - num_vars = "2 * num_tasks^2 + num_tasks", - num_constraints = "2 * num_tasks + num_tasks^2 * (num_tasks - 1) + 3 * num_tasks * (num_tasks - 1) + num_tasks * num_tasks", - num_nonzeros = "(2 * num_tasks^2 + num_tasks) * (2 * num_tasks + num_tasks^2 * (num_tasks - 1) + 3 * num_tasks * (num_tasks - 1) + num_tasks * num_tasks)", -})] +#[reduction( + transform = upper_bound { + max_constraint_magnitude_bits = "max_time_bits + num_tasks + 2", + num_vars = "2 * num_tasks^2 + num_tasks", + num_constraints = "2 * num_tasks + num_tasks^2 * (num_tasks - 1) + 3 * num_tasks * (num_tasks - 1) + num_tasks * num_tasks", + num_nonzeros = "(2 * num_tasks^2 + num_tasks) * (2 * num_tasks + num_tasks^2 * (num_tasks - 1) + 3 * num_tasks * (num_tasks - 1) + num_tasks * num_tasks)", + }, +)] impl ReduceTo> for SequencingWithDeadlinesAndSetUpTimes { type Result = ReductionSWDSTToILP; diff --git a/src/rules/sequencingwithinintervals_ilp.rs b/src/rules/sequencingwithinintervals_ilp.rs index 4182c20a7..1cb662236 100644 --- a/src/rules/sequencingwithinintervals_ilp.rs +++ b/src/rules/sequencingwithinintervals_ilp.rs @@ -77,6 +77,7 @@ impl ReductionResult for ReductionSWIToILP { impl crate::rules::AggregateReductionResult for ReductionSWIToILP {} #[reduction(transform = upper_bound { + max_constraint_magnitude_bits = "1", num_vars = "num_start_slots", num_constraints = "num_start_slots^2 + num_tasks", num_nonzeros = "num_start_slots * (num_start_slots^2 + num_tasks)", diff --git a/src/rules/sequencingwithreleasetimesanddeadlines_ilp.rs b/src/rules/sequencingwithreleasetimesanddeadlines_ilp.rs index 6d001eca3..48b960215 100644 --- a/src/rules/sequencingwithreleasetimesanddeadlines_ilp.rs +++ b/src/rules/sequencingwithreleasetimesanddeadlines_ilp.rs @@ -59,6 +59,7 @@ impl ReductionResult for ReductionSWRTDToILP { impl crate::rules::AggregateReductionResult for ReductionSWRTDToILP {} #[reduction(transform = upper_bound { + max_constraint_magnitude_bits = "1", num_vars = "num_tasks * time_horizon", num_constraints = "num_tasks * time_horizon + num_tasks + time_horizon", num_nonzeros = "(num_tasks * time_horizon) * (num_tasks * time_horizon + num_tasks + time_horizon)", diff --git a/src/rules/setsplitting_betweenness.rs b/src/rules/setsplitting_betweenness.rs index 547982fa4..afb1eeb63 100644 --- a/src/rules/setsplitting_betweenness.rs +++ b/src/rules/setsplitting_betweenness.rs @@ -51,12 +51,10 @@ impl ReductionResult for ReductionSetSplittingToBetweenness { #[crate::aggregate_reduction(identity)] impl crate::rules::AggregateReductionResult for ReductionSetSplittingToBetweenness {} -#[reduction( - transform = unavailable { - num_elements = "the exact target parameters depend on normalization statistics specific to this reduction", - num_triples = "the exact target parameters depend on normalization statistics specific to this reduction", - } -)] +#[reduction(transform = upper_bound { + num_elements = "universe_size + 1 + num_subsets * (4 * universe_size + 1)", + num_triples = "2 * num_subsets * (2 * universe_size + 1)", +})] impl ReduceTo for SetSplitting { type Result = ReductionSetSplittingToBetweenness; diff --git a/src/rules/setsplitting_ilp.rs b/src/rules/setsplitting_ilp.rs index ef0667f47..e7fc89213 100644 --- a/src/rules/setsplitting_ilp.rs +++ b/src/rules/setsplitting_ilp.rs @@ -52,6 +52,7 @@ impl crate::rules::AggregateReductionResult for ReductionSetSplittingToILP {} num_constraints = "2 * num_subsets", }, upper_bound { + max_constraint_magnitude_bits = "universe_size + 1", num_nonzeros = "universe_size * (2 * num_subsets)", }, })] @@ -63,6 +64,9 @@ impl ReduceTo> for SetSplitting { let mut constraints = Vec::new(); for subset in self.subsets() { + let mut subset = subset.clone(); + subset.sort_unstable(); + subset.dedup(); let terms: Vec<(usize, i64)> = subset.iter().map(|&e| (e, 1)).collect(); let k = >>::exact_i64( subset.len() - 1, @@ -72,7 +76,7 @@ impl ReduceTo> for SetSplitting { // At least one element in S2: sum >= 1 constraints.push(LinearConstraint::ge(terms.clone(), 1)); - // At least one element in S1: sum <= k - 1 + // At least one element in S1: sum <= |S| - 1 constraints.push(LinearConstraint::le(terms, k)); } diff --git a/src/rules/shortestcommonsupersequence_ilp.rs b/src/rules/shortestcommonsupersequence_ilp.rs index 784371a04..453e8e634 100644 --- a/src/rules/shortestcommonsupersequence_ilp.rs +++ b/src/rules/shortestcommonsupersequence_ilp.rs @@ -46,6 +46,7 @@ impl ReductionResult for ReductionSCSToILP { } #[reduction(transform = upper_bound { + max_constraint_magnitude_bits = "max_length + 1", num_vars = "max_length * (alphabet_size + 1) + total_length * max_length", num_constraints = "max_length + total_length + total_length * max_length + total_length + max_length", num_nonzeros = "(max_length * (alphabet_size + 1) + total_length * max_length) * (max_length + total_length + total_length * max_length + total_length + max_length)", diff --git a/src/rules/shortestweightconstrainedpath_ilp.rs b/src/rules/shortestweightconstrainedpath_ilp.rs index c94aab980..ee645c67d 100644 --- a/src/rules/shortestweightconstrainedpath_ilp.rs +++ b/src/rules/shortestweightconstrainedpath_ilp.rs @@ -6,7 +6,7 @@ //! bound constraint enforces the weight limit, and the objective minimizes //! total path length. -use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{Bounded, IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::ShortestWeightConstrainedPath; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -15,14 +15,14 @@ use crate::types::WeightElement; /// Result of reducing ShortestWeightConstrainedPath to ILP. /// -/// Variable layout (within `ILP`): +/// Variable layout (within `ILP`): /// - Arc variables: `a_{e,0}` and `a_{e,1}` for each undirected edge `e` /// (indices `0..2m`), bounded to {0, 1} /// - Order variables: `o_v` for each vertex `v` (indices `2m..2m+n`), /// bounded to `[0, n-1]` #[derive(Debug, Clone)] pub struct ReductionSWCPToILP { - target: ILP, + target: ILP, num_edges: usize, } @@ -34,9 +34,9 @@ impl ReductionSWCPToILP { impl ReductionResult for ReductionSWCPToILP { type Source = ShortestWeightConstrainedPath; - type Target = ILP; + type Target = ILP; - fn target_problem(&self) -> &ILP { + fn target_problem(&self) -> &ILP { &self.target } @@ -63,10 +63,11 @@ impl ReductionResult for ReductionSWCPToILP { num_constraints = "5 * num_edges + 4 * num_vertices + 2", }, upper_bound { + max_constraint_magnitude_bits = "max_weight_bits + num_vertices", num_nonzeros = "(2 * num_edges + num_vertices) * (5 * num_edges + 4 * num_vertices + 2)", }, })] -impl ReduceTo> for ShortestWeightConstrainedPath { +impl ReduceTo> for ShortestWeightConstrainedPath { type Result = ReductionSWCPToILP; fn reduce_to(&self) -> Result { @@ -96,7 +97,7 @@ impl ReduceTo> for ShortestWeightConstrainedPath { let mut constraints = Vec::new(); - // --- Arc variables are binary within `ILP`: 0 <= a_{e,d} <= 1 --- + // --- Arc variables are binary within `ILP`: 0 <= a_{e,d} <= 1 --- for edge_idx in 0..num_edges { constraints.push(LinearConstraint::le( vec![(ReductionSWCPToILP::arc_var(edge_idx, 0), 1)], @@ -246,7 +247,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) + crate::example_db::specs::rule_example_via_bounded_ilp::<_>(source) }, }] } diff --git a/src/rules/sparsematrixcompression_ilp.rs b/src/rules/sparsematrixcompression_ilp.rs index 6b493c63b..bb5efda1d 100644 --- a/src/rules/sparsematrixcompression_ilp.rs +++ b/src/rules/sparsematrixcompression_ilp.rs @@ -46,6 +46,7 @@ impl ReductionResult for ReductionSMCToILP { impl crate::rules::AggregateReductionResult for ReductionSMCToILP {} #[reduction(transform = upper_bound { + max_constraint_magnitude_bits = "1", num_vars = "num_rows * bound_k", num_constraints = "num_rows + num_rows^2 * num_cols^2 * bound_k", num_nonzeros = "(num_rows * bound_k) * (num_rows + num_rows^2 * num_cols^2 * bound_k)", diff --git a/src/rules/stackercrane_ilp.rs b/src/rules/stackercrane_ilp.rs index d4e15609a..1795e54ad 100644 --- a/src/rules/stackercrane_ilp.rs +++ b/src/rules/stackercrane_ilp.rs @@ -45,6 +45,7 @@ impl ReductionResult for ReductionSCToILP { } #[reduction(transform = upper_bound { + max_constraint_magnitude_bits = "2", num_vars = "num_arcs * num_arcs + num_arcs * num_arcs * num_arcs", num_constraints = "num_arcs + num_arcs + 4 * num_arcs * num_arcs * num_arcs", num_nonzeros = "(num_arcs * num_arcs + num_arcs * num_arcs * num_arcs) * (num_arcs + num_arcs + 4 * num_arcs * num_arcs * num_arcs)", diff --git a/src/rules/steinertree_ilp.rs b/src/rules/steinertree_ilp.rs index 2dbc30918..e3e9a276c 100644 --- a/src/rules/steinertree_ilp.rs +++ b/src/rules/steinertree_ilp.rs @@ -49,6 +49,7 @@ impl ReductionResult for ReductionSteinerTreeToILP { num_constraints = "num_vertices * (num_vertices - 1) + 2 * num_edges * num_vertices + num_terminals + 1", }, upper_bound { + max_constraint_magnitude_bits = "1", num_nonzeros = "(num_edges + num_vertices + 2 * num_edges * (num_vertices - 1)) * (num_vertices * (num_vertices - 1) + 2 * num_edges * num_vertices + num_terminals + 1)", }, })] diff --git a/src/rules/stringtostringcorrection_ilp.rs b/src/rules/stringtostringcorrection_ilp.rs index 1f99ce66c..f8b674bcf 100644 --- a/src/rules/stringtostringcorrection_ilp.rs +++ b/src/rules/stringtostringcorrection_ilp.rs @@ -116,6 +116,7 @@ impl ReductionResult for ReductionSTSCToILP { impl crate::rules::AggregateReductionResult for ReductionSTSCToILP {} #[reduction(transform = upper_bound { + max_constraint_magnitude_bits = "1", num_vars = "(bound + 1) * source_length^2 + (bound + 1) * source_length + 2 * bound * source_length + bound", num_constraints = "4 * bound * source_length^3 + 2 * bound * source_length^2 + source_length^2 + 6 * bound * source_length + 5 * source_length + bound + 1", num_nonzeros = "((bound + 1) * source_length^2 + (bound + 1) * source_length + 2 * bound * source_length + bound) * (4 * bound * source_length^3 + 2 * bound * source_length^2 + source_length^2 + 6 * bound * source_length + 5 * source_length + bound + 1)", diff --git a/src/rules/strongconnectivityaugmentation_ilp.rs b/src/rules/strongconnectivityaugmentation_ilp.rs index 5fddf756f..ecbfed4a9 100644 --- a/src/rules/strongconnectivityaugmentation_ilp.rs +++ b/src/rules/strongconnectivityaugmentation_ilp.rs @@ -1,4 +1,4 @@ -//! Reduction from StrongConnectivityAugmentation to `ILP`. +//! Reduction from StrongConnectivityAugmentation to `ILP`. //! //! Select candidate arcs under the budget and certify strong connectivity by //! sending flow both from a root to every vertex and back again. @@ -11,15 +11,15 @@ use crate::rules::traits::{ReduceTo, ReductionResult}; #[derive(Debug, Clone)] pub struct ReductionSCAToILP { - target: ILP, + target: ILP, num_candidates: usize, } impl ReductionResult for ReductionSCAToILP { type Source = StrongConnectivityAugmentation; - type Target = ILP; + type Target = ILP; - fn target_problem(&self) -> &ILP { + fn target_problem(&self) -> &ILP { &self.target } @@ -44,12 +44,17 @@ impl ReductionResult for ReductionSCAToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionSCAToILP {} -#[reduction(transform = upper_bound { - num_vars = "num_potential_arcs + 2 * num_vertices * (num_arcs + num_potential_arcs)", - num_constraints = "1 + num_potential_arcs + 2 * num_arcs + 2 * num_vertices * num_potential_arcs + 2 * num_vertices * num_vertices", - num_nonzeros = "(num_potential_arcs + 2 * num_vertices * (num_arcs + num_potential_arcs)) * (1 + num_potential_arcs + 2 * num_arcs + 2 * num_vertices * num_potential_arcs + 2 * num_vertices * num_vertices)", +#[reduction(transform = { + exact { + max_constraint_magnitude_bits = "max_numeric_magnitude_bits", + }, + upper_bound { + num_vars = "num_potential_arcs + 2 * num_vertices * (num_arcs + num_potential_arcs)", + num_constraints = "1 + num_potential_arcs + 2 * num_arcs + 2 * num_vertices * num_potential_arcs + 2 * num_vertices * num_vertices", + num_nonzeros = "(num_potential_arcs + 2 * num_vertices * (num_arcs + num_potential_arcs)) * (1 + num_potential_arcs + 2 * num_arcs + 2 * num_vertices * num_potential_arcs + 2 * num_vertices * num_vertices)", + }, })] -impl ReduceTo> for StrongConnectivityAugmentation { +impl ReduceTo> for StrongConnectivityAugmentation { type Result = ReductionSCAToILP; fn reduce_to(&self) -> Result { @@ -203,13 +208,13 @@ pub(crate) fn canonical_rule_example_specs() -> Vec>::reduce_to(&source) + crate::rules::ReduceTo::>::reduce_to(&source) .expect("reduction should succeed"); let ilp_sol = crate::solvers::ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be solvable"); let extracted = reduction.extract_solution(&ilp_sol).unwrap(); - crate::example_db::specs::rule_example_with_witness::<_, ILP>( + crate::example_db::specs::rule_example_with_witness::<_, ILP>( source, SolutionPair { source_config: serde_json::json!(extracted), diff --git a/src/rules/subgraphisomorphism_ilp.rs b/src/rules/subgraphisomorphism_ilp.rs index 55a07a90b..83873c85c 100644 --- a/src/rules/subgraphisomorphism_ilp.rs +++ b/src/rules/subgraphisomorphism_ilp.rs @@ -58,6 +58,7 @@ impl ReductionResult for ReductionSubIsoToILP { impl crate::rules::AggregateReductionResult for ReductionSubIsoToILP {} #[reduction(transform = upper_bound { + max_constraint_magnitude_bits = "2", num_vars = "num_pattern_vertices * num_host_vertices", num_constraints = "num_pattern_vertices + num_host_vertices + num_pattern_edges * num_host_vertices^2", num_nonzeros = "(num_pattern_vertices * num_host_vertices) * (num_pattern_vertices + num_host_vertices + num_pattern_edges * num_host_vertices^2)", diff --git a/src/rules/subsetsum_closestvectorproblem.rs b/src/rules/subsetsum_closestvectorproblem.rs index 8dbf54908..6a0142f0d 100644 --- a/src/rules/subsetsum_closestvectorproblem.rs +++ b/src/rules/subsetsum_closestvectorproblem.rs @@ -67,23 +67,21 @@ impl ReductionSubsetSumToClosestVectorProblem { } } -#[reduction( - transform = unavailable { - ambient_dimension = "2n+b depends on input bit length b, which is not a registered SubsetSum parameter", - num_basis_vectors = "n+b-1 depends on input bit length b, which is not a registered SubsetSum parameter", +#[reduction(transform = { + exact { + ambient_dimension = "2 * num_elements + max_numeric_magnitude_bits", + num_basis_vectors = "num_elements + max_numeric_magnitude_bits - 1", }, -)] + upper_bound { + max_numeric_magnitude_bits = "2", + } +})] impl ReduceTo> for SubsetSum { type Result = ReductionSubsetSumToClosestVectorProblem; fn reduce_to(&self) -> Result { let n = self.num_elements(); - let bit_width = self - .sizes() - .iter() - .fold(self.target().bits().max(1), |bits, size| { - bits.max(size.bits()) - }); + let bit_width = self.max_numeric_magnitude_bits(); let (bits, rows, columns) = ReductionSubsetSumToClosestVectorProblem::dimensions(n, bit_width)?; let mut basis = Vec::with_capacity(columns); diff --git a/src/rules/subsetsum_integerknapsack.rs b/src/rules/subsetsum_integerknapsack.rs index 26c93814b..ca262844e 100644 --- a/src/rules/subsetsum_integerknapsack.rs +++ b/src/rules/subsetsum_integerknapsack.rs @@ -32,10 +32,13 @@ inventory::submit! { source_variant_fn: ::variant, target_variant_fn: ::variant, parameter_declarations_fn: || ReductionParameterDeclarations { - fields: vec![("num_items", crate::parameters::ParameterRelation::Exact, Expr::variable("num_elements"))], + fields: vec![ + ("num_items", crate::parameters::ParameterRelation::Exact, Expr::variable("num_elements")), + ("capacity_bits", crate::parameters::ParameterRelation::UpperBound, Expr::variable("max_numeric_magnitude_bits")), + ], unavailable: vec![crate::rules::registry::UnavailableParameterField { field: "capacity", - reason: "the target capacity equals the SubsetSum target, which is not a registered source parameter", + reason: "raw capacity requires numeric magnitude values; downstream predictions use capacity_bits", }], }, module_path: module_path!(), diff --git a/src/rules/subsetsum_partition.rs b/src/rules/subsetsum_partition.rs index ae35fb792..d59308bc3 100644 --- a/src/rules/subsetsum_partition.rs +++ b/src/rules/subsetsum_partition.rs @@ -68,8 +68,10 @@ impl ReductionResult for ReductionSubsetSumToPartition { #[crate::aggregate_reduction(identity)] impl crate::rules::AggregateReductionResult for ReductionSubsetSumToPartition {} +// The padding |sum(sizes) - 2*target| is below (n+2)*2^h <= 2^(h+n+1). #[reduction( transform = upper_bound { + max_numeric_magnitude_bits = "max_numeric_magnitude_bits + num_elements + 1", num_elements = "num_elements + 1", })] impl ReduceTo for SubsetSum { diff --git a/src/rules/sumofsquarespartition_ilp.rs b/src/rules/sumofsquarespartition_ilp.rs index 57ad1b258..83d036587 100644 --- a/src/rules/sumofsquarespartition_ilp.rs +++ b/src/rules/sumofsquarespartition_ilp.rs @@ -72,13 +72,16 @@ impl ReductionResult for ReductionSSPToILP { } } -#[reduction( - transform = exact { +#[reduction(transform = { + exact { num_vars = "num_elements * num_groups + num_elements^2 * num_groups", num_constraints = "num_elements + 3 * num_elements^2 * num_groups", num_nonzeros = "7 * num_elements^2 * num_groups", }, -)] + upper_bound { + max_constraint_magnitude_bits = "2", + }, +})] impl ReduceTo> for SumOfSquaresPartition { type Result = ReductionSSPToILP; diff --git a/src/rules/threedimensionalmatching_ilp.rs b/src/rules/threedimensionalmatching_ilp.rs index a7a325b9e..14f62f061 100644 --- a/src/rules/threedimensionalmatching_ilp.rs +++ b/src/rules/threedimensionalmatching_ilp.rs @@ -38,6 +38,7 @@ impl crate::rules::AggregateReductionResult for ReductionThreeDimensionalMatchin #[reduction( transform = exact { + max_constraint_magnitude_bits = "1", num_vars = "num_triples", num_constraints = "3 * universe_size", num_nonzeros = "3 * num_triples", diff --git a/src/rules/threedimensionalmatching_threepartition.rs b/src/rules/threedimensionalmatching_threepartition.rs index 68262ca45..6a506ea64 100644 --- a/src/rules/threedimensionalmatching_threepartition.rs +++ b/src/rules/threedimensionalmatching_threepartition.rs @@ -345,6 +345,7 @@ impl crate::rules::AggregateReductionResult for ReductionThreeDimensionalMatchin #[reduction( transform = upper_bound { + max_numeric_magnitude_bits = "4 * universe_size + 36", num_elements = "24 * num_triples * num_triples - 3 * num_triples + 6", num_groups = "8 * num_triples * num_triples - num_triples + 2", })] diff --git a/src/rules/threepartition_resourceconstrainedscheduling.rs b/src/rules/threepartition_resourceconstrainedscheduling.rs index 7b4111b58..546554b23 100644 --- a/src/rules/threepartition_resourceconstrainedscheduling.rs +++ b/src/rules/threepartition_resourceconstrainedscheduling.rs @@ -58,6 +58,7 @@ impl crate::rules::AggregateReductionResult for ReductionThreePartitionToRCS {} #[reduction( transform = exact { + max_resource_bits = "max_numeric_magnitude_bits", num_tasks = "num_elements", deadline = "num_groups", num_resources = "1", diff --git a/src/rules/timetabledesign_ilp.rs b/src/rules/timetabledesign_ilp.rs index 0964aeae9..07a612bea 100644 --- a/src/rules/timetabledesign_ilp.rs +++ b/src/rules/timetabledesign_ilp.rs @@ -64,11 +64,14 @@ impl ReductionResult for ReductionTDToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionTDToILP {} -#[reduction(transform = upper_bound { - num_vars = "num_craftsmen * num_tasks * num_periods", - num_constraints = "num_craftsmen * num_periods + num_tasks * num_periods + num_craftsmen * num_tasks + num_craftsmen * num_tasks * num_periods", - num_nonzeros = "(num_craftsmen * num_tasks * num_periods) * (num_craftsmen * num_periods + num_tasks * num_periods + num_craftsmen * num_tasks + num_craftsmen * num_tasks * num_periods)", -})] +#[reduction( + transform = upper_bound { + max_constraint_magnitude_bits = "num_periods + 2", + num_vars = "num_craftsmen * num_tasks * num_periods", + num_constraints = "num_craftsmen * num_periods + num_tasks * num_periods + num_craftsmen * num_tasks + num_craftsmen * num_tasks * num_periods", + num_nonzeros = "(num_craftsmen * num_tasks * num_periods) * (num_craftsmen * num_periods + num_tasks * num_periods + num_craftsmen * num_tasks + num_craftsmen * num_tasks * num_periods)", + }, +)] impl ReduceTo> for TimetableDesign { type Result = ReductionTDToILP; @@ -77,6 +80,8 @@ impl ReduceTo> for TimetableDesign { let nt = self.num_tasks(); let nh = self.num_periods(); let requirements = self.requirements(); + // A pair can work at most nh periods. Keep out-of-range requirements infeasible. + let max_requirement = Self::exact_i64(nh, "encoding the period count")?.saturating_add(1); let num_vars = nc * nt * nh; let var = |c: usize, t: usize, h: usize| -> usize { ((c * nt) + t) * nh + h }; @@ -114,7 +119,10 @@ impl ReduceTo> for TimetableDesign { for (c, row) in requirements.iter().enumerate() { for (t, &requirement) in row.iter().enumerate() { let terms: Vec<(usize, i64)> = (0..nh).map(|h| (var(c, t, h), 1)).collect(); - constraints.push(LinearConstraint::eq(terms, requirement)); + constraints.push(LinearConstraint::eq( + terms, + requirement.clamp(-1, max_requirement), + )); } } diff --git a/src/rules/travelingsalesman_ilp.rs b/src/rules/travelingsalesman_ilp.rs index 6be6afa76..956ca8985 100644 --- a/src/rules/travelingsalesman_ilp.rs +++ b/src/rules/travelingsalesman_ilp.rs @@ -75,6 +75,7 @@ impl ReductionResult for ReductionTSPToILP { num_vars = "num_vertices^2 + 2 * num_vertices * num_edges", }, upper_bound { + max_constraint_magnitude_bits = "2", num_constraints = "num_vertices^2 * (num_vertices - 1) + 2 * num_vertices + 6 * num_vertices * num_edges", num_nonzeros = "2 * num_vertices^3 + 14 * num_vertices * num_edges", }, diff --git a/src/rules/undirectedflowlowerbounds_ilp.rs b/src/rules/undirectedflowlowerbounds_ilp.rs index b8cf95beb..3edc5e228 100644 --- a/src/rules/undirectedflowlowerbounds_ilp.rs +++ b/src/rules/undirectedflowlowerbounds_ilp.rs @@ -1,17 +1,17 @@ -//! Reduction from UndirectedFlowLowerBounds to `ILP`. +//! Reduction from UndirectedFlowLowerBounds to `ILP`. //! //! For each undirected edge e = {u,v} (indexed by e), we introduce: //! f_{uv} = 2*e (flow in u→v direction, ≥ 0) //! f_{vu} = 2*e + 1 (flow in v→u direction, ≥ 0) //! z_e = 2*|E| + e (binary orientation: 1 if u→v, 0 if v→u) //! -//! Constraints per edge (4 constraints): +//! Constraints per edge (up to 5 constraints): //! z_e ≤ 1 (force binary) //! f_{uv} ≤ cap[e] * z_e (only if oriented u→v) //! f_{vu} ≤ cap[e] * (1 - z_e) (only if oriented v→u) //! f_{uv} ≥ lower[e] * z_e (must carry at least lower bound if oriented u→v) //! f_{vu} ≥ lower[e] * (1 - z_e)(must carry at least lower bound if oriented v→u) -//! Since we need all 4: linearized as: +//! Linearized as: //! z_e ≤ 1 //! f_{uv} - cap[e]*z_e ≤ 0 //! f_{vu} + cap[e]*z_e ≤ cap[e] @@ -23,13 +23,14 @@ //! //! Size upper bound: 3*|E| variables, 5*|E| + |V| + 1 constraints. -use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{Bounded, IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::UndirectedFlowLowerBounds; use crate::reduction; +use crate::rules::ilp_helpers::bounded_flow_requirement; use crate::rules::traits::{ReduceTo, ReductionResult}; use crate::topology::Graph; -/// Result of reducing UndirectedFlowLowerBounds to `ILP`. +/// Result of reducing UndirectedFlowLowerBounds to `ILP`. /// /// Variable layout: /// - `f_{uv}` at 2*e (flow u→v on edge e) @@ -37,15 +38,15 @@ use crate::topology::Graph; /// - `z_e` at 2*|E| + e (orientation indicator: 1 = u→v direction) #[derive(Debug, Clone)] pub struct ReductionUFLBToILP { - target: ILP, + target: ILP, num_edges: usize, } impl ReductionResult for ReductionUFLBToILP { type Source = UndirectedFlowLowerBounds; - type Target = ILP; + type Target = ILP; - fn target_problem(&self) -> &ILP { + fn target_problem(&self) -> &ILP { &self.target } @@ -78,12 +79,15 @@ impl ReductionResult for ReductionUFLBToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionUFLBToILP {} -#[reduction(transform = upper_bound { - num_vars = "3 * num_edges", - num_constraints = "5 * num_edges + num_vertices + 1", - num_nonzeros = "(3 * num_edges) * (5 * num_edges + num_vertices + 1)", -})] -impl ReduceTo> for UndirectedFlowLowerBounds { +#[reduction( + transform = upper_bound { + max_constraint_magnitude_bits = "max_capacity_bits + num_edges + 1", + num_vars = "3 * num_edges", + num_constraints = "5 * num_edges + num_vertices + 1", + num_nonzeros = "(3 * num_edges) * (5 * num_edges + num_vertices + 1)", + }, +)] +impl ReduceTo> for UndirectedFlowLowerBounds { type Result = ReductionUFLBToILP; fn reduce_to(&self) -> Result { @@ -171,7 +175,9 @@ impl ReduceTo> for UndirectedFlowLowerBounds { sink_terms.push((f_vu(edge_idx), 1)); } } - constraints.push(LinearConstraint::ge(sink_terms, self.requirement())); + let requirement = + bounded_flow_requirement(self.requirement(), self.capacities().iter().copied()); + constraints.push(LinearConstraint::ge(sink_terms, requirement)); let mut variables = self .capacities() @@ -207,7 +213,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec(source) + crate::example_db::specs::rule_example_via_bounded_ilp::<_>(source) }, }] } diff --git a/src/rules/undirectedtwocommodityintegralflow_ilp.rs b/src/rules/undirectedtwocommodityintegralflow_ilp.rs index 61a69fa4d..be316f533 100644 --- a/src/rules/undirectedtwocommodityintegralflow_ilp.rs +++ b/src/rules/undirectedtwocommodityintegralflow_ilp.rs @@ -1,4 +1,4 @@ -//! Reduction from UndirectedTwoCommodityIntegralFlow to `ILP`. +//! Reduction from UndirectedTwoCommodityIntegralFlow to `ILP`. //! //! For each undirected edge {u,v} (indexed by e), we introduce 4 flow variables: //! f1_{uv} = 4*e + 0 (commodity 1 flow u→v) @@ -13,7 +13,7 @@ //! For each edge e with capacity c_e, the joint capacity constraint is: //! max(f1_{uv}, f1_{vu}) + max(f2_{uv}, f2_{vu}) ≤ c_e //! -//! Since this is `ILP`, we use direction indicators d1_e, d2_e ∈ {0,1} to linearize: +//! Since this is `ILP`, we use direction indicators d1_e, d2_e ∈ {0,1} to linearize: //! f1_{uv} ≤ c_e * d1_e; f1_{vu} ≤ c_e * (1 - d1_e) //! f2_{uv} ≤ c_e * d2_e; f2_{vu} ≤ c_e * (1 - d2_e) //! f1_{uv} + f1_{vu} + f2_{uv} + f2_{vu} ≤ c_e (joint capacity) @@ -24,13 +24,14 @@ //! //! Constraints per edge (7 per edge) + flow conservation (2 per non-terminal vertex) + net flow (2) -use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{Bounded, IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::UndirectedTwoCommodityIntegralFlow; use crate::reduction; +use crate::rules::ilp_helpers::bounded_flow_requirement; use crate::rules::traits::{ReduceTo, ReductionResult}; use crate::topology::Graph; -/// Result of reducing UndirectedTwoCommodityIntegralFlow to `ILP`. +/// Result of reducing UndirectedTwoCommodityIntegralFlow to `ILP`. /// /// Variable layout: /// - `f1_{uv}` at 4*e + 0, `f1_{vu}` at 4*e + 1 (commodity 1 flows on edge e) @@ -38,15 +39,15 @@ use crate::topology::Graph; /// - `d1_e` at 4*|E| + 2*e, `d2_e` at 4*|E| + 2*e + 1 (direction indicators) #[derive(Debug, Clone)] pub struct ReductionU2CIFToILP { - target: ILP, + target: ILP, num_edges: usize, } impl ReductionResult for ReductionU2CIFToILP { type Source = UndirectedTwoCommodityIntegralFlow; - type Target = ILP; + type Target = ILP; - fn target_problem(&self) -> &ILP { + fn target_problem(&self) -> &ILP { &self.target } @@ -75,10 +76,11 @@ impl crate::rules::AggregateReductionResult for ReductionU2CIFToILP {} num_constraints = "7 * num_edges + num_conservation_constraints + 2", }, upper_bound { + max_constraint_magnitude_bits = "max_capacity_bits + num_edges + 1", num_nonzeros = "(6 * num_edges) * (7 * num_edges + num_conservation_constraints + 2)", }, })] -impl ReduceTo> for UndirectedTwoCommodityIntegralFlow { +impl ReduceTo> for UndirectedTwoCommodityIntegralFlow { type Result = ReductionU2CIFToILP; fn reduce_to(&self) -> Result { @@ -169,36 +171,28 @@ impl ReduceTo> for UndirectedTwoCommodityIntegralFlow { } } - // Net flow into sinks ≥ requirements - // Commodity 1: net inflow at sink_1 ≥ requirement_1 - let sink_1 = self.sink_1(); - let mut sink1_terms: Vec<(usize, i64)> = Vec::new(); - for (edge_idx, &(u, v)) in edges.iter().enumerate() { - if sink_1 == v { - sink1_terms.push((f1_uv(edge_idx), 1)); - sink1_terms.push((f1_vu(edge_idx), -1)); - } - if sink_1 == u { - sink1_terms.push((f1_uv(edge_idx), -1)); - sink1_terms.push((f1_vu(edge_idx), 1)); - } - } - constraints.push(LinearConstraint::ge(sink1_terms, self.requirement_1())); - - // Commodity 2: net inflow at sink_2 ≥ requirement_2 - let sink_2 = self.sink_2(); - let mut sink2_terms: Vec<(usize, i64)> = Vec::new(); - for (edge_idx, &(u, v)) in edges.iter().enumerate() { - if sink_2 == v { - sink2_terms.push((f2_uv(edge_idx), 1)); - sink2_terms.push((f2_vu(edge_idx), -1)); - } - if sink_2 == u { - sink2_terms.push((f2_uv(edge_idx), -1)); - sink2_terms.push((f2_vu(edge_idx), 1)); + // Net flow into each sink must meet its normalized requirement. + for (sink, requirement, flow_offset) in [ + (self.sink_1(), self.requirement_1(), 0), + (self.sink_2(), self.requirement_2(), 2), + ] { + let mut terms = Vec::new(); + for (edge_idx, &(u, v)) in edges.iter().enumerate() { + let uv = 4 * edge_idx + flow_offset; + let vu = uv + 1; + if sink == v { + terms.push((uv, 1)); + terms.push((vu, -1)); + } + if sink == u { + terms.push((uv, -1)); + terms.push((vu, 1)); + } } + let requirement = + bounded_flow_requirement(requirement, self.capacities().iter().copied()); + constraints.push(LinearConstraint::ge(terms, requirement)); } - constraints.push(LinearConstraint::ge(sink2_terms, self.requirement_2())); let mut variables = self .capacities() @@ -239,13 +233,14 @@ pub(crate) fn canonical_rule_example_specs() -> Vec>::reduce_to(&source).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&source) + .expect("reduction should succeed"); let solver = crate::solvers::ILPSolver::new(); let target_config = solver .solve(reduction.target_problem()) .expect("canonical example should be feasible"); let source_config = reduction.extract_solution(&target_config).unwrap(); - crate::example_db::specs::rule_example_with_witness::<_, ILP>( + crate::example_db::specs::rule_example_with_witness::<_, ILP>( source, SolutionPair { source_config: serde_json::to_value(source_config) diff --git a/src/solvers/ilp/adapter.rs b/src/solvers/ilp/adapter.rs index 2fa797aca..b01123aa0 100644 --- a/src/solvers/ilp/adapter.rs +++ b/src/solvers/ilp/adapter.rs @@ -4,7 +4,9 @@ //! type-erased dispatch, and reduction-chain extraction belong to the caller. //! Optimality and infeasibility follow HiGHS numerical tolerances, not exact proofs. -use crate::models::algebraic::{Comparison, ILPCoefficient, ObjectiveSense, VariableDomain, ILP}; +use crate::models::algebraic::{ + BoundsPolicy, Comparison, ILPCoefficient, ObjectiveSense, VariableDomain, ILP, +}; use crate::types::{i64_to_exact_f64, MAX_EXACT_F64_INTEGER}; use highs::{HighsModelStatus, HighsSolutionStatus, RowProblem, Sense}; @@ -45,10 +47,11 @@ impl HighsAdapter { pub(crate) fn new(time_limit: Option) -> Self { Self { time_limit } } - pub(crate) fn solve(&self, problem: &ILP) -> Result, ILPSolveError> + pub(crate) fn solve(&self, problem: &ILP) -> Result, ILPSolveError> where V: VariableDomain, C: BackendCoefficient, + B: BoundsPolicy, { if self .time_limit @@ -61,14 +64,15 @@ impl HighsAdapter { self.solve_with_objective(problem, problem.objective()) } - fn solve_with_objective( + fn solve_with_objective( &self, - problem: &ILP, + problem: &ILP, objective_terms: &[(usize, C)], ) -> Result, ILPSolveError> where V: VariableDomain, C: BackendCoefficient, + B: BoundsPolicy, { let n = problem.num_vars(); if n == 0 { @@ -171,8 +175,8 @@ impl HighsAdapter { } } -fn decode_and_validate( - problem: &ILP, +fn decode_and_validate( + problem: &ILP, values: impl IntoIterator, ) -> Result, ILPSolveError> { let result = values diff --git a/src/solvers/ilp/solver.rs b/src/solvers/ilp/solver.rs index 05893d30a..1e01e79fa 100644 --- a/src/solvers/ilp/solver.rs +++ b/src/solvers/ilp/solver.rs @@ -1,7 +1,7 @@ //! ILP solver implementation using HiGHS. use super::adapter::HighsAdapter; -use crate::models::algebraic::ILP; +use crate::models::algebraic::{Bounded, ILP}; use crate::solvers::registry::solver_capability_registry; use crate::solvers::ExactProblemKey; use crate::traits::Problem; @@ -120,6 +120,9 @@ impl ILPSolver { if let Some(ilp) = any.downcast_ref::>() { return HighsAdapter::new(self.time_limit).solve(ilp); } + if let Some(ilp) = any.downcast_ref::>() { + return HighsAdapter::new(self.time_limit).solve(ilp); + } if let Some(ilp) = any.downcast_ref::>() { return HighsAdapter::new(self.time_limit).solve(ilp); } diff --git a/src/solvers/pipelines.rs b/src/solvers/pipelines.rs index d10636f6a..14355a834 100644 --- a/src/solvers/pipelines.rs +++ b/src/solvers/pipelines.rs @@ -22,202 +22,206 @@ macro_rules! register_ilp_pipeline { } register_ilp_pipeline! { - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { - ("ILP", [("variable", "i64"), ("coefficient", "i64")]), + ("ILP", [("variable", "i64"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), + ("ILP", [("variable", "bool"), ("coefficient", "f64"), ("bounds", "general")]), } register_ilp_pipeline! { - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), + ("ILP", [("variable", "i64"), ("coefficient", "f64"), ("bounds", "general")]), +} + +register_ilp_pipeline! { + ("ILP", [("variable", "i64"), ("coefficient", "i64"), ("bounds", "bounded")]), } register_ilp_pipeline! { ("AcyclicPartition", [("weight", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("BMF", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("BalancedCompleteBipartiteSubgraph", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("BicliqueCover", []), ("BMF", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("BiconnectivityAugmentation", [("graph", "SimpleGraph"), ("weight", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("BinPacking", [("weight", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("BottleneckTravelingSalesman", []), - ("ILP", [("variable", "i64"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("BoundedComponentSpanningForest", [("graph", "SimpleGraph"), ("weight", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "i64")]), + ("ILP", [("variable", "i64"), ("coefficient", "i64"), ("bounds", "bounded")]), } register_ilp_pipeline! { ("CapacityAssignment", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("CircuitSAT", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("ClosestString", []), - ("ILP", [("variable", "i64"), ("coefficient", "i64")]), + ("ILP", [("variable", "i64"), ("coefficient", "i64"), ("bounds", "bounded")]), } register_ilp_pipeline! { ("ClosestSubstring", []), - ("ILP", [("variable", "i64"), ("coefficient", "i64")]), + ("ILP", [("variable", "i64"), ("coefficient", "i64"), ("bounds", "bounded")]), } register_ilp_pipeline! { ("Clustering", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("ConsecutiveBlockMinimization", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("ConsecutiveOnesMatrixAugmentation", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("ConsecutiveOnesSubmatrix", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("ConsistencyOfDatabaseFrequencyTables", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("DecisionMinimumDominatingSet", [("graph", "SimpleGraph"), ("weight", "One")]), ("DecisionMinimumSumMulticenter", [("graph", "SimpleGraph"), ("weight", "i64")]), ("MinimumSumMulticenter", [("graph", "SimpleGraph"), ("weight", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("DecisionMinimumDominatingSet", [("graph", "SimpleGraph"), ("weight", "i64")]), ("MinimumDominatingSet", [("graph", "SimpleGraph"), ("weight", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("DecisionMinimumVertexCover", [("graph", "SimpleGraph"), ("weight", "i64")]), ("MinimumVertexCover", [("graph", "SimpleGraph"), ("weight", "i64")]), ("MinimumSetCovering", [("weight", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("DecisionOptimalLinearArrangement", [("graph", "SimpleGraph")]), ("OptimalLinearArrangement", [("graph", "SimpleGraph")]), - ("ILP", [("variable", "i64"), ("coefficient", "i64")]), + ("ILP", [("variable", "i64"), ("coefficient", "i64"), ("bounds", "bounded")]), } register_ilp_pipeline! { ("DirectedHamiltonianPath", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("DirectedTwoCommodityIntegralFlow", []), - ("ILP", [("variable", "i64"), ("coefficient", "i64")]), + ("ILP", [("variable", "i64"), ("coefficient", "i64"), ("bounds", "bounded")]), } register_ilp_pipeline! { ("DisjointConnectingPaths", [("graph", "SimpleGraph")]), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("EnsembleComputation", []), - ("ILP", [("variable", "i64"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("EulerianPath", []), - ("ILP", [("variable", "i64"), ("coefficient", "i64")]), + ("ILP", [("variable", "i64"), ("coefficient", "i64"), ("bounds", "bounded")]), } register_ilp_pipeline! { ("ExactCoverBy3Sets", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("ExpectedRetrievalCost", []), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), + ("ILP", [("variable", "bool"), ("coefficient", "f64"), ("bounds", "general")]), } register_ilp_pipeline! { ("Factoring", []), - ("ILP", [("variable", "i64"), ("coefficient", "i64")]), + ("ILP", [("variable", "i64"), ("coefficient", "i64"), ("bounds", "bounded")]), } register_ilp_pipeline! { ("FeasibleRegisterAssignment", []), - ("ILP", [("variable", "i64"), ("coefficient", "i64")]), + ("ILP", [("variable", "i64"), ("coefficient", "i64"), ("bounds", "bounded")]), } register_ilp_pipeline! { ("FlowShopScheduling", []), - ("ILP", [("variable", "i64"), ("coefficient", "i64")]), + ("ILP", [("variable", "i64"), ("coefficient", "i64"), ("bounds", "bounded")]), } register_ilp_pipeline! { ("GraphPartitioning", [("graph", "SimpleGraph")]), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("HamiltonianCircuit", [("graph", "SimpleGraph")]), ("DecisionLongestCircuit", [("graph", "SimpleGraph"), ("weight", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("HamiltonianPath", [("graph", "SimpleGraph")]), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("HighlyConnectedDeletion", [("graph", "SimpleGraph")]), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } // This exact variant also has a customized backend. Default dispatch selects the @@ -225,85 +229,85 @@ register_ilp_pipeline! { register_ilp_pipeline! { ("RootedTreeArrangement", [("graph", "SimpleGraph")]), ("RootedTreeStorageAssignment", []), - ("ILP", [("variable", "i64"), ("coefficient", "i64")]), + ("ILP", [("variable", "i64"), ("coefficient", "i64"), ("bounds", "bounded")]), } register_ilp_pipeline! { ("IntegralFlowBundles", []), - ("ILP", [("variable", "i64"), ("coefficient", "i64")]), + ("ILP", [("variable", "i64"), ("coefficient", "i64"), ("bounds", "bounded")]), } register_ilp_pipeline! { ("IntegralFlowHomologousArcs", []), - ("ILP", [("variable", "i64"), ("coefficient", "i64")]), + ("ILP", [("variable", "i64"), ("coefficient", "i64"), ("bounds", "bounded")]), } register_ilp_pipeline! { ("IntegralFlowWithMultipliers", []), - ("ILP", [("variable", "i64"), ("coefficient", "i64")]), + ("ILP", [("variable", "i64"), ("coefficient", "i64"), ("bounds", "bounded")]), } register_ilp_pipeline! { ("IsomorphicSpanningTree", [("graph", "SimpleGraph")]), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("KClique", [("graph", "SimpleGraph")]), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("KColoring", [("graph", "SimpleGraph"), ("k", "KN")]), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("KColoring", [("graph", "SimpleGraph"), ("k", "K3")]), ("Clustering", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("KSatisfiability", [("k", "KN")]), ("Satisfiability", []), ("NAESatisfiability", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("Knapsack", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("LengthBoundedDisjointPaths", [("graph", "SimpleGraph")]), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("LongestCircuit", [("graph", "SimpleGraph"), ("weight", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("LongestCommonSubsequence", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("LongestPath", [("graph", "SimpleGraph"), ("weight", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "i64")]), + ("ILP", [("variable", "i64"), ("coefficient", "i64"), ("bounds", "bounded")]), } register_ilp_pipeline! { ("MaximalIS", [("graph", "SimpleGraph"), ("weight", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("Maximum2Satisfiability", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { @@ -311,54 +315,54 @@ register_ilp_pipeline! { ("MaximumIndependentSet", [("graph", "SimpleGraph"), ("weight", "One")]), ("MaximumIndependentSet", [("graph", "SimpleGraph"), ("weight", "i64")]), ("MaximumSetPacking", [("weight", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("MaximumClique", [("graph", "SimpleGraph"), ("weight", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("MaximumCoKPlex", [("graph", "SimpleGraph"), ("k", "KN"), ("weight", "One")]), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("MaximumCoKPlex", [("graph", "SimpleGraph"), ("k", "KN"), ("weight", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("MaximumCommonEdgeSubgraph", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("MaximumContactMapOverlap", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("MaximumDomaticNumber", [("graph", "SimpleGraph")]), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("MaximumEdgeWeightedKClique", [("weight", "f64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), + ("ILP", [("variable", "bool"), ("coefficient", "f64"), ("bounds", "general")]), } register_ilp_pipeline! { ("MaximumEdgeWeightedKClique", [("weight", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("MaximumIndependentSet", [("graph", "SimpleGraph"), ("weight", "One")]), ("MaximumIndependentSet", [("graph", "SimpleGraph"), ("weight", "i64")]), ("MaximumSetPacking", [("weight", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { @@ -367,13 +371,13 @@ register_ilp_pipeline! { ("MaximumIndependentSet", [("graph", "SimpleGraph"), ("weight", "One")]), ("MaximumIndependentSet", [("graph", "SimpleGraph"), ("weight", "i64")]), ("MaximumClique", [("graph", "SimpleGraph"), ("weight", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("MaximumIndependentSet", [("graph", "SimpleGraph"), ("weight", "i64")]), ("MaximumSetPacking", [("weight", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { @@ -381,7 +385,7 @@ register_ilp_pipeline! { ("MaximumIndependentSet", [("graph", "SimpleGraph"), ("weight", "One")]), ("MaximumIndependentSet", [("graph", "SimpleGraph"), ("weight", "i64")]), ("MaximumClique", [("graph", "SimpleGraph"), ("weight", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { @@ -389,7 +393,7 @@ register_ilp_pipeline! { ("MaximumIndependentSet", [("graph", "UnitDiskGraph"), ("weight", "i64")]), ("MaximumIndependentSet", [("graph", "SimpleGraph"), ("weight", "i64")]), ("MaximumClique", [("graph", "SimpleGraph"), ("weight", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { @@ -397,473 +401,473 @@ register_ilp_pipeline! { ("MaximumIndependentSet", [("graph", "UnitDiskGraph"), ("weight", "i64")]), ("MaximumIndependentSet", [("graph", "SimpleGraph"), ("weight", "i64")]), ("MaximumClique", [("graph", "SimpleGraph"), ("weight", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("MaximumIndependentSet", [("graph", "UnitDiskGraph"), ("weight", "i64")]), ("MaximumIndependentSet", [("graph", "SimpleGraph"), ("weight", "i64")]), ("MaximumClique", [("graph", "SimpleGraph"), ("weight", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("MaximumLeafSpanningTree", [("graph", "SimpleGraph")]), - ("ILP", [("variable", "i64"), ("coefficient", "i64")]), + ("ILP", [("variable", "i64"), ("coefficient", "i64"), ("bounds", "bounded")]), } register_ilp_pipeline! { ("MaximumLikelihoodRanking", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("MaximumMatching", [("graph", "SimpleGraph"), ("weight", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("MaximumSetPacking", [("weight", "One")]), ("MaximumSetPacking", [("weight", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("MaximumSetPacking", [("weight", "f64")]), ("QUBO", [("weight", "f64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), + ("ILP", [("variable", "bool"), ("coefficient", "f64"), ("bounds", "general")]), } register_ilp_pipeline! { ("MaximumSetPacking", [("weight", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("MinMaxMulticenter", [("graph", "SimpleGraph"), ("weight", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "i64")]), + ("ILP", [("variable", "i64"), ("coefficient", "i64"), ("bounds", "bounded")]), } register_ilp_pipeline! { ("MinimumCapacitatedSpanningTree", [("graph", "SimpleGraph"), ("weight", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "i64")]), + ("ILP", [("variable", "i64"), ("coefficient", "i64"), ("bounds", "bounded")]), } register_ilp_pipeline! { ("MinimumCoveringByCliques", [("graph", "SimpleGraph")]), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("MinimumCutIntoBoundedSets", [("graph", "SimpleGraph"), ("weight", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("MinimumDiscretePlanarInverseKinematics", []), ("QUBO", [("weight", "f64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), + ("ILP", [("variable", "bool"), ("coefficient", "f64"), ("bounds", "general")]), } register_ilp_pipeline! { ("MinimumDominatingSet", [("graph", "SimpleGraph"), ("weight", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("MinimumEdgeCostFlow", []), - ("ILP", [("variable", "i64"), ("coefficient", "i64")]), + ("ILP", [("variable", "i64"), ("coefficient", "i64"), ("bounds", "bounded")]), } register_ilp_pipeline! { ("MinimumExternalMacroDataCompression", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("MinimumFaultDetectionTestSet", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("MinimumFeedbackArcSet", [("weight", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "i64")]), + ("ILP", [("variable", "i64"), ("coefficient", "i64"), ("bounds", "bounded")]), } register_ilp_pipeline! { ("MinimumFeedbackVertexSet", [("weight", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "i64")]), + ("ILP", [("variable", "i64"), ("coefficient", "i64"), ("bounds", "bounded")]), } register_ilp_pipeline! { ("MinimumGraphBandwidth", [("graph", "SimpleGraph")]), - ("ILP", [("variable", "i64"), ("coefficient", "i64")]), + ("ILP", [("variable", "i64"), ("coefficient", "i64"), ("bounds", "bounded")]), } register_ilp_pipeline! { ("MinimumHittingSet", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("MinimumInternalMacroDataCompression", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("MinimumMatrixCover", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("MinimumMaximalMatching", [("graph", "SimpleGraph")]), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("MinimumMetricDimension", [("graph", "SimpleGraph")]), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("MinimumMultiwayCut", [("graph", "SimpleGraph"), ("weight", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("MinimumSetCovering", [("weight", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("MinimumSumMulticenter", [("graph", "SimpleGraph"), ("weight", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("MinimumTardinessSequencing", [("weight", "One")]), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("MinimumTardinessSequencing", [("weight", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("MinimumVertexCover", [("graph", "SimpleGraph"), ("weight", "One")]), ("MinimumHittingSet", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("MinimumVertexCover", [("graph", "SimpleGraph"), ("weight", "i64")]), ("MinimumSetCovering", [("weight", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("MinimumWeightDecoding", []), - ("ILP", [("variable", "i64"), ("coefficient", "i64")]), + ("ILP", [("variable", "i64"), ("coefficient", "i64"), ("bounds", "bounded")]), } register_ilp_pipeline! { ("MixedChinesePostman", [("weight", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "i64")]), + ("ILP", [("variable", "i64"), ("coefficient", "i64"), ("bounds", "bounded")]), } register_ilp_pipeline! { ("MonochromaticTriangle", [("graph", "SimpleGraph")]), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("MultipleCopyFileAllocation", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("MultipleChoiceBranching", [("weight", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "i64")]), + ("ILP", [("variable", "i64"), ("coefficient", "i64"), ("bounds", "bounded")]), } register_ilp_pipeline! { ("MultiprocessorScheduling", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("NAESatisfiability", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("Numerical3DimensionalMatching", []), ("NumericalMatchingWithTargetSums", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("NumericalMatchingWithTargetSums", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("OpenShopScheduling", []), - ("ILP", [("variable", "i64"), ("coefficient", "i64")]), + ("ILP", [("variable", "i64"), ("coefficient", "i64"), ("bounds", "bounded")]), } register_ilp_pipeline! { ("OptimalLinearArrangement", [("graph", "SimpleGraph")]), - ("ILP", [("variable", "i64"), ("coefficient", "i64")]), + ("ILP", [("variable", "i64"), ("coefficient", "i64"), ("bounds", "bounded")]), } register_ilp_pipeline! { ("OptimumCommunicationSpanningTree", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("PaintShop", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("PartiallyOrderedKnapsack", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("Partition", []), ("MultiprocessorScheduling", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("PartitionIntoCliques", [("graph", "SimpleGraph")]), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("PartitionIntoPathsOfLength2", [("graph", "SimpleGraph")]), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("PartitionIntoTriangles", [("graph", "SimpleGraph")]), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("PathConstrainedNetworkFlow", []), - ("ILP", [("variable", "i64"), ("coefficient", "i64")]), + ("ILP", [("variable", "i64"), ("coefficient", "i64"), ("bounds", "bounded")]), } register_ilp_pipeline! { ("PrecedenceConstrainedScheduling", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("PreemptiveScheduling", []), - ("ILP", [("variable", "i64"), ("coefficient", "i64")]), + ("ILP", [("variable", "i64"), ("coefficient", "i64"), ("bounds", "bounded")]), } register_ilp_pipeline! { ("QUBO", [("weight", "f64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), + ("ILP", [("variable", "bool"), ("coefficient", "f64"), ("bounds", "general")]), } register_ilp_pipeline! { ("QUBO", [("weight", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("QuadraticAssignment", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("RectilinearPictureCompression", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("RegisterSufficiency", []), - ("ILP", [("variable", "i64"), ("coefficient", "i64")]), + ("ILP", [("variable", "i64"), ("coefficient", "i64"), ("bounds", "bounded")]), } register_ilp_pipeline! { ("ResourceConstrainedScheduling", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("RootedTreeStorageAssignment", []), - ("ILP", [("variable", "i64"), ("coefficient", "i64")]), + ("ILP", [("variable", "i64"), ("coefficient", "i64"), ("bounds", "bounded")]), } register_ilp_pipeline! { ("RuralPostman", [("graph", "SimpleGraph"), ("weight", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "i64")]), + ("ILP", [("variable", "i64"), ("coefficient", "i64"), ("bounds", "bounded")]), } register_ilp_pipeline! { ("Satisfiability", []), ("NAESatisfiability", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("SchedulingToMinimizeWeightedCompletionTime", []), - ("ILP", [("variable", "i64"), ("coefficient", "i64")]), + ("ILP", [("variable", "i64"), ("coefficient", "i64"), ("bounds", "bounded")]), } register_ilp_pipeline! { ("SchedulingWithIndividualDeadlines", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("SequencingToMinimizeMaximumCumulativeCost", []), - ("ILP", [("variable", "i64"), ("coefficient", "i64")]), + ("ILP", [("variable", "i64"), ("coefficient", "i64"), ("bounds", "bounded")]), } register_ilp_pipeline! { ("SequencingToMinimizeTardyTaskWeight", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("SequencingToMinimizeWeightedCompletionTime", []), - ("ILP", [("variable", "i64"), ("coefficient", "i64")]), + ("ILP", [("variable", "i64"), ("coefficient", "i64"), ("bounds", "bounded")]), } register_ilp_pipeline! { ("SequencingToMinimizeWeightedTardiness", []), - ("ILP", [("variable", "i64"), ("coefficient", "i64")]), + ("ILP", [("variable", "i64"), ("coefficient", "i64"), ("bounds", "bounded")]), } register_ilp_pipeline! { ("SequencingWithDeadlinesAndSetUpTimes", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("SequencingWithReleaseTimesAndDeadlines", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("SequencingWithinIntervals", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("SetSplitting", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("ShortestCommonSupersequence", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("ShortestWeightConstrainedPath", [("graph", "SimpleGraph"), ("weight", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "i64")]), + ("ILP", [("variable", "i64"), ("coefficient", "i64"), ("bounds", "bounded")]), } register_ilp_pipeline! { ("SparseMatrixCompression", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("SpinGlass", [("graph", "SimpleGraph"), ("weight", "f64")]), ("QUBO", [("weight", "f64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), + ("ILP", [("variable", "bool"), ("coefficient", "f64"), ("bounds", "general")]), } register_ilp_pipeline! { ("SpinGlass", [("graph", "SimpleGraph"), ("weight", "i64")]), ("QUBO", [("weight", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("StackerCrane", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("SteinerTree", [("graph", "SimpleGraph"), ("weight", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("StringToStringCorrection", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("StrongConnectivityAugmentation", [("weight", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("SubgraphIsomorphism", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("SumOfSquaresPartition", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("ThreeDimensionalMatching", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("ThreePartition", []), ("ResourceConstrainedScheduling", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("TravelingSalesman", [("graph", "SimpleGraph"), ("weight", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("UndirectedFlowLowerBounds", []), - ("ILP", [("variable", "i64"), ("coefficient", "i64")]), + ("ILP", [("variable", "i64"), ("coefficient", "i64"), ("bounds", "bounded")]), } register_ilp_pipeline! { ("UndirectedTwoCommodityIntegralFlow", []), - ("ILP", [("variable", "i64"), ("coefficient", "i64")]), + ("ILP", [("variable", "i64"), ("coefficient", "i64"), ("bounds", "bounded")]), } register_ilp_pipeline! { ("DecisionLongestCircuit", [("graph", "SimpleGraph"), ("weight", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("DecisionMaximum2Satisfiability", []), ("Maximum2Satisfiability", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("DecisionMaximumIndependentSet", [("graph", "SimpleGraph"), ("weight", "i64")]), ("MaximumIndependentSet", [("graph", "SimpleGraph"), ("weight", "i64")]), ("MaximumSetPacking", [("weight", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { @@ -871,66 +875,66 @@ register_ilp_pipeline! { ("MaximumIndependentSet", [("graph", "SimpleGraph"), ("weight", "One")]), ("MaximumIndependentSet", [("graph", "SimpleGraph"), ("weight", "i64")]), ("MaximumSetPacking", [("weight", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("DecisionMinimumCoveringByCliques", [("graph", "SimpleGraph")]), ("MinimumCoveringByCliques", [("graph", "SimpleGraph")]), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("DecisionMinimumSumMulticenter", [("graph", "SimpleGraph"), ("weight", "i64")]), ("MinimumSumMulticenter", [("graph", "SimpleGraph"), ("weight", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("DecisionOpenShopScheduling", []), - ("ILP", [("variable", "i64"), ("coefficient", "i64")]), + ("ILP", [("variable", "i64"), ("coefficient", "i64"), ("bounds", "bounded")]), } register_ilp_pipeline! { ("DecisionQUBO", [("weight", "i64")]), ("QUBO", [("weight", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("DecisionQuadraticAssignment", []), ("QuadraticAssignment", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("DecisionRuralPostman", [("graph", "SimpleGraph"), ("weight", "i64")]), ("RuralPostman", [("graph", "SimpleGraph"), ("weight", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "i64")]), + ("ILP", [("variable", "i64"), ("coefficient", "i64"), ("bounds", "bounded")]), } register_ilp_pipeline! { ("DecisionSequencingToMinimizeTardyTaskWeight", []), ("SequencingToMinimizeTardyTaskWeight", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("DecisionSpinGlass", [("graph", "SimpleGraph"), ("weight", "i64")]), ("SpinGlass", [("graph", "SimpleGraph"), ("weight", "i64")]), ("QUBO", [("weight", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("DecisionStackerCrane", []), ("StackerCrane", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { ("DecisionMinimumVertexCover", [("graph", "SimpleGraph"), ("weight", "One")]), ("MinimumVertexCover", [("graph", "SimpleGraph"), ("weight", "One")]), ("MinimumHittingSet", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } diff --git a/src/solvers/registry.rs b/src/solvers/registry.rs index c86d70ede..2fa82ac5f 100644 --- a/src/solvers/registry.rs +++ b/src/solvers/registry.rs @@ -50,12 +50,13 @@ impl ExactProblemKey { fn is_supported_ilp(&self) -> bool { self.name == "ILP" && matches!( - self.variant.get("variable").map(String::as_str), - Some("bool" | "i64") - ) - && matches!( - self.variant.get("coefficient").map(String::as_str), - Some("i64" | "f64") + ( + self.variant.get("variable").map(String::as_str), + self.variant.get("coefficient").map(String::as_str), + self.variant.get("bounds").map(String::as_str), + ), + (Some("bool" | "i64"), Some("i64" | "f64"), Some("general")) + | (Some("i64"), Some("i64"), Some("bounded")) ) } } diff --git a/src/types.rs b/src/types.rs index 03146feba..10d27bdcb 100644 --- a/src/types.rs +++ b/src/types.rs @@ -55,6 +55,41 @@ pub trait NumericSize: fn checked_mul_value(self, other: Self) -> Result; } +/// Smallest h >= 1 for which every finite input has magnitude below 2^h. +/// Halving preserves integer and floating power-of-two boundaries without +/// taking an absolute value (which would overflow for i64::MIN). +pub(crate) fn max_numeric_magnitude_bits( + values: impl IntoIterator, +) -> u64 { + fn bits(mut value: C) -> u64 { + let one = C::one(); + let two = one.clone() + one.clone(); + let negative = value < C::zero(); + let unit = if negative { C::zero() - one } else { one }; + let mut bits = 0; + while if negative { + value <= unit + } else { + value >= unit + } { + value = value / two.clone(); + bits += 1; + } + bits.max(1) + } + + // Only the extrema need bit counting; scanning all entries is linear. + let (mut minimum, mut maximum) = (C::zero(), C::zero()); + for value in values { + if value < minimum { + minimum = value; + } else if value > maximum { + maximum = value; + } + } + bits(minimum).max(bits(maximum)) +} + macro_rules! impl_integer_numeric_size { ($($type:ty),* $(,)?) => { $( diff --git a/src/unit_tests/example_db.rs b/src/unit_tests/example_db.rs index 168df6c82..a7fd51c9a 100644 --- a/src/unit_tests/example_db.rs +++ b/src/unit_tests/example_db.rs @@ -285,6 +285,7 @@ fn test_find_rule_example_integral_flow_bundles_to_ilp_contains_full_instances() variant: BTreeMap::from([ ("variable".to_string(), "i64".to_string()), ("coefficient".to_string(), "i64".to_string()), + ("bounds".to_string(), "bounded".to_string()), ]), }; @@ -315,6 +316,7 @@ fn test_find_rule_example_threedimensionalmatching_to_ilp_contains_full_instance variant: BTreeMap::from([ ("variable".to_string(), "bool".to_string()), ("coefficient".to_string(), "i64".to_string()), + ("bounds".to_string(), "general".to_string()), ]), }; diff --git a/src/unit_tests/ilp_overhead.rs b/src/unit_tests/ilp_overhead.rs new file mode 100644 index 000000000..320afb524 --- /dev/null +++ b/src/unit_tests/ilp_overhead.rs @@ -0,0 +1,796 @@ +//! Numeric parameter contracts and composed ILP/QUBO workflows. +use crate::models::algebraic::{Bounded, ILP, QUBO}; +use crate::models::graph::*; +use crate::models::misc::*; +use crate::parameters::ParameterRelation; +use crate::rules::{ReduceTo, ReductionGraph, ReductionPath, ReductionResult, ReductionStep}; +use crate::solvers::{BruteForce, BruteForceProblem, ILPSolver}; +use crate::topology::{DirectedGraph, SimpleGraph}; +use crate::{ + types::{Min, One}, + Problem, +}; + +type BoundedILP = ILP; + +#[test] +fn integer_knapsack_and_open_shop_bit_predictions_reach_qubo() { + use crate::models::{set::IntegerKnapsack, Decision}; + for capacity in [0, 2, 5] { + check_qubo::<_, BoundedILP>( + IntegerKnapsack::new(vec![2, 3], vec![3, 5], capacity).unwrap(), + ); + } + let source = OpenShopScheduling::new(1, vec![vec![2]]); + check_qubo::<_, BoundedILP>(source.clone()); + for bound in [1, 2] { + check_qubo::<_, BoundedILP>(Decision::new(source.clone(), bound)); + } +} + +#[test] +fn partition_open_shop_predictions_preserve_ilp_solution_recovery() { + use crate::models::Decision; + let graph = ReductionGraph::new(); + let path = ReductionPath { + steps: vec![ + step::(), + step::>(), + step::(), + ], + }; + for (sizes, feasible) in [(vec![1], false), (vec![1, 1], true), (vec![1, 3], false)] { + let source = Partition::new(sizes).unwrap(); + let predicted = graph + .compose_path_parameter_transform(&path) + .unwrap() + .unwrap() + .evaluate(&source.parameters()) + .unwrap(); + let chain = graph.reduce_along_path(&path, &source).unwrap().unwrap(); + let target = chain.target_problem::(); + for (field, actual) in target.parameters().iter() { + assert!(predicted.get(field).expect(field) >= actual); + } + match ILPSolver::new().solve(target) { + Ok(solution) => { + assert!(feasible); + let recovered = chain.extract_solution::, _>(&solution).unwrap(); + assert!(source.evaluate(&recovered).unwrap().0); + } + Err(crate::solvers::ILPSolveError::Infeasible) => assert!(!feasible), + Err(error) => panic!("{error}"), + } + } +} + +#[test] +fn partition_knapsack_qubo_predictions_and_solution_recovery() { + for sizes in [vec![1], vec![1, 1], vec![1, 2], vec![1, 3]] { + for through_ilp in [false, true] { + let mut steps = vec![step::(), step::()]; + if through_ilp { + steps.push(step::>()); + } + steps.push(step::>()); + check_path( + Partition::new(sizes.clone()).unwrap(), + ReductionPath { steps }, + ); + } + } + let path = ReductionPath { + steps: vec![step::(), step::(), step::>()], + }; + let source = Partition::new(vec![1 << 19; 10]).unwrap(); + let predicted = ReductionGraph::new() + .compose_path_parameter_transform(&path) + .unwrap() + .unwrap() + .evaluate(&source.parameters()) + .unwrap(); + assert!(predicted.get("num_vars").unwrap() <= 40); + assert!(predicted.get("num_quadratic_terms").unwrap() <= 1600); +} + +#[test] +fn subset_sum_lattice_qubo_predictions_and_solution_recovery() { + use crate::models::{algebraic::ClosestVectorProblem, Decision}; + for (sizes, target) in [(vec![1], 1), (vec![2], 1)] { + check_path( + SubsetSum::new(sizes, target), + ReductionPath { + steps: vec![ + step::(), + step::>(), + step::(), + step::>(), + ], + }, + ); + } +} + +#[test] +fn factoring_circuit_sat_qubo_predictions_and_solution_recovery() { + use crate::models::formula::{CircuitSAT, NAESatisfiability, Satisfiability}; + for target in [1u32, 3] { + check_path( + Factoring::with_factor_bits(target, 1, 1), + ReductionPath { + steps: vec![ + step::(), + step::(), + step::(), + step::(), + step::>(), + step::>(), + ], + }, + ); + } +} + +fn check_contract, T: Problem>(source: &S) { + let target = source.reduce_to().unwrap(); + let entry = crate::rules::registry::reduction_entries() + .into_iter() + .find(|entry| { + entry.source_name == S::NAME + && entry.target_name == T::NAME + && entry.source_variant() == S::variant() + && entry.target_variant() == T::variant() + }) + .unwrap(); + let contract = entry.parameter_contract().unwrap(); + assert!( + contract.unavailable().is_empty(), + "{} -> {}", + S::NAME, + T::NAME + ); + let transform = contract.transform().unwrap(); + let predicted = transform.evaluate(&source.parameters()).unwrap(); + for (field, actual) in target.target_problem().parameters().iter() { + let prediction = predicted.get(field).expect(field); + match transform.relation(field).unwrap() { + ParameterRelation::Exact => assert_eq!(prediction, actual, "{}: {field}", S::NAME), + ParameterRelation::UpperBound => assert!( + prediction >= actual, + "{}: {field}: {prediction} < {actual}", + S::NAME + ), + } + } +} + +fn step() -> ReductionStep { + ReductionStep { + name: P::NAME.into(), + variant: ReductionGraph::variant_to_map(&P::variant()), + } +} + +fn check_qubo(source: S) +where + S: BruteForceProblem + ReduceTo + 'static, + S::Solution: 'static, + S::Value: PartialEq + std::fmt::Debug + crate::types::SolutionAggregate + 'static, + T: Problem, +{ + check_contract::(&source); + let mut steps = vec![step::(), step::()]; + if T::variant() != ILP::::variant() { + steps.push(step::>()); + } + steps.push(step::>()); + check_path(source, ReductionPath { steps }); +} + +fn check_path(source: S, path: ReductionPath) +where + S: BruteForceProblem + 'static, + S::Solution: 'static, + S::Value: PartialEq + std::fmt::Debug + crate::types::SolutionAggregate + 'static, +{ + let graph = ReductionGraph::new(); + let predicted = graph + .compose_path_parameter_transform(&path) + .unwrap() + .unwrap() + .evaluate(&source.parameters()) + .unwrap(); + let chain = graph.reduce_along_path(&path, &source).unwrap().unwrap(); + let target = chain.target_problem::>(); + for (field, actual) in target.parameters().iter() { + assert!( + predicted.get(field).expect(field) >= actual, + "{}: {field}", + S::NAME + ); + } + let expected = BruteForce::new().solve(&source).unwrap(); + let solution = ILPSolver::new().solve(target).unwrap(); + let recovered = chain.extract_solution::(&solution); + match expected { + Some(witness) => assert_eq!( + source.evaluate(&recovered.unwrap()).unwrap(), + source.evaluate(&witness).unwrap() + ), + None => assert!(recovered.is_err()), + } +} + +#[test] +fn capacity_assignment_magnitude_and_qubo() { + for (delay, budget, bits) in [ + (0, 0, 1), + (7, 1, 3), + (8, 1, 4), + (1, 8, 4), + (-8, 1, 4), + (1, -8, 4), + (i64::MIN, 0, 64), + (0, i64::MIN, 64), + (i64::MAX, 0, 63), + ] { + let source = CapacityAssignment::new( + vec![i64::MAX], + vec![vec![i64::MAX]], + vec![vec![delay]], + budget, + ); + assert_eq!(source.parameters().get("max_delay_bits"), Some(bits)); + check_contract::<_, ILP>(&source); + } + check_contract::<_, ILP>(&CapacityAssignment::new(vec![1], vec![], vec![], 8)); + for budget in [-3, 0, 8] { + check_qubo::<_, ILP>(CapacityAssignment::new( + vec![1, 2], + vec![vec![1, 3]], + vec![vec![2, -2]], + budget, + )); + } +} + +#[test] +fn partially_ordered_knapsack_magnitude_and_qubo() { + for (weight, capacity, bits) in [ + (0, 0, 1), + (7, 1, 3), + (8, 1, 4), + (1, 8, 4), + (i64::MAX, 0, 63), + (0, i64::MAX, 63), + ] { + let source = PartiallyOrderedKnapsack::new(vec![weight], vec![i64::MAX], vec![], capacity); + assert_eq!(source.parameters().get("max_weight_bits"), Some(bits)); + check_contract::<_, ILP>(&source); + } + check_contract::<_, ILP>(&PartiallyOrderedKnapsack::new(vec![], vec![], vec![], 8)); + check_qubo::<_, ILP>(PartiallyOrderedKnapsack::new( + vec![2, 3], + vec![1, 9], + vec![(0, 1)], + 3, + )); +} + +#[test] +fn constrained_path_magnitude_and_qubo() { + for (weight, bound, bits) in [ + (1, 1, 1), + (8, 1, 4), + (1, 8, 4), + (i64::MAX, 1, 63), + (1, i64::MAX, 63), + ] { + let source = ShortestWeightConstrainedPath::new( + SimpleGraph::path(2), + vec![i64::MAX], + vec![weight], + 0, + 1, + bound, + ); + assert_eq!(source.parameters().get("max_weight_bits"), Some(bits)); + check_contract::<_, BoundedILP>(&source); + } + check_contract::<_, BoundedILP>(&ShortestWeightConstrainedPath::<_, i64>::new( + SimpleGraph::empty(8), + vec![], + vec![], + 0, + 0, + 1, + )); + for bound in [1, 2] { + check_qubo::<_, BoundedILP>(ShortestWeightConstrainedPath::new( + SimpleGraph::path(2), + vec![3], + vec![2], + 0, + 1, + bound, + )); + } +} + +#[test] +fn bounded_forest_magnitude_and_incoming_qubo() { + for (weights, bound, bits) in [ + (vec![], 8, 4), + (vec![8], 1, 4), + (vec![1], 8, 4), + (vec![i64::MAX], 1, 63), + (vec![1], i64::MAX, 63), + (vec![0; 8], 1, 1), + ] { + let source = BoundedComponentSpanningForest::new( + SimpleGraph::empty(weights.len()), + weights, + 1, + bound, + ); + assert_eq!(source.parameters().get("max_weight_bits"), Some(bits)); + check_contract::<_, BoundedILP>(&source); + } + check_qubo::<_, BoundedILP>(BoundedComponentSpanningForest::new( + SimpleGraph::path(2), + vec![1, 1], + 1, + 2, + )); + for graph in [SimpleGraph::empty(0), SimpleGraph::empty(3)] { + let source = PartitionIntoPathsOfLength2::new(graph); + check_contract::<_, BoundedComponentSpanningForest>(&source); + check_path( + source, + ReductionPath { + steps: vec![ + step::>(), + step::>(), + step::(), + step::>(), + step::>(), + ], + }, + ); + } +} + +#[test] +fn acyclic_partition_magnitude_and_qubo() { + for (weight, bound, bits) in [(0, 1, 1), (8, 1, 4), (1, -8, 4), (i64::MIN, 1, 64)] { + let source = AcyclicPartition::new(DirectedGraph::empty(1), vec![weight], vec![], bound, 0); + assert_eq!( + source.parameters().get("max_numeric_magnitude_bits"), + Some(bits) + ); + check_contract::<_, ILP>(&source); + } + check_contract::<_, ILP>(&AcyclicPartition::new( + DirectedGraph::new(1, vec![(0, 0)]), + vec![1], + vec![1], + 1, + 1, + )); + for bound in [1, 3] { + check_qubo::<_, ILP>(AcyclicPartition::new( + DirectedGraph::new(2, vec![(0, 1)]), + vec![1, 2], + vec![1], + bound, + 0, + )); + } + use crate::models::formula::{CNFClause, KSatisfiability}; + use crate::variant::K3; + for clauses in [ + vec![], + vec![ + CNFClause::new(vec![1, 1, 1]), + CNFClause::new(vec![-1, -1, -1]), + ], + vec![CNFClause::new(vec![1, 1, 1])], + ] { + check_contract::<_, AcyclicPartition>(&KSatisfiability::::new(1, clauses)); + } +} + +#[test] +fn branching_magnitude_and_qubo() { + for (weight, threshold, bits) in [(1, 8, 4), (8, 1, 4), (i64::MIN, 0, 64), (1, i64::MIN, 64)] { + let source = MultipleChoiceBranching::new( + DirectedGraph::new(2, vec![(0, 1)]), + vec![weight], + vec![vec![0]], + threshold, + ); + assert_eq!(source.parameters().get("max_weight_bits"), Some(bits)); + check_contract::<_, BoundedILP>(&source); + } + for threshold in [1, 3] { + check_qubo::<_, BoundedILP>(MultipleChoiceBranching::new( + DirectedGraph::new(2, vec![(0, 1)]), + vec![2], + vec![vec![0]], + threshold, + )); + } +} + +#[test] +fn capacitated_tree_magnitude_and_qubo() { + let source = MinimumCapacitatedSpanningTree::new( + SimpleGraph::path(3), + vec![i64::MAX; 2], + 0, + vec![0, 7, 7], + 8, + ); + assert_eq!(source.parameters().get("max_requirement_bits"), Some(4)); + check_contract::<_, BoundedILP>(&source); + for capacity in [1, 2] { + check_qubo::<_, BoundedILP>(MinimumCapacitatedSpanningTree::new( + SimpleGraph::path(2), + vec![3], + 0, + vec![0, 2], + capacity, + )); + } +} + +#[test] +fn multicenter_magnitude_products_and_qubo() { + let source = MinMaxMulticenter::new(SimpleGraph::path(3), vec![8; 3], vec![8; 2], 1); + assert_eq!( + source.parameters().get("max_numeric_magnitude_bits"), + Some(4) + ); + check_contract::<_, BoundedILP>(&source); + for graph in [SimpleGraph::path(2), SimpleGraph::empty(2)] { + let lengths = vec![2; crate::topology::Graph::num_edges(&graph)]; + check_qubo::<_, BoundedILP>(MinMaxMulticenter::new(graph, vec![2, 1], lengths, 1)); + } +} + +#[test] +fn multicenter_distance_overflow_is_typed() { + let source = MinMaxMulticenter::new(SimpleGraph::path(3), vec![1; 3], vec![i64::MAX; 2], 1); + assert!(matches!( + source.evaluate(&vec![true, false, false]), + Err(crate::traits::EvaluationError::IntegerOverflow(_)) + )); + assert!(matches!( + ReduceTo::::reduce_to(&source), + Err(crate::rules::ReductionError::IntegerOverflow { .. }) + )); +} + +#[test] +fn flow_shop_magnitude_and_qubo() { + for (time, deadline, bits) in [(1, 8, 4), (8, 1, 4), (0, 0, 1), (7, 7, 3)] { + let source = FlowShopScheduling::new(1, vec![vec![time]; 2], deadline); + assert_eq!(source.parameters().get("max_time_bits"), Some(bits)); + check_contract::<_, BoundedILP>(&source); + } + check_contract::<_, BoundedILP>(&FlowShopScheduling::new(0, vec![vec![]; 2], 8)); + for deadline in [1, 2] { + check_qubo::<_, BoundedILP>(FlowShopScheduling::new(1, vec![vec![1]; 2], deadline)); + } +} + +#[test] +fn minimum_tardiness_negative_deadlines_preserve_optimum() { + // Every schedule has exactly one tardy task, even for the smallest deadline. + for deadline in [-1, i64::MIN] { + let source = MinimumTardinessSequencing::::new(1, vec![deadline], vec![]); + assert_eq!(source.evaluate(&vec![0]).unwrap(), Min(Some(1))); + let reduced = ReduceTo::>::reduce_to(&source).unwrap(); + let witness = ILPSolver::new().solve(reduced.target_problem()).unwrap(); + assert_eq!( + source + .evaluate(&reduced.extract_solution(&witness).unwrap()) + .unwrap(), + Min(Some(1)) + ); + let source = + MinimumTardinessSequencing::::with_lengths(vec![2], vec![deadline], vec![]); + let reduced = ReduceTo::>::reduce_to(&source).unwrap(); + let witness = ILPSolver::new().solve(reduced.target_problem()).unwrap(); + assert_eq!( + source + .evaluate(&reduced.extract_solution(&witness).unwrap()) + .unwrap(), + Min(Some(1)) + ); + } +} + +#[test] +fn minimum_tardiness_magnitude_and_qubo() { + for deadline in [i64::MIN, 0, i64::MAX] { + check_qubo::<_, ILP>(MinimumTardinessSequencing::::new( + 2, + vec![deadline, 1], + vec![(0, 1)], + )); + let source = MinimumTardinessSequencing::::with_lengths( + vec![2, 1], + vec![deadline, 2], + vec![(0, 1)], + ); + assert_eq!(source.parameters().get("max_processing_time_bits"), Some(2)); + check_qubo::<_, ILP>(source); + } +} + +#[test] +fn completion_time_magnitude_and_qubo() { + let source = SchedulingToMinimizeWeightedCompletionTime::new(vec![7, 7], vec![i64::MAX; 2], 2); + assert_eq!(source.parameters().get("max_processing_time_bits"), Some(3)); + check_contract::<_, BoundedILP>(&source); + check_qubo::<_, BoundedILP>(SchedulingToMinimizeWeightedCompletionTime::new( + vec![1, 2], + vec![2, 1], + 2, + )); + let source = + SequencingToMinimizeWeightedCompletionTime::new(vec![7, 7], vec![i64::MAX; 2], vec![]); + assert_eq!(source.parameters().get("max_processing_time_bits"), Some(3)); + check_contract::<_, BoundedILP>(&source); + check_qubo::<_, BoundedILP>(SequencingToMinimizeWeightedCompletionTime::new( + vec![1, 2], + vec![1, 2], + vec![(0, 1)], + )); + let source = OptimalLinearArrangement::new(SimpleGraph::path(2)); + check_contract::<_, SequencingToMinimizeWeightedCompletionTime>(&source); + check_path( + source, + ReductionPath { + steps: vec![ + step::>(), + step::(), + step::(), + step::>(), + step::>(), + ], + }, + ); +} + +#[test] +fn cumulative_cost_magnitude_and_qubo() { + let source = SequencingToMinimizeMaximumCumulativeCost::new(vec![-8, 8], vec![]); + assert_eq!(source.parameters().get("max_cost_bits"), Some(4)); + check_contract::<_, BoundedILP>(&source); + for costs in [vec![-2, 3], vec![-2, -1], vec![]] { + check_qubo::<_, BoundedILP>(SequencingToMinimizeMaximumCumulativeCost::new( + costs, + vec![], + )); + } +} + +#[test] +fn tardy_task_weight_magnitude_and_incoming_qubo() { + for deadlines in [vec![i64::MIN; 2], vec![i64::MAX; 2]] { + let source = SequencingToMinimizeTardyTaskWeight::new(vec![-1, 2], vec![1, -1], deadlines); + assert_eq!(source.parameters().get("max_processing_time_bits"), Some(2)); + check_qubo::<_, ILP>(source); + } + use crate::models::Decision; + let source = Partition::new(vec![1, 1]).unwrap(); + check_contract::<_, Decision>(&source); + check_path( + source, + ReductionPath { + steps: vec![ + step::(), + step::>(), + step::(), + step::>(), + step::>(), + ], + }, + ); +} + +#[test] +fn weighted_tardiness_magnitude_and_qubo() { + for (length, weight, deadline, bound) in + [(8, 1, 1, 1), (1, 8, 1, 1), (1, 1, 8, 1), (1, 1, 1, 8)] + { + let source = SequencingToMinimizeWeightedTardiness::new( + vec![length], + vec![weight], + vec![deadline], + bound, + ); + assert_eq!( + source.parameters().get("max_numeric_magnitude_bits"), + Some(4) + ); + check_contract::<_, BoundedILP>(&source); + } + for bound in [0, 2] { + check_qubo::<_, BoundedILP>(SequencingToMinimizeWeightedTardiness::new( + vec![2], + vec![1], + vec![1], + bound, + )); + } +} + +#[test] +fn setup_time_magnitude_and_qubo() { + for (length, deadline, setup) in [(8, 1, 1), (1, 8, 1), (1, 1, 8)] { + let source = SequencingWithDeadlinesAndSetUpTimes::new( + vec![length; 2], + vec![deadline; 2], + vec![0, 1], + vec![setup; 2], + ); + assert_eq!(source.parameters().get("max_time_bits"), Some(4)); + check_contract::<_, ILP>(&source); + } + for deadline in [2, 3] { + check_qubo::<_, ILP>(SequencingWithDeadlinesAndSetUpTimes::new( + vec![1; 2], + vec![deadline; 2], + vec![0, 1], + vec![1; 2], + )); + } +} + +#[test] +fn resource_scheduling_packs_slots_and_ignores_excess_processors() { + let source = ResourceConstrainedScheduling::new(2, vec![2], vec![vec![1]; 2], 1000).unwrap(); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + assert!(reduction.target_problem().num_vars() <= 4); + // These instances must not allocate or iterate through the numeric deadline. + let source = + ResourceConstrainedScheduling::new(usize::MAX, vec![2], vec![vec![1]; 2], i64::MAX) + .unwrap(); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + assert!(reduction.target_problem().num_vars() <= 4); + assert!( + source + .evaluate(&vec![0, (i64::MAX - 1) as usize]) + .unwrap() + .0 + ); + let witness = ILPSolver::new().solve(reduction.target_problem()).unwrap(); + assert!( + source + .evaluate(&reduction.extract_solution(&witness).unwrap()) + .unwrap() + .0 + ); + let empty = ResourceConstrainedScheduling::new(0, vec![0], vec![], i64::MAX).unwrap(); + assert!(empty.evaluate(&vec![]).unwrap().0); + assert_eq!( + ReduceTo::>::reduce_to(&empty) + .unwrap() + .target_problem() + .num_vars(), + 0 + ); +} + +#[test] +fn resource_magnitude_and_incoming_predictions() { + for (requirement, bound, bits) in [ + (0, 0, 1), + (8, 1, 4), + (1, 8, 4), + (i64::MAX, 0, 63), + (0, i64::MAX, 63), + ] { + let source = + ResourceConstrainedScheduling::new(2, vec![bound], vec![vec![requirement]], 2).unwrap(); + assert_eq!(source.parameters().get("max_resource_bits"), Some(bits)); + check_contract::<_, ILP>(&source); + } + for processors in [0, 1, 2] { + check_qubo::<_, ILP>( + ResourceConstrainedScheduling::new(processors, vec![1], vec![vec![1]; 2], 2).unwrap(), + ); + } + let source = ThreePartition::new(vec![1; 3], 3); + check_contract::<_, ResourceConstrainedScheduling>(&source); + check_path( + source, + ReductionPath { + steps: vec![ + step::(), + step::(), + step::>(), + step::>(), + ], + }, + ); + use crate::models::set::ThreeDimensionalMatching; + for (size, triples) in [ + (0, vec![]), + (1, vec![]), + (1, vec![(0, 0, 0)]), + (2, vec![(0, 0, 0), (1, 1, 1)]), + ] { + let source = ThreeDimensionalMatching::new(size, triples); + check_contract::<_, ThreePartition>(&source); + let path = ReductionPath { + steps: vec![ + step::(), + step::(), + step::(), + step::>(), + step::>(), + ], + }; + let predicted = ReductionGraph::new() + .compose_path_parameter_transform(&path) + .unwrap() + .unwrap() + .evaluate(&source.parameters()) + .unwrap(); + assert!(predicted.get("num_vars").is_some()); + assert!(predicted.get("num_quadratic_terms").is_some()); + } +} + +#[test] +fn circuit_threshold_normalization_and_incoming_qubo() { + use crate::models::Decision; + for threshold in [i64::MIN, 0, 3, 4, i64::MAX] { + let source = Decision::new( + LongestCircuit::new(SimpleGraph::cycle(3), vec![1i64; 3]), + threshold, + ); + assert_eq!(source.parameters().get("max_length_bits"), Some(1)); + check_contract::<_, ILP>(&source); + let reduced = ReduceTo::>::reduce_to(&source).unwrap(); + assert!(reduced.target_problem().max_constraint_magnitude_bits() <= 5); + assert_eq!( + ILPSolver::new().solve(reduced.target_problem()).is_ok(), + threshold <= 3 + ); + } + let source = HamiltonianCircuit::new(SimpleGraph::cycle(3)); + check_contract::<_, Decision>>(&source); + check_path( + source, + ReductionPath { + steps: vec![ + step::>(), + step::>>(), + step::>(), + step::>(), + ], + }, + ); +} + +#[test] +fn multiprocessor_predictions_compose_and_recover_through_qubo() { + for sizes in [vec![1, 1], vec![3; 4], vec![1, 2], vec![2]] { + check_path( + Partition::new(sizes).unwrap(), + ReductionPath { + steps: vec![ + step::(), + step::(), + step::>(), + step::>(), + ], + }, + ); + } +} diff --git a/src/unit_tests/models/algebraic/ilp.rs b/src/unit_tests/models/algebraic/ilp.rs index 64bdc8143..886896488 100644 --- a/src/unit_tests/models/algebraic/ilp.rs +++ b/src/unit_tests/models/algebraic/ilp.rs @@ -3,6 +3,135 @@ use crate::solvers::{ILPSolveError, ILPSolver}; use crate::traits::Problem; use crate::types::Extremum; +#[test] +fn constraint_magnitude_bits_measure_normalized_integer_data() { + for (value, expected) in [ + (0, 1), + (1, 1), + (-1, 1), + (2, 2), + (-2, 2), + (7, 3), + (8, 4), + ((1_i64 << 54) - 1, 54), + (i64::MAX, 63), + (i64::MIN, 64), + ] { + for row in [ + LinearConstraint::le(vec![(0, value)], 0), + LinearConstraint::ge(vec![], value), + ] { + let source = binary_ilp(1, vec![row], vec![(0, i64::MAX)], ObjectiveSense::Minimize); + assert_eq!( + source.parameters().get("max_constraint_magnitude_bits"), + Some(expected), + "{value}" + ); + } + } + let source = binary_ilp( + 1, + vec![LinearConstraint::le( + vec![(0, 100), (0, -100), (0, 3), (0, 5)], + 0, + )], + vec![], + ObjectiveSense::Minimize, + ); + assert_eq!( + source.parameters().get("max_constraint_magnitude_bits"), + Some(4) + ); +} + +#[test] +fn constraint_magnitude_bits_include_finite_endpoints_without_requiring_boundedness() { + for (lower, upper, expected) in [ + (None, None, 1), + (Some(-8), None, 4), + (None, Some(16), 5), + (Some(i64::MIN), Some(i64::MAX), 64), + ] { + let source = ILP::::with_variables( + vec![IntegerVariable::new(lower, upper).unwrap()], + vec![], + vec![(0, i64::MAX)], + ObjectiveSense::Minimize, + ) + .unwrap(); + assert_eq!( + source.parameters().get("max_constraint_magnitude_bits"), + Some(expected) + ); + } + assert_eq!( + ILP::::empty() + .parameters() + .get("max_constraint_magnitude_bits"), + Some(1) + ); +} + +#[test] +fn constraint_magnitude_bits_cover_float_exponents_without_rounding_boundaries() { + for (value, expected) in [ + (0.0, 1), + (-0.0, 1), + (f64::from_bits(1), 1), + (0.5, 1), + (f64::from_bits(8.0_f64.to_bits() - 1), 3), + (8.0, 4), + (-8.0, 4), + (f64::MAX, 1024), + (-f64::MAX, 1024), + ] { + for row in [ + LinearConstraint::le(vec![(0, value)], 0.0), + LinearConstraint::ge(vec![], value), + ] { + let source = + ILP::::new(1, vec![row], vec![(0, f64::MAX)], ObjectiveSense::Minimize) + .unwrap(); + assert_eq!( + source.parameters().get("max_constraint_magnitude_bits"), + Some(expected), + "{value}" + ); + } + } +} + +#[test] +fn bounded_integer_ilp_loading_requires_finite_domains() { + assert!(ILP::::new(1, vec![], vec![], ObjectiveSense::Minimize).is_ok()); + assert!(ILP::::new(1, vec![], vec![], ObjectiveSense::Minimize).is_err()); + assert_eq!(ILP::::empty().num_vars(), 0); + let variant = crate::rules::ReductionGraph::variant_to_map(&[ + ("variable", "i64"), + ("coefficient", "i64"), + ("bounds", "bounded"), + ]); + let instance = serde_json::json!({ + "variables": [{"lower_bound": -2, "upper_bound": 3}], + "constraints": [], "objective": [[0, 1]], "sense": "Maximize" + }); + let loaded = crate::registry::load_dyn("ILP", &variant, instance.clone()) + .expect("bounded integer ILP must be registered"); + assert_eq!(loaded.serialize_json(), instance); + for (lower, upper) in [(None, Some(3)), (Some(-2), None), (None, None)] { + let mut invalid = instance.clone(); + invalid["variables"] = serde_json::json!([{"lower_bound": lower, "upper_bound": upper}]); + assert!(crate::registry::load_dyn("ILP", &variant, invalid).is_err()); + assert!(ILP::::with_variables( + vec![IntegerVariable::new(lower, upper).unwrap()], + vec![], + vec![], + ObjectiveSense::Minimize, + ) + .is_err()); + } +} + fn binary_ilp( num_vars: usize, constraints: Vec, @@ -16,7 +145,11 @@ fn binary_ilp( fn ilp_variant_identifies_variable_domain() { assert_eq!( as Problem>::variant(), - vec![("variable", "bool"), ("coefficient", "i64")] + vec![ + ("variable", "bool"), + ("coefficient", "i64"), + ("bounds", "general") + ] ); } @@ -24,7 +157,11 @@ fn ilp_variant_identifies_variable_domain() { fn ilp_variant_identifies_float_coefficients() { assert_eq!( as Problem>::variant(), - vec![("variable", "bool"), ("coefficient", "f64")] + vec![ + ("variable", "bool"), + ("coefficient", "f64"), + ("bounds", "general") + ] ); } diff --git a/src/unit_tests/models/graph/acyclic_partition.rs b/src/unit_tests/models/graph/acyclic_partition.rs index 85dd33d8b..1d00e1008 100644 --- a/src/unit_tests/models/graph/acyclic_partition.rs +++ b/src/unit_tests/models/graph/acyclic_partition.rs @@ -243,7 +243,10 @@ fn test_acyclic_partition_declares_problem_parameters() { .iter() .copied() .collect(); - assert_eq!(fields, HashSet::from(["num_vertices", "num_arcs"])); + assert_eq!( + fields, + HashSet::from(["num_vertices", "num_arcs", "max_numeric_magnitude_bits"]) + ); } #[test] fn create_spec_maps_weight_inputs_to_canonical_fields() { diff --git a/src/unit_tests/models/graph/integral_flow_with_multipliers.rs b/src/unit_tests/models/graph/integral_flow_with_multipliers.rs index f3caf8687..b1af8705e 100644 --- a/src/unit_tests/models/graph/integral_flow_with_multipliers.rs +++ b/src/unit_tests/models/graph/integral_flow_with_multipliers.rs @@ -146,10 +146,32 @@ fn test_integral_flow_with_multipliers_problem_name_and_parameters() { .collect(); assert_eq!( fields, - HashSet::from(["max_capacity", "num_arcs", "num_vertices", "requirement"]) + HashSet::from([ + "max_capacity", + "max_capacity_bits", + "num_arcs", + "num_vertices" + ]) ); } +#[test] +fn negative_requirement_has_nonnegative_prediction_parameters() { + let source = IntegralFlowWithMultipliers::new( + DirectedGraph::new(2, vec![(0, 1)]), + 0, + 1, + vec![1, 1], + vec![1], + i64::MIN, + ); + let restored: IntegralFlowWithMultipliers = + serde_json::from_value(serde_json::to_value(&source).unwrap()).unwrap(); + assert_eq!(restored.requirement(), i64::MIN); + assert!(restored.evaluate(&vec![0]).unwrap().0); + assert_eq!(restored.parameters().get("max_capacity_bits"), Some(1)); +} + #[cfg(feature = "example-db")] #[test] fn test_integral_flow_with_multipliers_canonical_example_spec() { diff --git a/src/unit_tests/models/graph/undirected_flow_lower_bounds.rs b/src/unit_tests/models/graph/undirected_flow_lower_bounds.rs index b230e7b65..3c5ef89f9 100644 --- a/src/unit_tests/models/graph/undirected_flow_lower_bounds.rs +++ b/src/unit_tests/models/graph/undirected_flow_lower_bounds.rs @@ -19,6 +19,8 @@ fn test_undirected_flow_lower_bounds_invalid_inputs() { ("requirement", serde_json::json!(0)), ("requirement", serde_json::json!(-1)), ("lower_bounds", serde_json::json!([3, 1, 0, 0, 1, 0, 1])), + ("lower_bounds", serde_json::json!([-1, 1, 0, 0, 1, 0, 1])), + ("capacities", serde_json::json!([-1, 2, 2, 2, 1, 3, 2])), ] { let mut invalid = valid.clone(); invalid[field] = value; diff --git a/src/unit_tests/models/misc/bin_packing.rs b/src/unit_tests/models/misc/bin_packing.rs index b0a8e77eb..9564a2dc0 100644 --- a/src/unit_tests/models/misc/bin_packing.rs +++ b/src/unit_tests/models/misc/bin_packing.rs @@ -154,3 +154,37 @@ fn test_bin_packing_rejects_non_finite_values() { assert!(BinPacking::new(vec![f64::NAN], 1.0).is_err()); assert!(BinPacking::new(vec![1.0], f64::INFINITY).is_err()); } + +#[test] +fn numeric_magnitude_bits_cover_sizes_capacity_and_float_boundaries() { + for (sizes, capacity, expected) in [ + (vec![], 0_i64, 1), + (vec![1], 8, 4), + (vec![8], 1, 4), + (vec![(1_i64 << 54) - 1], 1, 54), + (vec![i64::MIN], 1, 64), + (vec![1], i64::MAX, 63), + ] { + let source = BinPacking::new(sizes, capacity).unwrap(); + assert_eq!( + source.parameters().get("max_numeric_magnitude_bits"), + Some(expected) + ); + } + for (sizes, capacity, expected) in [ + (vec![f64::from_bits(1)], 0.5, 1), + (vec![], f64::from_bits(8.0_f64.to_bits() - 1), 3), + (vec![-8.0], 1.0, 4), + (vec![1.0], f64::MAX, 1024), + ] { + let source = BinPacking::new(sizes, capacity).unwrap(); + assert_eq!( + source.parameters().get("max_numeric_magnitude_bits"), + Some(expected) + ); + } + assert_eq!( + BinPacking::::parameter_names(), + BinPacking::::parameter_names() + ); +} diff --git a/src/unit_tests/models/misc/partition.rs b/src/unit_tests/models/misc/partition.rs index f6a969235..9177e1341 100644 --- a/src/unit_tests/models/misc/partition.rs +++ b/src/unit_tests/models/misc/partition.rs @@ -121,3 +121,20 @@ fn test_partition_rejects_zero_size() { fn test_partition_rejects_empty_input() { assert!(Partition::new(vec![]).is_err()); } + +#[test] +fn numeric_magnitude_bits_measure_elements_without_summing_them() { + for (sizes, expected) in [ + (vec![1, 1], 1), + (vec![7, 1], 3), + (vec![8, 1], 4), + (vec![(1_i64 << 54) - 1], 54), + (vec![i64::MAX], 63), + ] { + let source = Partition::new(sizes).unwrap(); + assert_eq!( + source.parameters().get("max_numeric_magnitude_bits"), + Some(expected) + ); + } +} diff --git a/src/unit_tests/models/misc/subset_sum.rs b/src/unit_tests/models/misc/subset_sum.rs index b9686cc93..fffee4f93 100644 --- a/src/unit_tests/models/misc/subset_sum.rs +++ b/src/unit_tests/models/misc/subset_sum.rs @@ -202,3 +202,22 @@ fn test_subsetsum_large_integer_input() { .evaluate(&vec![true, true, false, false, false, false]) .unwrap()); // 3 + 7 = 10 } + +#[test] +fn numeric_magnitude_bits_include_arbitrary_precision_sizes_and_target() { + let huge = BigUint::from(1_u8) << 1000_usize; + for (sizes, target, expected) in [ + (vec![], BigUint::from(0_u8), 1), + (vec![BigUint::from(7_u8)], BigUint::from(0_u8), 3), + (vec![BigUint::from(1_u8)], BigUint::from(8_u8), 4), + (vec![&huge - 1_u8], BigUint::from(0_u8), 1000), + (vec![huge.clone()], BigUint::from(0_u8), 1001), + (vec![BigUint::from(1_u8)], huge, 1001), + ] { + let source = SubsetSum::new(sizes, target); + assert_eq!( + source.parameters().get("max_numeric_magnitude_bits"), + Some(expected) + ); + } +} diff --git a/src/unit_tests/parameter_formula_validation.rs b/src/unit_tests/parameter_formula_validation.rs index 00ade3b27..68655d59f 100644 --- a/src/unit_tests/parameter_formula_validation.rs +++ b/src/unit_tests/parameter_formula_validation.rs @@ -138,8 +138,8 @@ fn source_for( } #[test] -fn integer_ilp_reductions_support_binary_encoding() { - use crate::models::algebraic::ILP; +fn bounded_ilp_reductions_support_binary_encoding() { + use crate::models::algebraic::{Bounded, ILP}; use crate::rules::ReduceTo; use crate::traits::Problem; @@ -147,7 +147,9 @@ fn integer_ilp_reductions_support_binary_encoding() { let mut failures = Vec::new(); let mut checked = 0; for entry in crate::rules::registry::reduction_entries() { - if entry.target_name != "ILP" || entry.target_variant() != ILP::::variant() { + if entry.target_name != "ILP" + || entry.target_variant() != ILP::::variant() + { continue; } let result = (|| { @@ -156,7 +158,7 @@ fn integer_ilp_reductions_support_binary_encoding() { entry.reduce_fn.unwrap()(source.as_any()).map_err(|error| error.to_string())?; let integer = reduced .target_problem_any() - .downcast_ref::>() + .downcast_ref::>() .unwrap(); ReduceTo::>::reduce_to(integer).map_err(|error| error.to_string())?; Ok::<_, String>(()) diff --git a/src/unit_tests/problem_parameters.rs b/src/unit_tests/problem_parameters.rs index 200f0a081..778b89fae 100644 --- a/src/unit_tests/problem_parameters.rs +++ b/src/unit_tests/problem_parameters.rs @@ -200,6 +200,7 @@ fn test_problem_parameters_biclique_cover() { let size = bc.parameters(); assert_eq!(size.get("left_size"), Some(2)); assert_eq!(size.get("right_size"), Some(3)); + assert_eq!(size.get("num_vertices"), Some(5)); assert_eq!(size.get("num_edges"), Some(3)); assert_eq!(size.get("rank"), Some(2)); } diff --git a/src/unit_tests/reduction_graph.rs b/src/unit_tests/reduction_graph.rs index 0370dbb4b..f3bbca2af 100644 --- a/src/unit_tests/reduction_graph.rs +++ b/src/unit_tests/reduction_graph.rs @@ -11,6 +11,162 @@ use crate::types::ProblemParameters; use crate::variant::{K3, KN}; use std::collections::BTreeMap; +#[test] +fn domination_magnitude_predictions_account_for_parallel_edges() { + let source = MinimumDominatingSet::new(SimpleGraph::new(2, vec![(0, 1); 8]), vec![1_i64; 2]); + let target = ReduceTo::>::reduce_to(&source).unwrap(); + assert_eq!( + target + .target_problem() + .parameters() + .get("max_constraint_magnitude_bits"), + Some(4) + ); + let entries = crate::rules::registry::reduction_entries(); + let entry = entries + .iter() + .find(|entry| entry.source_name == "MinimumDominatingSet" && entry.target_name == "ILP") + .unwrap(); + let contract = entry.parameter_contract().unwrap(); + let predicted = contract + .transform() + .unwrap() + .evaluate(&source.parameters()) + .unwrap(); + assert!(predicted.get("max_constraint_magnitude_bits").unwrap() >= 4); +} + +#[test] +fn decision_ilp_contracts_cover_numeric_bounds() { + for name in ["DecisionLongestCircuit", "DecisionOpenShopScheduling"] { + let entry = crate::rules::registry::reduction_entries() + .into_iter() + .find(|entry| entry.source_name == name && entry.target_name == "ILP") + .unwrap(); + let contract = entry.parameter_contract().unwrap(); + let transform = contract.transform().unwrap(); + assert!(contract.unavailable().is_empty(), "{name}"); + for field in [ + "max_constraint_magnitude_bits", + "num_vars", + "num_constraints", + "num_nonzeros", + ] { + assert!(transform.get(field).is_some(), "{name}: {field}"); + } + } +} + +#[test] +fn bounded_ilp_size_predictions_compose_through_translated_domains() { + use crate::models::algebraic::{Bounded, IntegerVariable, LinearConstraint, ObjectiveSense}; + let path = ReductionPath { + steps: vec![ + ReductionStep { + name: "ILP".into(), + variant: ReductionGraph::variant_to_map(&ILP::::variant()), + }, + ReductionStep { + name: "ILP".into(), + variant: ReductionGraph::variant_to_map(&ILP::::variant()), + }, + ReductionStep { + name: "QUBO".into(), + variant: ReductionGraph::variant_to_map(&QUBO::::variant()), + }, + ], + }; + let graph = ReductionGraph::new(); + let transform = graph + .compose_path_parameter_transform(&path) + .unwrap() + .unwrap(); + for lower in [-1, -8] { + let source = ILP::::with_variables( + vec![IntegerVariable::new(Some(lower), Some(lower + 1)).unwrap()], + vec![LinearConstraint::le(vec![(0, 1)], 0)], + vec![(0, 1)], + ObjectiveSense::Minimize, + ) + .unwrap(); + let binary = ReduceTo::>::reduce_to(&source).unwrap(); + let qubo = ReduceTo::>::reduce_to(binary.target_problem()).unwrap(); + let predicted = transform.evaluate(&source.parameters()).unwrap(); + for (field, actual) in qubo.target_problem().parameters().iter() { + assert!(predicted.get(field).expect("composed size bound") >= actual); + } + let solution = BruteForce::new() + .solve(qubo.target_problem()) + .unwrap() + .unwrap(); + let recovered = binary + .extract_solution(&qubo.extract_solution(&solution).unwrap()) + .unwrap(); + assert_eq!(recovered, vec![lower]); + } +} + +#[test] +fn weighted_qubo_round_trip_size_prediction_uses_only_structural_parameters() { + let path = ReductionPath { + steps: vec![ + ReductionStep { + name: "QUBO".into(), + variant: ReductionGraph::variant_to_map(&QUBO::::variant()), + }, + ReductionStep { + name: "ILP".into(), + variant: ReductionGraph::variant_to_map(&ILP::::variant()), + }, + ReductionStep { + name: "QUBO".into(), + variant: ReductionGraph::variant_to_map(&QUBO::::variant()), + }, + ], + }; + let transform = ReductionGraph::new() + .compose_path_parameter_transform(&path) + .unwrap() + .unwrap(); + for weight in [1, 1000] { + let source = QUBO::from_matrix(vec![vec![-weight, 1], vec![0, -weight]]).unwrap(); + let ilp = ReduceTo::>::reduce_to(&source).unwrap(); + let qubo = ReduceTo::>::reduce_to(ilp.target_problem()).unwrap(); + let predicted = transform.evaluate(&source.parameters()).unwrap(); + // Three binary ILP variables and three unit-magnitude rows give U=15. + assert_eq!(predicted.get("num_vars"), Some(15)); + assert_eq!(predicted.get("num_quadratic_terms"), Some(225)); + for (field, actual) in qubo.target_problem().parameters().iter() { + assert!(predicted.get(field).unwrap() >= actual); + } + } +} + +#[test] +fn integer_ilp_graph_requires_bounded_domains_for_binary_encoding() { + let graph = ReductionGraph::new(); + let general = ReductionGraph::variant_to_map(&ILP::::variant()); + let binary = ReductionGraph::variant_to_map(&ILP::::variant()); + assert!(graph + .find_all_paths("ILP", &general, "ILP", &binary) + .is_empty()); + let bounded = ReductionGraph::variant_to_map(&[ + ("variable", "i64"), + ("coefficient", "i64"), + ("bounds", "bounded"), + ]); + for (source, target) in [ + (&bounded, &binary), + (&bounded, &general), + (&binary, &bounded), + ] { + assert!(graph + .find_all_paths("ILP", source, "ILP", target) + .iter() + .any(|path| path.len() == 1)); + } +} + #[test] fn exact_transform_evaluates_without_path_ranking() { let graph = ReductionGraph::new(); @@ -1135,3 +1291,357 @@ fn test_find_paths_bounded_returns_shortest_when_truncated() { ); assert_eq!(lens, vec![1, 4]); } + +#[test] +fn knapsack_normalization_restores_composed_qubo_predictions() { + use crate::models::misc::Knapsack; + let path = ReductionPath { + steps: vec![ + ReductionStep { + name: Knapsack::NAME.into(), + variant: ReductionGraph::variant_to_map(&Knapsack::variant()), + }, + ReductionStep { + name: ILP::::NAME.into(), + variant: ReductionGraph::variant_to_map(&ILP::::variant()), + }, + ReductionStep { + name: QUBO::::NAME.into(), + variant: ReductionGraph::variant_to_map(&QUBO::::variant()), + }, + ], + }; + let transform = ReductionGraph::new() + .compose_path_parameter_transform(&path) + .unwrap() + .unwrap(); + for capacity in [0, 1] { + let source = Knapsack::new(vec![0, 1, i64::MAX], vec![2, 3, 4], capacity); + let predicted = transform.evaluate(&source.parameters()).unwrap(); + let ilp = ReduceTo::>::reduce_to(&source).unwrap(); + let qubo = ReduceTo::>::reduce_to(ilp.target_problem()).unwrap(); + for (field, actual) in qubo.target_problem().parameters().iter() { + assert!(predicted.get(field).unwrap() >= actual); + } + } +} + +#[test] +fn numeric_magnitude_bits_propagate_from_sat_to_qubo() { + use crate::models::formula::CNFClause; + use crate::models::misc::{BinPacking, Partition, SubsetSum}; + let path = ReductionPath { + steps: [ + ( + KSatisfiability::::NAME, + KSatisfiability::::variant(), + ), + (SubsetSum::NAME, SubsetSum::variant()), + (Partition::NAME, Partition::variant()), + (BinPacking::::NAME, BinPacking::::variant()), + (ILP::::NAME, ILP::::variant()), + (QUBO::::NAME, QUBO::::variant()), + ] + .into_iter() + .map(|(name, variant)| ReductionStep { + name: name.into(), + variant: ReductionGraph::variant_to_map(&variant), + }) + .collect(), + }; + let graph = ReductionGraph::new(); + let transform = graph + .compose_path_parameter_transform(&path) + .unwrap() + .unwrap(); + for source in [ + KSatisfiability::::new(1, vec![]), + KSatisfiability::::new(3, vec![CNFClause::new(vec![1, 2, 3])]), + ] { + let predicted = transform.evaluate(&source.parameters()).unwrap(); + let chain = graph.reduce_along_path(&path, &source).unwrap().unwrap(); + let target = chain.target_problem::>(); + for (field, actual) in target.parameters().iter() { + assert!(predicted.get(field).unwrap() >= actual); + } + if source.num_vars() == 1 { + let solution = BruteForce::new().solve(target).unwrap().unwrap(); + let recovered: Vec = chain.extract_solution(&solution).unwrap(); + assert!(source.evaluate(&recovered).unwrap().0); + } + } +} + +#[test] +fn numeric_magnitude_bits_cover_padding_and_empty_targets() { + use crate::models::misc::{BinPacking, Partition, SubsetSum}; + fn check(source: S) + where + S: Problem + ReduceTo, + T: Problem, + { + let reduction = source.reduce_to().unwrap(); + let entry = crate::rules::registry::reduction_entries() + .into_iter() + .find(|entry| { + entry.source_name == S::NAME + && entry.source_variant() == S::variant() + && entry.target_name == T::NAME + && entry.target_variant() == T::variant() + }) + .unwrap(); + let contract = entry.parameter_contract().unwrap(); + let predicted = contract + .transform() + .unwrap() + .evaluate(&source.parameters()) + .unwrap(); + for (field, actual) in reduction.target_problem().parameters().iter() { + assert!( + predicted.get(field).unwrap() >= actual, + "{} -> {}: {field}", + S::NAME, + T::NAME + ); + } + } + for target in [0_u32, 1, 2, 1000] { + check::<_, Partition>(SubsetSum::new(vec![1_u32, 1], target)); + } + for sizes in [vec![1], vec![1, 1], vec![7, 1], vec![7, 7, 7, 7]] { + check::<_, SubsetSum>(Partition::new(sizes.clone()).unwrap()); + check::<_, BinPacking>(Partition::new(sizes).unwrap()); + } + check::<_, ILP>(BinPacking::new(Vec::::new(), i64::MAX).unwrap()); + check::<_, ILP>(BinPacking::new(vec![8_i64], 1).unwrap()); +} + +#[test] +fn flow_capacity_bits_bound_ilp_parameters_at_numeric_boundaries() { + use crate::models::algebraic::Bounded; + use crate::models::graph::{ + IntegralFlowBundles, IntegralFlowWithMultipliers, UndirectedFlowLowerBounds, + UndirectedTwoCommodityIntegralFlow, + }; + use crate::topology::DirectedGraph; + + fn check>>(source: S, bits: u64) { + assert_eq!( + source.parameters().get("max_capacity_bits"), + Some(bits), + "{}", + S::NAME + ); + let reduction = source.reduce_to().unwrap(); + let entry = crate::rules::registry::reduction_entries() + .into_iter() + .find(|entry| entry.source_name == S::NAME && entry.target_name == "ILP") + .unwrap(); + let contract = entry.parameter_contract().unwrap(); + let predicted = contract + .transform() + .unwrap() + .evaluate(&source.parameters()) + .unwrap(); + for (field, actual) in reduction.target_problem().parameters().iter() { + assert!( + predicted.get(field).expect("complete flow bound") >= actual, + "{}: {field}", + S::NAME + ); + } + } + for (capacity, bits) in [(0, 1), (1, 1), (7, 3), (8, 4), (i64::MAX, 63)] { + if capacity > 0 { + check( + IntegralFlowBundles::new( + DirectedGraph::new(2, vec![(0, 1)]), + 0, + 1, + vec![vec![0]], + vec![capacity], + i64::MAX, + ), + bits, + ); + } + check( + IntegralFlowWithMultipliers::new( + DirectedGraph::new(3, vec![(0, 1), (1, 2)]), + 0, + 2, + vec![1, i64::MAX, 1], + vec![capacity; 2], + i64::MAX, + ), + bits, + ); + check( + UndirectedFlowLowerBounds::new( + SimpleGraph::new(2, vec![(0, 1)]), + vec![capacity], + vec![capacity], + 0, + 1, + i64::MAX, + ), + bits, + ); + check( + UndirectedTwoCommodityIntegralFlow::new( + SimpleGraph::new(2, vec![(0, 1)]), + vec![capacity], + 0, + 1, + 0, + 1, + i64::MIN, + i64::MAX, + ), + bits, + ); + } + check( + IntegralFlowBundles::new( + DirectedGraph::new(2, vec![]), + 0, + 1, + vec![], + vec![], + i64::MAX, + ), + 1, + ); +} + +#[test] +fn incoming_flow_predictions_compose_to_qubo_and_recover_witnesses() { + use crate::models::algebraic::Bounded; + use crate::models::graph::{IntegralFlowBundles, IntegralFlowWithMultipliers}; + use crate::models::misc::Partition; + + fn check(source: S, expected: bool) + where + S: Problem + 'static, + S::Solution: 'static, + F: Problem, + { + let path = ReductionPath { + steps: [ + (S::NAME, S::variant()), + (F::NAME, F::variant()), + ( + ILP::::NAME, + ILP::::variant(), + ), + (ILP::::NAME, ILP::::variant()), + (QUBO::::NAME, QUBO::::variant()), + ] + .into_iter() + .map(|(name, variant)| ReductionStep { + name: name.into(), + variant: ReductionGraph::variant_to_map(&variant), + }) + .collect(), + }; + let graph = ReductionGraph::new(); + let predicted = graph + .compose_path_parameter_transform(&path) + .unwrap() + .unwrap() + .evaluate(&source.parameters()) + .unwrap(); + let chain = graph.reduce_along_path(&path, &source).unwrap().unwrap(); + let target = chain.target_problem::>(); + for (field, actual) in target.parameters().iter() { + assert!(predicted.get(field).expect("composed flow bound") >= actual); + } + let solution = crate::solvers::ILPSolver::new().solve(target).unwrap(); + let recovered = chain.extract_solution::(&solution); + if expected { + assert!(source.evaluate(&recovered.unwrap()).unwrap().0); + } else { + assert!( + recovered.is_err(), + "infeasible flow must not decode a witness" + ); + } + } + for (sizes, feasible) in [(vec![1, 1], true), (vec![1, 2], false), (vec![2], false)] { + check::<_, IntegralFlowWithMultipliers>(Partition::new(sizes).unwrap(), feasible); + } + for bound in [0, 1, 2] { + check::<_, IntegralFlowBundles>( + Decision::new( + MaximumIndependentSet::new(SimpleGraph::new(1, vec![]), vec![One]), + bound, + ), + bound <= 1, + ); + } +} + +fn check_augmentation_predictions_through_qubo() { + let graph = ReductionGraph::new(); + let path = ReductionPath { + steps: [ + ( + HamiltonianCircuit::::NAME, + HamiltonianCircuit::::variant(), + ), + (A::NAME, A::variant()), + (ILP::::NAME, ILP::::variant()), + (QUBO::::NAME, QUBO::::variant()), + ] + .into_iter() + .map(|(name, variant)| ReductionStep { + name: name.into(), + variant: ReductionGraph::variant_to_map(&variant), + }) + .collect(), + }; + let transform = graph + .compose_path_parameter_transform(&path) + .unwrap() + .unwrap(); + for source_graph in [ + SimpleGraph::empty(0), + SimpleGraph::empty(1), + SimpleGraph::path(2), + SimpleGraph::cycle(3), + SimpleGraph::path(3), + ] { + let source = HamiltonianCircuit::new(source_graph); + let predicted = transform.evaluate(&source.parameters()).unwrap(); + let chain = graph.reduce_along_path(&path, &source).unwrap().unwrap(); + let target = chain.target_problem::>(); + for (field, actual) in target.parameters().iter() { + assert!(predicted.get(field).expect("composed augmentation bound") >= actual); + } + let expected = BruteForce::new().solve(&source).unwrap().is_some(); + let solution = crate::solvers::ILPSolver::new().solve(target).unwrap(); + let recovered = chain.extract_solution::, _>(&solution); + if expected { + assert!(source.evaluate(&recovered.unwrap()).unwrap().0); + } else { + assert!( + recovered.is_err(), + "infeasible augmentation cannot certify a circuit" + ); + } + } +} + +#[test] +fn biconnectivity_augmentation_predictions_compose_and_recover_through_qubo() { + check_augmentation_predictions_through_qubo::< + crate::models::graph::BiconnectivityAugmentation, + >(); +} + +#[test] +fn strong_connectivity_augmentation_predictions_compose_and_recover_through_qubo() { + check_augmentation_predictions_through_qubo::< + crate::models::graph::StrongConnectivityAugmentation, + >(); +} diff --git a/src/unit_tests/rules/acyclicpartition_ilp.rs b/src/unit_tests/rules/acyclicpartition_ilp.rs index 5574da900..c09f19a9d 100644 --- a/src/unit_tests/rules/acyclicpartition_ilp.rs +++ b/src/unit_tests/rules/acyclicpartition_ilp.rs @@ -21,7 +21,7 @@ fn small_instance() -> AcyclicPartition { fn test_acyclicpartition_to_ilp_closed_loop() { let source = small_instance(); let reduction: ReductionAcyclicPartitionToILP = - ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let ilp = reduction.target_problem(); // Solve source with brute force @@ -44,7 +44,7 @@ fn test_acyclicpartition_to_ilp_closed_loop() { fn test_reduction_num_vars() { let source = small_instance(); let reduction: ReductionAcyclicPartitionToILP = - ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let ilp = reduction.target_problem(); assert_eq!(ilp.num_vars(), 35); assert_eq!(ilp.num_constraints(), 75); @@ -69,12 +69,12 @@ fn signed_partition_weights_and_costs_are_checked_after_summing() { ), ] { assert!(source.evaluate(&witness).unwrap().0); - let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); crate::rules::test_helpers::assert_bf_vs_ilp(&source, &reduction); } let empty = AcyclicPartition::new(DirectedGraph::new(0, vec![]), vec![], vec![], 0, -1); assert!(!empty.evaluate(&vec![]).unwrap().0); - let reduction = ReduceTo::>::reduce_to(&empty).unwrap(); + let reduction = ReduceTo::>::reduce_to(&empty).unwrap(); assert!(ILPSolver::new().solve(reduction.target_problem()).is_err()); } @@ -82,7 +82,7 @@ fn signed_partition_weights_and_costs_are_checked_after_summing() { fn test_extract_solution() { let source = small_instance(); let reduction: ReductionAcyclicPartitionToILP = - ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let ilp = reduction.target_problem(); let solver = ILPSolver::new(); let ilp_sol = solver.solve(ilp).expect("ILP should be solvable"); @@ -105,7 +105,7 @@ fn test_infeasible_instance() { 0, ); let reduction: ReductionAcyclicPartitionToILP = - ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let ilp = reduction.target_problem(); let solver = ILPSolver::new(); assert!(solver.solve(ilp).is_err()); @@ -115,7 +115,7 @@ fn test_infeasible_instance() { fn test_acyclicpartition_to_ilp_bf_vs_ilp() { let source = small_instance(); let reduction: ReductionAcyclicPartitionToILP = - ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); crate::rules::test_helpers::assert_bf_vs_ilp(&source, &reduction); } @@ -129,7 +129,7 @@ fn test_acyclicpartition_to_ilp_regression_direct_topological_labels() { 10, ); let reduction: ReductionAcyclicPartitionToILP = - ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("the feasible source instance must yield a feasible ILP"); diff --git a/src/unit_tests/rules/aggregate_contracts.rs b/src/unit_tests/rules/aggregate_contracts.rs index 7c5d1723b..08c82ea67 100644 --- a/src/unit_tests/rules/aggregate_contracts.rs +++ b/src/unit_tests/rules/aggregate_contracts.rs @@ -1,3 +1,4 @@ +use crate::models::algebraic::Bounded; use crate::models::algebraic::MinimumWeightDecoding; use crate::models::formula::{CNFClause, KSatisfiability}; use crate::models::graph::{ @@ -141,7 +142,7 @@ fn empty_tree_storage_still_obeys_the_budget() { for n in 0..=2 { for bound in [-1, 0] { let source = RootedTreeStorageAssignment::new(n, vec![], bound); - let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); let expected = bound >= 0; assert_eq!( BruteForce::new().solve(&source).unwrap().is_some(), diff --git a/src/unit_tests/rules/biconnectivityaugmentation_ilp.rs b/src/unit_tests/rules/biconnectivityaugmentation_ilp.rs index 249dabc98..4f39a3578 100644 --- a/src/unit_tests/rules/biconnectivityaugmentation_ilp.rs +++ b/src/unit_tests/rules/biconnectivityaugmentation_ilp.rs @@ -19,7 +19,7 @@ fn small_instance() -> BiconnectivityAugmentation { fn test_biconnectivityaugmentation_to_ilp_closed_loop() { let source = small_instance(); let reduction: ReductionBiconnAugToILP = - ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let ilp = reduction.target_problem(); // Solve source with brute force @@ -42,7 +42,7 @@ fn test_biconnectivityaugmentation_to_ilp_closed_loop() { fn test_extract_solution() { let source = small_instance(); let reduction: ReductionBiconnAugToILP = - ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let ilp = reduction.target_problem(); let solver = ILPSolver::new(); let ilp_sol = solver.solve(ilp).expect("ILP should be solvable"); @@ -55,7 +55,7 @@ fn test_extract_solution() { fn test_trivial_single_vertex() { let source = BiconnectivityAugmentation::new(SimpleGraph::new(1, vec![]), vec![], 0); let reduction: ReductionBiconnAugToILP = - ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let ilp = reduction.target_problem(); let solver = ILPSolver::new(); let ilp_sol = solver.solve(ilp).expect("trivial ILP should be solvable"); @@ -72,7 +72,7 @@ fn test_already_biconnected() { 0, ); let reduction: ReductionBiconnAugToILP = - ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let ilp = reduction.target_problem(); let solver = ILPSolver::new(); let ilp_sol = solver @@ -86,7 +86,7 @@ fn test_already_biconnected() { fn test_biconnectivityaugmentation_to_ilp_bf_vs_ilp() { let source = small_instance(); let reduction: ReductionBiconnAugToILP = - ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); crate::rules::test_helpers::assert_bf_vs_ilp(&source, &reduction); } @@ -105,7 +105,7 @@ fn test_biconnectivityaugmentation_to_ilp_all_two_vertex_instances() { budget, ); let reduction: ReductionBiconnAugToILP = - ReduceTo::>::reduce_to(&source).unwrap(); + ReduceTo::>::reduce_to(&source).unwrap(); let expected = BruteForce::new().solve(&source).unwrap().is_some(); match ILPSolver::new().solve(reduction.target_problem()) { Ok(z) => { @@ -132,7 +132,7 @@ fn test_biconnectivityaugmentation_to_ilp_empty_negative_budget() { let source = BiconnectivityAugmentation::<_, i64>::new(SimpleGraph::empty(n), vec![], budget); let reduction: ReductionBiconnAugToILP = - ReduceTo::>::reduce_to(&source).unwrap(); + ReduceTo::>::reduce_to(&source).unwrap(); assert_eq!( reduction .target_problem() @@ -151,7 +151,7 @@ fn test_biconnectivityaugmentation_to_ilp_empty_negative_budget() { fn test_biconnectivityaugmentation_to_ilp_signed_cost_and_certificate_bounds() { for candidates in [vec![(0, 2, 2), (0, 3, -2)], vec![(0, 3, -2), (0, 2, 2)]] { let source = BiconnectivityAugmentation::new(SimpleGraph::path(4), candidates, 0); - let reduction: ReductionBiconnAugToILP = ReduceTo::>::reduce_to(&source).unwrap(); + let reduction: ReductionBiconnAugToILP = ReduceTo::>::reduce_to(&source).unwrap(); crate::rules::test_helpers::assert_bf_vs_ilp(&source, &reduction); let z = ILPSolver::new().solve(reduction.target_problem()).unwrap(); assert!( diff --git a/src/unit_tests/rules/bottlenecktravelingsalesman_ilp.rs b/src/unit_tests/rules/bottlenecktravelingsalesman_ilp.rs index 45383e2f4..967746104 100644 --- a/src/unit_tests/rules/bottlenecktravelingsalesman_ilp.rs +++ b/src/unit_tests/rules/bottlenecktravelingsalesman_ilp.rs @@ -14,7 +14,7 @@ fn k4_btsp() -> BottleneckTravelingSalesman { fn test_reduction_creates_valid_ilp() { let problem = k4_btsp(); let reduction: ReductionBTSPToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); // n=4, m=6: 16 position bits, 48 edge-use bits, 6 maximum selectors. assert_eq!(ilp.num_vars(), 70); @@ -29,7 +29,7 @@ fn test_bottlenecktravelingsalesman_to_ilp_closed_loop() { let bf_value = problem.evaluate(&bf_solution).unwrap(); let reduction: ReductionBTSPToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp_solver = ILPSolver::new(); let ilp_solution = ilp_solver .solve(reduction.target_problem()) @@ -59,7 +59,7 @@ fn test_bottlenecktravelingsalesman_to_ilp_c4() { let bf_value = problem.evaluate(&bf_solution).unwrap(); let reduction: ReductionBTSPToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp_solver = ILPSolver::new(); let ilp_solution = ilp_solver .solve(reduction.target_problem()) @@ -75,7 +75,7 @@ fn test_bottlenecktravelingsalesman_to_ilp_c4() { fn test_solution_extraction() { let problem = k4_btsp(); let reduction: ReductionBTSPToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp_solver = ILPSolver::new(); let ilp_solution = ilp_solver .solve(reduction.target_problem()) @@ -93,7 +93,7 @@ fn test_no_hamiltonian_cycle_infeasible() { vec![1, 1, 1], ); let reduction: ReductionBTSPToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp_solver = ILPSolver::new(); let result = ilp_solver.solve(reduction.target_problem()); assert!( @@ -106,7 +106,7 @@ fn test_no_hamiltonian_cycle_infeasible() { fn test_bottlenecktravelingsalesman_to_ilp_bf_vs_ilp() { let problem = k4_btsp(); let reduction: ReductionBTSPToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); crate::rules::test_helpers::assert_bf_vs_ilp(&problem, &reduction); } @@ -169,7 +169,7 @@ fn test_bottleneck_ilp_signed_full_range_and_native_cycles() { ), ] { let source = BottleneckTravelingSalesman::new(SimpleGraph::new(n, edges), weights); - let result = ReduceTo::>::reduce_to(&source).unwrap(); + let result = ReduceTo::>::reduce_to(&source).unwrap(); let witness = tour_witness(&source, &tour, &edge_order); let extracted = result.extract_solution(&witness).unwrap(); let expected = source.evaluate(&extracted).unwrap().unwrap(); @@ -192,7 +192,7 @@ fn test_bottleneck_ilp_signed_full_range_and_native_cycles() { #[test] fn test_bottleneck_ilp_maximum_must_be_used_and_dominate() { let source = k4_btsp(); - let result = ReduceTo::>::reduce_to(&source).unwrap(); + let result = ReduceTo::>::reduce_to(&source).unwrap(); let mut config = tour_witness(&source, &[0, 1, 2, 3], &[0, 3, 5, 2]); let selector = 4 * 4 + 2 * 6 * 4; config[selector..].fill(0); @@ -208,7 +208,7 @@ fn test_bottleneck_ilp_maximum_must_be_used_and_dominate() { fn test_bottleneck_ilp_empty_and_single_edge_are_infeasible() { for (n, edges, weights) in [(0, vec![], vec![]), (2, vec![(0, 1)], vec![1])] { let source = BottleneckTravelingSalesman::new(SimpleGraph::new(n, edges), weights); - let result = ReduceTo::>::reduce_to(&source).unwrap(); + let result = ReduceTo::>::reduce_to(&source).unwrap(); assert!(matches!( ILPSolver::new().solve(result.target_problem()), Err(crate::solvers::ILPSolveError::Infeasible) diff --git a/src/unit_tests/rules/boundedcomponentspanningforest_ilp.rs b/src/unit_tests/rules/boundedcomponentspanningforest_ilp.rs index 039d7f2c3..c2cbea23c 100644 --- a/src/unit_tests/rules/boundedcomponentspanningforest_ilp.rs +++ b/src/unit_tests/rules/boundedcomponentspanningforest_ilp.rs @@ -1,4 +1,5 @@ use super::*; +use crate::models::algebraic::Bounded; use crate::models::algebraic::ILP; use crate::models::graph::BoundedComponentSpanningForest; use crate::rules::ReduceTo; @@ -20,7 +21,7 @@ fn small_instance() -> BoundedComponentSpanningForest { fn test_boundedcomponentspanningforest_to_ilp_closed_loop() { let source = small_instance(); let reduction: ReductionBCSFToILP = - ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let ilp = reduction.target_problem(); // Solve source with brute force @@ -43,7 +44,7 @@ fn test_boundedcomponentspanningforest_to_ilp_closed_loop() { fn test_extract_solution() { let source = small_instance(); let reduction: ReductionBCSFToILP = - ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let ilp = reduction.target_problem(); let solver = ILPSolver::new(); let ilp_sol = solver.solve(ilp).expect("ILP should be solvable"); @@ -62,7 +63,7 @@ fn test_single_component() { 3, ); let reduction: ReductionBCSFToILP = - ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let ilp = reduction.target_problem(); let solver = ILPSolver::new(); let ilp_sol = solver @@ -82,7 +83,7 @@ fn test_infeasible_instance() { 5, ); let reduction: ReductionBCSFToILP = - ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let ilp = reduction.target_problem(); let solver = ILPSolver::new(); assert!(solver.solve(ilp).is_err()); @@ -92,6 +93,6 @@ fn test_infeasible_instance() { fn test_boundedcomponentspanningforest_to_ilp_bf_vs_ilp() { let source = small_instance(); let reduction: ReductionBCSFToILP = - ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); crate::rules::test_helpers::assert_bf_vs_ilp(&source, &reduction); } diff --git a/src/unit_tests/rules/closeststring_ilp.rs b/src/unit_tests/rules/closeststring_ilp.rs index 5714565a7..487b7f68e 100644 --- a/src/unit_tests/rules/closeststring_ilp.rs +++ b/src/unit_tests/rules/closeststring_ilp.rs @@ -1,5 +1,5 @@ use super::*; -use crate::models::algebraic::{ObjectiveSense, ILP}; +use crate::models::algebraic::{Bounded, ObjectiveSense, ILP}; use crate::models::misc::ClosestString; use crate::rules::test_helpers::assert_bf_vs_ilp; use crate::solvers::{BruteForce, ILPSolver}; @@ -19,7 +19,8 @@ fn issue_instance() -> ClosestString { #[test] fn test_closeststring_to_ilp_structure() { let source = issue_instance(); - let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + let reduction = + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let ilp = reduction.target_problem(); // q = 2, m = 3 -> 2*3 + 1 = 7 variables. @@ -51,7 +52,8 @@ fn test_closeststring_to_ilp_structure() { #[test] fn test_closeststring_to_ilp_closed_loop() { let source = issue_instance(); - let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + let reduction = + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let bf_value_solution = BruteForce::new().solve(&source).unwrap().unwrap(); @@ -72,7 +74,8 @@ fn test_closeststring_to_ilp_closed_loop() { #[test] fn test_closeststring_to_ilp_bf_vs_ilp() { let source = issue_instance(); - let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + let reduction = + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); assert_bf_vs_ilp(&source, &reduction); } @@ -81,7 +84,8 @@ fn test_closeststring_to_ilp_extract_known_center() { // Build the binary encoding of the center 000 by hand: // x_{0,0}=x_{1,0}=x_{2,0}=1, others 0, R = 2. let source = issue_instance(); - let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + let reduction = + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let mut target_solution = vec![0_i64; reduction.target_problem().num_vars()]; target_solution[0] = 1; // x_{0,0} @@ -97,7 +101,8 @@ fn test_closeststring_to_ilp_extract_known_center() { #[test] fn test_closeststring_to_ilp_rejects_missing_one_hot_symbol() { let source = ClosestString::new(2, vec![vec![0, 1]]); - let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + let reduction = + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let target_solution = vec![0; reduction.target_problem().num_vars()]; assert_eq!( @@ -114,7 +119,8 @@ fn test_closeststring_to_ilp_ternary_alphabet() { // q = 3, m = 2, three strings forcing a nonzero radius. The optimum // radius is 1 (any center matches at least one position of every string). let source = ClosestString::new(3, vec![vec![0, 1], vec![1, 2], vec![2, 0]]); - let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + let reduction = + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let ilp = reduction.target_problem(); // q * m + 1 = 3 * 2 + 1 = 7 variables; m + n = 2 + 3 = 5 constraints. @@ -130,7 +136,8 @@ fn test_closeststring_to_ilp_single_string_zero_radius() { // radius is 0. This guards against off-by-one errors in the radius // constraints. let source = ClosestString::new(2, vec![vec![1, 0, 1, 1]]); - let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + let reduction = + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) diff --git a/src/unit_tests/rules/closestsubstring_ilp.rs b/src/unit_tests/rules/closestsubstring_ilp.rs index bf3a9fed5..ed20f8b40 100644 --- a/src/unit_tests/rules/closestsubstring_ilp.rs +++ b/src/unit_tests/rules/closestsubstring_ilp.rs @@ -1,5 +1,5 @@ use super::*; -use crate::models::algebraic::{ObjectiveSense, ILP}; +use crate::models::algebraic::{Bounded, ObjectiveSense, ILP}; use crate::models::misc::ClosestSubstring; use crate::rules::test_helpers::assert_bf_vs_ilp; use crate::solvers::{BruteForce, ILPSolver}; @@ -24,7 +24,8 @@ fn issue_instance() -> ClosestSubstring { #[test] fn test_closestsubstring_to_ilp_structure() { let source = issue_instance(); - let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + let reduction = + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let ilp = reduction.target_problem(); // q = 2, ell = 3, total windows W = 3 + 3 + 3 = 9. @@ -74,7 +75,8 @@ fn test_closestsubstring_to_ilp_structure() { #[test] fn test_closestsubstring_to_ilp_rejects_missing_one_hot_symbol() { let source = issue_instance(); - let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + let reduction = + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let target_solution = vec![0; reduction.target_problem().num_vars()]; assert_eq!( @@ -89,7 +91,8 @@ fn test_closestsubstring_to_ilp_rejects_missing_one_hot_symbol() { #[test] fn test_closestsubstring_to_ilp_closed_loop() { let source = issue_instance(); - let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + let reduction = + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let bf_value_solution = BruteForce::new().solve(&source).unwrap().unwrap(); @@ -112,7 +115,8 @@ fn test_closestsubstring_to_ilp_closed_loop() { #[test] fn test_closestsubstring_to_ilp_bf_vs_ilp() { let source = issue_instance(); - let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + let reduction = + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); assert_bf_vs_ilp(&source, &reduction); } @@ -126,7 +130,8 @@ fn test_closestsubstring_to_ilp_zero_radius_when_common_substring_exists() { 3, ) .unwrap(); - let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + let reduction = + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) @@ -144,7 +149,8 @@ fn test_closestsubstring_to_ilp_ternary_alphabet() { // enough to cross-check via the closed loop. let source = ClosestSubstring::new(3, vec![vec![0, 1, 2], vec![1, 2, 0], vec![2, 0, 1]], 2).unwrap(); - let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + let reduction = + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let ilp = reduction.target_problem(); // q*ell + W + 1 with W = 2 + 2 + 2 = 6: num_vars = 6 + 6 + 1 = 13. @@ -162,7 +168,8 @@ fn test_closestsubstring_to_ilp_extract_known_solution() { // y_{1,0}=y_{2,1}=y_{3,0}=1, R = 1. Then verify the extracted source // config matches and gives radius 1. let source = issue_instance(); - let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + let reduction = + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let ilp = reduction.target_problem(); let mut target_solution = vec![0_i64; ilp.num_vars()]; diff --git a/src/unit_tests/rules/consecutiveblockminimization_ilp.rs b/src/unit_tests/rules/consecutiveblockminimization_ilp.rs index f778a2f96..fe9267c0a 100644 --- a/src/unit_tests/rules/consecutiveblockminimization_ilp.rs +++ b/src/unit_tests/rules/consecutiveblockminimization_ilp.rs @@ -62,3 +62,29 @@ fn test_cbm_to_ilp_trivial() { // x: 1, a: 1, b: 1 => 3 assert_eq!(ilp.num_vars(), 3); } + +#[test] +fn test_block_bound_normalization_preserves_feasibility() { + for matrix in [vec![], vec![vec![]], vec![vec![true]]] { + for bound in [i64::MIN, -1, 0, 1, i64::MAX] { + let source = ConsecutiveBlockMinimization::new(matrix.clone(), bound); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + assert_eq!( + reduction.target_problem().max_constraint_magnitude_bits(), + 1 + ); + let solution = ILPSolver::new().solve(reduction.target_problem()); + let minimum_blocks = i64::from(source.num_cols() != 0); + assert_eq!(solution.is_ok(), bound >= minimum_blocks); + if let Ok(solution) = solution { + let recovered = reduction.extract_solution(&solution).unwrap(); + assert_eq!(source.evaluate(&recovered).unwrap(), Or(true)); + } else { + assert_eq!( + solution.unwrap_err(), + crate::solvers::ILPSolveError::Infeasible + ); + } + } + } +} diff --git a/src/unit_tests/rules/consecutiveonesmatrixaugmentation_ilp.rs b/src/unit_tests/rules/consecutiveonesmatrixaugmentation_ilp.rs index 1a9169afe..6c53a903d 100644 --- a/src/unit_tests/rules/consecutiveonesmatrixaugmentation_ilp.rs +++ b/src/unit_tests/rules/consecutiveonesmatrixaugmentation_ilp.rs @@ -73,3 +73,22 @@ fn test_coma_to_ilp_trivial() { // x: 1, a+l+u+h+f: 5*1=5 => 6 assert_eq!(ilp.num_vars(), 6); } + +#[test] +fn test_augmentation_threshold_normalization() { + for bound in [0, 1, i64::MAX] { + let source = ConsecutiveOnesMatrixAugmentation::new(vec![vec![true]], bound); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + assert_eq!( + reduction.target_problem().max_constraint_magnitude_bits(), + 1 + ); + let solution = ILPSolver::new().solve(reduction.target_problem()).unwrap(); + assert_eq!( + source + .evaluate(&reduction.extract_solution(&solution).unwrap()) + .unwrap(), + Or(true) + ); + } +} diff --git a/src/unit_tests/rules/directedtwocommodityintegralflow_ilp.rs b/src/unit_tests/rules/directedtwocommodityintegralflow_ilp.rs index 263c6947e..63b9b4e3c 100644 --- a/src/unit_tests/rules/directedtwocommodityintegralflow_ilp.rs +++ b/src/unit_tests/rules/directedtwocommodityintegralflow_ilp.rs @@ -1,5 +1,5 @@ use super::*; -use crate::models::algebraic::{ObjectiveSense, ILP}; +use crate::models::algebraic::{Bounded, ObjectiveSense, ILP}; use crate::solvers::{BruteForce, ILPSolver}; use crate::topology::DirectedGraph; use crate::traits::Problem; @@ -16,7 +16,7 @@ fn sink_self_loop_cannot_supply_commodity_flow() { 1, 0, ); - let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); assert!(!source.evaluate(&vec![1, 0]).unwrap().0); assert!(reduction .target_problem() @@ -73,7 +73,7 @@ fn infeasible_instance() -> DirectedTwoCommodityIntegralFlow { fn test_directedtwocommodityintegralflow_to_ilp_structure() { let problem = feasible_instance(); let reduction: ReductionD2CIFToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); // 8 arcs → 2*8 = 16 variables @@ -103,7 +103,7 @@ fn test_directedtwocommodityintegralflow_to_ilp_closed_loop() { ); let reduction: ReductionD2CIFToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be feasible"); @@ -119,7 +119,7 @@ fn test_directedtwocommodityintegralflow_to_ilp_closed_loop() { fn test_directedtwocommodityintegralflow_to_ilp_infeasible() { let problem = infeasible_instance(); let reduction: ReductionD2CIFToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); assert!( ILPSolver::new().solve(reduction.target_problem()).is_err(), "infeasible flow instance should produce infeasible ILP" @@ -132,7 +132,7 @@ fn test_directedtwocommodityintegralflow_to_ilp_disallows_using_other_commodity_ let problem = DirectedTwoCommodityIntegralFlow::new(graph, vec![1, 1], 0, 1, 2, 3, 1, 0); let reduction: ReductionD2CIFToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); assert!( ILPSolver::new().solve(reduction.target_problem()).is_err(), "commodity 1 must conserve flow at commodity 2's source in the ILP reduction" @@ -143,7 +143,7 @@ fn test_directedtwocommodityintegralflow_to_ilp_disallows_using_other_commodity_ fn test_directedtwocommodityintegralflow_to_ilp_extract_solution() { let problem = feasible_instance(); let reduction: ReductionD2CIFToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); // f1 routes via (0,2),(2,4): arcs 0,4 = 1; rest 0 for commodity 1 // f2 routes via (1,3),(3,5): arcs 3,7 = 1; rest 0 for commodity 2 @@ -165,7 +165,7 @@ fn test_directedtwocommodityintegralflow_to_ilp_extract_solution() { fn test_directedtwocommodityintegralflow_to_ilp_bf_vs_ilp() { let problem = feasible_instance(); let reduction: ReductionD2CIFToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); crate::rules::test_helpers::assert_bf_vs_ilp(&problem, &reduction); } @@ -183,8 +183,37 @@ fn test_directedtwocommodityintegralflow_to_ilp_preserves_large_exact_capacity() 1, ); - let reduction = ReduceTo::>::reduce_to(&problem).unwrap(); + let reduction = ReduceTo::>::reduce_to(&problem).unwrap(); let capacity_constraint = &reduction.target_problem().constraints()[0]; assert_eq!(capacity_constraint.terms(), vec![(0, 1), (1, 1)]); assert_eq!(capacity_constraint.rhs(), capacity); } + +#[test] +fn test_flow_requirement_normalization_preserves_feasibility() { + for requirement in [0, 1, 2, i64::MAX] { + let source = DirectedTwoCommodityIntegralFlow::new( + DirectedGraph::new(4, vec![(0, 1), (2, 3)]), + vec![1, 1], + 0, + 1, + 2, + 3, + requirement, + requirement, + ); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + assert!(reduction.target_problem().max_constraint_magnitude_bits() <= 2); + match ILPSolver::new().solve(reduction.target_problem()) { + Ok(solution) => { + assert!(requirement <= 1); + let recovered = reduction.extract_solution(&solution).unwrap(); + assert!(source.evaluate(&recovered).unwrap().0); + } + Err(error) => { + assert!(requirement > 1); + assert_eq!(error, crate::solvers::ILPSolveError::Infeasible); + } + } + } +} diff --git a/src/unit_tests/rules/ensemblecomputation_ilp.rs b/src/unit_tests/rules/ensemblecomputation_ilp.rs index 3be3c0aaa..09b185376 100644 --- a/src/unit_tests/rules/ensemblecomputation_ilp.rs +++ b/src/unit_tests/rules/ensemblecomputation_ilp.rs @@ -12,7 +12,7 @@ fn feasible_instance() -> EnsembleComputation { #[test] fn test_ensemblecomputation_to_ilp_structure() { - let reduction = ReduceTo::>::reduce_to(&feasible_instance()).unwrap(); + let reduction = ReduceTo::>::reduce_to(&feasible_instance()).unwrap(); assert_eq!(reduction.target_problem().num_vars(), 75); assert_eq!(reduction.target_problem().constraints().len(), 154); } @@ -20,7 +20,7 @@ fn test_ensemblecomputation_to_ilp_structure() { #[test] fn test_ensemblecomputation_to_ilp_closed_loop() { let source = feasible_instance(); - let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); let target_solution = ILPSolver::new().solve(reduction.target_problem()).unwrap(); let extracted = reduction.extract_solution(&target_solution).unwrap(); assert_eq!(source.evaluate(&extracted).unwrap(), Min(Some(3))); @@ -29,21 +29,21 @@ fn test_ensemblecomputation_to_ilp_closed_loop() { #[test] fn test_ensemblecomputation_to_ilp_infeasible_budget() { let source = EnsembleComputation::new(3, vec![vec![0, 1, 2]], 1); - let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); assert!(ILPSolver::new().solve(reduction.target_problem()).is_err()); } #[test] fn test_ensemblecomputation_to_ilp_rejects_singleton_target() { let source = EnsembleComputation::new(3, vec![vec![0]], 2); - let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); assert!(ILPSolver::new().solve(reduction.target_problem()).is_err()); } #[test] fn test_ensemblecomputation_to_ilp_empty_family() { let source = EnsembleComputation::new(1, vec![], 2); - let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); let target_solution = ILPSolver::new().solve(reduction.target_problem()).unwrap(); let extracted = reduction.extract_solution(&target_solution).unwrap(); assert_eq!(source.evaluate(&extracted).unwrap(), Min(Some(0))); diff --git a/src/unit_tests/rules/eulerianpath_ilp.rs b/src/unit_tests/rules/eulerianpath_ilp.rs index 7d9f5c3c4..1a3abc688 100644 --- a/src/unit_tests/rules/eulerianpath_ilp.rs +++ b/src/unit_tests/rules/eulerianpath_ilp.rs @@ -1,5 +1,5 @@ use super::*; -use crate::models::algebraic::{ObjectiveSense, ILP}; +use crate::models::algebraic::{Bounded, ObjectiveSense, ILP}; use crate::models::graph::EulerianPath; use crate::solvers::ILPSolver; use crate::topology::DirectedGraph; @@ -15,7 +15,8 @@ fn issue_instance() -> EulerianPath { #[test] fn test_eulerianpath_to_ilp_issue_structure() { let source = issue_instance(); - let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + let reduction = + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let ilp = reduction.target_problem(); // m = 4 arcs. Compatible pairs: head(a) = tail(b), a != b: @@ -44,7 +45,8 @@ fn test_eulerianpath_to_ilp_issue_structure() { fn test_eulerianpath_to_ilp_empty_instance() { // m = 0: empty ILP, vacuously feasible. let source = EulerianPath::new(DirectedGraph::empty(3)); - let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + let reduction = + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let ilp = reduction.target_problem(); assert_eq!(ilp.num_vars(), 0); assert_eq!(ilp.constraints().len(), 0); @@ -62,7 +64,8 @@ fn test_eulerianpath_to_ilp_closed_loop() { // Solve the ILP on the canonical instance and verify the extracted ordering // is a valid directed Eulerian trail in the source. let source = issue_instance(); - let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + let reduction = + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be feasible for a YES instance"); @@ -83,7 +86,8 @@ fn test_eulerianpath_to_ilp_infeasible_no_instance() { // degree-balance criterion: vertex 0 has out-degree 2 / in-degree 0, so // no Eulerian trail exists. let source = EulerianPath::new(DirectedGraph::new(3, vec![(0, 1), (0, 2)])); - let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + let reduction = + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); // The ILP must report infeasibility for a NO instance. let solution = ILPSolver::new().solve(reduction.target_problem()); @@ -99,7 +103,8 @@ fn test_eulerianpath_to_ilp_closed_circuit_with_loop() { // Loop + closed trail: arcs (0,0), (0,1), (1,0). // Trail (0,0) -> (0,1) -> (1,0) is a valid closed Eulerian trail. let source = EulerianPath::new(DirectedGraph::new(2, vec![(0, 0), (0, 1), (1, 0)])); - let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + let reduction = + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) diff --git a/src/unit_tests/rules/factoring_ilp.rs b/src/unit_tests/rules/factoring_ilp.rs index a7ec16aa1..1a1eb5c55 100644 --- a/src/unit_tests/rules/factoring_ilp.rs +++ b/src/unit_tests/rules/factoring_ilp.rs @@ -1,4 +1,5 @@ use super::*; +use crate::models::algebraic::Bounded; use crate::solvers::{BruteForce, ILPSolver}; use num_bigint::BigUint; @@ -7,7 +8,7 @@ fn test_reduction_creates_valid_ilp() { // Factor 6 with 2-bit factors let problem = Factoring::with_factor_bits(6, 2, 2); let reduction: ReductionFactoringToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); // Check variable count: m + n + m*n + (m+n) = 2 + 2 + 4 + 4 = 12 @@ -23,7 +24,7 @@ fn test_reduction_creates_valid_ilp() { fn test_variable_layout() { let problem = Factoring::with_factor_bits(6, 2, 3); let reduction: ReductionFactoringToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); // p variables: [0, 1] assert_eq!(reduction.p_var(0), 0); @@ -49,7 +50,7 @@ fn test_factor_6() { // 6 = 2 × 3 or 3 × 2 let problem = Factoring::with_factor_bits(6, 2, 2); let reduction: ReductionFactoringToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); let ilp_solver = ILPSolver::new(); @@ -72,7 +73,7 @@ fn test_factor_15() { // 2. Reduce to ILP let reduction: ReductionFactoringToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); // 3. Solve ILP @@ -93,7 +94,7 @@ fn test_factor_35() { // 35 = 5 × 7 or 7 × 5 let problem = Factoring::with_factor_bits(35, 3, 3); let reduction: ReductionFactoringToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); let ilp_solver = ILPSolver::new(); @@ -111,7 +112,7 @@ fn test_factor_one() { // 1 = 1 × 1 let problem = Factoring::with_factor_bits(1, 2, 2); let reduction: ReductionFactoringToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); let ilp_solver = ILPSolver::new(); @@ -129,7 +130,7 @@ fn test_factor_prime() { // 7 is prime: 7 = 1 × 7 or 7 × 1 let problem = Factoring::with_factor_bits(7, 3, 3); let reduction: ReductionFactoringToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); let ilp_solver = ILPSolver::new(); @@ -147,7 +148,7 @@ fn test_factor_square() { // 9 = 3 × 3 let problem = Factoring::with_factor_bits(9, 3, 3); let reduction: ReductionFactoringToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); let ilp_solver = ILPSolver::new(); @@ -165,7 +166,7 @@ fn test_infeasible_target_too_large() { // Target 100 with 2-bit factors (max product is 3 × 3 = 9) let problem = Factoring::with_factor_bits(100, 2, 2); let reduction: ReductionFactoringToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); let ilp_solver = ILPSolver::new(); @@ -178,7 +179,7 @@ fn test_infeasible_target_too_large() { fn test_factoring_to_ilp_closed_loop() { let problem = Factoring::with_factor_bits(6, 2, 2); let reduction: ReductionFactoringToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); // Get ILP solution @@ -207,7 +208,7 @@ fn test_factoring_to_ilp_closed_loop() { fn test_solution_extraction() { let problem = Factoring::with_factor_bits(6, 2, 2); let reduction: ReductionFactoringToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); // Manually construct ILP solution for 2 × 3 = 6 // p = 2 = binary 10 -> p_0=0, p_1=1 @@ -230,7 +231,7 @@ fn test_solution_extraction() { fn test_target_ilp_structure() { let problem = Factoring::with_factor_bits(12, 3, 4); let reduction: ReductionFactoringToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); // num_vars = 3 + 4 + 12 + 7 = 26 @@ -245,7 +246,7 @@ fn test_integer_ilp_pipeline_solution() { let problem = Factoring::with_factor_bits(6, 2, 2); let reduction: ReductionFactoringToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); let ilp_solver = ILPSolver::new(); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); @@ -259,7 +260,7 @@ fn test_asymmetric_bit_widths() { // 12 = 3 × 4 or 4 × 3 or 2 × 6 or 6 × 2 or 1 × 12 or 12 × 1 let problem = Factoring::with_factor_bits(12, 2, 4); let reduction: ReductionFactoringToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); let ilp_solver = ILPSolver::new(); @@ -276,7 +277,7 @@ fn test_asymmetric_bit_widths() { fn test_oversized_biguint_target_makes_ilp_infeasible() { let target = BigUint::from(1u32) << 70; let problem = Factoring::with_factor_bits(target, 2, 2); - let reduction = ReduceTo::>::reduce_to(&problem).unwrap(); + let reduction = ReduceTo::>::reduce_to(&problem).unwrap(); assert!(ILPSolver::new().solve(reduction.target_problem()).is_err()); } @@ -287,7 +288,8 @@ fn test_constraint_count_formula() { for (m, n) in [(2, 2), (3, 3), (2, 4), (3, 4)] { let problem = Factoring::with_factor_bits(1, m, n); let reduction: ReductionFactoringToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem) + .expect("reduction should succeed"); let ilp = reduction.target_problem(); let expected = 3 * m * n + 4 * m + 4 * n + 1; @@ -307,7 +309,8 @@ fn test_variable_count_formula() { for (m, n) in [(2, 2), (3, 3), (2, 4), (3, 4)] { let problem = Factoring::with_factor_bits(1, m, n); let reduction: ReductionFactoringToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem) + .expect("reduction should succeed"); let ilp = reduction.target_problem(); let expected = m + n + m * n + (m + n); @@ -325,6 +328,6 @@ fn test_variable_count_formula() { fn test_factoring_to_ilp_bf_vs_ilp() { let problem = Factoring::with_factor_bits(6, 2, 2); let reduction: ReductionFactoringToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); crate::rules::test_helpers::assert_bf_vs_ilp(&problem, &reduction); } diff --git a/src/unit_tests/rules/feasibleregisterassignment_ilp.rs b/src/unit_tests/rules/feasibleregisterassignment_ilp.rs index 6b18e1e2a..3f296cc80 100644 --- a/src/unit_tests/rules/feasibleregisterassignment_ilp.rs +++ b/src/unit_tests/rules/feasibleregisterassignment_ilp.rs @@ -1,4 +1,5 @@ use super::*; +use crate::models::algebraic::Bounded; use crate::solvers::ILPSolver; use crate::traits::Problem; use crate::types::Or; @@ -10,7 +11,8 @@ fn feasible_example() -> FeasibleRegisterAssignment { #[test] fn test_feasible_register_assignment_to_ilp_structure() { let source = feasible_example(); - let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + let reduction = + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let ilp = reduction.target_problem(); assert_eq!(ilp.num_vars(), 14); @@ -22,7 +24,8 @@ fn test_feasible_register_assignment_to_ilp_structure() { #[test] fn test_feasible_register_assignment_to_ilp_closed_loop() { let source = feasible_example(); - let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + let reduction = + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) @@ -38,7 +41,8 @@ fn test_feasible_register_assignment_to_ilp_closed_loop() { #[test] fn test_feasible_register_assignment_to_ilp_infeasible() { let source = FeasibleRegisterAssignment::new(3, vec![(0, 1), (0, 2), (1, 2)], 1, vec![0, 0, 0]); - let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + let reduction = + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); assert!( ILPSolver::new().solve(reduction.target_problem()).is_err(), @@ -49,6 +53,7 @@ fn test_feasible_register_assignment_to_ilp_infeasible() { #[test] fn test_feasible_register_assignment_to_ilp_bf_vs_ilp() { let source = feasible_example(); - let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + let reduction = + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); crate::rules::test_helpers::assert_bf_vs_ilp(&source, &reduction); } diff --git a/src/unit_tests/rules/flowshopscheduling_ilp.rs b/src/unit_tests/rules/flowshopscheduling_ilp.rs index 7b8011ec3..580b2e6a8 100644 --- a/src/unit_tests/rules/flowshopscheduling_ilp.rs +++ b/src/unit_tests/rules/flowshopscheduling_ilp.rs @@ -1,4 +1,5 @@ use super::*; +use crate::models::algebraic::Bounded; use crate::models::algebraic::ILP; use crate::solvers::{BruteForce, ILPSolver}; use crate::traits::Problem; @@ -7,7 +8,7 @@ use crate::types::Or; #[test] fn zero_duration_jobs_preserve_the_common_machine_order() { let source = FlowShopScheduling::new(2, vec![vec![3, 0], vec![1, 10]], 11); - let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); // Job 1 precedes job 0, but both finish on machine 1 at time 11. let assignment = vec![0, 4, 11, 1, 11]; assert!(reduction @@ -21,7 +22,7 @@ fn zero_duration_jobs_preserve_the_common_machine_order() { assert_eq!(source.evaluate(&decoded).unwrap(), Or(true)); let no_machines = FlowShopScheduling::new(0, vec![vec![], vec![]], 0); - let reduction = ReduceTo::>::reduce_to(&no_machines).unwrap(); + let reduction = ReduceTo::>::reduce_to(&no_machines).unwrap(); crate::rules::test_helpers::assert_bf_vs_ilp(&no_machines, &reduction); } @@ -29,7 +30,8 @@ fn zero_duration_jobs_preserve_the_common_machine_order() { fn test_flowshopscheduling_to_ilp_closed_loop() { // 2 machines, 3 jobs, deadline 10 let problem = FlowShopScheduling::new(2, vec![vec![2, 3], vec![3, 2], vec![1, 4]], 10); - let reduction = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + let reduction = + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let bf = BruteForce::new(); let bf_witness = bf @@ -53,7 +55,8 @@ fn test_flowshopscheduling_to_ilp_closed_loop() { fn test_flowshopscheduling_to_ilp_infeasible() { // 2 machines, 3 jobs with large processing times, very tight deadline let problem = FlowShopScheduling::new(2, vec![vec![5, 5], vec![5, 5], vec![5, 5]], 6); - let reduction = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + let reduction = + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); assert!( ILPSolver::new().solve(reduction.target_problem()).is_err(), "infeasible FSS should produce infeasible ILP" @@ -64,7 +67,8 @@ fn test_flowshopscheduling_to_ilp_infeasible() { fn test_flowshopscheduling_to_ilp_single_job() { // 2 machines, 1 job, deadline 10 let problem = FlowShopScheduling::new(2, vec![vec![3, 4]], 10); - let reduction = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + let reduction = + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("single-job ILP should be solvable"); @@ -75,7 +79,8 @@ fn test_flowshopscheduling_to_ilp_single_job() { #[test] fn test_flowshopscheduling_to_ilp_bf_vs_ilp() { let problem = FlowShopScheduling::new(2, vec![vec![2, 3], vec![3, 2], vec![1, 4]], 10); - let reduction = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + let reduction = + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let bf = BruteForce::new(); let bf_witness = bf.solve(&problem).unwrap().expect("should be feasible"); diff --git a/src/unit_tests/rules/highlyconnecteddeletion_ilp.rs b/src/unit_tests/rules/highlyconnecteddeletion_ilp.rs index 073077e69..a52bffcc8 100644 --- a/src/unit_tests/rules/highlyconnecteddeletion_ilp.rs +++ b/src/unit_tests/rules/highlyconnecteddeletion_ilp.rs @@ -1,142 +1,172 @@ use super::*; - -#[test] -fn two_vertices_reduce_to_singleton_clusters() { - let source = HighlyConnectedDeletion::new(SimpleGraph::new(2, vec![(0, 1)])); - let reduction = ReduceTo::>::reduce_to(&source).unwrap(); - assert_eq!(reduction.target_problem().num_vars(), 2); - assert_bf_vs_ilp(&source, &reduction); -} -use crate::models::algebraic::{ObjectiveSense, ILP}; -use crate::models::graph::HighlyConnectedDeletion; +use crate::models::algebraic::QUBO; use crate::rules::test_helpers::assert_bf_vs_ilp; -use crate::topology::SimpleGraph; +use crate::rules::{ReductionGraph, ReductionPath, ReductionStep}; +use crate::solvers::{BruteForce, ILPSolver}; use crate::traits::Problem; use crate::types::Min; -/// Canonical issue #1023 instance: triangle {0,1,2} with leaf vertex 3 -/// attached at vertex 2. Optimum deletes only the leaf edge (2,3). -fn issue_instance() -> HighlyConnectedDeletion { +fn triangle_with_leaf() -> HighlyConnectedDeletion { HighlyConnectedDeletion::new(SimpleGraph::new(4, vec![(0, 1), (0, 2), (1, 2), (2, 3)])) } #[test] -fn test_highlyconnecteddeletion_to_ilp_issue_structure() { - let source = issue_instance(); - let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); - let ilp = reduction.target_problem(); - - // 4 singletons + the triangle cluster {0,1,2}: 5 variables in total. - assert_eq!(ilp.num_vars(), 5); - assert_eq!(ilp.constraints().len(), 4); - assert_eq!(ilp.sense(), ObjectiveSense::Maximize); - - // The induced-edge counts: singletons contribute 0, triangle contributes 3. - let triangle_coeffs: Vec = ilp - .objective() - .iter() - .filter(|(_, w)| *w > 0) - .map(|(_, w)| *w) - .collect(); - assert_eq!(triangle_coeffs, vec![3]); - - // Vertex 3 only appears in its own singleton, so its partition constraint - // is `x_{3} = 1` -- a single-term equality with rhs 1. - let v3_constraint = &ilp.constraints()[3]; - assert_eq!(v3_constraint.terms().len(), 1); - assert_eq!(v3_constraint.rhs(), 1); - - // Vertex 0 appears in two clusters (its singleton and the triangle). - let v0_constraint = &ilp.constraints()[0]; - assert_eq!(v0_constraint.terms().len(), 2); - assert_eq!(v0_constraint.rhs(), 1); +fn polynomial_encoding_preserves_small_graph_optima_and_witnesses() { + let entry = crate::rules::registry::reduction_entries() + .into_iter() + .find(|entry| { + entry.source_name == HighlyConnectedDeletion::::NAME + && entry.target_name == ILP::::NAME + }) + .unwrap(); + let contract = entry.parameter_contract().unwrap(); + let transform = contract.transform().unwrap(); + for n in 0..=4 { + let pairs: Vec<_> = (0..n) + .flat_map(|u| (u + 1..n).map(move |v| (u, v))) + .collect(); + for graph_mask in 0..1_usize << pairs.len() { + let edges = pairs + .iter() + .enumerate() + .filter_map(|(i, &edge)| (graph_mask & (1 << i) != 0).then_some(edge)) + .collect(); + let source = HighlyConnectedDeletion::new(SimpleGraph::new(n, edges)); + let reduction = source.reduce_to().unwrap(); + let target = reduction.target_problem(); + let predicted = transform.evaluate(&source.parameters()).unwrap(); + for (field, actual) in target.parameters().iter() { + assert!(predicted.get(field).expect("complete polynomial contract") >= actual); + } + let reference = BruteForce::new().solve(&source).unwrap().unwrap(); + let expected = source.evaluate(&reference).unwrap(); + let mut best_deleted = None; + for mask in 0..1_usize << target.num_vars() { + let solution: Vec = (0..target.num_vars()) + .map(|i| ((mask >> i) & 1) as i64) + .collect(); + if target.is_feasible(&solution).unwrap() { + let recovered = reduction.extract_solution(&solution).unwrap(); + let deleted = source + .evaluate(&recovered) + .unwrap() + .0 + .expect("decoded witness must be feasible"); + // For a loop-free graph the ILP objective counts every kept edge. + assert_eq!( + target.evaluate_objective(&solution).unwrap() + deleted, + source.num_edges() as i64 + ); + best_deleted = + Some(best_deleted.map_or(deleted, |best: i64| best.min(deleted))); + } + } + assert_eq!(Min(best_deleted), expected, "n={n}, graph={graph_mask}"); + } + } } #[test] -fn test_highlyconnecteddeletion_to_ilp_closed_loop() { - let source = issue_instance(); - let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); - assert_bf_vs_ilp(&source, &reduction); -} - -#[test] -fn test_highlyconnecteddeletion_to_ilp_bf_vs_ilp() { - let source = issue_instance(); - let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); - assert_bf_vs_ilp(&source, &reduction); -} - -#[test] -fn test_highlyconnecteddeletion_to_ilp_extract_solution_decode() { - let source = issue_instance(); - let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); - - // ILP solution: pick triangle cluster {0,1,2} and singleton {3}. - // The triangle cluster is the last variable (index 4); singleton {3} is - // index 3. Build the assignment directly. - let mut target_solution = vec![0; reduction.target_problem().num_vars()]; - target_solution[3] = 1; // singleton {3} - target_solution[4] = 1; // triangle {0,1,2} - - let extracted = reduction.extract_solution(&target_solution).unwrap(); - - // Edges in input order: (0,1), (0,2), (1,2) all inside the triangle (kept); - // (2,3) crosses clusters and is deleted. - assert_eq!(extracted, vec![false, false, false, true]); - assert_eq!(source.evaluate(&extracted).unwrap(), Min(Some(1))); - assert!(source.is_valid_solution(&extracted)); +fn polynomial_encoding_handles_more_than_a_word_of_vertices() { + let source = HighlyConnectedDeletion::new(SimpleGraph::new(64, vec![])); + let reduction = source.reduce_to().unwrap(); + let target = reduction.target_problem(); + assert!(target.num_vars() <= 64 * 64); + assert!(target.num_constraints() <= 64_usize.pow(3) + 2 * 64); + let recovered = reduction + .extract_solution(&vec![0; target.num_vars()]) + .unwrap(); + assert_eq!(source.evaluate(&recovered).unwrap(), Min(Some(0))); } #[test] -fn test_highlyconnecteddeletion_to_ilp_rejects_unassigned_vertex() { - let source = issue_instance(); - let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); - let target_solution = vec![0; reduction.target_problem().num_vars()]; - +fn pair_encoding_decodes_clusters_and_rejects_invalid_assignments() { + let source = triangle_with_leaf(); + let reduction = source.reduce_to().unwrap(); + // Pair variables: 01, 02, 03, 12, 13, 23; then four non-singleton flags. + let valid = vec![1, 1, 0, 1, 0, 0, 1, 1, 1, 0]; assert_eq!( - reduction - .extract_solution(&target_solution) - .unwrap_err() - .to_string(), - "vertex 0 has no selected cluster" + reduction.extract_solution(&valid).unwrap(), + vec![false, false, false, true] ); + for invalid in [ + vec![0; 9], + vec![2; 10], + vec![-1; 10], + vec![1; 10], // The leaf prevents the whole graph from being highly connected. + vec![1, 0, 0, 1, 0, 0, 1, 1, 1, 0], // Non-transitive membership. + ] { + assert!(reduction.extract_solution(&invalid).is_err()); + } } #[test] -fn test_highlyconnecteddeletion_to_ilp_disconnected_no_cluster() { - // Two disjoint K3's stitched by a single bridge edge. The bridge is the - // only "bad" edge: removing it leaves two K3's, both highly connected. +fn polynomial_encoding_preserves_parallel_edge_costs_and_self_loops() { let source = HighlyConnectedDeletion::new(SimpleGraph::new( - 6, + 4, vec![ - // Triangle on {0,1,2}. + (0, 0), + (0, 1), (0, 1), (0, 2), (1, 2), - // Triangle on {3,4,5}. - (3, 4), - (3, 5), - (4, 5), - // Bridge edge. (2, 3), + (2, 3), + (3, 3), ], )); - let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); - let ilp = reduction.target_problem(); - - // No cluster of size >= 3 may straddle the bridge (the only sets {2,3} or - // any 4+ subsets crossing it fail edge-connectivity). The two triangles - // are feasible; mixed 4-vertex sets are not. - assert_eq!(ilp.sense(), ObjectiveSense::Maximize); - let large_cluster_count = ilp.objective().iter().filter(|(_, w)| *w > 0).count(); - assert_eq!(large_cluster_count, 2); + let reduction = source.reduce_to().unwrap(); + let solution = ILPSolver::new().solve(reduction.target_problem()).unwrap(); + let recovered = reduction.extract_solution(&solution).unwrap(); + assert_eq!( + recovered, + vec![false, false, false, false, false, true, true, false] + ); + assert_eq!(source.evaluate(&recovered).unwrap(), Min(Some(2))); +} +#[test] +fn test_highlyconnecteddeletion_to_ilp_closed_loop() { + let source = HighlyConnectedDeletion::new(SimpleGraph::new( + 6, + vec![(0, 1), (0, 2), (1, 2), (3, 4), (3, 5), (4, 5), (2, 3)], + )); + let reduction = source.reduce_to().unwrap(); assert_bf_vs_ilp(&source, &reduction); } + #[test] -fn test_highly_connected_deletion_rejects_mask_overflow() { - let source = HighlyConnectedDeletion::new(SimpleGraph::new(64, vec![])); - assert!( - as ReduceTo>>::reduce_to(&source).is_err() - ); +fn polynomial_size_predictions_and_recovery_work_through_qubo() { + let source = triangle_with_leaf(); + let graph = ReductionGraph::new(); + let path = ReductionPath { + steps: [ + ( + HighlyConnectedDeletion::::NAME, + HighlyConnectedDeletion::::variant(), + ), + (ILP::::NAME, ILP::::variant()), + (QUBO::::NAME, QUBO::::variant()), + ] + .into_iter() + .map(|(name, variant)| ReductionStep { + name: name.into(), + variant: ReductionGraph::variant_to_map(&variant), + }) + .collect(), + }; + let predicted = graph + .compose_path_parameter_transform(&path) + .unwrap() + .unwrap() + .evaluate(&source.parameters()) + .unwrap(); + let chain = graph.reduce_along_path(&path, &source).unwrap().unwrap(); + let qubo = chain.target_problem::>(); + for (field, actual) in qubo.parameters().iter() { + assert!(predicted.get(field).expect("composed polynomial bound") >= actual); + } + let solution = ILPSolver::new().solve(qubo).unwrap(); + let recovered: Vec = chain.extract_solution(&solution).unwrap(); + assert_eq!(source.evaluate(&recovered).unwrap(), Min(Some(1))); } diff --git a/src/unit_tests/rules/ilp_bool_ilp_i64.rs b/src/unit_tests/rules/ilp_bool_ilp_i64.rs index 2c31e6393..6394c2e30 100644 --- a/src/unit_tests/rules/ilp_bool_ilp_i64.rs +++ b/src/unit_tests/rules/ilp_bool_ilp_i64.rs @@ -1,4 +1,4 @@ -use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{Bounded, LinearConstraint, ObjectiveSense, ILP}; use crate::rules::traits::{ReduceTo, ReductionResult}; use crate::solvers::ILPSolver; use crate::traits::Problem; @@ -20,7 +20,8 @@ fn test_ilp_bool_to_ilp_i64_closed_loop() { let source_best = ILPSolver::new().solve(&source).unwrap(); let source_obj = source.evaluate(&source_best).unwrap(); - let result = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + let result = + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let target = result.target_problem(); // Target should have same number of variables @@ -37,7 +38,8 @@ fn test_ilp_bool_to_ilp_i64_closed_loop() { #[test] fn test_ilp_bool_to_ilp_i64_empty() { let source = ILP::::empty(); - let result = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + let result = + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let target = result.target_problem(); assert_eq!(target.num_vars(), 0); assert!(target.constraints().is_empty()); @@ -58,7 +60,8 @@ fn test_ilp_bool_to_ilp_i64_preserves_constraints() { ) .unwrap(); - let result = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + let result = + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let target = result.target_problem(); assert_eq!(target.constraints(), source.constraints()); diff --git a/src/unit_tests/rules/ilp_bounded_ilp.rs b/src/unit_tests/rules/ilp_bounded_ilp.rs new file mode 100644 index 000000000..62aa97aee --- /dev/null +++ b/src/unit_tests/rules/ilp_bounded_ilp.rs @@ -0,0 +1,40 @@ +use super::*; +use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense}; +use crate::rules::AggregateReductionResult; +use crate::solvers::ILPSolver; +use crate::traits::Problem; +use crate::types::Extremum; + +#[test] +fn bounded_ilp_embedding_preserves_domains_optima_and_values() { + for (sense, expected) in [ + (ObjectiveSense::Minimize, -6), + (ObjectiveSense::Maximize, 3), + ] { + let source = ILP::::with_variables( + vec![IntegerVariable::new(Some(-2), Some(4)).unwrap()], + vec![LinearConstraint::le(vec![(0, 1)], 1)], + vec![(0, 3)], + sense, + ) + .unwrap(); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + let target = ReductionResult::target_problem(&reduction); + assert_eq!(target.variables(), source.variables()); + assert_eq!(target.parameters(), source.parameters()); + let solution = ILPSolver::new().solve(target).unwrap(); + let recovered = reduction.extract_solution(&solution).unwrap(); + let value = match sense { + ObjectiveSense::Minimize => Extremum::minimize(Some(expected)), + ObjectiveSense::Maximize => Extremum::maximize(Some(expected)), + }; + assert_eq!(source.evaluate(&recovered).unwrap(), value); + assert_eq!( + reduction.extract_value(target.evaluate(&solution).unwrap()), + value + ); + assert!(reduction.extract_solution(&vec![-3]).is_err()); + assert!(reduction.extract_solution(&vec![2]).is_err()); + assert!(reduction.extract_solution(&vec![]).is_err()); + } +} diff --git a/src/unit_tests/rules/ilp_i64_ilp_bool.rs b/src/unit_tests/rules/ilp_i64_ilp_bool.rs index 75be4dc93..94aec190b 100644 --- a/src/unit_tests/rules/ilp_i64_ilp_bool.rs +++ b/src/unit_tests/rules/ilp_i64_ilp_bool.rs @@ -1,4 +1,4 @@ -use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{Bounded, IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::rules::traits::{ReduceTo, ReductionResult}; use crate::solvers::ILPSolver; @@ -7,7 +7,7 @@ fn integer_ilp( constraints: Vec, objective: Vec<(usize, i64)>, sense: ObjectiveSense, -) -> ILP { +) -> ILP { ILP::with_variables( bounds .iter() @@ -20,7 +20,7 @@ fn integer_ilp( .unwrap() } -fn solve_via_bool(source: &ILP) -> Option<(Vec, i64)> { +fn solve_via_bool(source: &ILP) -> Option<(Vec, i64)> { let reduction = ReduceTo::>::reduce_to(source).expect("reduction should succeed"); let witness = ILPSolver::new().solve(reduction.target_problem()).ok()?; let source_solution = reduction.extract_solution(&witness).unwrap(); @@ -59,7 +59,7 @@ fn test_ilp_i64_to_ilp_bool_maximize() { #[test] fn test_ilp_i64_to_ilp_bool_empty() { - let source = ILP::::empty(); + let source = ILP::::empty(); let reduction = ReduceTo::>::reduce_to(&source).unwrap(); assert_eq!(reduction.target_problem().num_vars(), 0); assert!(reduction.target_problem().constraints().is_empty()); diff --git a/src/unit_tests/rules/ilp_qubo.rs b/src/unit_tests/rules/ilp_qubo.rs index 3f89c648d..3ba2dada1 100644 --- a/src/unit_tests/rules/ilp_qubo.rs +++ b/src/unit_tests/rules/ilp_qubo.rs @@ -57,6 +57,20 @@ fn parameter_bounds_cover_slack_boundaries_and_cancellation() { ); } } + + // A unit row needs a small, data-dependent bound even when objective weights grow. + for weight in [0, 1000] { + let source = ILP::::new( + 1, + vec![LinearConstraint::le(vec![(0, 1)], 1)], + vec![(0, weight)], + ObjectiveSense::Minimize, + ) + .unwrap(); + let predicted = transform.evaluate(&source.parameters()).unwrap(); + assert_eq!(predicted.get("num_vars"), Some(3)); + assert_eq!(predicted.get("num_quadratic_terms"), Some(9)); + } } #[test] diff --git a/src/unit_tests/rules/integerknapsack_ilp.rs b/src/unit_tests/rules/integerknapsack_ilp.rs index 8b421114a..2c51e527f 100644 --- a/src/unit_tests/rules/integerknapsack_ilp.rs +++ b/src/unit_tests/rules/integerknapsack_ilp.rs @@ -1,6 +1,6 @@ #[cfg(feature = "example-db")] use super::canonical_rule_example_specs; -use crate::models::algebraic::{Comparison, ObjectiveSense, ILP}; +use crate::models::algebraic::{Bounded, Comparison, ObjectiveSense, ILP}; use crate::models::set::IntegerKnapsack; use crate::rules::test_helpers::assert_bf_vs_ilp; use crate::rules::{ReduceTo, ReductionResult}; @@ -9,7 +9,8 @@ use crate::solvers::ILPSolver; #[test] fn test_integerknapsack_to_ilp_closed_loop() { let source = IntegerKnapsack::new(vec![3, 4, 5], vec![4, 5, 7], 10).unwrap(); - let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + let reduction = + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); assert_bf_vs_ilp(&source, &reduction); @@ -23,7 +24,8 @@ fn test_integerknapsack_to_ilp_closed_loop() { #[test] fn test_integerknapsack_to_ilp_structure() { let source = IntegerKnapsack::new(vec![3, 4, 5], vec![4, 5, 7], 10).unwrap(); - let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + let reduction = + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let ilp = reduction.target_problem(); assert_eq!(ilp.num_vars(), 3); @@ -59,7 +61,8 @@ fn test_integerknapsack_to_ilp_structure() { #[test] fn test_integerknapsack_to_ilp_zero_capacity() { let source = IntegerKnapsack::new(vec![1, 2], vec![10, 20], 0).unwrap(); - let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + let reduction = + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) @@ -104,3 +107,29 @@ fn test_integerknapsack_to_ilp_canonical_example_spec() { serde_json::json!([0, 0, 2]) ); } + +#[test] +fn test_integer_knapsack_normalization_excludes_oversized_items_through_qubo() { + use crate::models::algebraic::QUBO; + use crate::traits::Problem; + for capacity in [0, 2] { + let source = IntegerKnapsack::new(vec![1, i64::MAX], vec![3, 4], capacity).unwrap(); + let ilp = ReduceTo::>::reduce_to(&source).unwrap(); + assert!(ilp.target_problem().max_constraint_magnitude_bits() <= 2); + let binary = ReduceTo::>::reduce_to(ilp.target_problem()).unwrap(); + let qubo = ReduceTo::>::reduce_to(binary.target_problem()).unwrap(); + let solution = crate::solvers::BruteForce::new() + .solve(qubo.target_problem()) + .unwrap() + .unwrap(); + let recovered = ilp + .extract_solution( + &binary + .extract_solution(&qubo.extract_solution(&solution).unwrap()) + .unwrap(), + ) + .unwrap(); + assert_eq!(recovered, vec![usize::try_from(capacity).unwrap(), 0]); + assert_eq!(source.evaluate(&recovered).unwrap().0, Some(3 * capacity)); + } +} diff --git a/src/unit_tests/rules/integralflowbundles_ilp.rs b/src/unit_tests/rules/integralflowbundles_ilp.rs index b9bea620c..0cb6c4c88 100644 --- a/src/unit_tests/rules/integralflowbundles_ilp.rs +++ b/src/unit_tests/rules/integralflowbundles_ilp.rs @@ -1,5 +1,5 @@ use super::*; -use crate::models::algebraic::{Comparison, ObjectiveSense, ILP}; +use crate::models::algebraic::{Bounded, Comparison, ObjectiveSense, ILP}; use crate::solvers::{BruteForce, ILPSolver}; use crate::topology::DirectedGraph; use crate::traits::Problem; @@ -34,7 +34,7 @@ fn satisfying_config() -> Vec { fn test_integral_flow_bundles_to_ilp_structure() { let problem = yes_instance(); let reduction: ReductionIFBToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); assert_eq!(ilp.num_vars(), 6); @@ -74,7 +74,7 @@ fn test_integral_flow_bundles_to_ilp_closed_loop() { assert!(problem.evaluate(&direct).unwrap()); let reduction: ReductionIFBToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be feasible"); @@ -87,7 +87,7 @@ fn test_integral_flow_bundles_to_ilp_closed_loop() { fn test_integral_flow_bundles_to_ilp_extract_solution_is_identity() { let problem = yes_instance(); let reduction: ReductionIFBToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); assert_eq!( reduction.extract_solution(&satisfying_config()).unwrap(), vec![1, 0, 1, 0, 0, 0] @@ -98,7 +98,7 @@ fn test_integral_flow_bundles_to_ilp_extract_solution_is_identity() { fn test_integral_flow_bundles_to_ilp_unsat_instance_is_infeasible() { let problem = no_instance(); let reduction: ReductionIFBToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); assert!(ILPSolver::new().solve(reduction.target_problem()).is_err()); } @@ -106,7 +106,7 @@ fn test_integral_flow_bundles_to_ilp_unsat_instance_is_infeasible() { fn test_integral_flow_bundles_to_ilp_sink_requirement_constraint() { let problem = yes_instance(); let reduction: ReductionIFBToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); let sink_constraint = ilp @@ -122,6 +122,6 @@ fn test_integral_flow_bundles_to_ilp_sink_requirement_constraint() { fn test_integralflowbundles_to_ilp_bf_vs_ilp() { let problem = yes_instance(); let reduction: ReductionIFBToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); crate::rules::test_helpers::assert_bf_vs_ilp(&problem, &reduction); } diff --git a/src/unit_tests/rules/integralflowhomologousarcs_ilp.rs b/src/unit_tests/rules/integralflowhomologousarcs_ilp.rs index 07888d3b8..60bc0a9b7 100644 --- a/src/unit_tests/rules/integralflowhomologousarcs_ilp.rs +++ b/src/unit_tests/rules/integralflowhomologousarcs_ilp.rs @@ -1,4 +1,5 @@ use super::*; +use crate::models::algebraic::Bounded; use crate::models::algebraic::ILP; use crate::rules::ReduceTo; use crate::solvers::{BruteForce, ILPSolver}; @@ -23,7 +24,8 @@ fn test_integralflowhomologousarcs_to_ilp_closed_loop() { .expect("source instance should be satisfiable"); assert!(source.evaluate(&direct).unwrap()); - let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + let reduction = + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be feasible"); @@ -42,6 +44,34 @@ fn test_integralflowhomologousarcs_to_ilp_bf_vs_ilp() { 2, vec![(0, 1)], ); - let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + let reduction = + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); crate::rules::test_helpers::assert_bf_vs_ilp(&source, &reduction); } + +#[test] +fn test_flow_requirement_normalization_preserves_feasibility() { + for requirement in [i64::MIN, 0, 1, 2, i64::MAX] { + let source = IntegralFlowHomologousArcs::new( + DirectedGraph::new(2, vec![(0, 1)]), + vec![1], + 0, + 1, + requirement, + vec![], + ); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + assert!(reduction.target_problem().max_constraint_magnitude_bits() <= 2); + match ILPSolver::new().solve(reduction.target_problem()) { + Ok(solution) => { + assert!(requirement <= 1); + let recovered = reduction.extract_solution(&solution).unwrap(); + assert!(source.evaluate(&recovered).unwrap()); + } + Err(error) => { + assert!(requirement > 1); + assert_eq!(error, crate::solvers::ILPSolveError::Infeasible); + } + } + } +} diff --git a/src/unit_tests/rules/integralflowwithmultipliers_ilp.rs b/src/unit_tests/rules/integralflowwithmultipliers_ilp.rs index bf0662855..f4c223796 100644 --- a/src/unit_tests/rules/integralflowwithmultipliers_ilp.rs +++ b/src/unit_tests/rules/integralflowwithmultipliers_ilp.rs @@ -1,4 +1,5 @@ use super::*; +use crate::models::algebraic::Bounded; use crate::models::algebraic::ILP; use crate::rules::ReduceTo; use crate::solvers::{BruteForce, ILPSolver}; @@ -22,7 +23,8 @@ fn test_integralflowwithmultipliers_to_ilp_closed_loop() { .expect("source instance should be satisfiable"); assert!(source.evaluate(&direct).unwrap()); - let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + let reduction = + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be feasible"); @@ -41,6 +43,49 @@ fn test_integralflowwithmultipliers_to_ilp_bf_vs_ilp() { vec![2, 2, 2, 2], 2, ); - let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + let reduction = + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); crate::rules::test_helpers::assert_bf_vs_ilp(&source, &reduction); } + +#[test] +fn normalized_multipliers_preserve_all_small_flows() { + // Unit capacities bound the flow despite arbitrarily large multipliers. + // Include a loop to exercise merged coefficients. + for multiplier in [1, 2, 3, 4, i64::MAX] { + for requirement in [i64::MIN, 0, 1, 2, i64::MAX] { + let source = IntegralFlowWithMultipliers::new( + DirectedGraph::new(3, vec![(0, 1), (1, 2), (1, 1)]), + 0, + 2, + vec![1, multiplier, 1], + vec![1; 3], + requirement, + ); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + let target = reduction.target_problem(); + // Capacity sum is three, so four bounds every normalized coefficient + // and demand regardless of the original multiplier or requirement. + assert!( + target + .parameters() + .get("max_constraint_magnitude_bits") + .unwrap() + <= 3 + ); + for mask in 0..8 { + let assignment: Vec = (0..3).map(|i| (mask >> i) & 1).collect(); + let incoming = assignment[0] + assignment[2]; + let outgoing = assignment[1] + assignment[2]; + let expected = i128::from(outgoing) + == i128::from(multiplier) * i128::from(incoming) + && assignment[1] >= requirement; + assert_eq!(target.is_feasible(&assignment).unwrap(), expected); + if expected { + let recovered = reduction.extract_solution(&assignment).unwrap(); + assert!(source.evaluate(&recovered).unwrap().0); + } + } + } + } +} diff --git a/src/unit_tests/rules/knapsack_ilp.rs b/src/unit_tests/rules/knapsack_ilp.rs index bf72d4cff..36c11a5c4 100644 --- a/src/unit_tests/rules/knapsack_ilp.rs +++ b/src/unit_tests/rules/knapsack_ilp.rs @@ -130,3 +130,26 @@ fn test_knapsack_to_ilp_canonical_example_spec() { }] ); } + +#[test] +fn test_knapsack_normalization_excludes_oversized_items_through_qubo() { + use crate::models::algebraic::QUBO; + for capacity in [0, 1] { + let source = Knapsack::new(vec![0, 1, i64::MAX], vec![2, 3, 4], capacity); + let ilp = ReduceTo::>::reduce_to(&source).unwrap(); + assert_eq!(ilp.target_problem().max_constraint_magnitude_bits(), 1); + let qubo = ReduceTo::>::reduce_to(ilp.target_problem()).unwrap(); + let solution = BruteForce::new() + .solve(qubo.target_problem()) + .unwrap() + .unwrap(); + let recovered = ilp + .extract_solution(&qubo.extract_solution(&solution).unwrap()) + .unwrap(); + assert_eq!(recovered, vec![true, capacity == 1, false]); + assert_eq!( + source.evaluate(&recovered).unwrap().0, + Some(2 + 3 * capacity) + ); + } +} diff --git a/src/unit_tests/rules/ksatisfiability_directedtwocommodityintegralflow.rs b/src/unit_tests/rules/ksatisfiability_directedtwocommodityintegralflow.rs index 6ad979fc3..ef995f0ab 100644 --- a/src/unit_tests/rules/ksatisfiability_directedtwocommodityintegralflow.rs +++ b/src/unit_tests/rules/ksatisfiability_directedtwocommodityintegralflow.rs @@ -1,6 +1,7 @@ #[cfg(feature = "example-db")] use super::canonical_rule_example_specs; use super::*; +use crate::models::algebraic::Bounded; use crate::models::algebraic::ILP; use crate::models::formula::CNFClause; #[cfg(feature = "example-db")] @@ -39,7 +40,8 @@ fn all_assignments(num_vars: usize) -> Vec> { fn solve_target_via_ilp( problem: &crate::models::graph::DirectedTwoCommodityIntegralFlow, ) -> Option> { - let reduction = ReduceTo::>::reduce_to(problem).expect("reduction should succeed"); + let reduction = + ReduceTo::>::reduce_to(problem).expect("reduction should succeed"); let ilp_solution = ILPSolver::new().solve(reduction.target_problem()).ok()?; let extracted = reduction.extract_solution(&ilp_solution).unwrap(); problem.evaluate(&extracted).unwrap().0.then_some(extracted) diff --git a/src/unit_tests/rules/ksatisfiability_feasibleregisterassignment.rs b/src/unit_tests/rules/ksatisfiability_feasibleregisterassignment.rs index ef7624f3d..e242c81cf 100644 --- a/src/unit_tests/rules/ksatisfiability_feasibleregisterassignment.rs +++ b/src/unit_tests/rules/ksatisfiability_feasibleregisterassignment.rs @@ -1,4 +1,5 @@ use super::*; +use crate::models::algebraic::Bounded; use crate::models::algebraic::ILP; use crate::models::formula::CNFClause; use crate::solvers::ILPSolver; @@ -140,7 +141,7 @@ fn test_ksatisfiability_to_feasible_register_assignment_closed_loop_via_ilp() { let source = issue_example(); let reduction = ReduceTo::::reduce_to(&source) .expect("reduction should succeed"); - let fra_to_ilp = ReduceTo::>::reduce_to(reduction.target_problem()) + let fra_to_ilp = ReduceTo::>::reduce_to(reduction.target_problem()) .expect("reduction should succeed"); let ilp_solution = ILPSolver::new() @@ -167,7 +168,7 @@ fn test_ksatisfiability_to_feasible_register_assignment_unsatisfiable_instance() ); let reduction = ReduceTo::::reduce_to(&source) .expect("reduction should succeed"); - let fra_to_ilp = ReduceTo::>::reduce_to(reduction.target_problem()) + let fra_to_ilp = ReduceTo::>::reduce_to(reduction.target_problem()) .expect("reduction should succeed"); assert!( diff --git a/src/unit_tests/rules/lengthboundeddisjointpaths_ilp.rs b/src/unit_tests/rules/lengthboundeddisjointpaths_ilp.rs index c9ca738b9..595c3bc38 100644 --- a/src/unit_tests/rules/lengthboundeddisjointpaths_ilp.rs +++ b/src/unit_tests/rules/lengthboundeddisjointpaths_ilp.rs @@ -116,3 +116,19 @@ fn test_lengthboundeddisjointpaths_to_ilp_rejects_invalid_target_solutions() { assert!(reduction.extract_solution(&solution).is_err()); } } + +#[test] +fn test_path_length_threshold_normalization() { + for bound in [1, 2, 1000] { + let source = + LengthBoundedDisjointPaths::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), 0, 2, bound); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + assert!(reduction.target_problem().max_constraint_magnitude_bits() <= 2); + let solution = ILPSolver::new().solve(reduction.target_problem()).unwrap(); + let recovered = reduction.extract_solution(&solution).unwrap(); + assert_eq!( + source.evaluate(&recovered).unwrap(), + Max(Some(i64::from(bound >= 2))) + ); + } +} diff --git a/src/unit_tests/rules/longestpath_ilp.rs b/src/unit_tests/rules/longestpath_ilp.rs index 8e23b37be..45d443943 100644 --- a/src/unit_tests/rules/longestpath_ilp.rs +++ b/src/unit_tests/rules/longestpath_ilp.rs @@ -1,5 +1,5 @@ use super::*; -use crate::models::algebraic::{ObjectiveSense, ILP}; +use crate::models::algebraic::{Bounded, ObjectiveSense, ILP}; use crate::solvers::{BruteForce, ILPSolver}; use crate::topology::SimpleGraph; use crate::traits::Problem; @@ -36,7 +36,7 @@ fn simple_path_problem() -> LongestPath { fn test_reduction_creates_expected_ilp_shape() { let problem = simple_path_problem(); let reduction: ReductionLongestPathToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); assert_eq!(ilp.num_vars(), 7); @@ -67,7 +67,7 @@ fn test_longestpath_to_ilp_closed_loop_on_issue_example() { assert_eq!(best_value, Max(Some(20))); let reduction: ReductionLongestPathToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp_solver = ILPSolver::new(); let ilp_solution = ilp_solver .solve(reduction.target_problem()) @@ -82,7 +82,7 @@ fn test_longestpath_to_ilp_closed_loop_on_issue_example() { fn test_solution_extraction_from_handcrafted_ilp_assignment() { let problem = simple_path_problem(); let reduction: ReductionLongestPathToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); // x_{0->1}, x_{1->0}, x_{1->2}, x_{2->1}, o_0, o_1, o_2 let target_solution = vec![1, 0, 1, 0, 0, 1, 2]; @@ -101,7 +101,7 @@ fn test_source_equals_target_uses_empty_path() { 1, ); let reduction: ReductionLongestPathToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp_solver = ILPSolver::new(); let ilp_solution = ilp_solver .solve(reduction.target_problem()) @@ -116,6 +116,6 @@ fn test_source_equals_target_uses_empty_path() { fn test_longestpath_to_ilp_bf_vs_ilp() { let problem = simple_path_problem(); let reduction: ReductionLongestPathToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); crate::rules::test_helpers::assert_bf_vs_ilp(&problem, &reduction); } diff --git a/src/unit_tests/rules/maximumcokplex_ilp.rs b/src/unit_tests/rules/maximumcokplex_ilp.rs index aa92d32f2..8e479e61f 100644 --- a/src/unit_tests/rules/maximumcokplex_ilp.rs +++ b/src/unit_tests/rules/maximumcokplex_ilp.rs @@ -88,3 +88,19 @@ fn test_maximumcokplex_to_ilp_extract_solution_identity() { assert_eq!(extracted, vec![true, false, true, false, true]); assert_eq!(source.evaluate(&extracted).unwrap(), Max(Some(12))); } + +#[test] +fn test_cokplex_threshold_normalization() { + for k in [1, 2, 1000] { + let source = + MaximumCoKPlex::<_, i64, KN>::with_k(SimpleGraph::new(2, vec![(0, 1)]), vec![2, 3], k); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + assert!(reduction.target_problem().max_constraint_magnitude_bits() <= 2); + let solution = ILPSolver::new().solve(reduction.target_problem()).unwrap(); + let recovered = reduction.extract_solution(&solution).unwrap(); + assert_eq!( + source.evaluate(&recovered).unwrap(), + Max(Some(if k == 1 { 3 } else { 5 })) + ); + } +} diff --git a/src/unit_tests/rules/maximumdomaticnumber_ilp.rs b/src/unit_tests/rules/maximumdomaticnumber_ilp.rs index 54a6e2e1a..47a49f21c 100644 --- a/src/unit_tests/rules/maximumdomaticnumber_ilp.rs +++ b/src/unit_tests/rules/maximumdomaticnumber_ilp.rs @@ -118,3 +118,18 @@ fn test_maximumdomaticnumber_to_ilp_solution_extraction() { let value = problem.evaluate(&extracted).unwrap(); assert_eq!(value, Max(Some(2))); } + +#[test] +fn test_domatic_normalization_ignores_parallel_edges_and_loops() { + let mut edges = vec![(0, 1); 8]; + edges.extend([(0, 0), (1, 1)]); + let source = MaximumDomaticNumber::new(SimpleGraph::new(2, edges)); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + assert_eq!( + reduction.target_problem().max_constraint_magnitude_bits(), + 1 + ); + let solution = ILPSolver::new().solve(reduction.target_problem()).unwrap(); + let recovered = reduction.extract_solution(&solution).unwrap(); + assert_eq!(source.evaluate(&recovered).unwrap(), Max(Some(2))); +} diff --git a/src/unit_tests/rules/maximumleafspanningtree_ilp.rs b/src/unit_tests/rules/maximumleafspanningtree_ilp.rs index be3068b63..1c5034599 100644 --- a/src/unit_tests/rules/maximumleafspanningtree_ilp.rs +++ b/src/unit_tests/rules/maximumleafspanningtree_ilp.rs @@ -1,5 +1,5 @@ use super::*; -use crate::models::algebraic::{ObjectiveSense, ILP}; +use crate::models::algebraic::{Bounded, ObjectiveSense, ILP}; use crate::models::graph::MaximumLeafSpanningTree; use crate::rules::ReduceTo; use crate::solvers::{BruteForce, ILPSolver}; @@ -35,7 +35,7 @@ fn canonical_instance() -> MaximumLeafSpanningTree { fn test_reduction_creates_expected_ilp_shape() { let problem = small_instance(); let reduction: ReductionMaximumLeafSpanningTreeToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); // n=4, m=4: num_vars = 3*4 + 4 = 16 @@ -49,7 +49,7 @@ fn test_reduction_creates_expected_ilp_shape() { fn test_maximumleafspanningtree_to_ilp_closed_loop() { let problem = small_instance(); let reduction: ReductionMaximumLeafSpanningTreeToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); let bf = BruteForce::new(); @@ -69,7 +69,7 @@ fn test_maximumleafspanningtree_to_ilp_closed_loop() { fn test_maximumleafspanningtree_to_ilp_canonical_closed_loop() { let problem = canonical_instance(); let reduction: ReductionMaximumLeafSpanningTreeToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); let bf = BruteForce::new(); @@ -87,7 +87,7 @@ fn test_maximumleafspanningtree_to_ilp_canonical_closed_loop() { fn test_solution_extraction_reads_edge_selector_prefix() { let problem = small_instance(); let reduction: ReductionMaximumLeafSpanningTreeToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); // 16 variables total, first 4 are edge selectors let mut target_solution = vec![0; 16]; @@ -105,7 +105,7 @@ fn test_solution_extraction_reads_edge_selector_prefix() { fn test_reduce_and_solve_via_ilp() { let problem = canonical_instance(); let reduction: ReductionMaximumLeafSpanningTreeToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be solvable"); @@ -118,7 +118,7 @@ fn test_reduce_and_solve_via_ilp() { fn test_maximumleafspanningtree_to_ilp_bf_vs_ilp() { let problem = canonical_instance(); let reduction: ReductionMaximumLeafSpanningTreeToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); crate::rules::test_helpers::assert_bf_vs_ilp(&problem, &reduction); } @@ -127,7 +127,7 @@ fn test_maximumleafspanningtree_to_ilp_path_graph() { // Path P4: 0-1-2-3, only spanning tree is the path itself => 2 leaves let problem = MaximumLeafSpanningTree::new(SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)])); let reduction: ReductionMaximumLeafSpanningTreeToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); let ilp_solver = ILPSolver::new(); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); @@ -140,7 +140,7 @@ fn test_maximumleafspanningtree_to_ilp_star_graph() { // Star K1,3: center 0, leaves 1,2,3 => 3 leaves let problem = MaximumLeafSpanningTree::new(SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3)])); let reduction: ReductionMaximumLeafSpanningTreeToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); let ilp_solver = ILPSolver::new(); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); @@ -161,7 +161,7 @@ fn test_maximumleafspanningtree_to_ilp_complete_graph() { let bf_value = problem.evaluate(&bf_solutions[0]).unwrap(); let reduction: ReductionMaximumLeafSpanningTreeToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); let ilp_solution = ILPSolver::new().solve(ilp).expect("ILP should be solvable"); let extracted = reduction.extract_solution(&ilp_solution).unwrap(); diff --git a/src/unit_tests/rules/maximumsetpacking_ilp.rs b/src/unit_tests/rules/maximumsetpacking_ilp.rs index d8a91d468..5bd83b2d7 100644 --- a/src/unit_tests/rules/maximumsetpacking_ilp.rs +++ b/src/unit_tests/rules/maximumsetpacking_ilp.rs @@ -163,3 +163,18 @@ fn test_maximumsetpacking_to_ilp_bf_vs_ilp() { ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); crate::rules::test_helpers::assert_bf_vs_ilp(&problem, &reduction); } + +#[test] +fn test_set_packing_normalization_preserves_repeated_membership() { + let source = + MaximumSetPacking::with_weights(vec![vec![0, 0], vec![0], vec![1, 1]], vec![3, 2, 4]) + .unwrap(); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + let solution = ILPSolver::new().solve(reduction.target_problem()).unwrap(); + let recovered = reduction.extract_solution(&solution).unwrap(); + assert_eq!(source.evaluate(&recovered).unwrap(), Max(Some(7))); + assert_eq!( + reduction.target_problem().max_constraint_magnitude_bits(), + 1 + ); +} diff --git a/src/unit_tests/rules/minimumcapacitatedspanningtree_ilp.rs b/src/unit_tests/rules/minimumcapacitatedspanningtree_ilp.rs index e3725ece6..18af512bd 100644 --- a/src/unit_tests/rules/minimumcapacitatedspanningtree_ilp.rs +++ b/src/unit_tests/rules/minimumcapacitatedspanningtree_ilp.rs @@ -1,5 +1,5 @@ use super::*; -use crate::models::algebraic::{ObjectiveSense, ILP}; +use crate::models::algebraic::{Bounded, ObjectiveSense, ILP}; use crate::models::graph::MinimumCapacitatedSpanningTree; use crate::rules::ReduceTo; use crate::solvers::{BruteForce, ILPSolver}; @@ -45,7 +45,7 @@ fn canonical_instance() -> MinimumCapacitatedSpanningTree { fn test_reduction_creates_expected_ilp_shape() { let problem = small_instance(); let reduction: ReductionMinimumCapacitatedSpanningTreeToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); // m=5: num_vars = 5*5 = 25 @@ -57,7 +57,7 @@ fn test_reduction_creates_expected_ilp_shape() { fn test_minimumcapacitatedspanningtree_to_ilp_closed_loop() { let problem = small_instance(); let reduction: ReductionMinimumCapacitatedSpanningTreeToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); let bf = BruteForce::new(); @@ -76,7 +76,7 @@ fn test_minimumcapacitatedspanningtree_to_ilp_closed_loop() { fn test_minimumcapacitatedspanningtree_to_ilp_canonical_closed_loop() { let problem = canonical_instance(); let reduction: ReductionMinimumCapacitatedSpanningTreeToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); let bf = BruteForce::new(); @@ -94,7 +94,7 @@ fn test_minimumcapacitatedspanningtree_to_ilp_canonical_closed_loop() { fn test_solution_extraction_reads_edge_selector_prefix() { let problem = small_instance(); let reduction: ReductionMinimumCapacitatedSpanningTreeToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); // 25 variables total, first 5 are edge selectors let mut target_solution = vec![0; 25]; @@ -112,7 +112,7 @@ fn test_solution_extraction_reads_edge_selector_prefix() { fn test_minimumcapacitatedspanningtree_to_ilp_bf_vs_ilp() { let problem = canonical_instance(); let reduction: ReductionMinimumCapacitatedSpanningTreeToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); crate::rules::test_helpers::assert_bf_vs_ilp(&problem, &reduction); } @@ -128,7 +128,7 @@ fn test_minimumcapacitatedspanningtree_to_ilp_star_tree() { 1, // capacity = 1 forces star tree ); let reduction: ReductionMinimumCapacitatedSpanningTreeToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); let ilp_solution = ILPSolver::new().solve(ilp).expect("ILP should be solvable"); let extracted = reduction.extract_solution(&ilp_solution).unwrap(); @@ -148,7 +148,7 @@ fn test_minimumcapacitatedspanningtree_to_ilp_path_graph() { 3, ); let reduction: ReductionMinimumCapacitatedSpanningTreeToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); let ilp_solution = ILPSolver::new().solve(ilp).expect("ILP should be solvable"); let extracted = reduction.extract_solution(&ilp_solution).unwrap(); @@ -166,6 +166,6 @@ fn test_zero_requirement_vertex_still_must_be_connected() { 2, ); let reduction: ReductionMinimumCapacitatedSpanningTreeToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); assert!(ILPSolver::new().solve(reduction.target_problem()).is_err()); } diff --git a/src/unit_tests/rules/minimumcutintoboundedsets_ilp.rs b/src/unit_tests/rules/minimumcutintoboundedsets_ilp.rs index 1f31f58a5..ae22e673d 100644 --- a/src/unit_tests/rules/minimumcutintoboundedsets_ilp.rs +++ b/src/unit_tests/rules/minimumcutintoboundedsets_ilp.rs @@ -70,3 +70,29 @@ fn test_minimumcutintoboundedsets_to_ilp_bf_vs_ilp() { ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); crate::rules::test_helpers::assert_bf_vs_ilp(&source, &reduction); } + +#[test] +fn test_partition_bound_normalization_preserves_the_optimum() { + for bound in [0, 1, 2, 1000] { + let source = MinimumCutIntoBoundedSets::new( + SimpleGraph::new(2, vec![(0, 1)]), + vec![3_i64], + 0, + 1, + bound, + ); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + assert!(reduction.target_problem().max_constraint_magnitude_bits() <= 2); + let solution = crate::solvers::ILPSolver::new().solve(reduction.target_problem()); + assert_eq!(solution.is_ok(), bound >= 1); + if let Ok(solution) = solution { + let recovered = reduction.extract_solution(&solution).unwrap(); + assert_eq!(source.evaluate(&recovered).unwrap().0, Some(3)); + } else { + assert_eq!( + solution.unwrap_err(), + crate::solvers::ILPSolveError::Infeasible + ); + } + } +} diff --git a/src/unit_tests/rules/minimumedgecostflow_ilp.rs b/src/unit_tests/rules/minimumedgecostflow_ilp.rs index a2f9a9415..d03cb6962 100644 --- a/src/unit_tests/rules/minimumedgecostflow_ilp.rs +++ b/src/unit_tests/rules/minimumedgecostflow_ilp.rs @@ -1,5 +1,5 @@ use super::*; -use crate::models::algebraic::{ObjectiveSense, ILP}; +use crate::models::algebraic::{Bounded, ObjectiveSense, ILP}; use crate::solvers::{BruteForce, ILPSolver}; use crate::topology::DirectedGraph; use crate::traits::Problem; @@ -47,7 +47,7 @@ fn infeasible_instance() -> MinimumEdgeCostFlow { fn test_minimumedgecostflow_to_ilp_structure() { let problem = issue_instance(); let reduction: ReductionMECFToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); crate::rules::test_helpers::assert_parameter_predictions(&problem, &reduction); @@ -75,7 +75,7 @@ fn test_minimumedgecostflow_to_ilp_closed_loop() { assert_eq!(bf_value, Min(Some(3))); let reduction: ReductionMECFToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be feasible"); @@ -97,7 +97,7 @@ fn test_minimumedgecostflow_to_ilp_small_closed_loop() { assert_eq!(bf_value, Min(Some(8))); let reduction: ReductionMECFToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be feasible"); @@ -109,7 +109,7 @@ fn test_minimumedgecostflow_to_ilp_small_closed_loop() { fn test_minimumedgecostflow_to_ilp_infeasible() { let problem = infeasible_instance(); let reduction: ReductionMECFToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); assert!( ILPSolver::new().solve(reduction.target_problem()).is_err(), "infeasible instance should produce infeasible ILP" @@ -120,7 +120,7 @@ fn test_minimumedgecostflow_to_ilp_infeasible() { fn test_minimumedgecostflow_to_ilp_bf_vs_ilp() { let problem = issue_instance(); let reduction: ReductionMECFToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); crate::rules::test_helpers::assert_bf_vs_ilp(&problem, &reduction); } @@ -128,7 +128,7 @@ fn test_minimumedgecostflow_to_ilp_bf_vs_ilp() { fn test_minimumedgecostflow_to_ilp_extract_solution() { let problem = issue_instance(); let reduction: ReductionMECFToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); // Manually construct a target solution: route 1 via v2, 2 via v3 // f = [0, 1, 2, 0, 1, 2], y = [0, 1, 1, 0, 1, 1] @@ -147,3 +147,33 @@ fn test_minimumedgecostflow_to_ilp_extract_solution() { assert_eq!(extracted, vec![0, 1, 2, 0, 1, 2]); assert_eq!(problem.evaluate(&extracted).unwrap(), Min(Some(3))); } + +#[test] +fn test_flow_requirement_normalization_preserves_feasibility() { + for requirement in [i64::MIN, 0, 1, 2, i64::MAX] { + let source = MinimumEdgeCostFlow::new( + DirectedGraph::new(2, vec![(0, 1)]), + vec![3], + vec![1], + 0, + 1, + requirement, + ); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + assert!(reduction.target_problem().max_constraint_magnitude_bits() <= 2); + match ILPSolver::new().solve(reduction.target_problem()) { + Ok(solution) => { + assert!(requirement <= 1); + let recovered = reduction.extract_solution(&solution).unwrap(); + assert_eq!( + source.evaluate(&recovered).unwrap(), + Min(Some(if requirement == 1 { 3 } else { 0 })) + ); + } + Err(error) => { + assert!(requirement > 1); + assert_eq!(error, crate::solvers::ILPSolveError::Infeasible); + } + } + } +} diff --git a/src/unit_tests/rules/minimumfeedbackarcset_ilp.rs b/src/unit_tests/rules/minimumfeedbackarcset_ilp.rs index eb9e8aa51..59dd53eaf 100644 --- a/src/unit_tests/rules/minimumfeedbackarcset_ilp.rs +++ b/src/unit_tests/rules/minimumfeedbackarcset_ilp.rs @@ -1,4 +1,5 @@ use super::*; +use crate::models::algebraic::Bounded; use crate::solvers::{BruteForce, ILPSolver}; use crate::topology::DirectedGraph; use crate::traits::Problem; @@ -10,7 +11,7 @@ fn feedback_arc_set_solves_through_integer_binary_ilp_and_qubo() { let source = MinimumFeedbackArcSet::new(DirectedGraph::new(2, vec![(0, 1), (1, 0)]), vec![2_i64, 5]); - let integer = ReduceTo::>::reduce_to(&source).unwrap(); + let integer = ReduceTo::>::reduce_to(&source).unwrap(); let binary = ReduceTo::>::reduce_to(integer.target_problem()).unwrap(); let qubo = ReduceTo::>::reduce_to(binary.target_problem()).unwrap(); let optimum = BruteForce::new() @@ -34,7 +35,7 @@ fn test_reduction_creates_valid_ilp() { let graph = DirectedGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]); let problem = MinimumFeedbackArcSet::new(graph, vec![1i64; 3]); let reduction: ReductionFASToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); // m + n = 3 + 3 = 6 variables (3 binary y_a + 3 integer o_v) @@ -55,7 +56,7 @@ fn test_minimumfeedbackarcset_to_ilp_bf_vs_ilp() { let graph = DirectedGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]); let problem = MinimumFeedbackArcSet::new(graph, vec![1i64; 3]); let reduction: ReductionFASToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); let bf = BruteForce::new(); @@ -81,7 +82,7 @@ fn test_solution_extraction() { let graph = DirectedGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]); let problem = MinimumFeedbackArcSet::new(graph, vec![1i64; 3]); let reduction: ReductionFASToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); // Simulate ILP solution: y_0=0, y_1=0, y_2=1, o_0=0, o_1=1, o_2=2 let ilp_solution = vec![0, 0, 1, 0, 1, 2]; @@ -101,7 +102,7 @@ fn test_minimumfeedbackarcset_to_ilp_trivial() { let graph = DirectedGraph::new(3, vec![(0, 1), (1, 2)]); let problem = MinimumFeedbackArcSet::new(graph, vec![1i64; 2]); let reduction: ReductionFASToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); // m=2, n=3 → 5 variables; 2 + 3 + 2 = 7 constraints diff --git a/src/unit_tests/rules/minimumfeedbackvertexset_ilp.rs b/src/unit_tests/rules/minimumfeedbackvertexset_ilp.rs index 8b47862ae..e37eb44cb 100644 --- a/src/unit_tests/rules/minimumfeedbackvertexset_ilp.rs +++ b/src/unit_tests/rules/minimumfeedbackvertexset_ilp.rs @@ -1,4 +1,5 @@ use super::*; +use crate::models::algebraic::Bounded; use crate::solvers::{BruteForce, ILPSolver}; use crate::topology::DirectedGraph; use crate::traits::Problem; @@ -10,7 +11,7 @@ fn test_reduction_creates_valid_ilp() { let graph = DirectedGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]); let problem = MinimumFeedbackVertexSet::new(graph, vec![1i64; 3]); let reduction: ReductionMFVSToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); // 2n = 6 variables (3 binary x_i + 3 integer o_i) @@ -27,7 +28,7 @@ fn test_minimumfeedbackvertexset_to_ilp_closed_loop() { let graph = DirectedGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]); let problem = MinimumFeedbackVertexSet::new(graph, vec![1i64; 3]); let reduction: ReductionMFVSToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); let bf = BruteForce::new(); @@ -76,7 +77,7 @@ fn test_cycle_of_triangles() { let graph = DirectedGraph::new(9, arcs); let problem = MinimumFeedbackVertexSet::new(graph, vec![1i64; 9]); let reduction: ReductionMFVSToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); // Verify ILP structure @@ -101,7 +102,7 @@ fn test_dag_no_removal() { let graph = DirectedGraph::new(3, vec![(0, 1), (1, 2)]); let problem = MinimumFeedbackVertexSet::new(graph, vec![1i64; 3]); let reduction: ReductionMFVSToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); let ilp_solver = ILPSolver::new(); @@ -119,7 +120,7 @@ fn test_single_vertex() { let graph = DirectedGraph::new(1, vec![]); let problem = MinimumFeedbackVertexSet::new(graph, vec![1i64]); let reduction: ReductionMFVSToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); assert_eq!(ilp.num_vars(), 2); @@ -141,7 +142,7 @@ fn test_weighted() { let graph = DirectedGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]); let problem = MinimumFeedbackVertexSet::new(graph, vec![10, 1, 10]); let reduction: ReductionMFVSToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); // Check that weights are correctly transferred to objective @@ -172,7 +173,7 @@ fn test_two_disjoint_cycles() { let bf_size = problem.evaluate(&bf_solutions[0]).unwrap(); let reduction: ReductionMFVSToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); let ilp_solver = ILPSolver::new(); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); @@ -189,7 +190,7 @@ fn test_solution_extraction() { let graph = DirectedGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]); let problem = MinimumFeedbackVertexSet::new(graph, vec![1i64; 3]); let reduction: ReductionMFVSToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); // Simulate ILP solution: x_0=1, x_1=0, x_2=0, o_0=0, o_1=0, o_2=1 let ilp_solution = vec![1, 0, 0, 0, 0, 1]; @@ -205,6 +206,6 @@ fn test_minimumfeedbackvertexset_to_ilp_bf_vs_ilp() { let graph = DirectedGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]); let problem = MinimumFeedbackVertexSet::new(graph, vec![1i64; 3]); let reduction: ReductionMFVSToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); crate::rules::test_helpers::assert_bf_vs_ilp(&problem, &reduction); } diff --git a/src/unit_tests/rules/minimumgraphbandwidth_ilp.rs b/src/unit_tests/rules/minimumgraphbandwidth_ilp.rs index 9eec5a8d3..af0d5374d 100644 --- a/src/unit_tests/rules/minimumgraphbandwidth_ilp.rs +++ b/src/unit_tests/rules/minimumgraphbandwidth_ilp.rs @@ -1,4 +1,5 @@ use super::*; +use crate::models::algebraic::Bounded; use crate::solvers::{BruteForce, ILPSolver}; use crate::topology::SimpleGraph; use crate::traits::Problem; @@ -8,7 +9,7 @@ fn test_reduction_creates_valid_ilp() { // Star S4: 4 vertices, 3 edges let problem = MinimumGraphBandwidth::new(SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3)])); let reduction: ReductionMGBToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); // num_x=16, pos_v=4, B=1, total=21 assert_eq!(ilp.num_vars(), 21); @@ -30,7 +31,7 @@ fn test_minimumgraphbandwidth_to_ilp_closed_loop() { // Solve via ILP let reduction: ReductionMGBToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp_solver = ILPSolver::new(); let ilp_solution = ilp_solver .solve(reduction.target_problem()) @@ -56,7 +57,7 @@ fn test_minimumgraphbandwidth_to_ilp_path() { let problem = MinimumGraphBandwidth::new(SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)])); let reduction: ReductionMGBToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp_solver = ILPSolver::new(); let ilp_solution = ilp_solver .solve(reduction.target_problem()) @@ -75,7 +76,7 @@ fn test_minimumgraphbandwidth_to_ilp_bf_vs_ilp() { // Star S4 let problem = MinimumGraphBandwidth::new(SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3)])); let reduction: ReductionMGBToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); crate::rules::test_helpers::assert_bf_vs_ilp(&problem, &reduction); } @@ -85,6 +86,6 @@ fn test_minimumgraphbandwidth_to_ilp_cycle() { let problem = MinimumGraphBandwidth::new(SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3), (3, 0)])); let reduction: ReductionMGBToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); crate::rules::test_helpers::assert_bf_vs_ilp(&problem, &reduction); } diff --git a/src/unit_tests/rules/minimumweightdecoding_ilp.rs b/src/unit_tests/rules/minimumweightdecoding_ilp.rs index 09eb28337..c4d322a78 100644 --- a/src/unit_tests/rules/minimumweightdecoding_ilp.rs +++ b/src/unit_tests/rules/minimumweightdecoding_ilp.rs @@ -1,5 +1,5 @@ use super::*; -use crate::models::algebraic::{ObjectiveSense, ILP}; +use crate::models::algebraic::{Bounded, ObjectiveSense, ILP}; use crate::solvers::{BruteForce, ILPSolver}; use crate::traits::Problem; use crate::types::Min; @@ -35,7 +35,7 @@ fn infeasible_instance() -> MinimumWeightDecoding { fn test_minimumweightdecoding_to_ilp_structure() { let problem = issue_instance(); let reduction: ReductionMinimumWeightDecodingToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); // 4 cols + 3 rows = 7 variables @@ -61,7 +61,7 @@ fn test_minimumweightdecoding_to_ilp_closed_loop() { assert_eq!(bf_value, Min(Some(1))); let reduction: ReductionMinimumWeightDecodingToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be feasible"); @@ -83,7 +83,7 @@ fn test_minimumweightdecoding_to_ilp_small_closed_loop() { assert_eq!(bf_value, Min(Some(1))); let reduction: ReductionMinimumWeightDecodingToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be feasible"); @@ -95,7 +95,7 @@ fn test_minimumweightdecoding_to_ilp_small_closed_loop() { fn test_minimumweightdecoding_to_ilp_infeasible() { let problem = infeasible_instance(); let reduction: ReductionMinimumWeightDecodingToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); assert!( ILPSolver::new().solve(reduction.target_problem()).is_err(), "infeasible instance should produce infeasible ILP" @@ -106,7 +106,7 @@ fn test_minimumweightdecoding_to_ilp_infeasible() { fn test_minimumweightdecoding_to_ilp_bf_vs_ilp() { let problem = issue_instance(); let reduction: ReductionMinimumWeightDecodingToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); crate::rules::test_helpers::assert_bf_vs_ilp(&problem, &reduction); } @@ -114,7 +114,7 @@ fn test_minimumweightdecoding_to_ilp_bf_vs_ilp() { fn test_minimumweightdecoding_to_ilp_extract_solution() { let problem = issue_instance(); let reduction: ReductionMinimumWeightDecodingToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); // Manually construct a valid target solution: x=[0,0,1,0], k=[0,0,0] // (k_i values are the integer slack from mod-2) diff --git a/src/unit_tests/rules/minmaxmulticenter_ilp.rs b/src/unit_tests/rules/minmaxmulticenter_ilp.rs index 743658bfb..517661794 100644 --- a/src/unit_tests/rules/minmaxmulticenter_ilp.rs +++ b/src/unit_tests/rules/minmaxmulticenter_ilp.rs @@ -1,5 +1,5 @@ use super::*; -use crate::models::algebraic::{ObjectiveSense, ILP}; +use crate::models::algebraic::{Bounded, ObjectiveSense, ILP}; use crate::models::graph::MinMaxMulticenter; use crate::solvers::{BruteForce, ILPSolver}; use crate::topology::SimpleGraph; @@ -16,7 +16,7 @@ fn test_reduction_creates_valid_ilp() { 1, ); let reduction: ReductionMMCToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); // num_vars = n + n^2 + 1 = 3 + 9 + 1 = 13 assert_eq!(ilp.num_vars(), 13, "n + n^2 + 1 variables"); @@ -43,7 +43,7 @@ fn test_minmaxmulticenter_to_ilp_bf_vs_ilp() { 1, ); let reduction: ReductionMMCToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); let bf = BruteForce::new(); @@ -72,7 +72,7 @@ fn test_solution_extraction() { 1, ); let reduction: ReductionMMCToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); // Manually construct a valid ILP solution: // x = [0, 1, 0]; each vertex assigned to center 1; z = 1 @@ -104,7 +104,7 @@ fn test_minmaxmulticenter_to_ilp_weighted() { assert_eq!(bf_value, Min(Some(100))); let reduction: ReductionMMCToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be solvable"); @@ -117,7 +117,7 @@ fn test_minmaxmulticenter_to_ilp_trivial() { // Single vertex, K=1: the only vertex is the center, distance = 0 let problem = MinMaxMulticenter::new(SimpleGraph::new(1, vec![]), vec![5i64], vec![], 1); let reduction: ReductionMMCToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); // num_vars = 1 + 1 + 1 = 3 assert_eq!(ilp.num_vars(), 3); diff --git a/src/unit_tests/rules/mixedchinesepostman_ilp.rs b/src/unit_tests/rules/mixedchinesepostman_ilp.rs index 46e5817de..24e44a7bb 100644 --- a/src/unit_tests/rules/mixedchinesepostman_ilp.rs +++ b/src/unit_tests/rules/mixedchinesepostman_ilp.rs @@ -1,4 +1,5 @@ use super::*; +use crate::models::algebraic::Bounded; use crate::models::algebraic::ILP; use crate::rules::ReduceTo; use crate::solvers::{BruteForce, ILPSolver}; @@ -19,7 +20,8 @@ fn test_mixedchinesepostman_to_ilp_closed_loop() { .expect("source instance should have an optimal solution"); assert!(source.evaluate(&direct).unwrap().0.is_some()); - let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + let reduction = + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be feasible"); @@ -41,7 +43,8 @@ fn test_mixedchinesepostman_to_ilp_bf_vs_ilp() { let bf_value = source.evaluate(&bf_value_solution).unwrap(); - let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + let reduction = + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be feasible"); @@ -67,7 +70,8 @@ fn test_mixedchinesepostman_to_ilp_weighted() { let bf_value = source.evaluate(&bf_value_solution).unwrap(); - let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + let reduction = + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be feasible"); @@ -91,7 +95,7 @@ fn test_mixedchinesepostman_to_ilp_with_isolated_vertices() { vec![4, 5, 1, 12, 9], vec![6, 1, 13, 7], ); - let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); let ilp_solution = ILPSolver::new().solve(reduction.target_problem()).unwrap(); let extracted = reduction.extract_solution(&ilp_solution).unwrap(); diff --git a/src/unit_tests/rules/multiplechoicebranching_ilp.rs b/src/unit_tests/rules/multiplechoicebranching_ilp.rs index 6166a1180..10460819f 100644 --- a/src/unit_tests/rules/multiplechoicebranching_ilp.rs +++ b/src/unit_tests/rules/multiplechoicebranching_ilp.rs @@ -1,4 +1,5 @@ use super::*; +use crate::models::algebraic::Bounded; use crate::solvers::{BruteForce, ILPSolver}; use crate::topology::DirectedGraph; use crate::traits::Problem; @@ -14,7 +15,7 @@ fn test_multiplechoicebranching_to_ilp_closed_loop() { threshold, ); let expected = BruteForce::new().solve(&problem).unwrap(); - let reduction = ReduceTo::>::reduce_to(&problem).unwrap(); + let reduction = ReduceTo::>::reduce_to(&problem).unwrap(); match expected { Some(_) => { let target = ILPSolver::new().solve(reduction.target_problem()).unwrap(); @@ -34,7 +35,7 @@ fn test_multiplechoicebranching_to_ilp_rejects_forced_cycle() { vec![vec![0], vec![1]], 2, ); - let reduction = ReduceTo::>::reduce_to(&problem).unwrap(); + let reduction = ReduceTo::>::reduce_to(&problem).unwrap(); assert!(ILPSolver::new().solve(reduction.target_problem()).is_err()); } @@ -46,7 +47,7 @@ fn test_multiplechoicebranching_to_ilp_size() { vec![vec![0, 1], vec![2, 3]], 3, ); - let reduction = ReduceTo::>::reduce_to(&problem).unwrap(); + let reduction = ReduceTo::>::reduce_to(&problem).unwrap(); assert_eq!(reduction.target_problem().num_vars(), 7); assert_eq!(reduction.target_problem().num_constraints(), 17); } @@ -54,7 +55,7 @@ fn test_multiplechoicebranching_to_ilp_size() { #[test] fn test_multiplechoicebranching_to_ilp_empty_graph() { let problem = MultipleChoiceBranching::new(DirectedGraph::new(0, vec![]), vec![], vec![], 0); - let reduction = ReduceTo::>::reduce_to(&problem).unwrap(); + let reduction = ReduceTo::>::reduce_to(&problem).unwrap(); let target = ILPSolver::new().solve(reduction.target_problem()).unwrap(); assert_eq!( reduction.extract_solution(&target).unwrap(), diff --git a/src/unit_tests/rules/openshopscheduling_ilp.rs b/src/unit_tests/rules/openshopscheduling_ilp.rs index 27b813623..f8a4c1641 100644 --- a/src/unit_tests/rules/openshopscheduling_ilp.rs +++ b/src/unit_tests/rules/openshopscheduling_ilp.rs @@ -1,4 +1,5 @@ use super::*; +use crate::models::algebraic::Bounded; use crate::models::algebraic::ILP; use crate::models::misc::OpenShopScheduling; use crate::solvers::ILPSolver; @@ -14,12 +15,12 @@ fn small_instance() -> OpenShopScheduling { #[test] fn test_decision_openshopscheduling_to_ilp_bound_is_a_constraint() { let inner = small_instance(); - let optimization = ReduceTo::>::reduce_to(&inner).unwrap(); + let optimization = ReduceTo::>::reduce_to(&inner).unwrap(); let solver = ILPSolver::new(); let optimal = solver.solve(optimization.target_problem()).unwrap(); for bound in [-1, 2, 3, 4] { let source = Decision::new(inner.clone(), bound); - let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); let target = reduction.target_problem(); assert!(target.objective().is_empty()); assert_eq!(target.num_vars(), optimization.target_problem().num_vars()); @@ -61,7 +62,7 @@ fn medium_instance() -> OpenShopScheduling { fn test_openshopscheduling_to_ilp_structure_small() { let p = small_instance(); let reduction: ReductionOSSToILP = - ReduceTo::>::reduce_to(&p).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&p).expect("reduction should succeed"); let ilp = reduction.target_problem(); // n=2, m=2: @@ -97,7 +98,7 @@ fn test_openshopscheduling_to_ilp_structure_small() { fn test_openshopscheduling_to_ilp_closed_loop_small() { let p = small_instance(); let reduction: ReductionOSSToILP = - ReduceTo::>::reduce_to(&p).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&p).expect("reduction should succeed"); let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be feasible"); @@ -116,7 +117,7 @@ fn test_openshopscheduling_to_ilp_closed_loop_small() { fn test_openshopscheduling_to_ilp_closed_loop_medium() { let p = medium_instance(); let reduction: ReductionOSSToILP = - ReduceTo::>::reduce_to(&p).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&p).expect("reduction should succeed"); let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be feasible"); @@ -139,7 +140,7 @@ fn test_openshopscheduling_to_ilp_closed_loop_medium() { fn test_openshopscheduling_to_ilp_extract_solution_respects_start_times() { let p = small_instance(); let reduction: ReductionOSSToILP = - ReduceTo::>::reduce_to(&p).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&p).expect("reduction should succeed"); let target_solution = vec![1, 0, 0, 1, 1, 0, 1, 0, 3]; let extracted = reduction.extract_solution(&target_solution).unwrap(); assert_eq!(extracted, vec![0, 1, 1, 0]); @@ -153,7 +154,7 @@ fn test_openshopscheduling_to_ilp_single_job() { // 1 job, 2 machines: trivial, makespan = sum of processing times let p = OpenShopScheduling::new(2, vec![vec![3, 4]]); let reduction: ReductionOSSToILP = - ReduceTo::>::reduce_to(&p).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&p).expect("reduction should succeed"); let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be feasible"); @@ -168,7 +169,7 @@ fn test_openshopscheduling_to_ilp_single_machine() { // 3 jobs, 1 machine: serial schedule, makespan = sum of all processing times let p = OpenShopScheduling::new(1, vec![vec![2], vec![3], vec![1]]); let reduction: ReductionOSSToILP = - ReduceTo::>::reduce_to(&p).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&p).expect("reduction should succeed"); let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be feasible"); @@ -177,3 +178,30 @@ fn test_openshopscheduling_to_ilp_single_machine() { assert!(value.0.is_some()); assert_eq!(value, Min(Some(6))); } + +#[test] +fn test_decision_makespan_threshold_normalization() { + for bound in [i64::MIN, -1, 0, 1, i64::MAX] { + let source = crate::models::Decision::new(OpenShopScheduling::new(1, vec![vec![1]]), bound); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + assert_eq!( + reduction.target_problem().max_constraint_magnitude_bits(), + 1 + ); + match ILPSolver::new().solve(reduction.target_problem()) { + Ok(solution) => { + assert!(bound >= 1); + assert!( + source + .evaluate(&reduction.extract_solution(&solution).unwrap()) + .unwrap() + .0 + ); + } + Err(error) => { + assert!(bound < 1); + assert_eq!(error, crate::solvers::ILPSolveError::Infeasible); + } + } + } +} diff --git a/src/unit_tests/rules/optimallineararrangement_ilp.rs b/src/unit_tests/rules/optimallineararrangement_ilp.rs index c45f0bf41..ca28a7d1c 100644 --- a/src/unit_tests/rules/optimallineararrangement_ilp.rs +++ b/src/unit_tests/rules/optimallineararrangement_ilp.rs @@ -1,4 +1,5 @@ use super::*; +use crate::models::algebraic::Bounded; use crate::solvers::{BruteForce, ILPSolver}; use crate::topology::SimpleGraph; use crate::traits::Problem; @@ -8,7 +9,7 @@ fn test_reduction_creates_valid_ilp() { // Path P4: 0-1-2-3 let problem = OptimalLinearArrangement::new(SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)])); let reduction: ReductionOLAToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); // num_x=16, p_v=4, z_e=3, total=23 assert_eq!(ilp.num_vars(), 23); @@ -29,7 +30,7 @@ fn test_optimallineararrangement_to_ilp_closed_loop() { // Solve via ILP let reduction: ReductionOLAToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp_solver = ILPSolver::new(); let ilp_solution = ilp_solver .solve(reduction.target_problem()) @@ -59,7 +60,7 @@ fn test_optimallineararrangement_to_ilp_with_chords() { // Solve via ILP let reduction: ReductionOLAToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp_solver = ILPSolver::new(); let ilp_solution = ilp_solver .solve(reduction.target_problem()) @@ -72,7 +73,7 @@ fn test_optimallineararrangement_to_ilp_with_chords() { fn test_solution_extraction() { let problem = OptimalLinearArrangement::new(SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)])); let reduction: ReductionOLAToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp_solver = ILPSolver::new(); let ilp_solution = ilp_solver .solve(reduction.target_problem()) @@ -85,6 +86,6 @@ fn test_solution_extraction() { fn test_optimallineararrangement_to_ilp_bf_vs_ilp() { let problem = OptimalLinearArrangement::new(SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)])); let reduction: ReductionOLAToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); crate::rules::test_helpers::assert_bf_vs_ilp(&problem, &reduction); } diff --git a/src/unit_tests/rules/partition_openshopscheduling.rs b/src/unit_tests/rules/partition_openshopscheduling.rs index afab0667b..0ed35ce0f 100644 --- a/src/unit_tests/rules/partition_openshopscheduling.rs +++ b/src/unit_tests/rules/partition_openshopscheduling.rs @@ -1,4 +1,5 @@ use super::*; +use crate::models::algebraic::Bounded; use crate::models::algebraic::ILP; use crate::models::decision::Decision; use crate::models::misc::{OpenShopScheduling, Partition}; @@ -6,7 +7,8 @@ use crate::solvers::ILPSolver; use crate::traits::Problem; fn solve_target(target: &OpenShopScheduling) -> Vec { - let reduction = ReduceTo::>::reduce_to(target).expect("ILP reduction should succeed"); + let reduction = ReduceTo::>::reduce_to(target) + .expect("ILP reduction should succeed"); let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("open-shop target should be feasible"); diff --git a/src/unit_tests/rules/pathconstrainednetworkflow_ilp.rs b/src/unit_tests/rules/pathconstrainednetworkflow_ilp.rs index ac4111f81..e23268e8d 100644 --- a/src/unit_tests/rules/pathconstrainednetworkflow_ilp.rs +++ b/src/unit_tests/rules/pathconstrainednetworkflow_ilp.rs @@ -1,4 +1,5 @@ use super::*; +use crate::models::algebraic::Bounded; use crate::models::algebraic::ILP; use crate::rules::ReduceTo; use crate::solvers::{BruteForce, ILPSolver}; @@ -22,7 +23,8 @@ fn test_pathconstrainednetworkflow_to_ilp_closed_loop() { .expect("source instance should be satisfiable"); assert!(source.evaluate(&direct).unwrap()); - let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + let reduction = + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be feasible"); @@ -41,6 +43,34 @@ fn test_pathconstrainednetworkflow_to_ilp_bf_vs_ilp() { vec![vec![0, 1], vec![2]], 2, ); - let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + let reduction = + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); crate::rules::test_helpers::assert_bf_vs_ilp(&source, &reduction); } + +#[test] +fn test_flow_requirement_normalization_preserves_feasibility() { + for requirement in [i64::MIN, 0, 1, 2, i64::MAX] { + let source = PathConstrainedNetworkFlow::new( + DirectedGraph::new(2, vec![(0, 1)]), + vec![1], + 0, + 1, + vec![vec![0]], + requirement, + ); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + assert!(reduction.target_problem().max_constraint_magnitude_bits() <= 2); + match ILPSolver::new().solve(reduction.target_problem()) { + Ok(solution) => { + assert!(requirement <= 1); + let recovered = reduction.extract_solution(&solution).unwrap(); + assert!(source.evaluate(&recovered).unwrap()); + } + Err(error) => { + assert!(requirement > 1); + assert_eq!(error, crate::solvers::ILPSolveError::Infeasible); + } + } + } +} diff --git a/src/unit_tests/rules/precedenceconstrainedscheduling_ilp.rs b/src/unit_tests/rules/precedenceconstrainedscheduling_ilp.rs index 3f54767c4..e2b54f808 100644 --- a/src/unit_tests/rules/precedenceconstrainedscheduling_ilp.rs +++ b/src/unit_tests/rules/precedenceconstrainedscheduling_ilp.rs @@ -90,3 +90,23 @@ fn test_precedenceconstrainedscheduling_to_ilp_bf_vs_ilp() { ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); crate::rules::test_helpers::assert_bf_vs_ilp(&problem, &reduction); } + +#[test] +fn test_processor_normalization_preserves_scheduling_feasibility() { + for processors in [1, 2, 1000] { + let source = PrecedenceConstrainedScheduling::new(2, processors, 1, vec![]); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + assert!(reduction.target_problem().max_constraint_magnitude_bits() <= 2); + let solution = ILPSolver::new().solve(reduction.target_problem()); + assert_eq!(solution.is_ok(), processors >= 2); + if let Ok(solution) = solution { + let recovered = reduction.extract_solution(&solution).unwrap(); + assert!(source.evaluate(&recovered).unwrap().0); + } else { + assert_eq!( + solution.unwrap_err(), + crate::solvers::ILPSolveError::Infeasible + ); + } + } +} diff --git a/src/unit_tests/rules/preemptivescheduling_ilp.rs b/src/unit_tests/rules/preemptivescheduling_ilp.rs index cd0a78b23..4f14b120b 100644 --- a/src/unit_tests/rules/preemptivescheduling_ilp.rs +++ b/src/unit_tests/rules/preemptivescheduling_ilp.rs @@ -1,4 +1,5 @@ use super::*; +use crate::models::algebraic::Bounded; use crate::models::algebraic::ILP; use crate::solvers::ILPSolver; use crate::traits::Problem; @@ -25,7 +26,7 @@ fn test_preemptivescheduling_to_ilp_structure() { let p = small_instance(); // n=2, D_max=2 → 2*2+1 = 5 variables let reduction: ReductionPSToILP = - ReduceTo::>::reduce_to(&p).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&p).expect("reduction should succeed"); let ilp = reduction.target_problem(); assert_eq!(ilp.num_vars(), 5, "expected n*D_max+1 = 5 variables"); assert_eq!( @@ -45,7 +46,7 @@ fn test_preemptivescheduling_to_ilp_structure() { fn test_preemptivescheduling_to_ilp_closed_loop() { let p = small_instance(); let reduction: ReductionPSToILP = - ReduceTo::>::reduce_to(&p).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&p).expect("reduction should succeed"); let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be feasible"); @@ -62,7 +63,7 @@ fn test_solve_via_registered_integer_ilp_pipeline() { let problem = small_instance(); let solution = ILPSolver::new() .solve(&problem) - .expect("direct ILP reduction should be solvable"); + .expect("direct ILP reduction should be solvable"); assert!(problem.evaluate(&solution).unwrap().0.is_some()); } @@ -71,7 +72,7 @@ fn test_solve_via_registered_integer_ilp_pipeline() { fn test_preemptivescheduling_to_ilp_medium_closed_loop() { let p = medium_instance(); let reduction: ReductionPSToILP = - ReduceTo::>::reduce_to(&p).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&p).expect("reduction should succeed"); let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be feasible"); @@ -98,7 +99,7 @@ fn test_preemptivescheduling_to_ilp_infeasible() { // Use a cycle-free precedence that is always schedulable. let p = PreemptiveScheduling::new(vec![1, 1], 1, vec![(0, 1)]).unwrap(); let reduction: ReductionPSToILP = - ReduceTo::>::reduce_to(&p).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&p).expect("reduction should succeed"); let sol = ILPSolver::new().solve(reduction.target_problem()); // 1 processor, t0 at slot 0, t1 at slot 1 → always feasible assert!(sol.is_ok(), "should be feasible"); @@ -112,7 +113,7 @@ fn test_preemptivescheduling_to_ilp_extract_solution() { // x_{0,0}=1, x_{0,1}=0, x_{1,0}=0, x_{1,1}=1, M=2 let p = small_instance(); let reduction: ReductionPSToILP = - ReduceTo::>::reduce_to(&p).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&p).expect("reduction should succeed"); let ilp_solution = vec![1, 0, 0, 1, 2]; // last element is M let extracted = reduction.extract_solution(&ilp_solution).unwrap(); assert_eq!(extracted, vec![vec![true, false], vec![false, true]]); diff --git a/src/unit_tests/rules/rectilinearpicturecompression_ilp.rs b/src/unit_tests/rules/rectilinearpicturecompression_ilp.rs index 056c224de..39920f7a4 100644 --- a/src/unit_tests/rules/rectilinearpicturecompression_ilp.rs +++ b/src/unit_tests/rules/rectilinearpicturecompression_ilp.rs @@ -55,3 +55,30 @@ fn test_rectilinearpicturecompression_to_ilp_trivial() { let ilp = reduction.target_problem(); assert_eq!(ilp.num_vars(), 0); // no maximal rects } + +#[test] +fn test_rectangle_threshold_normalization() { + for bound in [i64::MIN, -1, 0, 1, i64::MAX] { + let source = RectilinearPictureCompression::new(vec![vec![true]], bound); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + assert_eq!( + reduction.target_problem().max_constraint_magnitude_bits(), + 1 + ); + match ILPSolver::new().solve(reduction.target_problem()) { + Ok(solution) => { + assert!(bound >= 1); + assert_eq!( + source + .evaluate(&reduction.extract_solution(&solution).unwrap()) + .unwrap(), + Or(true) + ); + } + Err(error) => { + assert!(bound < 1); + assert_eq!(error, crate::solvers::ILPSolveError::Infeasible); + } + } + } +} diff --git a/src/unit_tests/rules/reduction_path_parity.rs b/src/unit_tests/rules/reduction_path_parity.rs index 5dc4cd245..cb1490a2a 100644 --- a/src/unit_tests/rules/reduction_path_parity.rs +++ b/src/unit_tests/rules/reduction_path_parity.rs @@ -2,6 +2,7 @@ //! Verifies that explicit chained reductions via `reduce_along_path` //! produce correct solutions matching direct source solves. +use crate::models::algebraic::Bounded; use crate::models::algebraic::QUBO; use crate::models::graph::{MaxCut, SpinGlass}; use crate::models::misc::Factoring; @@ -141,7 +142,8 @@ fn test_jl_parity_factoring_to_spinglass_path() { use crate::models::algebraic::ILP; use crate::rules::traits::{ReduceTo, ReductionResult}; let ilp_solver = ILPSolver::new(); - let reduction = ReduceTo::>::reduce_to(&factoring).expect("reduction should succeed"); + let reduction = ReduceTo::>::reduce_to(&factoring) + .expect("reduction should succeed"); let ilp = reduction.target_problem(); let ilp_solution = ilp_solver .solve(ilp) diff --git a/src/unit_tests/rules/registersufficiency_ilp.rs b/src/unit_tests/rules/registersufficiency_ilp.rs index beaef71d7..c2b08e675 100644 --- a/src/unit_tests/rules/registersufficiency_ilp.rs +++ b/src/unit_tests/rules/registersufficiency_ilp.rs @@ -1,4 +1,5 @@ use super::*; +use crate::models::algebraic::Bounded; use crate::models::misc::RegisterSufficiency; use crate::solvers::ILPSolver; use crate::traits::Problem; @@ -33,7 +34,8 @@ fn canonical_example() -> RegisterSufficiency { #[test] fn test_register_sufficiency_to_ilp_structure() { let source = feasible_example(); - let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + let reduction = + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let ilp = reduction.target_problem(); assert_eq!(ilp.num_vars(), 62); @@ -45,7 +47,8 @@ fn test_register_sufficiency_to_ilp_structure() { #[test] fn test_register_sufficiency_to_ilp_closed_loop() { let source = feasible_example(); - let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + let reduction = + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) @@ -61,7 +64,8 @@ fn test_register_sufficiency_to_ilp_closed_loop() { #[test] fn test_register_sufficiency_to_ilp_infeasible() { let source = infeasible_example(); - let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + let reduction = + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); assert!( ILPSolver::new().solve(reduction.target_problem()).is_err(), @@ -72,7 +76,8 @@ fn test_register_sufficiency_to_ilp_infeasible() { #[test] fn test_register_sufficiency_to_ilp_bf_vs_ilp() { let source = feasible_example(); - let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + let reduction = + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); crate::rules::test_helpers::assert_bf_vs_ilp(&source, &reduction); } @@ -107,7 +112,8 @@ fn test_register_sufficiency_to_ilp_canonical_example_spec() { assert_eq!(example.solutions.len(), 1); let source = canonical_example(); - let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + let reduction = + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let solution = &example.solutions[0]; let source_config: Vec = serde_json::from_value(solution.source_config.clone()).unwrap(); let target_config: Vec = serde_json::from_value(solution.target_config.clone()).unwrap(); diff --git a/src/unit_tests/rules/rootedtreestorageassignment_ilp.rs b/src/unit_tests/rules/rootedtreestorageassignment_ilp.rs index 737aec834..c276916c7 100644 --- a/src/unit_tests/rules/rootedtreestorageassignment_ilp.rs +++ b/src/unit_tests/rules/rootedtreestorageassignment_ilp.rs @@ -1,5 +1,5 @@ use super::*; -use crate::models::algebraic::{ObjectiveSense, ILP}; +use crate::models::algebraic::{Bounded, ObjectiveSense, ILP}; use crate::solvers::{BruteForce, ILPSolver}; use crate::traits::Problem; use crate::types::Or; @@ -8,7 +8,7 @@ use crate::types::Or; fn test_reduction_creates_valid_ilp() { let problem = RootedTreeStorageAssignment::new(3, vec![vec![0, 1], vec![1, 2]], 1); let reduction: ReductionRTSAToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); // n=3, r=2 (both subsets have size 2) @@ -31,7 +31,7 @@ fn test_rootedtreestorageassignment_to_ilp_bf_vs_ilp() { .unwrap_or(Or(false)); let reduction: ReductionRTSAToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp_solver = ILPSolver::new(); let ilp_result = ilp_solver.solve(reduction.target_problem()); @@ -62,7 +62,7 @@ fn test_rootedtreestorageassignment_to_ilp_infeasible() { let bf_witness = bf.solve(&problem).unwrap(); let reduction: ReductionRTSAToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp_solver = ILPSolver::new(); let ilp_result = ilp_solver.solve(reduction.target_problem()); assert!(bf_witness.is_none(), "source should be infeasible"); @@ -73,7 +73,7 @@ fn test_rootedtreestorageassignment_to_ilp_infeasible() { fn test_solution_extraction() { let problem = RootedTreeStorageAssignment::new(3, vec![vec![0, 1, 2]], 0); let reduction: ReductionRTSAToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp_solver = ILPSolver::new(); let ilp_solution = ilp_solver .solve(reduction.target_problem()) @@ -82,3 +82,30 @@ fn test_solution_extraction() { assert_eq!(extracted.len(), 3); assert_eq!(problem.evaluate(&extracted).unwrap(), Or(true)); } + +#[test] +fn test_storage_threshold_normalization() { + for n in [0, 2] { + for bound in [i64::MIN, -1, 0, i64::MAX] { + let subsets = if n == 0 { vec![] } else { vec![vec![0, 1]] }; + let source = RootedTreeStorageAssignment::new(n, subsets, bound); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + assert!(reduction.target_problem().max_constraint_magnitude_bits() <= 2); + match ILPSolver::new().solve(reduction.target_problem()) { + Ok(solution) => { + assert!(bound >= 0); + assert_eq!( + source + .evaluate(&reduction.extract_solution(&solution).unwrap()) + .unwrap(), + Or(true) + ); + } + Err(error) => { + assert!(bound < 0); + assert_eq!(error, crate::solvers::ILPSolveError::Infeasible); + } + } + } + } +} diff --git a/src/unit_tests/rules/ruralpostman_ilp.rs b/src/unit_tests/rules/ruralpostman_ilp.rs index 567aec646..dd57d8305 100644 --- a/src/unit_tests/rules/ruralpostman_ilp.rs +++ b/src/unit_tests/rules/ruralpostman_ilp.rs @@ -1,4 +1,5 @@ use super::*; +use crate::models::algebraic::Bounded; use crate::models::algebraic::ILP; use crate::rules::ReduceTo; use crate::solvers::{BruteForce, ILPSolver}; @@ -19,7 +20,8 @@ fn test_ruralpostman_to_ilp_closed_loop() { .expect("source instance should have an optimal solution"); assert!(source.evaluate(&direct).unwrap().0.is_some()); - let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + let reduction = + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be feasible"); @@ -45,7 +47,8 @@ fn test_ruralpostman_to_ilp_optimization() { let bf_value = source.evaluate(&bf_witness).unwrap(); // ILP reduction optimal - let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + let reduction = + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be feasible"); @@ -66,7 +69,8 @@ fn test_ruralpostman_to_ilp_bf_vs_ilp() { vec![1, 1, 1], vec![0], ); - let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + let reduction = + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); crate::rules::test_helpers::assert_bf_vs_ilp(&source, &reduction); } @@ -77,7 +81,7 @@ fn test_ruralpostman_empty_required_set_extracts_zero_multiplicities() { vec![4, 7], vec![], ); - let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); crate::rules::test_helpers::assert_parameter_predictions(&source, &reduction); let target = ILPSolver::new().solve(reduction.target_problem()).unwrap(); let extracted = reduction.extract_solution(&target).unwrap(); diff --git a/src/unit_tests/rules/schedulingtominimizeweightedcompletiontime_ilp.rs b/src/unit_tests/rules/schedulingtominimizeweightedcompletiontime_ilp.rs index eaab98e84..52f8d9957 100644 --- a/src/unit_tests/rules/schedulingtominimizeweightedcompletiontime_ilp.rs +++ b/src/unit_tests/rules/schedulingtominimizeweightedcompletiontime_ilp.rs @@ -1,5 +1,5 @@ use super::*; -use crate::models::algebraic::{ObjectiveSense, ILP}; +use crate::models::algebraic::{Bounded, ObjectiveSense, ILP}; use crate::models::misc::SchedulingToMinimizeWeightedCompletionTime; use crate::solvers::{BruteForce, ILPSolver}; use crate::traits::Problem; @@ -10,7 +10,7 @@ fn test_reduction_creates_valid_ilp_structure() { // 3 tasks, 2 processors let problem = SchedulingToMinimizeWeightedCompletionTime::new(vec![1, 2, 3], vec![4, 2, 1], 2); let reduction: ReductionSMWCTToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); // n=3, m=2: x vars = 3*2=6, C vars = 3, y vars = 3*2/2=3, total=12 @@ -37,7 +37,7 @@ fn test_reduction_creates_valid_ilp_structure() { fn test_solution_extraction() { let problem = SchedulingToMinimizeWeightedCompletionTime::new(vec![1, 2], vec![3, 1], 2); let reduction: ReductionSMWCTToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); // Build a manual ILP solution: // x_{0,0}=1, x_{0,1}=0, x_{1,0}=0, x_{1,1}=1 => task 0 on P0, task 1 on P1 @@ -74,7 +74,7 @@ fn test_ilp_matches_bruteforce_small() { let bf_value = problem.evaluate(&bf_witness).unwrap(); let reduction: ReductionSMWCTToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); let ilp_solution = ILPSolver::new().solve(ilp).expect("ILP should be solvable"); let extracted = reduction.extract_solution(&ilp_solution).unwrap(); @@ -93,7 +93,7 @@ fn test_issue_example_closed_loop() { ); let reduction: ReductionSMWCTToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); let ilp_solution = ILPSolver::new().solve(ilp).expect("ILP should be solvable"); let extracted = reduction.extract_solution(&ilp_solution).unwrap(); @@ -105,7 +105,7 @@ fn test_issue_example_closed_loop() { fn test_single_task_single_processor() { let problem = SchedulingToMinimizeWeightedCompletionTime::new(vec![5], vec![3], 1); let reduction: ReductionSMWCTToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); let ilp_solution = ILPSolver::new().solve(ilp).expect("ILP should be solvable"); @@ -127,7 +127,7 @@ fn test_equal_tasks_multiple_processors() { let bf_value = problem.evaluate(&bf_witness).unwrap(); let reduction: ReductionSMWCTToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); let ilp_solution = ILPSolver::new().solve(ilp).expect("ILP should be solvable"); let extracted = reduction.extract_solution(&ilp_solution).unwrap(); diff --git a/src/unit_tests/rules/schedulingwithindividualdeadlines_ilp.rs b/src/unit_tests/rules/schedulingwithindividualdeadlines_ilp.rs index 8838a3843..64c85c59f 100644 --- a/src/unit_tests/rules/schedulingwithindividualdeadlines_ilp.rs +++ b/src/unit_tests/rules/schedulingwithindividualdeadlines_ilp.rs @@ -117,3 +117,27 @@ fn test_schedulingwithindividualdeadlines_to_ilp_bf_vs_ilp() { ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); crate::rules::test_helpers::assert_bf_vs_ilp(&problem, &reduction); } + +#[test] +fn test_individual_deadline_threshold_normalization() { + for processors in [1, 2, 1000] { + let source = SchedulingWithIndividualDeadlines::new(2, processors, vec![1, 1], vec![]); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + assert!(reduction.target_problem().max_constraint_magnitude_bits() <= 2); + match ILPSolver::new().solve(reduction.target_problem()) { + Ok(solution) => { + assert!(processors >= 2); + assert!( + source + .evaluate(&reduction.extract_solution(&solution).unwrap()) + .unwrap() + .0 + ); + } + Err(error) => { + assert_eq!(processors, 1); + assert_eq!(error, crate::solvers::ILPSolveError::Infeasible); + } + } + } +} diff --git a/src/unit_tests/rules/sequencingtominimizemaximumcumulativecost_ilp.rs b/src/unit_tests/rules/sequencingtominimizemaximumcumulativecost_ilp.rs index 617788d35..b3ce0d01f 100644 --- a/src/unit_tests/rules/sequencingtominimizemaximumcumulativecost_ilp.rs +++ b/src/unit_tests/rules/sequencingtominimizemaximumcumulativecost_ilp.rs @@ -1,4 +1,5 @@ use super::*; +use crate::models::algebraic::Bounded; use crate::models::algebraic::ILP; use crate::rules::ReduceTo; use crate::solvers::{BruteForce, ILPSolver}; @@ -7,7 +8,8 @@ use crate::traits::Problem; #[test] fn test_sequencingtominimizemaximumcumulativecost_to_ilp_closed_loop() { let problem = SequencingToMinimizeMaximumCumulativeCost::new(vec![2, -1, 3, -2], vec![(0, 2)]); - let reduction = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + let reduction = + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); // Brute-force the source to get the optimal value let bf = BruteForce::new(); @@ -34,7 +36,8 @@ fn test_sequencingtominimizemaximumcumulativecost_to_ilp_closed_loop() { #[test] fn test_sequencingtominimizemaximumcumulativecost_to_ilp_bf_vs_ilp() { let problem = SequencingToMinimizeMaximumCumulativeCost::new(vec![2, -1, 3, -2], vec![(0, 2)]); - let reduction = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + let reduction = + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let bf_witness = BruteForce::new() .solve(&problem) @@ -52,7 +55,8 @@ fn test_sequencingtominimizemaximumcumulativecost_to_ilp_bf_vs_ilp() { #[test] fn test_sequencingtominimizemaximumcumulativecost_to_ilp_no_precedences() { let problem = SequencingToMinimizeMaximumCumulativeCost::new(vec![3, -2, 1], vec![]); - let reduction = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + let reduction = + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be solvable"); diff --git a/src/unit_tests/rules/sequencingtominimizeweightedcompletiontime_ilp.rs b/src/unit_tests/rules/sequencingtominimizeweightedcompletiontime_ilp.rs index 62b7503de..9a4340b8a 100644 --- a/src/unit_tests/rules/sequencingtominimizeweightedcompletiontime_ilp.rs +++ b/src/unit_tests/rules/sequencingtominimizeweightedcompletiontime_ilp.rs @@ -1,5 +1,5 @@ use super::*; -use crate::models::algebraic::{ObjectiveSense, ILP}; +use crate::models::algebraic::{Bounded, ObjectiveSense, ILP}; use crate::models::misc::SequencingToMinimizeWeightedCompletionTime; use crate::solvers::{BruteForce, ILPSolver}; use crate::traits::Problem; @@ -9,7 +9,7 @@ use crate::types::Min; fn test_reduction_creates_expected_ilp_shape() { let problem = SequencingToMinimizeWeightedCompletionTime::new(vec![2, 1], vec![3, 5], vec![]); let reduction: ReductionSTMWCTToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); // 2 completion variables + 1 pair-order variable. @@ -28,7 +28,7 @@ fn test_variable_layout_helpers() { let problem = SequencingToMinimizeWeightedCompletionTime::new(vec![2, 1, 3], vec![3, 5, 1], vec![(0, 2)]); let reduction: ReductionSTMWCTToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); assert_eq!(reduction.completion_var(0), 0); assert_eq!(reduction.completion_var(2), 2); @@ -41,7 +41,7 @@ fn test_variable_layout_helpers() { fn test_extract_solution_encodes_schedule_as_lehmer_code() { let problem = SequencingToMinimizeWeightedCompletionTime::new(vec![2, 1], vec![3, 5], vec![]); let reduction: ReductionSTMWCTToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); // Completion times C0 = 3, C1 = 1 imply schedule [1, 0]. // y_{0,1} = 0 means task 1 before task 0. @@ -58,7 +58,7 @@ fn test_issue_example_closed_loop() { vec![(0, 2), (1, 4)], ); let reduction: ReductionSTMWCTToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); let ilp_solution = ILPSolver::new().solve(ilp).expect("ILP should be solvable"); @@ -84,7 +84,7 @@ fn test_ilp_matches_bruteforce_optimum() { let brute_force_metric = problem.evaluate(&brute_force_solution).unwrap(); let reduction: ReductionSTMWCTToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); let ilp_solution = ILPSolver::new().solve(ilp).expect("ILP should be solvable"); let extracted = reduction.extract_solution(&ilp_solution).unwrap(); @@ -101,7 +101,7 @@ fn test_cyclic_precedence_instance_is_infeasible() { vec![(0, 1), (1, 0)], ); let reduction: ReductionSTMWCTToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); assert!( @@ -115,7 +115,7 @@ fn test_reduction_rejects_total_processing_time_outside_i64_domain() { let problem = SequencingToMinimizeWeightedCompletionTime::new(vec![i64::MAX, 1], vec![1, 1], vec![]); assert!(matches!( - ReduceTo::>::reduce_to(&problem), + ReduceTo::>::reduce_to(&problem), Err(crate::rules::ReductionError::IntegerOverflow { .. }) )); } @@ -124,7 +124,7 @@ fn test_reduction_rejects_total_processing_time_outside_i64_domain() { fn test_reduction_preserves_a_weight_outside_exact_f64_integer_range() { let problem = SequencingToMinimizeWeightedCompletionTime::new(vec![1], vec![(1i64 << 53) + 1], vec![]); - let reduction = ReduceTo::>::reduce_to(&problem).unwrap(); + let reduction = ReduceTo::>::reduce_to(&problem).unwrap(); assert_eq!( reduction.target_problem().objective(), &[(0, (1i64 << 53) + 1)] @@ -135,7 +135,7 @@ fn test_reduction_preserves_a_weight_outside_exact_f64_integer_range() { fn test_reduction_preserves_large_weighted_completion_objective() { let problem = SequencingToMinimizeWeightedCompletionTime::new(vec![1, 1], vec![1 << 52, 1 << 52], vec![]); - let reduction = ReduceTo::>::reduce_to(&problem).unwrap(); + let reduction = ReduceTo::>::reduce_to(&problem).unwrap(); assert_eq!( reduction.target_problem().objective(), &[(0, 1 << 52), (1, 1 << 52)] @@ -150,7 +150,7 @@ fn test_ilp_pipeline_matches_source_optimum() { vec![(0, 2), (1, 4)], ); let reduction: ReductionSTMWCTToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be solvable"); @@ -164,6 +164,6 @@ fn test_ilp_pipeline_matches_source_optimum() { fn test_sequencingtominimizeweightedcompletiontime_to_ilp_bf_vs_ilp() { let problem = SequencingToMinimizeWeightedCompletionTime::new(vec![2, 1], vec![3, 5], vec![]); let reduction: ReductionSTMWCTToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); crate::rules::test_helpers::assert_bf_vs_ilp(&problem, &reduction); } diff --git a/src/unit_tests/rules/sequencingtominimizeweightedtardiness_ilp.rs b/src/unit_tests/rules/sequencingtominimizeweightedtardiness_ilp.rs index 3768789a1..072d5b2d1 100644 --- a/src/unit_tests/rules/sequencingtominimizeweightedtardiness_ilp.rs +++ b/src/unit_tests/rules/sequencingtominimizeweightedtardiness_ilp.rs @@ -1,4 +1,5 @@ use super::*; +use crate::models::algebraic::Bounded; use crate::models::algebraic::ILP; use crate::solvers::{BruteForce, ILPSolver}; use crate::traits::Problem; @@ -8,9 +9,10 @@ use crate::types::Or; fn test_sequencingtominimizeweightedtardiness_to_ilp_closed_loop() { let problem = SequencingToMinimizeWeightedTardiness::new(vec![3, 4, 2], vec![2, 3, 1], vec![5, 8, 4], 10); - let reduction = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + let reduction = + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); - // Use ILPSolver directly (BruteForce cannot enumerate `ILP`) + // Use ILPSolver directly (BruteForce cannot enumerate `ILP`) let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be solvable"); @@ -22,7 +24,8 @@ fn test_sequencingtominimizeweightedtardiness_to_ilp_closed_loop() { fn test_sequencingtominimizeweightedtardiness_to_ilp_bf_vs_ilp() { let problem = SequencingToMinimizeWeightedTardiness::new(vec![3, 4, 2], vec![2, 3, 1], vec![5, 8, 4], 10); - let reduction = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + let reduction = + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let bf_witness = BruteForce::new() .solve(&problem) @@ -42,7 +45,8 @@ fn test_sequencingtominimizeweightedtardiness_to_ilp_infeasible() { // All jobs have length 10, deadline 1, weight 1, bound 0: impossible let problem = SequencingToMinimizeWeightedTardiness::new(vec![10, 10], vec![1, 1], vec![1, 1], 0); - let reduction = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + let reduction = + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); assert!( ILPSolver::new().solve(reduction.target_problem()).is_err(), "infeasible STMWT should produce infeasible ILP" @@ -58,7 +62,8 @@ fn test_sequencingtominimizeweightedtardiness_to_ilp_no_tardiness() { vec![10, 10, 10], 0, ); - let reduction = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + let reduction = + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be solvable"); diff --git a/src/unit_tests/rules/setsplitting_ilp.rs b/src/unit_tests/rules/setsplitting_ilp.rs index c4cb8d97c..3acc5f16d 100644 --- a/src/unit_tests/rules/setsplitting_ilp.rs +++ b/src/unit_tests/rules/setsplitting_ilp.rs @@ -108,3 +108,32 @@ fn test_overhead_dimensions() { assert_eq!(ilp.num_vars(), 5); assert_eq!(ilp.constraints().len(), 6); // 2 per subset } + +#[test] +fn test_set_splitting_normalization_preserves_all_colorings() { + for subset in [vec![0, 0, 0, 0, 0, 0, 0, 0, 1], vec![0, 0]] { + let source = SetSplitting::new(2, vec![subset]); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + for a in [false, true] { + for b in [false, true] { + let expected = source.evaluate(&vec![a, b]).unwrap().0; + let solution = vec![i64::from(a), i64::from(b)]; + assert_eq!( + reduction + .target_problem() + .evaluate(&solution) + .unwrap() + .is_valid(), + expected + ); + if expected { + assert_eq!(reduction.extract_solution(&solution).unwrap(), vec![a, b]); + } + } + } + assert_eq!( + reduction.target_problem().max_constraint_magnitude_bits(), + 1 + ); + } +} diff --git a/src/unit_tests/rules/shortestweightconstrainedpath_ilp.rs b/src/unit_tests/rules/shortestweightconstrainedpath_ilp.rs index 8fe434aab..0de30cfae 100644 --- a/src/unit_tests/rules/shortestweightconstrainedpath_ilp.rs +++ b/src/unit_tests/rules/shortestweightconstrainedpath_ilp.rs @@ -1,5 +1,5 @@ use super::*; -use crate::models::algebraic::{ObjectiveSense, ILP}; +use crate::models::algebraic::{Bounded, ObjectiveSense, ILP}; use crate::solvers::{BruteForce, ILPSolver}; use crate::topology::SimpleGraph; use crate::traits::Problem; @@ -21,7 +21,7 @@ fn simple_path_problem() -> ShortestWeightConstrainedPath { fn test_reduction_creates_valid_ilp() { let problem = simple_path_problem(); let reduction: ReductionSWCPToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); // 2 edges => 4 arc vars + 3 order vars = 7 @@ -50,7 +50,7 @@ fn test_shortestweightconstrainedpath_to_ilp_bf_vs_ilp() { let bf_value = problem.evaluate(&bf_value_solution).unwrap(); let reduction: ReductionSWCPToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp_solver = ILPSolver::new(); let ilp_result = ilp_solver.solve(reduction.target_problem()); @@ -72,7 +72,7 @@ fn test_shortestweightconstrainedpath_to_ilp_bf_vs_ilp() { fn test_solution_extraction() { let problem = simple_path_problem(); let reduction: ReductionSWCPToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); // Handcrafted ILP solution: path 0->1->2 // a_{0,fwd}=1, a_{0,rev}=0, a_{1,fwd}=1, a_{1,rev}=0, o_0=0, o_1=1, o_2=2 @@ -96,7 +96,7 @@ fn test_shortestweightconstrainedpath_to_ilp_trivial() { 4, // weight_bound ); let reduction: ReductionSWCPToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp_solver = ILPSolver::new(); let ilp_solution = ilp_solver .solve(reduction.target_problem()) diff --git a/src/unit_tests/rules/strongconnectivityaugmentation_ilp.rs b/src/unit_tests/rules/strongconnectivityaugmentation_ilp.rs index 0fd893b32..a9062a5f4 100644 --- a/src/unit_tests/rules/strongconnectivityaugmentation_ilp.rs +++ b/src/unit_tests/rules/strongconnectivityaugmentation_ilp.rs @@ -19,7 +19,7 @@ fn small_instance() -> StrongConnectivityAugmentation { fn test_strongconnectivityaugmentation_to_ilp_closed_loop() { let source = small_instance(); let reduction: ReductionSCAToILP = - ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let ilp = reduction.target_problem(); // Solve source with brute force @@ -42,7 +42,7 @@ fn test_strongconnectivityaugmentation_to_ilp_closed_loop() { fn test_extract_solution() { let source = small_instance(); let reduction: ReductionSCAToILP = - ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let ilp = reduction.target_problem(); let solver = ILPSolver::new(); let ilp_sol = solver.solve(ilp).expect("ILP should be solvable"); @@ -55,7 +55,7 @@ fn test_extract_solution() { fn test_trivial_single_vertex() { let source = StrongConnectivityAugmentation::new(DirectedGraph::new(1, vec![]), vec![], 0); let reduction: ReductionSCAToILP = - ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let ilp = reduction.target_problem(); let solver = ILPSolver::new(); let ilp_sol = solver.solve(ilp).expect("trivial should be solvable"); @@ -68,7 +68,7 @@ fn test_single_vertex_candidate_selection_must_still_respect_budget() { let source = StrongConnectivityAugmentation::new(DirectedGraph::new(1, vec![]), vec![(0, 0, 1)], 0); let reduction: ReductionSCAToILP = - ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let ilp = reduction.target_problem(); let mut config = vec![0; ilp.num_vars()]; config[0] = 1; @@ -92,7 +92,7 @@ fn test_infeasible_budget() { 5, ); let reduction: ReductionSCAToILP = - ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let ilp = reduction.target_problem(); let solver = ILPSolver::new(); assert!(solver.solve(ilp).is_err()); @@ -102,6 +102,6 @@ fn test_infeasible_budget() { fn test_strongconnectivityaugmentation_to_ilp_bf_vs_ilp() { let source = small_instance(); let reduction: ReductionSCAToILP = - ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); crate::rules::test_helpers::assert_bf_vs_ilp(&source, &reduction); } diff --git a/src/unit_tests/rules/subsetsum_integerknapsack.rs b/src/unit_tests/rules/subsetsum_integerknapsack.rs index 15a2978f8..36f190079 100644 --- a/src/unit_tests/rules/subsetsum_integerknapsack.rs +++ b/src/unit_tests/rules/subsetsum_integerknapsack.rs @@ -34,6 +34,52 @@ fn subset_sum_embedding(source: &SubsetSum) -> IntegerKnapsack { .unwrap() } +#[test] +fn test_subsetsum_integerknapsack_capacity_bits_propagate() { + use crate::models::algebraic::{Bounded, ILP}; + use crate::rules::{ReduceTo, ReductionResult}; + let entries = crate::rules::registry::reduction_entries(); + let embedding = entries + .iter() + .find(|entry| { + entry.source_name == SubsetSum::NAME && entry.target_name == IntegerKnapsack::NAME + }) + .unwrap() + .parameter_contract() + .unwrap(); + let ilp = entries + .iter() + .find(|entry| entry.source_name == IntegerKnapsack::NAME && entry.target_name == "ILP") + .unwrap() + .parameter_contract() + .unwrap(); + let composed = embedding + .transform() + .unwrap() + .compose(ilp.transform().unwrap(), "SubsetSum -> ILP") + .unwrap(); + for (sizes, target, bits) in [(vec![1], 0, 1), (vec![1], 8, 4), (vec![8], 1, 4)] { + let source = SubsetSum::new(sizes, target); + let intermediate = subset_sum_embedding(&source); + let prediction = embedding + .transform() + .unwrap() + .evaluate(&source.parameters()) + .unwrap(); + assert_eq!(prediction.get("capacity_bits"), Some(bits)); + assert!(prediction.get("capacity").is_none()); + assert!( + prediction.get("capacity_bits").unwrap() + >= intermediate.parameters().get("capacity_bits").unwrap() + ); + let reduced = ReduceTo::>::reduce_to(&intermediate).unwrap(); + let predicted = composed.evaluate(&source.parameters()).unwrap(); + for (field, actual) in reduced.target_problem().parameters().iter() { + assert!(predicted.get(field).expect(field) >= actual); + } + } +} + #[test] fn test_subsetsum_to_integerknapsack_forward_example() { let source = SubsetSum::new(vec![3u32, 7, 1, 8, 5], 16u32); diff --git a/src/unit_tests/rules/timetabledesign_ilp.rs b/src/unit_tests/rules/timetabledesign_ilp.rs index 14fd854ac..a37656505 100644 --- a/src/unit_tests/rules/timetabledesign_ilp.rs +++ b/src/unit_tests/rules/timetabledesign_ilp.rs @@ -88,3 +88,34 @@ fn test_timetabledesign_to_ilp_identity_extraction() { ); assert_eq!(problem.evaluate(&extracted).unwrap(), Or(true)); } + +#[test] +fn test_timetable_threshold_normalization() { + for requirement in [i64::MIN, 0, 1, 2, i64::MAX] { + let source = TimetableDesign::new( + 1, + 1, + 1, + vec![vec![true]], + vec![vec![true]], + vec![vec![requirement]], + ); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + assert!(reduction.target_problem().max_constraint_magnitude_bits() <= 2); + match ILPSolver::new().solve(reduction.target_problem()) { + Ok(solution) => { + assert!((0..=1).contains(&requirement)); + assert_eq!( + source + .evaluate(&reduction.extract_solution(&solution).unwrap()) + .unwrap(), + Or(true) + ); + } + Err(error) => { + assert!(!(0..=1).contains(&requirement)); + assert_eq!(error, crate::solvers::ILPSolveError::Infeasible); + } + } + } +} diff --git a/src/unit_tests/rules/undirectedflowlowerbounds_ilp.rs b/src/unit_tests/rules/undirectedflowlowerbounds_ilp.rs index 9180733e6..bc4f5d3a0 100644 --- a/src/unit_tests/rules/undirectedflowlowerbounds_ilp.rs +++ b/src/unit_tests/rules/undirectedflowlowerbounds_ilp.rs @@ -1,5 +1,5 @@ use super::*; -use crate::models::algebraic::{ObjectiveSense, ILP}; +use crate::models::algebraic::{Bounded, ObjectiveSense, ILP}; use crate::solvers::{BruteForce, ILPSolver}; use crate::topology::SimpleGraph; use crate::traits::Problem; @@ -28,7 +28,7 @@ fn sink_self_loop_cannot_supply_net_flow() { 1, ); assert!(BruteForce::new().solve(&source).unwrap().is_none()); - let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); let assignment = vec![1, 0, 1]; assert!(reduction .target_problem() @@ -60,7 +60,7 @@ fn infeasible_instance() -> UndirectedFlowLowerBounds { fn test_undirectedflowlowerbounds_to_ilp_structure() { let problem = feasible_instance(); let reduction: ReductionUFLBToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); // 2 edges → 3*2 = 6 variables @@ -83,7 +83,7 @@ fn test_undirectedflowlowerbounds_to_ilp_closed_loop() { ); let reduction: ReductionUFLBToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be feasible"); @@ -101,7 +101,7 @@ fn test_undirectedflowlowerbounds_to_ilp_closed_loop() { fn test_undirectedflowlowerbounds_to_ilp_infeasible() { let problem = infeasible_instance(); let reduction: ReductionUFLBToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); assert!( ILPSolver::new().solve(reduction.target_problem()).is_err(), "infeasible instance should produce infeasible ILP" @@ -112,7 +112,7 @@ fn test_undirectedflowlowerbounds_to_ilp_infeasible() { fn test_undirectedflowlowerbounds_to_ilp_extract_solution() { let problem = feasible_instance(); let reduction: ReductionUFLBToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); // f_{01}=1, f_{10}=0, f_{12}=1, f_{21}=0, z_0=1, z_1=1 // z_e=1 means u→v direction; model expects config[e]=0 for u→v → extract returns 1-z_e @@ -130,6 +130,6 @@ fn test_undirectedflowlowerbounds_to_ilp_extract_solution() { fn test_undirectedflowlowerbounds_to_ilp_bf_vs_ilp() { let problem = feasible_instance(); let reduction: ReductionUFLBToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); crate::rules::test_helpers::assert_bf_vs_ilp(&problem, &reduction); } diff --git a/src/unit_tests/rules/undirectedtwocommodityintegralflow_ilp.rs b/src/unit_tests/rules/undirectedtwocommodityintegralflow_ilp.rs index bf0efae03..1416f8fd9 100644 --- a/src/unit_tests/rules/undirectedtwocommodityintegralflow_ilp.rs +++ b/src/unit_tests/rules/undirectedtwocommodityintegralflow_ilp.rs @@ -1,5 +1,5 @@ use super::*; -use crate::models::algebraic::{ObjectiveSense, ILP}; +use crate::models::algebraic::{Bounded, ObjectiveSense, ILP}; use crate::solvers::{BruteForce, ILPSolver}; use crate::topology::SimpleGraph; use crate::traits::Problem; @@ -33,7 +33,7 @@ fn sink_self_loop_cannot_supply_either_commodity() { second, ); assert!(BruteForce::new().solve(&source).unwrap().is_none()); - let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); let assignment = vec![first, 0, second, 0, 1, 1]; assert!(reduction .target_problem() @@ -69,7 +69,7 @@ fn infeasible_instance() -> UndirectedTwoCommodityIntegralFlow { fn test_undirectedtwocommodityintegralflow_to_ilp_structure() { let problem = feasible_instance(); let reduction: ReductionU2CIFToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); // 3 edges → 4 flow vars + 2 direction vars per edge = 18 variables. @@ -83,7 +83,7 @@ fn test_undirectedtwocommodityintegralflow_to_ilp_structure() { fn test_undirectedtwocommodityintegralflow_to_ilp_overhead_matches_target() { let problem = feasible_instance(); let reduction: ReductionU2CIFToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); let entry = crate::rules::registry::reduction_entries() @@ -96,7 +96,7 @@ fn test_undirectedtwocommodityintegralflow_to_ilp_overhead_matches_target() { .iter() .any(|(key, value)| *key == "variable" && *value == "i64") }) - .expect("U2CIF -> ILP reduction should be registered"); + .expect("U2CIF -> ILP reduction should be registered"); let source_size = problem.parameters(); let predicted = entry @@ -130,7 +130,7 @@ fn test_undirectedtwocommodityintegralflow_to_ilp_closed_loop() { ); let reduction: ReductionU2CIFToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be feasible"); @@ -146,7 +146,7 @@ fn test_undirectedtwocommodityintegralflow_to_ilp_closed_loop() { fn test_undirectedtwocommodityintegralflow_to_ilp_infeasible() { let problem = infeasible_instance(); let reduction: ReductionU2CIFToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); assert!( ILPSolver::new().solve(reduction.target_problem()).is_err(), "infeasible flow instance should yield infeasible ILP" @@ -166,7 +166,7 @@ fn test_other_commodity_source_cannot_create_flow() { 0, ); let reduction: ReductionU2CIFToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); assert!(ILPSolver::new().solve(reduction.target_problem()).is_err()); } @@ -174,7 +174,7 @@ fn test_other_commodity_source_cannot_create_flow() { fn test_undirectedtwocommodityintegralflow_to_ilp_extract_solution() { let problem = feasible_instance(); let reduction: ReductionU2CIFToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); // Manual solution: edge 0 (0,2): f1_uv=1, f1_vu=0, f2_uv=0, f2_vu=0 // edge 1 (1,2): f1_uv=0, f1_vu=0, f2_uv=1, f2_vu=0 @@ -201,6 +201,6 @@ fn test_undirectedtwocommodityintegralflow_to_ilp_extract_solution() { fn test_undirectedtwocommodityintegralflow_to_ilp_bf_vs_ilp() { let problem = feasible_instance(); let reduction: ReductionU2CIFToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); crate::rules::test_helpers::assert_bf_vs_ilp(&problem, &reduction); } diff --git a/src/unit_tests/solvers/registry.rs b/src/unit_tests/solvers/registry.rs index c4480fa49..230871390 100644 --- a/src/unit_tests/solvers/registry.rs +++ b/src/unit_tests/solvers/registry.rs @@ -1,8 +1,16 @@ use super::*; use std::collections::BTreeMap; -const FLOAT_BOOL_VARIANT: &[(&str, &str)] = &[("variable", "bool"), ("coefficient", "f64")]; -const FLOAT_I64_VARIANT: &[(&str, &str)] = &[("variable", "i64"), ("coefficient", "f64")]; +const FLOAT_BOOL_VARIANT: &[(&str, &str)] = &[ + ("variable", "bool"), + ("coefficient", "f64"), + ("bounds", "general"), +]; +const FLOAT_I64_VARIANT: &[(&str, &str)] = &[ + ("variable", "i64"), + ("coefficient", "f64"), + ("bounds", "general"), +]; const NO_VARIANT: &[(&str, &str)] = &[]; #[test] @@ -264,6 +272,7 @@ fn solver_capability_registry_duplicate_ilp_registration_is_rejected_independent BTreeMap::from([ ("variable".to_string(), "bool".to_string()), ("coefficient".to_string(), "f64".to_string()), + ("bounds".to_string(), "general".to_string()), ]), )]); for pipelines in [ @@ -316,6 +325,7 @@ fn solver_capability_registry_unknown_pipeline_variant_is_rejected() { BTreeMap::from([ ("variable".to_string(), "bool".to_string()), ("coefficient".to_string(), "i64".to_string()), + ("bounds".to_string(), "general".to_string()), ]), )]); let error = build_registry( @@ -365,6 +375,7 @@ fn solver_capability_registry_pipeline_with_missing_exact_edge_is_rejected() { BTreeMap::from([ ("variable".to_string(), "bool".to_string()), ("coefficient".to_string(), "f64".to_string()), + ("bounds".to_string(), "general".to_string()), ]), ), ]); @@ -390,6 +401,7 @@ fn solver_capability_registry_pipeline_must_stop_at_first_supported_ilp_node() { BTreeMap::from([ ("variable".to_string(), "bool".to_string()), ("coefficient".to_string(), "f64".to_string()), + ("bounds".to_string(), "general".to_string()), ]), ), ExactProblemKey::new( @@ -397,6 +409,7 @@ fn solver_capability_registry_pipeline_must_stop_at_first_supported_ilp_node() { BTreeMap::from([ ("variable".to_string(), "i64".to_string()), ("coefficient".to_string(), "f64".to_string()), + ("bounds".to_string(), "general".to_string()), ]), ), ]); @@ -455,7 +468,7 @@ fn solver_capability_registry_exposes_representative_capability_classes() { assert!(direct_ilp.customized.is_none()); assert_eq!( direct_ilp.ilp.unwrap().path_labels(), - ["MaximumClique", "ILP"] + ["MaximumClique", "ILP"] ); let multihop_ilp = solver_capabilities(&key( @@ -478,9 +491,19 @@ fn solver_capability_registry_exposes_representative_capability_classes() { assert!(brute_force_only.customized.is_none()); assert!(brute_force_only.ilp.is_none()); - let ilp_itself = - solver_capabilities(&key("ILP", &[("variable", "bool"), ("coefficient", "i64")])).unwrap(); - assert_eq!(ilp_itself.ilp.unwrap().path_labels(), ["ILP"]); + let ilp_itself = solver_capabilities(&key( + "ILP", + &[ + ("variable", "bool"), + ("coefficient", "i64"), + ("bounds", "general"), + ], + )) + .unwrap(); + assert_eq!( + ilp_itself.ilp.unwrap().path_labels(), + ["ILP"] + ); } #[test] diff --git a/src/unit_tests/solvers/resolver.rs b/src/unit_tests/solvers/resolver.rs index f98171f99..9d40551e9 100644 --- a/src/unit_tests/solvers/resolver.rs +++ b/src/unit_tests/solvers/resolver.rs @@ -435,6 +435,7 @@ fn deterministic_solver_dispatch_integer_ilp_uses_native_terminal() { &BTreeMap::from([ ("variable".to_string(), "bool".to_string()), ("coefficient".to_string(), "i64".to_string()), + ("bounds".to_string(), "general".to_string()), ]), serde_json::to_value(problem).unwrap(), ) @@ -444,7 +445,7 @@ fn deterministic_solver_dispatch_integer_ilp_uses_native_terminal() { assert_eq!( result.solver, SolverExecution::Ilp { - reduction_path: vec!["ILP".to_string()] + reduction_path: vec!["ILP".to_string()] } ); assert!(matches!( @@ -470,6 +471,7 @@ fn deterministic_solver_dispatch_ilp_infeasibility_does_not_fall_back() { &BTreeMap::from([ ("variable".to_string(), "bool".to_string()), ("coefficient".to_string(), "i64".to_string()), + ("bounds".to_string(), "general".to_string()), ]), serde_json::to_value(problem).unwrap(), ) @@ -494,12 +496,12 @@ fn deterministic_solver_execution_has_stable_tagged_json_contract() { ); assert_eq!( serde_json::to_value(SolverExecution::Ilp { - reduction_path: vec!["Source".to_string(), "ILP".to_string()] + reduction_path: vec!["Source".to_string(), "ILP".to_string()] }) .unwrap(), serde_json::json!({ "kind": "ilp", - "reduction_path": ["Source", "ILP"] + "reduction_path": ["Source", "ILP"] }) ); assert_eq!( @@ -560,7 +562,7 @@ fn deterministic_solver_dispatch_fixed_multihop_pipeline_is_repeatable() { "MaximumIndependentSet", "MaximumIndependentSet", "MaximumSetPacking", - "ILP", + "ILP", ] ); } diff --git a/src/unit_tests/symbolic_parameter_contracts.rs b/src/unit_tests/symbolic_parameter_contracts.rs index 535deeb1d..c095f9eab 100644 --- a/src/unit_tests/symbolic_parameter_contracts.rs +++ b/src/unit_tests/symbolic_parameter_contracts.rs @@ -1,4 +1,5 @@ use crate::models::algebraic::AlgebraicEquationsOverGF2; +use crate::models::algebraic::Bounded; use crate::models::graph::{MaximumClique, MaximumIndependentSet}; use crate::models::set::ExactCoverBy3Sets; use crate::parameters::ParameterRelation; @@ -7,6 +8,39 @@ use crate::topology::SimpleGraph; use crate::types::ProblemParameters; use crate::Problem; +#[test] +fn parameter_schemas_keep_distinct_counts_without_synonymous_aliases() { + let graph = ReductionGraph::new(); + for (model, retained) in [ + ("ExactCoverBy3Sets", &["num_subsets"][..]), + ("ClosestString", &["string_length", "total_length"]), + ("ClosestSubstring", &["total_length", "total_num_windows"]), + ("ThreePartition", &["num_elements", "num_groups"]), + ("PaintShop", &["num_cars", "num_sequence"]), + ( + "MinimumCodeGenerationOneRegister", + &["num_vertices", "num_leaves", "num_internal"], + ), + ( + "HamiltonianPath", + &["num_vertices", "num_consecutive_positions"], + ), + ( + "LongestCommonSubsequence", + &["max_length", "num_transitions"], + ), + ] { + let fields = graph.parameter_names(model); + for field in retained { + assert!(fields.iter().any(|name| name == field), "{model}: {field}"); + } + } + assert!(!graph + .parameter_names("ExactCoverBy3Sets") + .iter() + .any(|field| field == "num_sets")); +} + #[test] fn exact_rule_formula_matches_the_constructed_target() { let source = MaximumIndependentSet::::new( @@ -169,19 +203,25 @@ where let contract = entry.parameter_contract().unwrap(); let transform = contract.transform().expect("symbolic transform exists"); let predicted = transform.evaluate(&source.parameters()).unwrap(); + // Auxiliary fields have their own relations (for example, ILP magnitude + // bounds alongside exact variable counts). Check each against the target. for (field, _) in transform.expressions() { let predicted = predicted.get(field).unwrap(); let actual = actual.get(field).unwrap(); - match transform.relation(field).unwrap() { - ParameterRelation::Exact => assert_eq!(predicted, actual, "{field}"), - ParameterRelation::UpperBound => assert!(predicted >= actual, "{field}"), - } + assert!( + match transform.relation(field).unwrap() { + ParameterRelation::Exact => predicted == actual, + ParameterRelation::UpperBound => predicted >= actual, + }, + "{} -> {}: {field}: predicted {predicted}, measured {actual}", + S::NAME, + T::NAME + ); } for &field in fields { - assert_eq!(transform.relation(field), Some(relation), "{field}"); assert_eq!( - predicted.get(field), - actual.get(field), + transform.relation(field), + Some(relation), "{} -> {}: {field}", S::NAME, T::NAME @@ -199,7 +239,7 @@ where } #[test] -fn newly_exact_parameters_match_reduced_instances() { +fn parameter_relations_match_reduced_instances() { use crate::models::algebraic::MinimumMatrixCover; use crate::models::algebraic::{ IntegerVariable, LinearConstraint, ObjectiveSense, QuadraticAssignment, BMF, ILP, @@ -223,7 +263,7 @@ fn newly_exact_parameters_match_reduced_instances() { &["num_nonzeros"], exact, ); - check_reduced_parameters::<_, ILP>( + check_reduced_parameters::<_, ILP>( ClosestString::new(2, vec![vec![0, 1], vec![1, 0]]), &["num_nonzeros"], exact, @@ -243,15 +283,15 @@ fn newly_exact_parameters_match_reduced_instances() { &["num_nonzeros"], exact, ); - check_reduced_parameters::<_, ILP>( + check_reduced_parameters::<_, ILP>( FeasibleRegisterAssignment::new(4, vec![(0, 1), (0, 2), (1, 3)], 2, vec![0, 1, 0, 0]), &["num_nonzeros"], exact, ); - check_reduced_parameters::<_, ILP>( + check_reduced_parameters::<_, ILP>( IntegerKnapsack::new(vec![3, 4], vec![5, 6], 7).unwrap(), &["num_nonzeros"], - exact, + ParameterRelation::UpperBound, ); check_reduced_parameters::<_, ILP>( LongestCommonSubsequence::new(2, vec![vec![0, 1], vec![1, 0, 1]]), @@ -273,7 +313,7 @@ fn newly_exact_parameters_match_reduced_instances() { &["num_nonzeros"], exact, ); - check_reduced_parameters::<_, ILP>( + check_reduced_parameters::<_, ILP>( RegisterSufficiency::new(4, vec![(2, 0), (3, 1)], 2), &["num_nonzeros"], exact, @@ -304,7 +344,7 @@ fn newly_exact_parameters_match_reduced_instances() { exact, ); check_reduced_parameters::<_, ILP>( - ILP::::with_variables( + ILP::::with_variables( vec![IntegerVariable::new(Some(0), Some(3)).unwrap()], vec![LinearConstraint::le(vec![(0, 1)], 2)], vec![], @@ -338,10 +378,8 @@ fn newly_exact_parameters_match_reduced_instances() { #[test] fn exact_parameter_formulas_cover_sparse_and_boundary_instances() { - use crate::models::algebraic::{QuadraticAssignment, BMF, ILP}; - use crate::models::graph::{ - BicliqueCover, HamiltonianCircuit, HamiltonianPath, MinimumVertexCover, - }; + use crate::models::algebraic::{QuadraticAssignment, ILP}; + use crate::models::graph::{HamiltonianCircuit, HamiltonianPath, MinimumVertexCover}; use crate::models::misc::{ ConsistencyOfDatabaseFrequencyTables, FrequencyTable, KnownValue, LongestCommonSubsequence, MaximumLikelihoodRanking, RegisterSufficiency, @@ -368,7 +406,7 @@ fn exact_parameter_formulas_cover_sparse_and_boundary_instances() { &["num_nonzeros"], exact, ); - check_reduced_parameters::<_, ILP>( + check_reduced_parameters::<_, ILP>( RegisterSufficiency::new(0, vec![], 0), &["num_nonzeros"], exact, @@ -408,21 +446,471 @@ fn exact_parameter_formulas_cover_sparse_and_boundary_instances() { &["sum_triangular_lengths"], exact, ); +} - let source = BMF::new(vec![vec![true, false], vec![false, true]], 1); - let reduction = ReduceTo::::reduce_to(&source).unwrap(); - assert_eq!( - reduction.target_problem().parameters().get("num_edges"), - Some(2) - ); - let entry = crate::rules::registry::reduction_entries() - .into_iter() - .find(|entry| entry.source_name == "BMF" && entry.target_name == "BicliqueCover") - .unwrap(); - let contract = entry.parameter_contract().unwrap(); - assert!(contract.transform().unwrap().get("num_edges").is_none()); - assert!(contract - .unavailable() - .iter() - .any(|field| field.field == "num_edges")); +#[test] +fn multiprocessor_magnitude_predictions_cover_lengths_and_deadlines() { + use crate::models::algebraic::ILP; + use crate::models::misc::{MultiprocessorScheduling, Partition}; + + for (lengths, deadline, bits) in [ + (vec![], 0, 1), + (vec![], 8, 4), + (vec![0], 0, 1), + (vec![7], 1, 3), + (vec![8], 1, 4), + (vec![1], 8, 4), + (vec![i64::MAX], 0, 63), + (vec![0], i64::MAX, 63), + ] { + let source = MultiprocessorScheduling::new(lengths, 2, deadline); + assert_eq!( + source.parameters().get("max_numeric_magnitude_bits"), + Some(bits) + ); + check_reduced_parameters::<_, ILP>( + source, + &["max_constraint_magnitude_bits"], + ParameterRelation::Exact, + ); + } + // The deadline can require more bits than any individual input size. + for sizes in [vec![1], vec![3; 4], vec![1, 2], vec![i64::MAX]] { + check_reduced_parameters::<_, MultiprocessorScheduling>( + Partition::new(sizes).unwrap(), + &["num_tasks", "num_processors"], + ParameterRelation::Exact, + ); + } +} + +#[test] +fn augmentation_magnitude_predictions_cover_weights_and_budgets() { + use crate::models::algebraic::ILP; + use crate::models::graph::{BiconnectivityAugmentation, StrongConnectivityAugmentation}; + use crate::topology::DirectedGraph; + + fn check>>(source: S, bits: u64) { + assert_eq!( + source.parameters().get("max_numeric_magnitude_bits"), + Some(bits) + ); + check_reduced_parameters::<_, ILP>( + source, + &["max_constraint_magnitude_bits"], + ParameterRelation::Exact, + ); + } + for (weight, budget, bits) in [ + (0, 0, 1), + (1, 8, 4), + (8, 1, 4), + (7, 1, 3), + (-8, 1, 4), + (1, -8, 4), + (i64::MIN, 0, 64), + (0, i64::MIN, 64), + (i64::MAX, 0, 63), + (1, i64::MAX, 63), + ] { + check( + BiconnectivityAugmentation::new(SimpleGraph::empty(2), vec![(0, 1, weight)], budget), + bits, + ); + if weight > 0 && budget >= 0 { + check( + StrongConnectivityAugmentation::new( + DirectedGraph::empty(2), + vec![(0, 1, weight)], + budget, + ), + bits, + ); + } + } + for (budget, bits) in [(0, 1), (8, 4), (i64::MAX, 63)] { + check( + BiconnectivityAugmentation::<_, i64>::new(SimpleGraph::empty(0), vec![], budget), + bits, + ); + check( + StrongConnectivityAugmentation::::new(DirectedGraph::empty(0), vec![], budget), + bits, + ); + } +} +#[test] +fn missing_structural_bounds_cover_sparse_and_normalized_instances() { + use crate::models::{ + algebraic::BMF, + graph::{BalancedCompleteBipartiteSubgraph, BicliqueCover, KClique}, + set::MaximumSetPacking, + }; + use crate::types::One; + let upper = ParameterRelation::UpperBound; + for matrix in [ + vec![], + vec![vec![]], + vec![vec![false; 3]; 2], + vec![vec![true, false], vec![false, true]], + vec![vec![true; 3]; 2], + ] { + check_reduced_parameters::<_, BicliqueCover>(BMF::new(matrix, 1), &["num_edges"], upper); + } + for subsets in [vec![], vec![[0, 1, 2]], vec![[0, 1, 2], [0, 1, 2]]] { + check_reduced_parameters::<_, MaximumSetPacking>( + ExactCoverBy3Sets::new(6, subsets), + &["universe_size"], + upper, + ); + } + for graph in [ + SimpleGraph::empty(3), + SimpleGraph::new(3, vec![(0, 0), (0, 1), (0, 1)]), + SimpleGraph::complete(3), + ] { + check_reduced_parameters::<_, BalancedCompleteBipartiteSubgraph>( + KClique::new(graph, 2), + &["num_vertices"], + upper, + ); + } +} + +#[test] +fn sat_bounds_cover_empty_short_and_repeated_clauses() { + use crate::models::{ + formula::{CNFClause, CircuitSAT, KSatisfiability, NAESatisfiability, Satisfiability}, + graph::IntegralFlowHomologousArcs, + }; + use crate::variant::K3; + for clauses in [ + vec![], + vec![CNFClause::new(vec![])], + vec![CNFClause::new(vec![1])], + vec![CNFClause::new(vec![1, -1, 2, 2, 3])], + ] { + let source = Satisfiability::new(4, clauses); + check_reduced_parameters::<_, KSatisfiability>( + source.clone(), + &["num_literals"], + ParameterRelation::UpperBound, + ); + check_reduced_parameters::<_, NAESatisfiability>( + source.clone(), + &["num_literal_pairs"], + ParameterRelation::UpperBound, + ); + check_reduced_parameters::<_, CircuitSAT>( + source.clone(), + &["num_expression_nodes", "num_assignment_outputs"], + ParameterRelation::UpperBound, + ); + check_reduced_parameters::<_, IntegralFlowHomologousArcs>( + source, + &["max_capacity"], + ParameterRelation::UpperBound, + ); + } +} + +#[test] +fn circuit_bounds_cover_fanin_constants_and_multiple_outputs() { + use crate::models::formula::{Assignment, BooleanExpr, Circuit, CircuitSAT, Satisfiability}; + for expr in [ + BooleanExpr::constant(true), + BooleanExpr::not(BooleanExpr::var("x")), + BooleanExpr::xor(vec![BooleanExpr::var("x"); 8]), + BooleanExpr::and(vec![ + BooleanExpr::or(vec![ + BooleanExpr::var("x"), + BooleanExpr::var("y") + ]); + 4 + ]), + ] { + for outputs in [vec![], vec!["a".into(), "b".into()]] { + let source = + CircuitSAT::new(Circuit::new(vec![Assignment::new(outputs, expr.clone())])); + check_reduced_parameters::<_, Satisfiability>( + source, + &["num_vars", "num_clauses", "num_literals"], + ParameterRelation::UpperBound, + ); + } + } +} + +#[test] +fn factoring_circuit_bounds_cover_zero_width_and_overflow_sentinels() { + use crate::models::{formula::CircuitSAT, misc::Factoring}; + for m in 0..=3 { + for n in m..=3 { + for target in [0u64, 1, 255] { + check_reduced_parameters::<_, CircuitSAT>( + Factoring::with_factor_bits(target, m, n), + &["num_assignment_outputs", "num_expression_nodes"], + ParameterRelation::UpperBound, + ); + } + } + } +} + +#[test] +fn sat_flow_and_scheduling_bounds_cover_fixed_outputs() { + use crate::models::{ + formula::{CNFClause, KSatisfiability}, + graph::DirectedTwoCommodityIntegralFlow, + misc::PreemptiveScheduling, + }; + use crate::variant::K3; + for clauses in [ + vec![], + vec![CNFClause::new(vec![])], + vec![CNFClause::new(vec![1, 1, -2])], + ] { + let source = KSatisfiability::::new_allow_less(2, clauses); + check_reduced_parameters::<_, DirectedTwoCommodityIntegralFlow>( + source.clone(), + &["max_capacity"], + ParameterRelation::Exact, + ); + check_reduced_parameters::<_, PreemptiveScheduling>( + source, + &["num_precedences"], + ParameterRelation::UpperBound, + ); + } +} + +#[test] +fn set_splitting_bounds_cover_deduplication_and_large_subsets() { + use crate::models::{misc::Betweenness, set::SetSplitting}; + for subsets in [ + vec![], + vec![vec![0, 0]], + vec![vec![0, 1]], + vec![(0..8).collect()], + vec![vec![0; 20], (0..8).collect()], + ] { + check_reduced_parameters::<_, Betweenness>( + SetSplitting::new(8, subsets), + &["num_elements", "num_triples"], + ParameterRelation::UpperBound, + ); + } +} + +#[test] +fn decision_cover_bounds_do_not_depend_on_threshold_magnitude() { + use crate::models::{ + graph::{HamiltonianCircuit, MinimumVertexCover}, + Decision, + }; + use crate::types::One; + for graph in [ + SimpleGraph::empty(0), + SimpleGraph::path(3), + SimpleGraph::new(3, vec![(0, 0), (0, 1), (1, 2)]), + ] { + for threshold in [i64::MIN, 0, 1, 2, i64::MAX] { + check_reduced_parameters::<_, HamiltonianCircuit>( + Decision::new( + MinimumVertexCover::<_, One>::new( + graph.clone(), + vec![One; crate::topology::Graph::num_vertices(&graph)], + ), + threshold, + ), + &["num_vertices", "num_edges"], + ParameterRelation::UpperBound, + ); + } + } +} + +#[test] +fn subset_lattice_dimensions_use_existing_numeric_magnitude() { + use crate::models::{algebraic::ClosestVectorProblem, misc::SubsetSum, Decision}; + for (sizes, target) in [ + (vec![], 0u64), + (vec![1], 0), + (vec![7], 8), + (vec![8], 7), + (vec![1, 3], 4), + ] { + check_reduced_parameters::<_, Decision>( + SubsetSum::new(sizes, target), + &["ambient_dimension", "num_basis_vectors"], + ParameterRelation::Exact, + ); + } +} + +#[test] +fn knapsack_qubo_bounds_cover_capacity_boundaries() { + use crate::models::{algebraic::QUBO, misc::Knapsack}; + for (capacity, bits) in [(0, 1), (1, 1), (2, 2), (3, 2), (4, 3), (7, 3), (8, 4)] { + let source = Knapsack::new(vec![0, 1, 2], vec![0, 2, 1], capacity); + assert_eq!(source.parameters().get("capacity_bits"), Some(bits)); + check_reduced_parameters::<_, QUBO>(source, &["num_vars"], ParameterRelation::Exact); + } +} + +#[test] +fn knapsack_ilp_magnitude_uses_capacity_bits() { + use crate::models::{algebraic::ILP, misc::Knapsack}; + for (capacity, bits) in [(0, 1), (7, 3), (8, 4), (i64::MAX, 63)] { + let source = Knapsack::new(vec![0, 1, i64::MAX], vec![1, 2, 3], capacity); + assert_eq!(source.parameters().get("capacity_bits"), Some(bits)); + check_reduced_parameters::<_, ILP>( + source, + &["max_constraint_magnitude_bits"], + ParameterRelation::UpperBound, + ); + } +} + +#[test] +fn partition_propagates_capacity_bits_without_raw_capacity() { + use crate::models::misc::{Knapsack, Partition}; + for sizes in [vec![1], vec![1, 2], vec![7, 8], vec![i64::MAX]] { + check_reduced_parameters::<_, Knapsack>( + Partition::new(sizes).unwrap(), + &["capacity_bits"], + ParameterRelation::UpperBound, + ); + } +} + +#[test] +fn integer_knapsack_capacity_bits_bound_ilp_magnitudes() { + use crate::models::{algebraic::ILP, set::IntegerKnapsack}; + for (capacity, bits) in [(0, 1), (7, 3), (8, 4), (i64::MAX, 63)] { + let source = IntegerKnapsack::new(vec![1, i64::MAX], vec![1, 2], capacity).unwrap(); + assert_eq!(source.parameters().get("capacity_bits"), Some(bits)); + check_reduced_parameters::<_, ILP>( + source, + &["max_constraint_magnitude_bits"], + ParameterRelation::UpperBound, + ); + } +} + +#[test] +fn open_shop_horizon_bits_cover_totals_and_decision_bounds() { + use crate::models::{algebraic::ILP, misc::OpenShopScheduling, Decision}; + for (machines, times, bits) in [ + (0, vec![vec![]], 1), + (2, vec![], 1), + (2, vec![vec![0, 0]], 1), + (2, vec![vec![3, 4]], 3), + (2, vec![vec![4, 4]], 4), + (1, vec![vec![i64::MAX]], 63), + ] { + let source = OpenShopScheduling::new(machines, times); + assert_eq!(source.parameters().get("schedule_horizon_bits"), Some(bits)); + check_reduced_parameters::<_, ILP>( + source.clone(), + &["max_constraint_magnitude_bits"], + ParameterRelation::UpperBound, + ); + for bound in [i64::MIN, 0, i64::MAX] { + check_reduced_parameters::<_, ILP>( + Decision::new(source.clone(), bound), + &["max_constraint_magnitude_bits"], + ParameterRelation::UpperBound, + ); + } + } +} + +#[test] +fn partition_propagates_open_shop_horizon_bits() { + use crate::models::{ + misc::{OpenShopScheduling, Partition}, + Decision, + }; + for sizes in [vec![1], vec![1, 1], vec![1, 2], vec![1 << 20; 2]] { + check_reduced_parameters::<_, Decision>( + Partition::new(sizes).unwrap(), + &["schedule_horizon_bits"], + ParameterRelation::UpperBound, + ); + } +} + +#[test] +fn closest_vector_size_bounds_cover_numeric_and_rank_variation() { + use crate::models::algebraic::{ClosestVectorProblem, QUBO}; + for (basis, target, bits) in [ + (vec![], vec![], 1), + (vec![], vec![-8], 4), + (vec![vec![1]], vec![0], 1), + (vec![vec![-8]], vec![1], 4), + (vec![vec![1]], vec![8], 4), + (vec![vec![2, 0], vec![1, 2]], vec![3, 2], 2), + (vec![vec![1, 8], vec![0, 1]], vec![-1, 1], 4), + ] { + let source = ClosestVectorProblem::new(basis, target).unwrap(); + assert_eq!( + source.parameters().get("max_numeric_magnitude_bits"), + Some(bits) + ); + check_reduced_parameters::<_, QUBO>( + source, + &["num_vars", "num_quadratic_terms"], + ParameterRelation::UpperBound, + ); + } + for value in [i64::MIN, i64::MAX] { + let source = ClosestVectorProblem::new(vec![vec![value]], vec![value]).unwrap(); + assert_eq!( + source.parameters().get("max_numeric_magnitude_bits"), + Some(if value == i64::MIN { 64 } else { 63 }) + ); + } +} + +#[test] +fn incongruence_pair_bounds_cover_prime_growth_and_repeated_literals() { + use crate::models::{ + algebraic::SimultaneousIncongruences, + formula::{CNFClause, KSatisfiability}, + }; + use crate::variant::K3; + for variables in 0..=12 { + let clauses = if variables == 0 { + vec![] + } else { + vec![CNFClause::new(vec![1, 1, -1])] + }; + check_reduced_parameters::<_, SimultaneousIncongruences>( + KSatisfiability::::new(variables, clauses), + &["num_pairs"], + ParameterRelation::UpperBound, + ); + } +} + +#[test] +fn vertex_cover_lcs_transition_bounds_cover_empty_strings() { + use crate::models::{graph::MinimumVertexCover, misc::LongestCommonSubsequence}; + use crate::{ + topology::{Graph, SimpleGraph}, + types::One, + }; + for graph in [ + SimpleGraph::empty(0), + SimpleGraph::empty(1), + SimpleGraph::path(3), + ] { + let weights = vec![One; graph.num_vertices()]; + check_reduced_parameters::<_, LongestCommonSubsequence>( + MinimumVertexCover::new(graph, weights), + &["num_transitions"], + ParameterRelation::UpperBound, + ); + } } diff --git a/tests/suites/register_assignment_reductions.rs b/tests/suites/register_assignment_reductions.rs index 04afcc570..61b98c6bc 100644 --- a/tests/suites/register_assignment_reductions.rs +++ b/tests/suites/register_assignment_reductions.rs @@ -1,4 +1,4 @@ -use problemreductions::models::algebraic::ILP; +use problemreductions::models::algebraic::{Bounded, ILP}; use problemreductions::models::formula::{CNFClause, KSatisfiability}; use problemreductions::models::misc::FeasibleRegisterAssignment; use problemreductions::prelude::*; @@ -21,7 +21,7 @@ fn ksat_to_fra_path() -> ReductionPath { fn fra_to_ilp_path() -> ReductionPath { let graph = ReductionGraph::new(); let src = ReductionGraph::variant_to_map(&FeasibleRegisterAssignment::variant()); - let dst = ReductionGraph::variant_to_map(&ILP::::variant()); + let dst = ReductionGraph::variant_to_map(&ILP::::variant()); graph .find_all_paths("FeasibleRegisterAssignment", &src, "ILP", &dst) .into_iter() @@ -64,7 +64,7 @@ fn test_ksat_to_fra_structure_and_closed_loop_via_ilp() { .reduce_along_path(&fra_path, fra as &dyn std::any::Any) .expect("FRA -> ILP reduction should not fail") .expect("FRA -> ILP reduction should execute"); - let ilp = fra_chain.target_problem::>(); + let ilp = fra_chain.target_problem::>(); let ilp_solution = ILPSolver::new() .solve(ilp) @@ -99,7 +99,7 @@ fn test_unsatisfiable_ksat_stays_infeasible_through_fra_to_ilp() { assert!( ILPSolver::new() - .solve(fra_chain.target_problem::>()) + .solve(fra_chain.target_problem::>()) .is_err(), "unsatisfiable source instance should yield an infeasible ILP" ); From bf09cce890bf519e5a48f3ebd47f44cc1e769c2a Mon Sep 17 00:00:00 2001 From: Xiwei Pan <90967972+isPANN@users.noreply.github.com> Date: Tue, 29 Sep 2026 05:51:11 -0700 Subject: [PATCH 9/9] Correct ILP parameter bounds and simplify reduction code --- docs/paper/reductions.typ | 6 ++++ .../misc/resource_constrained_scheduling.rs | 36 ++++++++----------- src/rules/acyclicpartition_ilp.rs | 6 ++-- src/rules/biconnectivityaugmentation_ilp.rs | 11 ++---- src/rules/bottlenecktravelingsalesman_ilp.rs | 9 ++--- src/rules/coloring_ilp.rs | 5 +-- src/rules/ensemblecomputation_ilp.rs | 9 ++--- src/rules/maximumedgeweightedkclique_ilp.rs | 11 +++--- src/rules/minimumcoveringbycliques_ilp.rs | 7 ++-- .../strongconnectivityaugmentation_ilp.rs | 6 ++-- .../parameter_formula_validation.rs | 12 ------- src/unit_tests/rules/coloring_ilp.rs | 20 ++++++----- .../rules/maximumedgeweightedkclique_ilp.rs | 20 +++++++---- .../rules/minimumcoveringbycliques_ilp.rs | 11 +++--- .../symbolic_parameter_contracts.rs | 2 +- 15 files changed, 82 insertions(+), 89 deletions(-) diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index c06aca6f3..dd2a0164e 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -13224,6 +13224,8 @@ where $P$ is a penalty weight large enough that any constraint violation costs m _Correctness._ ($arrow.r.double$) A valid $k$-coloring assigns exactly one color per vertex with different colors on adjacent vertices; setting $x_(v,c) = 1$ for the assigned color satisfies all constraints. ($arrow.l.double$) Any feasible ILP solution has exactly one $x_(v,c) = 1$ per vertex; this defines a coloring, and constraint (2) ensures adjacent vertices differ. + _Parameter bounds._ For $n$ vertices and $m$ stored edges, assignment rows contribute $n k$ nonzeros and edge rows at most $2 m k$. Thus the normalized ILP has at most $k(n+2m)$ nonzeros. A self-loop merges two endpoint terms, so this is an upper bound rather than an equality. + _Solution extraction._ For each vertex $v$, find $c$ with $x_(v,c) = 1$; assign color $c$ to $v$. ] @@ -13624,6 +13626,8 @@ The following reductions to Integer Linear Programming are straightforward formu _Correctness._ ($arrow.r.double$) Any $k$-clique $S subset.eq V$ yields a feasible solution by setting $x_v = 1$ iff $v in S$ and $y_(u v) = 1$ iff $u, v in S$; the non-edge constraints are satisfied because $G[S]$ is a clique, and the McCormick triple enforces $y_(u v) = x_u and x_v$. The objective equals $sum_({u, v} in E(S)) w_(u v)$. ($arrow.l.double$) Any feasible solution with cardinality $k$ selects $k$ vertices forming a clique (the non-edge constraints rule out non-adjacent pairs), and the McCormick lower bound $y_(u v) >= x_u + x_v - 1$ forces $y_(u v) = 1$ whenever both endpoints are selected, even when $w_(u v) < 0$. + _Parameter bounds._ For $n$ vertices and $m$ stored edge occurrences, there is one cardinality row, at most $n(n-1)/2$ missing-pair rows, and $3m$ product rows. The nonzero count is at most $n+n(n-1)+7m=n^2+7m$. These bounds cover both coefficient variants, including repeated edges and loops; normalization can only reduce the nonzero count. + _Solution extraction._ Take the first $|V|$ entries of the ILP solution as the source selection vector. ] @@ -14707,6 +14711,8 @@ The following reductions to Integer Linear Programming are straightforward formu ($arrow.l.double$) Conversely, let $(x, z, y)$ be any feasible ILP solution. For each slot $k$, the vertices with $x_(v,k) = 1$ form a clique because every non-edge pair is forbidden from appearing together in that slot. If $y_({u,v},k) = 1$, the McCormick constraints force both endpoints $u$ and $v$ into slot $k$, so the edge is indeed contained in that clique. The coverage inequalities therefore certify that every source edge lies in at least one clique slot, giving a valid edge-clique cover. Since the objective counts active slots, minimizing it yields a minimum cover. + _Parameter bounds._ With $n$ vertices and $m$ stored edge occurrences, the four construction blocks contribute $n m$ activation rows, at most $n(n-1)m/2$ missing-pair rows, $3m^2$ product rows, and $m$ coverage rows. Their nonzeros are bounded respectively by $2n m$, $n(n-1)m$, $7m^2$, and $m^2$, giving at most $2n m+n(n-1)m+8m^2$. Repeated edges and loops are retained as occurrences; subtracting $m$ from the number of distinct vertex pairs would not count missing pairs correctly. + _Solution extraction._ For each source edge $e$, choose any slot $k$ with $y_(e,k) = 1$ and output the label $k$. The extracted edge-to-slot labeling is valid because every slot induces a clique and every edge is assigned to at least one covering slot. ] diff --git a/src/models/misc/resource_constrained_scheduling.rs b/src/models/misc/resource_constrained_scheduling.rs index c186a6f0a..ba7b48ccd 100644 --- a/src/models/misc/resource_constrained_scheduling.rs +++ b/src/models/misc/resource_constrained_scheduling.rs @@ -225,33 +225,25 @@ impl Problem for ResourceConstrainedScheduling { // Empty slots consume no resources. Keep ascending slot order and // task order within each slot for checked accumulation. - let mut occupied = config.clone(); - occupied.sort_unstable(); - occupied.dedup(); - for u in occupied { - // Collect tasks scheduled at time slot u - let mut task_count = 0usize; + let mut tasks: Vec<_> = (0..n).collect(); + tasks.sort_unstable_by_key(|&task| (config[task], task)); + for slot_tasks in tasks.chunk_by(|&a, &b| config[a] == config[b]) { let mut resource_usage = vec![0i64; r]; - - for (t, &slot) in config.iter().enumerate() { - if slot == u { - task_count += 1; - // Accumulate resource usage - for (usage, &req) in resource_usage - .iter_mut() - .zip(self.resource_requirements[t].iter()) - { - *usage = usage.checked_add(req).ok_or_else(|| { - crate::traits::EvaluationError::IntegerOverflow( - "summing scheduled resource usage".to_string(), - ) - })?; - } + for &task in slot_tasks { + for (usage, &req) in resource_usage + .iter_mut() + .zip(self.resource_requirements[task].iter()) + { + *usage = usage.checked_add(req).ok_or_else(|| { + crate::traits::EvaluationError::IntegerOverflow( + "summing scheduled resource usage".to_string(), + ) + })?; } } // Check processor capacity - if task_count > self.num_processors { + if slot_tasks.len() > self.num_processors { return Ok(crate::types::Or(false)); } diff --git a/src/rules/acyclicpartition_ilp.rs b/src/rules/acyclicpartition_ilp.rs index a409c6fe1..cfdbfd1dd 100644 --- a/src/rules/acyclicpartition_ilp.rs +++ b/src/rules/acyclicpartition_ilp.rs @@ -4,7 +4,7 @@ //! crossing flags y_t, and partition labels used directly as a topological order. //! See the paper entry for the full formulation. -use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::AcyclicPartition; use crate::reduction; use crate::rules::ilp_helpers::mccormick_product; @@ -147,9 +147,7 @@ impl ReduceTo> for AcyclicPartition { constraints.push(LinearConstraint::le(terms, 0)); } - let variables = vec![IntegerVariable::binary(); num_vars]; - - let target = ILP::with_variables(variables, constraints, vec![], ObjectiveSense::Minimize) + let target = ILP::new(num_vars, constraints, vec![], ObjectiveSense::Minimize) .map_err(Self::target_construction)?; Ok(ReductionAcyclicPartitionToILP { target, n }) diff --git a/src/rules/biconnectivityaugmentation_ilp.rs b/src/rules/biconnectivityaugmentation_ilp.rs index 09f9ef368..9c5bcf819 100644 --- a/src/rules/biconnectivityaugmentation_ilp.rs +++ b/src/rules/biconnectivityaugmentation_ilp.rs @@ -4,7 +4,7 @@ //! certify that the remaining augmented graph stays connected via unit-flow //! commodities from a surviving root to every other surviving vertex. -use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::BiconnectivityAugmentation; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -220,13 +220,8 @@ impl ReduceTo> for BiconnectivityAugmentation { } } - let target = ILP::with_variables( - vec![IntegerVariable::binary(); num_vars], - constraints, - vec![], - ObjectiveSense::Minimize, - ) - .map_err(Self::target_construction)?; + let target = ILP::new(num_vars, constraints, vec![], ObjectiveSense::Minimize) + .map_err(Self::target_construction)?; Ok(ReductionBiconnAugToILP { target, num_candidates: p, diff --git a/src/rules/bottlenecktravelingsalesman_ilp.rs b/src/rules/bottlenecktravelingsalesman_ilp.rs index 9845c359f..a1bd5bce0 100644 --- a/src/rules/bottlenecktravelingsalesman_ilp.rs +++ b/src/rules/bottlenecktravelingsalesman_ilp.rs @@ -1,6 +1,6 @@ //! Bottleneck TSP to ILP using cyclic positions and a selected maximum edge. -use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::BottleneckTravelingSalesman; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -175,11 +175,8 @@ impl ReduceTo> for BottleneckTravelingSalesman { .enumerate() .map(|(edge, weight)| (q(edge), weight)) .collect(); - let variables = vec![IntegerVariable::binary(); num_vars]; - - let target = - ILP::with_variables(variables, constraints, objective, ObjectiveSense::Minimize) - .map_err(Self::target_construction)?; + let target = ILP::new(num_vars, constraints, objective, ObjectiveSense::Minimize) + .map_err(Self::target_construction)?; Ok(ReductionBTSPToILP { target, num_vertices: n, diff --git a/src/rules/coloring_ilp.rs b/src/rules/coloring_ilp.rs index 7383ec766..7806d5dc6 100644 --- a/src/rules/coloring_ilp.rs +++ b/src/rules/coloring_ilp.rs @@ -112,14 +112,15 @@ impl crate::rules::Aggregate { } -// Register only the KN variant in the reduction graph +// Register only the KN variant in the reduction graph. Each edge row has at most +// two nonzeros: a self-loop merges both endpoint terms into one coefficient. #[reduction(transform = { exact { num_vars = "num_vertices * num_colors", num_constraints = "num_vertices + num_edges * num_colors", - num_nonzeros = "num_colors * (num_vertices + 2 * num_edges)", }, upper_bound { + num_nonzeros = "num_colors * (num_vertices + 2 * num_edges)", max_constraint_magnitude_bits = "2", }, })] diff --git a/src/rules/ensemblecomputation_ilp.rs b/src/rules/ensemblecomputation_ilp.rs index ef8a079df..9011e5a72 100644 --- a/src/rules/ensemblecomputation_ilp.rs +++ b/src/rules/ensemblecomputation_ilp.rs @@ -1,6 +1,6 @@ //! Polynomial-size circuit-slot reduction from EnsembleComputation to `ILP`. -use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::EnsembleComputation; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -280,11 +280,8 @@ impl ReduceTo> for EnsembleComputation { } let objective = (0..budget).map(|step| (activity_base + step, 1)).collect(); - let variables = vec![IntegerVariable::binary(); num_vars]; - - let target = - ILP::with_variables(variables, constraints, objective, ObjectiveSense::Minimize) - .map_err(Self::target_construction)?; + let target = ILP::new(num_vars, constraints, objective, ObjectiveSense::Minimize) + .map_err(Self::target_construction)?; Ok(ReductionEnsembleComputationToILP { target, universe_size: u, diff --git a/src/rules/maximumedgeweightedkclique_ilp.rs b/src/rules/maximumedgeweightedkclique_ilp.rs index 06ad62928..bc6e68832 100644 --- a/src/rules/maximumedgeweightedkclique_ilp.rs +++ b/src/rules/maximumedgeweightedkclique_ilp.rs @@ -138,14 +138,17 @@ where }) } +// One cardinality row, at most n(n-1)/2 missing-pair rows, and three +// product rows per stored edge. Their nonzeros total at most n² + 7m; +// repeated edges and loops do not subtract from the missing-pair bound. #[reduction(transform = { exact { num_vars = "num_vertices + num_edges", - num_constraints = "1 + num_vertices * (num_vertices - 1) / 2 + 2 * num_edges", }, upper_bound { + num_constraints = "1 + num_vertices * (num_vertices - 1) / 2 + 3 * num_edges", max_constraint_magnitude_bits = "num_vertices + 1", - num_nonzeros = "(num_vertices + num_edges) * (1 + num_vertices * (num_vertices - 1) / 2 + 2 * num_edges)", + num_nonzeros = "num_vertices^2 + 7 * num_edges", }, })] impl ReduceTo> for MaximumEdgeWeightedKClique { @@ -159,11 +162,11 @@ impl ReduceTo> for MaximumEdgeWeightedKClique { #[reduction(transform = { exact { num_vars = "num_vertices + num_edges", - num_constraints = "1 + num_vertices * (num_vertices - 1) / 2 + 2 * num_edges", }, upper_bound { + num_constraints = "1 + num_vertices * (num_vertices - 1) / 2 + 3 * num_edges", max_constraint_magnitude_bits = "num_vertices + 1", - num_nonzeros = "(num_vertices + num_edges) * (1 + num_vertices * (num_vertices - 1) / 2 + 2 * num_edges)", + num_nonzeros = "num_vertices^2 + 7 * num_edges", }, })] impl ReduceTo> for MaximumEdgeWeightedKClique { diff --git a/src/rules/minimumcoveringbycliques_ilp.rs b/src/rules/minimumcoveringbycliques_ilp.rs index 37bc8a8ff..05ed2aa03 100644 --- a/src/rules/minimumcoveringbycliques_ilp.rs +++ b/src/rules/minimumcoveringbycliques_ilp.rs @@ -61,14 +61,17 @@ impl ReductionResult for ReductionMinimumCoveringByCliquesToILP { } } +// With n vertices and m stored edges, activation, missing-pair, product, and +// coverage blocks contribute nm, at most n(n-1)m/2, 3m², and m rows. +// Their nonzeros are bounded by 2nm, n(n-1)m, 7m², and m². Stored edges may repeat. #[reduction(transform = { exact { num_vars = "num_vertices * num_edges + num_edges + num_edges * num_edges", - num_constraints = "num_vertices * num_edges + (num_vertices * (num_vertices - 1) / 2 - num_edges) * num_edges + 3 * num_edges * num_edges + num_edges", }, upper_bound { + num_constraints = "num_vertices * num_edges + num_vertices * (num_vertices - 1) / 2 * num_edges + 3 * num_edges^2 + num_edges", max_constraint_magnitude_bits = "2", - num_nonzeros = "(num_vertices * num_edges + num_edges + num_edges * num_edges) * (num_vertices * num_edges + (num_vertices * (num_vertices - 1) / 2 - num_edges) * num_edges + 3 * num_edges * num_edges + num_edges)", + num_nonzeros = "2 * num_vertices * num_edges + num_vertices * (num_vertices - 1) * num_edges + 8 * num_edges^2", }, })] impl ReduceTo> for MinimumCoveringByCliques { diff --git a/src/rules/strongconnectivityaugmentation_ilp.rs b/src/rules/strongconnectivityaugmentation_ilp.rs index ecbfed4a9..c2eb6d16d 100644 --- a/src/rules/strongconnectivityaugmentation_ilp.rs +++ b/src/rules/strongconnectivityaugmentation_ilp.rs @@ -4,7 +4,7 @@ //! sending flow both from a root to every vertex and back again. //! See the paper entry for the full formulation. -use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::StrongConnectivityAugmentation; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; @@ -183,9 +183,7 @@ impl ReduceTo> for StrongConnectivityAugmentation { } // Each connectivity certificate can be a simple unit-flow path; cycles are unnecessary. - let variables = vec![IntegerVariable::binary(); num_vars]; - - let target = ILP::with_variables(variables, constraints, vec![], ObjectiveSense::Minimize) + let target = ILP::new(num_vars, constraints, vec![], ObjectiveSense::Minimize) .map_err(Self::target_construction)?; Ok(ReductionSCAToILP { target, diff --git a/src/unit_tests/parameter_formula_validation.rs b/src/unit_tests/parameter_formula_validation.rs index 68655d59f..117fbfa5a 100644 --- a/src/unit_tests/parameter_formula_validation.rs +++ b/src/unit_tests/parameter_formula_validation.rs @@ -100,18 +100,6 @@ fn source_for( } } } - if entry.source_name == "ILP" && variant.get("variable").is_some_and(|v| v == "i64") { - use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; - return Ok(Box::new( - ILP::::with_variables( - vec![IntegerVariable::new(Some(0), Some(3)).unwrap()], - vec![LinearConstraint::le(vec![(0, 1)], 3)], - vec![(0, 1)], - ObjectiveSense::Minimize, - ) - .unwrap(), - )); - } // Reuse existing examples with the same model name when a compatible variant // has no dedicated example; its factory still enforces the concrete type. for ((name, _), examples) in sources { diff --git a/src/unit_tests/rules/coloring_ilp.rs b/src/unit_tests/rules/coloring_ilp.rs index 045fd3b51..43d642468 100644 --- a/src/unit_tests/rules/coloring_ilp.rs +++ b/src/unit_tests/rules/coloring_ilp.rs @@ -46,14 +46,18 @@ fn test_reduction_path_graph() { #[test] fn runtime_color_count_controls_exact_ilp_parameters() { - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); - for colors in [1, 2, 3, 4, 5] { - let problem = KColoring::::with_k(graph.clone(), colors); - let reduction = ReduceTo::>::reduce_to(&problem).unwrap(); - let target = reduction.target_problem(); - - assert_eq!(target.num_vars(), 3 * colors); - assert_eq!(target.num_constraints(), 3 + 2 * colors); + // A loop merges the two edge coefficients without changing allocation counts. + for edges in [vec![(0, 1), (1, 2)], vec![(0, 0), (1, 2)]] { + let graph = SimpleGraph::new(3, edges); + for colors in [1, 2, 3, 4, 5] { + let problem = KColoring::::with_k(graph.clone(), colors); + let reduction = ReduceTo::>::reduce_to(&problem).unwrap(); + let target = reduction.target_problem(); + + assert_eq!(target.num_vars(), 3 * colors); + assert_eq!(target.num_constraints(), 3 + 2 * colors); + crate::rules::test_helpers::assert_parameter_predictions(&problem, &reduction); + } } } diff --git a/src/unit_tests/rules/maximumedgeweightedkclique_ilp.rs b/src/unit_tests/rules/maximumedgeweightedkclique_ilp.rs index aaf58b7ee..4b2f7525e 100644 --- a/src/unit_tests/rules/maximumedgeweightedkclique_ilp.rs +++ b/src/unit_tests/rules/maximumedgeweightedkclique_ilp.rs @@ -26,15 +26,23 @@ fn test_maximumedgeweightedkclique_to_ilp_closed_loop() { #[test] fn test_maximumedgeweightedkclique_to_ilp_structure() { - let source = issue_instance(); - let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + let graph = SimpleGraph::new(2, vec![(0, 1), (0, 1)]); + let source = MaximumEdgeWeightedKClique::new(graph.clone(), vec![1, 1], 2).unwrap(); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); let ilp = reduction.target_problem(); - // 4 vertex variables + 5 edge variables = 9. - assert_eq!(ilp.num_vars(), 9); + assert_eq!(ilp.num_vars(), 4); + // One cardinality row, no missing pairs, three product rows per stored edge. + assert_eq!(ilp.num_constraints(), 7); assert_eq!(ilp.sense(), ObjectiveSense::Maximize); - // Objective is on the edge variables (indices 4..9). - assert_eq!(ilp.objective(), vec![(4, 5), (5, 4), (6, -1), (7, 1)]); + assert_eq!(ilp.objective(), vec![(2, 1), (3, 1)]); + crate::rules::test_helpers::assert_parameter_predictions(&source, &reduction); + assert_bf_vs_ilp(&source, &reduction); + + let source = MaximumEdgeWeightedKClique::new(graph, vec![1.0, 1.0], 2).unwrap(); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + crate::rules::test_helpers::assert_parameter_predictions(&source, &reduction); + assert_bf_vs_ilp(&source, &reduction); } #[test] diff --git a/src/unit_tests/rules/minimumcoveringbycliques_ilp.rs b/src/unit_tests/rules/minimumcoveringbycliques_ilp.rs index 4e322beef..1b9d0d93a 100644 --- a/src/unit_tests/rules/minimumcoveringbycliques_ilp.rs +++ b/src/unit_tests/rules/minimumcoveringbycliques_ilp.rs @@ -7,14 +7,17 @@ use crate::traits::Problem; use crate::types::Min; #[test] -fn test_reduction_shape_on_path_p3() { - let source = MinimumCoveringByCliques::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)])); +fn test_reduction_shape_with_repeated_edges() { + let source = MinimumCoveringByCliques::new(SimpleGraph::new(2, vec![(0, 1), (0, 1)])); let reduction: ReductionMinimumCoveringByCliquesToILP = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let ilp = reduction.target_problem(); - assert_eq!(ilp.num_vars(), 12); - assert_eq!(ilp.constraints().len(), 22); + assert_eq!(ilp.num_vars(), 10); + // Four activation rows, no missing pairs, twelve product rows, two coverage rows. + assert_eq!(ilp.constraints().len(), 18); + crate::rules::test_helpers::assert_parameter_predictions(&source, &reduction); + crate::rules::test_helpers::assert_bf_vs_ilp(&source, &reduction); assert_eq!(ilp.sense(), ObjectiveSense::Minimize); } diff --git a/src/unit_tests/symbolic_parameter_contracts.rs b/src/unit_tests/symbolic_parameter_contracts.rs index c095f9eab..3dd3c3cee 100644 --- a/src/unit_tests/symbolic_parameter_contracts.rs +++ b/src/unit_tests/symbolic_parameter_contracts.rs @@ -128,6 +128,7 @@ fn every_registered_rule_has_one_valid_parameter_contract() { #[cfg(feature = "example-db")] #[test] fn canonical_examples_satisfy_upper_bound_parameter_contracts() { + let graph = ReductionGraph::new(); for spec in crate::rules::canonical_rule_example_specs() { let example = (spec.build)(); let source = crate::registry::load_dyn( @@ -142,7 +143,6 @@ fn canonical_examples_satisfy_upper_bound_parameter_contracts() { example.target.instance.clone(), ) .unwrap(); - let graph = ReductionGraph::new(); let entry = graph .find_entry( &example.source.problem,