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 01/22] 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 02/22] 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 03/22] 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 04/22] 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 05/22] 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 06/22] 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 07/22] 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 08/22] 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 09/22] 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, From 2ba9a0368070ff6553078a85bc8b46d785c8cfb2 Mon Sep 17 00:00:00 2001 From: Xiwei Pan <90967972+isPANN@users.noreply.github.com> Date: Tue, 29 Sep 2026 12:11:50 -0700 Subject: [PATCH 10/22] Calibrate parity and universe-size reduction bounds Derive upper bounds from conditional even padding and the set-packing endpoint universe. Preserve the exact edge-to-set count and verify the registered promises in existing parity and isolated-vertex tests. --- docs/paper/reductions.typ | 4 +++- src/rules/maxcut_minimumcutintoboundedsets.rs | 4 +++- src/rules/maximummatching_maximumsetpacking.rs | 8 +++++--- src/unit_tests/rules/maxcut_minimumcutintoboundedsets.rs | 6 +++++- .../rules/maximummatching_maximumsetpacking.rs | 9 ++++++++- 5 files changed, 24 insertions(+), 7 deletions(-) diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index dd2a0164e..742ce9eb5 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -11944,7 +11944,7 @@ the displayed rule, extracted from the corresponding `pred path` entry. #reduction-rule("MaximumMatching", "MaximumSetPacking")[ A matching selects edges that share no endpoints; set packing selects sets that share no elements. By representing each edge as the 2-element set of its endpoints and using vertices as the universe, two edges conflict (share an endpoint) if and only if their sets overlap. This embeds matching as a special case of set packing where every set has size exactly 2. ][ - _Construction._ Universe $U = V$ (vertices, indexed $0, ..., |V|-1$). For each edge $e = (u, v)$, define $S_e = {u, v}$ with weight $w(S_e) = w(e)$. Variables correspond one-to-one: edge $e$ maps to set $S_e$. + _Construction._ For each edge $e = (u, v)$, define $S_e = {u, v}$ with weight $w(S_e) = w(e)$. Variables correspond one-to-one: edge $e$ maps to set $S_e$. The implementation infers the universe size as one plus the largest endpoint label, or zero when there are no edges. Thus the number of sets is exactly $|E|$, while the universe size is at most $|V|$; trailing isolated vertices make this bound strict. _Correctness._ ($arrow.r.double$) If $M$ is a matching, then for any $e_1, e_2 in M$, the edges share no endpoint, so $S_(e_1) inter S_(e_2) = emptyset$ — the sets are mutually disjoint, forming a valid packing. ($arrow.l.double$) If ${S_e : e in P}$ is a packing, then for any $e_1, e_2 in P$, $S_(e_1) inter S_(e_2) = emptyset$, meaning the edges share no vertex, so $P$ is a valid matching. Weight sums are identical, so optimality is preserved. @@ -19399,6 +19399,8 @@ The following table shows concrete target-variable counts for example instances, ][ _Construction._ Given $G = (V, E, w)$ with $n = |V|$. Set $n' = n + (n mod 2)$, $N = 2n'$, $w_"max" = 1 + max_(e in E) w(e)$. Build $K_N$ with $tilde(w)(i,j) = w_"max" - w(i,j)$ for edges in $E$, else $w_"max"$. Designate $s = n'$, $t = n' + 1$, bound $b = n'$. + _Parameter bounds._ The target has exactly $2n'$ vertices and $n'(2n'-1)$ edges. Since $n' lt.eq n+1$, these are at most $2n+2$ and $(n+1)(2n+1)$ respectively. Both declared bounds are attained for odd $n$; for even $n$, the actual counts are $2n$ and $n(2n-1)$. + _Correctness._ ($arrow.r.double$) A max-cut extended to a balanced bisection gives a feasible target instance. ($arrow.l.double$) Minimizing $tilde(w)$-cut cost is equivalent to maximizing original weight crossing the cut, since $tilde(w) = w_"max" - w$. _Solution extraction._ Return the first $n$ entries of the target assignment. diff --git a/src/rules/maxcut_minimumcutintoboundedsets.rs b/src/rules/maxcut_minimumcutintoboundedsets.rs index 795c46ffb..5c47b8b0f 100644 --- a/src/rules/maxcut_minimumcutintoboundedsets.rs +++ b/src/rules/maxcut_minimumcutintoboundedsets.rs @@ -40,8 +40,10 @@ impl ReductionResult for ReductionMaxCutToMinCutBounded { } } +// With p = n + (n mod 2), the target is K_(2p): 2p vertices and p(2p-1) edges. +// Since p <= n+1, these bounds are tight for odd n and overestimate for even n. #[reduction( - transform = exact { + transform = upper_bound { num_vertices = "2 * num_vertices + 2", num_edges = "(num_vertices + 1) * (2 * num_vertices + 1)", } diff --git a/src/rules/maximummatching_maximumsetpacking.rs b/src/rules/maximummatching_maximumsetpacking.rs index 1d98ef7a1..b39ba5dcf 100644 --- a/src/rules/maximummatching_maximumsetpacking.rs +++ b/src/rules/maximummatching_maximumsetpacking.rs @@ -40,10 +40,12 @@ where } } +// Each edge contributes exactly one set. The target universe ends at the largest +// endpoint label (or is empty), so trailing isolated vertices make its size < n. #[reduction( - transform = exact { - num_sets = "num_edges", - universe_size = "num_vertices", + transform = { + exact { num_sets = "num_edges" }, + upper_bound { universe_size = "num_vertices" }, } )] impl ReduceTo> for MaximumMatching { diff --git a/src/unit_tests/rules/maxcut_minimumcutintoboundedsets.rs b/src/unit_tests/rules/maxcut_minimumcutintoboundedsets.rs index 51c788b17..43c5a4376 100644 --- a/src/unit_tests/rules/maxcut_minimumcutintoboundedsets.rs +++ b/src/unit_tests/rules/maxcut_minimumcutintoboundedsets.rs @@ -1,6 +1,8 @@ use super::*; use crate::models::graph::{MaxCut, MinimumCutIntoBoundedSets}; -use crate::rules::test_helpers::assert_optimization_round_trip_from_optimization_target; +use crate::rules::test_helpers::{ + assert_optimization_round_trip_from_optimization_target, assert_parameter_predictions, +}; use crate::rules::traits::ReduceTo; use crate::topology::SimpleGraph; @@ -74,6 +76,7 @@ fn test_maxcut_to_minimumcutintoboundedsets_target_structure() { ); let reduction = ReduceTo::>::reduce_to(&source) .expect("reduction should succeed"); + assert_parameter_predictions(&source, &reduction); let target = reduction.target_problem(); // n=3, n'=3+1=4, N=8 @@ -96,6 +99,7 @@ fn test_maxcut_to_minimumcutintoboundedsets_even_vertices() { ); let reduction = ReduceTo::>::reduce_to(&source) .expect("reduction should succeed"); + assert_parameter_predictions(&source, &reduction); let target = reduction.target_problem(); // n=4, n'=4, N=8 diff --git a/src/unit_tests/rules/maximummatching_maximumsetpacking.rs b/src/unit_tests/rules/maximummatching_maximumsetpacking.rs index e2a96c257..f839f0387 100644 --- a/src/unit_tests/rules/maximummatching_maximumsetpacking.rs +++ b/src/unit_tests/rules/maximummatching_maximumsetpacking.rs @@ -1,5 +1,7 @@ use super::*; -use crate::rules::test_helpers::assert_optimization_round_trip_from_optimization_target; +use crate::rules::test_helpers::{ + assert_optimization_round_trip_from_optimization_target, assert_parameter_predictions, +}; use crate::solvers::BruteForce; use crate::topology::SimpleGraph; use crate::traits::Problem; @@ -75,9 +77,11 @@ fn test_matching_to_setpacking_empty() { let matching = MaximumMatching::<_, i64>::unit_weights(SimpleGraph::new(3, vec![])); let reduction = ReduceTo::>::reduce_to(&matching).expect("reduction should succeed"); + assert_parameter_predictions(&matching, &reduction); let sp = reduction.target_problem(); assert_eq!(sp.num_sets(), 0); + assert_eq!(sp.universe_size(), 0); } #[test] @@ -85,6 +89,7 @@ fn test_matching_to_setpacking_single_edge() { let matching = MaximumMatching::<_, i64>::unit_weights(SimpleGraph::new(2, vec![(0, 1)])); let reduction = ReduceTo::>::reduce_to(&matching).expect("reduction should succeed"); + assert_parameter_predictions(&matching, &reduction); let sp = reduction.target_problem(); assert_eq!(sp.num_sets(), 1); @@ -119,10 +124,12 @@ fn test_reduction_structure() { MaximumMatching::<_, i64>::unit_weights(SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3)])); let reduction = ReduceTo::>::reduce_to(&matching).expect("reduction should succeed"); + assert_parameter_predictions(&matching, &reduction); let sp = reduction.target_problem(); // SP should have same number of sets as edges in matching assert_eq!(sp.num_sets(), 3); + assert_eq!(sp.universe_size(), 4); } #[test] From de787aa70230c0a02159bc7aa5c3d1d3436d9b9c Mon Sep 17 00:00:00 2001 From: Xiwei Pan <90967972+isPANN@users.noreply.github.com> Date: Tue, 29 Sep 2026 07:43:07 -0700 Subject: [PATCH 11/22] Remove invalid reduction catalog edges --- docs/paper/reductions.typ | 100 +----------- ...nimumvertexcover_minimummaximalmatching.rs | 63 -------- src/rules/mod.rs | 4 - src/rules/subsetsum_integerknapsack.rs | 90 ----------- src/unit_tests/example_db.rs | 45 ++---- src/unit_tests/reduction_graph.rs | 56 +++---- ...nimumvertexcover_minimummaximalmatching.rs | 88 ---------- .../rules/subsetsum_integerknapsack.rs | 150 ------------------ 8 files changed, 37 insertions(+), 559 deletions(-) delete mode 100644 src/rules/minimumvertexcover_minimummaximalmatching.rs delete mode 100644 src/rules/subsetsum_integerknapsack.rs delete mode 100644 src/unit_tests/rules/minimumvertexcover_minimummaximalmatching.rs delete mode 100644 src/unit_tests/rules/subsetsum_integerknapsack.rs diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index 742ce9eb5..54fd70378 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -725,7 +725,7 @@ #block(width: 85%, inset: (x: 1em, y: 0.8em))[ #set text(size: 9.5pt) #set par(justify: true) - *Abstract.* We present formal definitions for computational problems and polynomial-time reductions implemented in the `problem-reductions` library. For each reduction, we state a theorem with a constructive proof; when a reduction is proof-only rather than solver-executable, that restriction is stated explicitly in the rule text. + *Abstract.* We present formal definitions for computational problems and polynomial-time reductions implemented in the `problem-reductions` library. For each reduction, we state a theorem with a constructive proof. ] ] @@ -11635,49 +11635,6 @@ the displayed rule, extracted from the corresponding `pred path` entry. _Value and solution extraction._ The target is Decision Minimum Sum Multicenter with bound $B$ (or $-1$ for a negative source bound). Its predicate enforces the cost bound. Decode a YES witness by removing the auxiliary coordinates; completed YES and NO answers pass through unchanged. ] -#let mvc_mmm = load-example("MinimumVertexCover", "MinimumMaximalMatching") -#let mvc_mmm_sol = mvc_mmm.solutions.at(0) -#reduction-rule("MinimumVertexCover", "MinimumMaximalMatching", - example: true, - example-caption: [Cycle $C_5$: the forward implication is exact, but the backward gap is strict], - extra: [ - #{ - let target-edges = mvc_mmm.target.instance.graph.edges - let source-cover = mvc_mmm_sol.source_config.enumerate().filter(((i, x)) => x).map(((i, x)) => i) - let matching = mvc_mmm_sol.target_config.enumerate().filter(((i, x)) => x).map(((i, x)) => target-edges.at(i)) - let fmt-edge(e) = "(" + str(e.at(0)) + ", " + str(e.at(1)) + ")" - [ - #pred-commands( - "pred create MinimumVertexCover --graph 0-1,1-2,2-3,3-4,4-0 --weights 1,1,1,1,1 -o mvc.json", - "pred solve mvc.json", - "pred create MinimumMaximalMatching --graph 0-1,1-2,2-3,3-4,4-0 -o mmm.json", - "pred solve mmm.json", - ) - - *Step 1 -- Shared instance.* Both problems use the same 5-cycle, so $n = #graph-num-vertices(mvc_mmm.source.instance)$ and $|E| = #graph-num-edges(mvc_mmm.source.instance)$. - - *Step 2 -- Source optimum.* The canonical minimum vertex cover is $C = {#fmt-values(source-cover)}$, so $"mvc"(C_5) = #source-cover.len() = 3$. - - *Step 3 -- Target optimum.* The canonical minimum maximal matching is $M = {#matching.map(fmt-edge).join(", ")}$, so $"mmm"(C_5) = #matching.len() = 2$. - - *Step 4 -- Backward gap.* The endpoint set of $M$ is ${0, 1, 2, 3}$, a valid vertex cover of size $4$. Pruning can recover an optimal cover of size $3$, but not one of size $2$, so the same-bound backward implication fails. - - *Runtime note:* This catalog edge is proof-only. The CLI can solve the two instances separately, but runtime reduction search does not traverse this edge because there is no exact witness or aggregate extractor. - ] - } - ], -)[ - This size-preserving identity map records the forward implication used in the classical NP-hardness proof for Minimum Maximal Matching (equivalently, Minimum Edge Dominating Set) on bounded-degree graphs: every unit-weight vertex cover of $G$ can be greedily converted into a maximal matching of size at most the cover size. The converse loses a factor of two in general, so the edge is documented but intentionally disabled for runtime reduction search. -][ - _Construction._ Given a unit-weight Minimum Vertex Cover instance $(G = (V, E), K)$, build the Minimum Maximal Matching instance on the same graph $G$. The target uses one binary variable per source edge, so the graph structure and parameters are unchanged. - - _Correctness._ ($arrow.r.double$) Let $C subset.eq V$ be a vertex cover with $|C| lt.eq K$. Start with $M = emptyset$ and process the vertices of $C$ in arbitrary order. Whenever $v in C$ is unmatched, choose any edge $\{v, u\} in E$ whose other endpoint $u$ is also unmatched, add that edge to $M$, and mark both endpoints matched. Because only unmatched endpoints are paired, $M$ is a matching. If some edge $\{x, y\} in E$ were disjoint from every edge of $M$ at the end, then both $x$ and $y$ would still be unmatched. Since $C$ covers every edge, at least one endpoint, say $x$, lies in $C$, and when the algorithm processed $x$ it could have added $\{x, y\}$, a contradiction. Hence $M$ is maximal and $|M| lt.eq |C| lt.eq K$. - - ($arrow.l.double$) Let $M$ be any maximal matching. The set of all endpoints of edges in $M$ is a vertex cover, so $"mvc"(G) lt.eq 2 dot |M|$. This yields the standard bound $"mmm"(G) lt.eq "mvc"(G) lt.eq 2 dot "mmm"(G)$, but it does not recover an exact same-bound inverse. On $C_5$, the target optimum is $2$ while the source optimum is $3$. - - _Solution extraction._ No runtime extractor is registered. The endpoint map always returns a valid vertex cover and greedy pruning can shrink it, but neither approach guarantees an optimal cover from an optimal maximal matching witness, and the target optimum value does not determine the source optimum value exactly. -] - #let mvc_lcs = load-example("MinimumVertexCover", "LongestCommonSubsequence") #let mvc_lcs_sol = mvc_lcs.solutions.at(0) #reduction-rule("MinimumVertexCover", "LongestCommonSubsequence", @@ -18311,61 +18268,6 @@ The following table shows concrete target-variable counts for example instances, _Solution extraction._ Given a Partition solution $c in {0,1}^m$: if $d = 0$, return $c[0..n]$. If $Sigma > 2T$, the $S$-elements on the same side as the padding form the subset summing to $T$. If $Sigma < 2T$, the $S$-elements on the opposite side from the padding form the subset summing to $T$. ] -#let ss_ik = load-example("SubsetSum", "IntegerKnapsack") -#let ss_ik_sol = ss_ik.solutions.at(0) -#reduction-rule("SubsetSum", "IntegerKnapsack", - example: true, - example-caption: [#subsetsum-num-elements(ss_ik.source.instance) elements, target $B = #ss_ik.source.instance.target$: exact forward witness, but multiplicities create a backward gap], - extra: [ - #{ - let sizes = ss_ik.source.instance.sizes.map(s => int(s)) - let B = int(ss_ik.source.instance.target) - let chosen = ss_ik_sol.source_config.enumerate().filter(((i, x)) => x).map(((i, x)) => i) - let chosen_sum = chosen.map(i => sizes.at(i)).sum() - [ - #pred-commands( - "pred create --example " + rule-spec(ss_ik) + " -o subsetsum.json", - "pred solve subsetsum.json", - "pred create --example " + problem-spec(ss_ik.target) + " -o integer-knapsack.json", - "pred solve integer-knapsack.json", - ) - - *Step 1 -- Source instance.* The canonical Subset Sum instance has sizes $(#fmt-values(sizes))$ and target $B = #B$. The stored witness $(#fmt-values(ss_ik_sol.source_config))$ selects elements ${#fmt-values(chosen)}$, whose values sum to $#chosen_sum = B$ #sym.checkmark. - - *Step 2 -- Build the target.* Copy each source size into both the size and value lists. The Integer Knapsack instance therefore has sizes $(#fmt-values(ss_ik.target.instance.sizes))$, values $(#fmt-values(ss_ik.target.instance.values))$, and the same capacity $B = #ss_ik.target.instance.capacity$. - - *Step 3 -- Verify the forward witness.* Reuse the same 0-1 vector as multiplicities: $(#fmt-values(ss_ik_sol.target_config))$. Its total size is $#chosen_sum <= #ss_ik.target.instance.capacity$, and because size equals value coordinate-wise, its total value is also $#chosen_sum = B$ #sym.checkmark. - - *Step 4 -- Backward gap.* For the source instance $A = {3}$ with target $B = 6$, Subset Sum is NO, but Integer Knapsack can set multiplicity $c_0 = 2$ and achieve total size/value $6$. This is why the catalog records the edge for proof topology only and disables all runtime reduction modes. - ] - } - ], -)[ - This size-preserving embedding from Garey and Johnson's Integer Knapsack entry @garey1979[MP10] copies each Subset Sum number into both the size and value of a knapsack item and sets the capacity to the target sum. Any exact subset-sum witness becomes a feasible Integer Knapsack witness of value $B$. The converse fails for the implemented unbounded model because target witnesses may use multiplicities greater than $1$, so the edge is documented but intentionally proof-only. -][ - _Construction._ Given Subset Sum instance $(S = {a_1, dots, a_n}, B)$, create $n$ Integer Knapsack items. For each $i$, set the item size and value to the same number: - $ - s_i = a_i, quad v_i = a_i. - $ - 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 - $ - and, because $v_i = s_i$, also - $ - sum_i c_i v_i = B. - $ - So every YES instance of Subset Sum maps to an Integer Knapsack witness achieving value $B$. - - ($arrow.l.double$) The backward implication is false for the implemented target model. Integer Knapsack allows arbitrary non-negative multiplicities, while Subset Sum is 0-1. For example, with $S = {3}$ and $B = 6$, the target witness $c_0 = 2$ is feasible and attains value $6$, but the source has no subset summing to $6$. Hence neither exact witness recovery nor exact optimum-value recovery is available from the target side. - - _Solution extraction._ No runtime extractor is registered. The forward map is enough for the NP-hardness proof, but unbounded multiplicities prevent an exact inverse map back to Subset Sum. -] - // 2. Satisfiability → NonTautology (#868) #let sat_nt = load-example("Satisfiability", "NonTautology") #let sat_nt_sol = sat_nt.solutions.at(0) diff --git a/src/rules/minimumvertexcover_minimummaximalmatching.rs b/src/rules/minimumvertexcover_minimummaximalmatching.rs deleted file mode 100644 index 5568dec44..000000000 --- a/src/rules/minimumvertexcover_minimummaximalmatching.rs +++ /dev/null @@ -1,63 +0,0 @@ -//! Forward-only reduction from MinimumVertexCover (unit-weight) to -//! MinimumMaximalMatching. -//! -//! The construction is the identity map on the underlying graph. This edge is -//! registered for topology and documentation purposes only: it intentionally has -//! no witness, aggregate, or Turing execution capability because an optimal -//! maximal matching does not determine an optimal vertex cover in general -//! (for example, on `C5`, `mmm(G) = 2` but `mvc(G) = 3`). - -use crate::models::graph::{MinimumMaximalMatching, MinimumVertexCover}; -use crate::rules::registry::ReductionParameterDeclarations; -use crate::rules::ReductionEntry; -use crate::topology::SimpleGraph; -use crate::traits::Problem; -use crate::types::One; - -inventory::submit! { - ReductionEntry { - source_name: MinimumVertexCover::::NAME, - target_name: MinimumMaximalMatching::::NAME, - 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")), - ], - unavailable: vec![], - }, - module_path: module_path!(), - reduce_fn: None, - reduce_aggregate_fn: None, - aggregate_view_fn: None, - turing: false, - } -} - -#[cfg(feature = "example-db")] -pub(crate) fn canonical_rule_example_specs() -> Vec { - use crate::example_db::specs::assemble_rule_example; - use crate::export::SolutionPair; - - vec![crate::example_db::specs::RuleExampleSpec { - id: "minimumvertexcover_to_minimummaximalmatching", - build: || { - let edges = vec![(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)]; - let source = MinimumVertexCover::new(SimpleGraph::new(5, edges.clone()), vec![One; 5]); - let target = MinimumMaximalMatching::new(SimpleGraph::new(5, edges)); - assemble_rule_example( - &source, - &target, - vec![SolutionPair { - source_config: serde_json::json!(vec![true, true, false, true, false]), - target_config: serde_json::json!(vec![true, false, true, false, false]), - }], - ) - }, - }] -} - -#[cfg(test)] -#[path = "../unit_tests/rules/minimumvertexcover_minimummaximalmatching.rs"] -mod tests; diff --git a/src/rules/mod.rs b/src/rules/mod.rs index 2251492c5..e6dd286ee 100644 --- a/src/rules/mod.rs +++ b/src/rules/mod.rs @@ -103,7 +103,6 @@ pub(crate) mod minimumvertexcover_maximumindependentset; pub(crate) mod minimumvertexcover_minimumfeedbackarcset; pub(crate) mod minimumvertexcover_minimumfeedbackvertexset; pub(crate) mod minimumvertexcover_minimumhittingset; -pub(crate) mod minimumvertexcover_minimummaximalmatching; pub(crate) mod minimumvertexcover_minimumsetcovering; pub(crate) mod minimumvertexcover_minimumweightandorgraph; pub(crate) mod naesatisfiability_maxcut; @@ -144,7 +143,6 @@ pub(crate) mod spinglass_maxcut; pub(crate) mod spinglass_qubo; pub(crate) mod subsetsum_closestvectorproblem; pub(crate) mod subsetsum_integerexpressionmembership; -pub(crate) mod subsetsum_integerknapsack; pub(crate) mod subsetsum_partition; #[cfg(test)] pub(crate) mod test_helpers; @@ -414,7 +412,6 @@ pub(crate) fn canonical_rule_example_specs() -> Vec Vec i64 { - value - .to_i64() - .unwrap_or_else(|| panic!("SubsetSum -> IntegerKnapsack requires {what} to fit in i64")) -} - -inventory::submit! { - ReductionEntry { - source_name: SubsetSum::NAME, - target_name: IntegerKnapsack::NAME, - source_variant_fn: ::variant, - target_variant_fn: ::variant, - parameter_declarations_fn: || ReductionParameterDeclarations { - 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: "raw capacity requires numeric magnitude values; downstream predictions use capacity_bits", - }], - }, - module_path: module_path!(), - reduce_fn: None, - reduce_aggregate_fn: None, - aggregate_view_fn: None, - turing: false, - } -} - -#[cfg(feature = "example-db")] -pub(crate) fn canonical_rule_example_specs() -> Vec { - use crate::example_db::specs::assemble_rule_example; - use crate::export::SolutionPair; - - vec![crate::example_db::specs::RuleExampleSpec { - id: "subsetsum_to_integerknapsack", - build: || { - let source = SubsetSum::new(vec![3u32, 7, 1, 8, 5], 16u32); - let target = IntegerKnapsack::new( - source - .sizes() - .iter() - .map(|size| biguint_to_i64(size, "sizes")) - .collect(), - source - .sizes() - .iter() - .map(|value| biguint_to_i64(value, "sizes")) - .collect(), - biguint_to_i64(source.target(), "target"), - ) - .unwrap(); - - assemble_rule_example( - &source, - &target, - vec![SolutionPair { - source_config: serde_json::json!(vec![true, false, false, true, true]), - target_config: serde_json::json!(vec![1, 0, 0, 1, 1]), - }], - ) - }, - }] -} - -#[cfg(test)] -#[path = "../unit_tests/rules/subsetsum_integerknapsack.rs"] -mod tests; diff --git a/src/unit_tests/example_db.rs b/src/unit_tests/example_db.rs index a7fd51c9a..3bc887a26 100644 --- a/src/unit_tests/example_db.rs +++ b/src/unit_tests/example_db.rs @@ -662,31 +662,10 @@ fn rule_specs_solution_pairs_are_consistent() { ) .iter() .any(|path| path.len() == 1); - if !has_aggregate_path { - assert!( - graph.has_direct_reduction_by_name( - &example.source.problem, - &example.target.problem - ), - "No direct witness, aggregate, or proof-only reduction for {label}" - ); - assert!( - !graph.has_direct_reduction_by_name_mode( - &example.source.problem, - &example.target.problem, - crate::rules::ReductionMode::Witness, - ), - "Proof-only edge unexpectedly exposed witness mode for {label}" - ); - assert!( - !graph.has_direct_reduction_by_name_mode( - &example.source.problem, - &example.target.problem, - crate::rules::ReductionMode::Aggregate, - ), - "Proof-only edge unexpectedly exposed aggregate mode for {label}" - ); - } + assert!( + has_aggregate_path, + "No direct witness or aggregate reduction for {label}" + ); } // Only do witness round-trip when a witness path exists @@ -1132,7 +1111,7 @@ fn test_find_rule_example_minimumvertexcover_to_minimumhittingset() { } #[test] -fn test_find_rule_example_minimumvertexcover_to_minimummaximalmatching() { +fn invalid_embeddings_have_no_canonical_rule_examples() { let source = ProblemRef { name: "MinimumVertexCover".to_string(), variant: BTreeMap::from([ @@ -1144,9 +1123,17 @@ fn test_find_rule_example_minimumvertexcover_to_minimummaximalmatching() { name: "MinimumMaximalMatching".to_string(), variant: BTreeMap::from([("graph".to_string(), "SimpleGraph".to_string())]), }; - let example = find_rule_example(&source, &target).unwrap(); - assert_eq!(example.source.problem, "MinimumVertexCover"); - assert_eq!(example.target.problem, "MinimumMaximalMatching"); + assert!(find_rule_example(&source, &target).is_err()); + + let source = ProblemRef { + name: "SubsetSum".to_string(), + variant: BTreeMap::new(), + }; + let target = ProblemRef { + name: "IntegerKnapsack".to_string(), + variant: BTreeMap::new(), + }; + assert!(find_rule_example(&source, &target).is_err()); } #[test] diff --git a/src/unit_tests/reduction_graph.rs b/src/unit_tests/reduction_graph.rs index f3bbca2af..5a65141ad 100644 --- a/src/unit_tests/reduction_graph.rs +++ b/src/unit_tests/reduction_graph.rs @@ -472,25 +472,15 @@ fn test_json_export() { } #[test] -fn test_subsetsum_to_integerknapsack_is_proof_only() { - let graph = ReductionGraph::new(); +fn subset_sum_embedding_does_not_define_a_catalog_reduction() { + let source = SubsetSum::new(vec![3u32], 6u32); + let target = IntegerKnapsack::new(vec![3], vec![3], 6).unwrap(); + let solver = BruteForce::new(); - assert!(graph.has_direct_reduction_by_name("SubsetSum", "IntegerKnapsack")); - assert!(!graph.has_direct_reduction_by_name_mode( - "SubsetSum", - "IntegerKnapsack", - ReductionMode::Witness, - )); - assert!(!graph.has_direct_reduction_by_name_mode( - "SubsetSum", - "IntegerKnapsack", - ReductionMode::Aggregate, - )); - assert!(!graph.has_direct_reduction_by_name_mode( - "SubsetSum", - "IntegerKnapsack", - ReductionMode::Turing, - )); + assert!(solver.solve(&source).unwrap().is_none()); + let witness = solver.solve(&target).unwrap().unwrap(); + assert_eq!(target.evaluate(&witness).unwrap(), Max(Some(6))); + assert!(!ReductionGraph::new().has_direct_reduction_by_name("SubsetSum", "IntegerKnapsack")); } #[test] @@ -956,25 +946,19 @@ fn test_has_direct_reduction_by_name_mode() { } #[test] -fn test_minimumvertexcover_to_minimummaximalmatching_is_proof_only_direct_edge() { - let graph = ReductionGraph::new(); +fn vertex_cover_identity_does_not_define_a_matching_reduction() { + use crate::models::graph::MinimumMaximalMatching; + let graph = SimpleGraph::cycle(5); + let source = MinimumVertexCover::new(graph.clone(), vec![One; 5]); + let target = MinimumMaximalMatching::new(graph); + let solver = BruteForce::new(); - assert!(graph.has_direct_reduction_by_name("MinimumVertexCover", "MinimumMaximalMatching",)); - assert!(!graph.has_direct_reduction_by_name_mode( - "MinimumVertexCover", - "MinimumMaximalMatching", - ReductionMode::Witness, - )); - assert!(!graph.has_direct_reduction_by_name_mode( - "MinimumVertexCover", - "MinimumMaximalMatching", - ReductionMode::Aggregate, - )); - assert!(!graph.has_direct_reduction_by_name_mode( - "MinimumVertexCover", - "MinimumMaximalMatching", - ReductionMode::Turing, - )); + let source_witness = solver.solve(&source).unwrap().unwrap(); + let target_witness = solver.solve(&target).unwrap().unwrap(); + assert_eq!(source.evaluate(&source_witness).unwrap(), Min(Some(3))); + assert_eq!(target.evaluate(&target_witness).unwrap(), Min(Some(2))); + assert!(!ReductionGraph::new() + .has_direct_reduction_by_name("MinimumVertexCover", "MinimumMaximalMatching",)); } #[test] diff --git a/src/unit_tests/rules/minimumvertexcover_minimummaximalmatching.rs b/src/unit_tests/rules/minimumvertexcover_minimummaximalmatching.rs deleted file mode 100644 index 8677416d5..000000000 --- a/src/unit_tests/rules/minimumvertexcover_minimummaximalmatching.rs +++ /dev/null @@ -1,88 +0,0 @@ -use crate::models::graph::{MinimumMaximalMatching, MinimumVertexCover}; -use crate::rules::{ReductionGraph, ReductionMode}; -use crate::solvers::BruteForce; -use crate::topology::SimpleGraph; -use crate::traits::Problem; -use crate::types::{Min, One}; - -fn graph_from_mask(n: usize, mask: usize) -> SimpleGraph { - let mut edges = Vec::new(); - let mut bit = 0usize; - for u in 0..n { - for v in (u + 1)..n { - if (mask >> bit) & 1 == 1 { - edges.push((u, v)); - } - bit += 1; - } - } - SimpleGraph::new(n, edges) -} - -#[test] -fn test_minimumvertexcover_to_minimummaximalmatching_c5_gap() { - let graph = SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)]); - let mvc = MinimumVertexCover::new(graph.clone(), vec![One; 5]); - let mmm = MinimumMaximalMatching::new(graph); - let solver = BruteForce::new(); - - assert_eq!( - mvc.evaluate(&solver.solve(&mvc).unwrap().unwrap()).unwrap(), - Min(Some(3)) - ); - assert_eq!( - mmm.evaluate(&solver.solve(&mmm).unwrap().unwrap()).unwrap(), - Min(Some(2)) - ); -} - -#[test] -fn test_minimumvertexcover_to_minimummaximalmatching_forward_bound_on_small_graphs() { - let solver = BruteForce::new(); - - for n in 0usize..=5 { - let num_possible_edges = n * (n.saturating_sub(1)) / 2; - for mask in 0usize..(1usize << num_possible_edges) { - let graph = graph_from_mask(n, mask); - let mvc = MinimumVertexCover::new(graph.clone(), vec![One; n]); - let mmm = MinimumMaximalMatching::new(graph); - let mvc_value_solution = solver.solve(&mvc).unwrap().unwrap(); - let mvc_value = mvc.evaluate(&mvc_value_solution).unwrap(); - let mmm_value_solution = solver.solve(&mmm).unwrap().unwrap(); - let mmm_value = mmm.evaluate(&mmm_value_solution).unwrap(); - - let Min(Some(mvc_size)) = mvc_value else { - panic!("MinimumVertexCover should always have an optimal solution"); - }; - let Min(Some(mmm_size)) = mmm_value else { - panic!("MinimumMaximalMatching should always have an optimal solution"); - }; - assert!( - mmm_size <= mvc_size, - "expected mmm(G) <= mvc(G) for n={n}, mask={mask:#b}, got {mmm_size} > {mvc_size}", - ); - } - } -} - -#[test] -fn test_minimumvertexcover_to_minimummaximalmatching_has_no_runtime_modes() { - let graph = ReductionGraph::new(); - - assert!(graph.has_direct_reduction_by_name("MinimumVertexCover", "MinimumMaximalMatching",)); - assert!(!graph.has_direct_reduction_by_name_mode( - "MinimumVertexCover", - "MinimumMaximalMatching", - ReductionMode::Witness, - )); - assert!(!graph.has_direct_reduction_by_name_mode( - "MinimumVertexCover", - "MinimumMaximalMatching", - ReductionMode::Aggregate, - )); - assert!(!graph.has_direct_reduction_by_name_mode( - "MinimumVertexCover", - "MinimumMaximalMatching", - ReductionMode::Turing, - )); -} diff --git a/src/unit_tests/rules/subsetsum_integerknapsack.rs b/src/unit_tests/rules/subsetsum_integerknapsack.rs deleted file mode 100644 index 36f190079..000000000 --- a/src/unit_tests/rules/subsetsum_integerknapsack.rs +++ /dev/null @@ -1,150 +0,0 @@ -#[cfg(feature = "example-db")] -use super::canonical_rule_example_specs; -use crate::models::misc::SubsetSum; -use crate::models::set::IntegerKnapsack; -use crate::solvers::BruteForce; -use crate::traits::Problem; -use crate::types::Max; -use num_traits::ToPrimitive; - -fn subset_sum_embedding(source: &SubsetSum) -> IntegerKnapsack { - IntegerKnapsack::new( - source - .sizes() - .iter() - .map(|size| { - size.to_i64() - .expect("test fixture sizes should fit in i64 for IntegerKnapsack") - }) - .collect(), - source - .sizes() - .iter() - .map(|value| { - value - .to_i64() - .expect("test fixture values should fit in i64 for IntegerKnapsack") - }) - .collect(), - source - .target() - .to_i64() - .expect("test fixture target should fit in i64 for 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); - let target = subset_sum_embedding(&source); - let source_witness = vec![true, false, false, true, true]; - - assert!(source.evaluate(&source_witness).unwrap().is_valid()); - let target_witness = source_witness.iter().copied().map(usize::from).collect(); - assert_eq!(target.evaluate(&target_witness).unwrap(), Max(Some(16))); -} - -#[test] -fn test_subsetsum_to_integerknapsack_counterexample_demonstrates_gap() { - let source = SubsetSum::new(vec![3u32], 6u32); - let target = subset_sum_embedding(&source); - let solver = BruteForce::new(); - - assert!(solver.solve(&source).unwrap().is_none()); - assert_eq!( - target - .evaluate(&solver.solve(&target).unwrap().unwrap()) - .unwrap(), - Max(Some(6)) - ); -} - -#[cfg(feature = "example-db")] -#[test] -fn test_subsetsum_to_integerknapsack_canonical_example_spec() { - let example = (canonical_rule_example_specs() - .into_iter() - .find(|spec| spec.id == "subsetsum_to_integerknapsack") - .expect("missing canonical SubsetSum -> IntegerKnapsack example spec") - .build)(); - - assert_eq!(example.source.problem, "SubsetSum"); - assert_eq!(example.target.problem, "IntegerKnapsack"); - assert_eq!( - example.target.instance["sizes"], - serde_json::json!([3, 7, 1, 8, 5]) - ); - assert_eq!( - example.target.instance["values"], - serde_json::json!([3, 7, 1, 8, 5]) - ); - assert_eq!(example.target.instance["capacity"], 16); - assert_eq!(example.solutions.len(), 1); - assert_eq!( - example.solutions[0].source_config, - serde_json::json!([true, false, false, true, true]) - ); - assert_eq!( - example.solutions[0].target_config, - serde_json::json!([1, 0, 0, 1, 1]) - ); - - let source: SubsetSum = serde_json::from_value(example.source.instance.clone()) - .expect("source example deserializes"); - let target: IntegerKnapsack = serde_json::from_value(example.target.instance.clone()) - .expect("target example deserializes"); - - let source_config: Vec = - serde_json::from_value(example.solutions[0].source_config.clone()).unwrap(); - let target_config: Vec = - serde_json::from_value(example.solutions[0].target_config.clone()).unwrap(); - assert!(source.evaluate(&source_config).unwrap().is_valid()); - assert_eq!(target.evaluate(&target_config).unwrap(), Max(Some(16))); -} From 086605a636f1a9e58d211fd2fb8b953a8f9801c2 Mon Sep 17 00:00:00 2001 From: Xiwei Pan <90967972+isPANN@users.noreply.github.com> Date: Wed, 30 Sep 2026 02:07:46 -0700 Subject: [PATCH 12/22] Add direct binary ILP pipelines for exact-one SAT and graph kernels --- docs/paper/reductions.typ | 48 ++++++++++ src/rules/kernel_ilp.rs | 86 +++++++++++++++++ src/rules/mod.rs | 4 + src/rules/oneinthreesatisfiability_ilp.rs | 94 +++++++++++++++++++ src/solvers/pipelines.rs | 10 ++ src/unit_tests/rules/kernel_ilp.rs | 48 ++++++++++ .../rules/oneinthreesatisfiability_ilp.rs | 51 ++++++++++ src/unit_tests/solvers/ilp/solver.rs | 56 +++++++++++ 8 files changed, 397 insertions(+) create mode 100644 src/rules/kernel_ilp.rs create mode 100644 src/rules/oneinthreesatisfiability_ilp.rs create mode 100644 src/unit_tests/rules/kernel_ilp.rs create mode 100644 src/unit_tests/rules/oneinthreesatisfiability_ilp.rs diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index 54fd70378..790b3e095 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -14306,6 +14306,54 @@ The following reductions to Integer Linear Programming are straightforward formu _Solution extraction._ $cal(C) = {T_j : x_j = 1}$. ] +#reduction-rule("OneInThreeSatisfiability", "ILP", example: true)[ + Encode exact-one clauses directly with binary variables and one equality per clause. + The elementary encoding below is derived directly from the literal semantics. +][ + _Construction._ Introduce one binary variable $x_i$ per source variable. + For each clause let $q$ count its negative literal occurrences. Add + $ sum_(i in P) x_i - sum_(i in N) x_i = 1-q, $ + where $P$ and $N$ retain repeated occurrences. Use a zero minimization objective. + + _Correctness._ The left side plus $q$ counts precisely the true literal + occurrences. ($arrow.r.double$) A satisfying source assignment therefore satisfies + every equality. ($arrow.l.double$) Every feasible target assignment makes exactly + one occurrence true in each clause, including clauses with repeated or opposite + literals. The empty formula is handled by the same construction. + + _Overhead._ For $n$ variables and $m$ clauses there are exactly $n$ variables + and $m$ rows. Normalization leaves at most $3m$ nonzeros. Coefficients have + magnitude at most three and right-hand sides at most two, requiring at most + two magnitude bits. + + _Solution extraction._ Validate target feasibility and read each binary variable + as its source truth value. Target feasibility maps to the source Boolean value. +] + +#reduction-rule("Kernel", "ILP", example: true)[ + Encode independence and outgoing absorption with one binary variable per vertex. + The elementary encoding below follows the two defining kernel conditions. +][ + _Construction._ For each arc occurrence $(u,v)$ add $x_u+x_v<=1$. + For each vertex $u$ add $x_u+sum_(v in N^+(u))x_v>=1$, using the set of + distinct outgoing neighbors. All variables are binary; the objective is zero. + + _Correctness._ ($arrow.r.double$) A kernel satisfies every independence row; + each unselected vertex has a selected outgoing neighbor, so absorption also + holds. ($arrow.l.double$) Independence rows forbid two selected arc endpoints, + while absorption rows ensure every unselected vertex reaches a selected one. + A self-loop gives $2x_u<=1$ and correctly forbids selecting its vertex. + Parallel arcs repeat independence rows without changing feasibility. + + _Overhead._ With $n$ vertices and $m$ arc occurrences there are exactly $n$ + variables and $m+n$ rows. The independence rows contribute at most $2m$ + nonzeros and absorption at most $n+m$. Normalized coefficients have magnitude + at most two, so two magnitude bits suffice. + + _Solution extraction._ Validate target feasibility and return the selected + vertices as a Boolean vector. Target feasibility maps to the source Boolean value. +] + #reduction-rule("NAESatisfiability", "ILP")[ Each clause must have at least one true and at least one false literal, encoded as a pair of linear inequalities per clause. ][ diff --git a/src/rules/kernel_ilp.rs b/src/rules/kernel_ilp.rs new file mode 100644 index 000000000..0caff8cf5 --- /dev/null +++ b/src/rules/kernel_ilp.rs @@ -0,0 +1,86 @@ +//! Binary independence and absorption constraints for a directed graph kernel. + +use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; +use crate::models::graph::Kernel; +use crate::reduction; +use crate::rules::traits::{ReduceTo, ReductionResult}; + +#[derive(Debug, Clone)] +pub struct ReductionKernelToILP { + target: ILP, +} + +impl ReductionResult for ReductionKernelToILP { + type Source = Kernel; + 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( + self.target_problem(), + solution, + |value| value.value.is_some(), + "target ILP assignment does not select a kernel", + )?; + Ok(solution.iter().map(|&value| value == 1).collect()) + } +} + +#[crate::aggregate_reduction(ilp_feasibility)] +impl crate::rules::AggregateReductionResult for ReductionKernelToILP {} + +#[reduction(transform = { + exact { + num_vars = "num_vertices", + num_constraints = "num_arcs + num_vertices", + }, + upper_bound { + num_nonzeros = "3 * num_arcs + num_vertices", + max_constraint_magnitude_bits = "2", + }, +})] +impl ReduceTo> for Kernel { + type Result = ReductionKernelToILP; + + fn reduce_to(&self) -> Result { + let mut constraints = Vec::new(); + for (u, v) in self.graph().arcs() { + constraints.push(LinearConstraint::le(vec![(u, 1), (v, 1)], 1)); + } + for u in 0..self.num_vertices() { + let mut successors = self.graph().successors(u); + successors.sort_unstable(); + successors.dedup(); + let mut terms = vec![(u, 1)]; + terms.extend(successors.into_iter().map(|v| (v, 1))); + constraints.push(LinearConstraint::ge(terms, 1)); + } + let target = ILP::new( + self.num_vertices(), + constraints, + vec![], + ObjectiveSense::Minimize, + ) + .map_err(Self::target_construction)?; + Ok(Self::Result { target }) + } +} + +#[cfg(feature = "example-db")] +pub(crate) fn canonical_rule_example_specs() -> Vec { + use crate::topology::DirectedGraph; + vec![crate::example_db::specs::RuleExampleSpec { + id: "kernel_to_ilp", + build: || { + let source = Kernel::new(DirectedGraph::new(3, vec![(0, 1), (1, 2)])); + crate::example_db::specs::rule_example_via_ilp::<_, bool>(source) + }, + }] +} + +#[cfg(test)] +#[path = "../unit_tests/rules/kernel_ilp.rs"] +mod tests; diff --git a/src/rules/mod.rs b/src/rules/mod.rs index e6dd286ee..7e544af85 100644 --- a/src/rules/mod.rs +++ b/src/rules/mod.rs @@ -197,6 +197,7 @@ pub(crate) mod integralflowhomologousarcs_ilp; pub(crate) mod integralflowwithmultipliers_ilp; pub(crate) mod isomorphicspanningtree_ilp; pub(crate) mod kclique_ilp; +pub(crate) mod kernel_ilp; pub(crate) mod knapsack_ilp; pub(crate) mod lengthboundeddisjointpaths_ilp; pub(crate) mod longestcircuit_ilp; @@ -242,6 +243,7 @@ pub(crate) mod multiplecopyfileallocation_ilp; pub(crate) mod multiprocessorscheduling_ilp; pub(crate) mod naesatisfiability_ilp; pub(crate) mod numericalmatchingwithtargetsums_ilp; +pub(crate) mod oneinthreesatisfiability_ilp; pub(crate) mod openshopscheduling_ilp; pub(crate) mod optimallineararrangement_ilp; pub(crate) mod optimumcommunicationspanningtree_ilp; @@ -562,6 +564,8 @@ pub(crate) fn canonical_rule_example_specs() -> Vec, +} + +impl ReductionResult for ReductionOneInThreeSatisfiabilityToILP { + type Source = OneInThreeSatisfiability; + 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( + self.target_problem(), + solution, + |value| value.value.is_some(), + "target ILP assignment does not satisfy the exact-one clauses", + )?; + Ok(solution.iter().map(|&value| value == 1).collect()) + } +} + +#[crate::aggregate_reduction(ilp_feasibility)] +impl crate::rules::AggregateReductionResult for ReductionOneInThreeSatisfiabilityToILP {} + +#[reduction(transform = { + exact { + num_vars = "num_vars", + num_constraints = "num_clauses", + }, + upper_bound { + num_nonzeros = "3 * num_clauses", + max_constraint_magnitude_bits = "2", + }, +})] +impl ReduceTo> for OneInThreeSatisfiability { + type Result = ReductionOneInThreeSatisfiabilityToILP; + + fn reduce_to(&self) -> Result { + let mut constraints = Vec::new(); + for clause in self.clauses() { + let mut terms = Vec::new(); + let mut rhs = 1; + for (variable, &literal) in clause.variables().into_iter().zip(&clause.literals) { + if literal > 0 { + terms.push((variable, 1)); + } else { + terms.push((variable, -1)); + rhs -= 1; + } + } + constraints.push(LinearConstraint::eq(terms, rhs)); + } + let target = ILP::new( + self.num_vars(), + constraints, + vec![], + ObjectiveSense::Minimize, + ) + .map_err(Self::target_construction)?; + Ok(Self::Result { target }) + } +} + +#[cfg(feature = "example-db")] +pub(crate) fn canonical_rule_example_specs() -> Vec { + use crate::models::formula::CNFClause; + vec![crate::example_db::specs::RuleExampleSpec { + id: "oneinthreesatisfiability_to_ilp", + build: || { + let source = OneInThreeSatisfiability::new( + 3, + vec![ + CNFClause::new(vec![1, 2, 3]), + CNFClause::new(vec![-1, -2, 3]), + ], + ); + crate::example_db::specs::rule_example_via_ilp::<_, bool>(source) + }, + }] +} + +#[cfg(test)] +#[path = "../unit_tests/rules/oneinthreesatisfiability_ilp.rs"] +mod tests; diff --git a/src/solvers/pipelines.rs b/src/solvers/pipelines.rs index 14355a834..6ab66ea38 100644 --- a/src/solvers/pipelines.rs +++ b/src/solvers/pipelines.rs @@ -51,6 +51,16 @@ register_ilp_pipeline! { ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } +register_ilp_pipeline! { + ("OneInThreeSatisfiability", []), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), +} + +register_ilp_pipeline! { + ("Kernel", []), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), +} + register_ilp_pipeline! { ("BalancedCompleteBipartiteSubgraph", []), ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), diff --git a/src/unit_tests/rules/kernel_ilp.rs b/src/unit_tests/rules/kernel_ilp.rs new file mode 100644 index 000000000..9cdd43149 --- /dev/null +++ b/src/unit_tests/rules/kernel_ilp.rs @@ -0,0 +1,48 @@ +use super::*; +use crate::rules::AggregateReductionResult; +use crate::topology::DirectedGraph; +use crate::traits::Problem; + +#[test] +fn test_kernel_to_ilp_preserves_selections_with_loops_and_parallel_arcs() { + for arc_mask in 0..512 { + let mut arcs = Vec::new(); + for u in 0..3 { + for v in 0..3 { + if arc_mask & (1 << (3 * u + v)) != 0 { + arcs.extend([(u, v), (u, v), (u, v)]); + } + } + } + let source = Kernel::new(DirectedGraph::new(3, arcs)); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + let target = ReductionResult::target_problem(&reduction); + assert_eq!(target.num_vars(), 3); + assert_eq!(target.num_constraints(), source.num_arcs() + 3); + assert!(target.num_nonzeros() <= 3 * source.num_arcs() + 3); + assert!(target.max_constraint_magnitude_bits() <= 2); + for mask in 0..8 { + let bits: Vec = (0..3).map(|i| mask & (1 << i) != 0).collect(); + let values = bits.iter().copied().map(i64::from).collect(); + let expected = source.evaluate(&bits).unwrap(); + let actual = target.evaluate(&values).unwrap(); + assert_eq!(actual.value.is_some(), expected.0); + assert_eq!(reduction.extract_value(actual).unwrap(), expected); + if expected.0 { + assert_eq!(reduction.extract_solution(&values).unwrap(), bits); + } else { + assert!(reduction.extract_solution(&values).is_err()); + } + } + } +} + +#[test] +fn test_kernel_to_ilp_closed_loop() { + let source = Kernel::new(DirectedGraph::new(3, vec![(0, 1), (1, 2)])); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + crate::rules::test_helpers::assert_bf_vs_ilp(&source, &reduction); + for malformed in [vec![1, 0], vec![1, 0, 1, 0], vec![2, 0, 1]] { + assert!(reduction.extract_solution(&malformed).is_err()); + } +} diff --git a/src/unit_tests/rules/oneinthreesatisfiability_ilp.rs b/src/unit_tests/rules/oneinthreesatisfiability_ilp.rs new file mode 100644 index 000000000..ac604c6e4 --- /dev/null +++ b/src/unit_tests/rules/oneinthreesatisfiability_ilp.rs @@ -0,0 +1,51 @@ +use super::*; +use crate::models::formula::CNFClause; +use crate::rules::AggregateReductionResult; +use crate::traits::Problem; + +#[test] +fn test_one_in_three_to_ilp_preserves_literal_occurrences() { + let literals = [-3, -2, -1, 1, 2, 3]; + for a in literals { + for b in literals { + for c in literals { + let source = OneInThreeSatisfiability::new(3, vec![CNFClause::new(vec![a, b, c])]); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + let target = ReductionResult::target_problem(&reduction); + assert_eq!(target.num_vars(), 3); + assert_eq!(target.num_constraints(), 1); + assert!(target.num_nonzeros() <= 3); + assert!(target.max_constraint_magnitude_bits() <= 2); + for mask in 0..8 { + let bits: Vec = (0..3).map(|i| mask & (1 << i) != 0).collect(); + let values = bits.iter().copied().map(i64::from).collect(); + let expected = source.evaluate(&bits).unwrap(); + let actual = target.evaluate(&values).unwrap(); + assert_eq!(actual.value.is_some(), expected.0); + assert_eq!(reduction.extract_value(actual).unwrap(), expected); + if expected.0 { + assert_eq!(reduction.extract_solution(&values).unwrap(), bits); + } else { + assert!(reduction.extract_solution(&values).is_err()); + } + } + } + } + } +} + +#[test] +fn test_oneinthreesatisfiability_to_ilp_closed_loop() { + let source = OneInThreeSatisfiability::new( + 3, + vec![ + CNFClause::new(vec![1, 2, 3]), + CNFClause::new(vec![-1, -2, 3]), + ], + ); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + crate::rules::test_helpers::assert_bf_vs_ilp(&source, &reduction); + for malformed in [vec![1, 0], vec![1, 0, 0, 0], vec![2, 0, 0]] { + assert!(reduction.extract_solution(&malformed).is_err()); + } +} diff --git a/src/unit_tests/solvers/ilp/solver.rs b/src/unit_tests/solvers/ilp/solver.rs index 0b3107ef9..4b4b6fe6b 100644 --- a/src/unit_tests/solvers/ilp/solver.rs +++ b/src/unit_tests/solvers/ilp/solver.rs @@ -252,6 +252,62 @@ fn test_registered_ilp_pipeline_success() { assert!(problem.evaluate(&solution).unwrap().is_valid()); } +#[test] +fn test_one_in_three_registered_ilp_feasibility() { + use crate::models::formula::{CNFClause, OneInThreeSatisfiability}; + for (problem, feasible) in [ + (OneInThreeSatisfiability::new(0, vec![]), true), + ( + OneInThreeSatisfiability::new( + 3, + vec![ + CNFClause::new(vec![1, 2, 3]), + CNFClause::new(vec![-1, -2, 3]), + ], + ), + true, + ), + ( + OneInThreeSatisfiability::new(3, vec![CNFClause::new(vec![1, 1, 1])]), + false, + ), + ] { + match ILPSolver::new().solve(&problem) { + Ok(solution) => { + assert!(feasible); + assert!(problem.evaluate(&solution).unwrap().0); + } + Err(error) => { + assert!(!feasible, "{error}"); + assert!(matches!(error, ILPSolveError::Infeasible)); + } + } + } +} + +#[test] +fn test_kernel_registered_ilp_feasibility() { + use crate::models::graph::Kernel; + use crate::topology::DirectedGraph; + for (graph, feasible) in [ + (DirectedGraph::new(0, vec![]), true), + (DirectedGraph::new(3, vec![(0, 1), (1, 2)]), true), + (DirectedGraph::new(3, vec![(0, 1), (1, 2), (2, 0)]), false), + ] { + let problem = Kernel::new(graph); + match ILPSolver::new().solve(&problem) { + Ok(solution) => { + assert!(feasible); + assert!(problem.evaluate(&solution).unwrap().0); + } + Err(error) => { + assert!(!feasible, "{error}"); + assert!(matches!(error, ILPSolveError::Infeasible)); + } + } + } +} + #[test] fn test_ilp_solve_dyn_bool() { let ilp = ILP::::new(1, vec![], vec![(0, 1.0)], ObjectiveSense::Maximize).unwrap(); From 0a59045c21398eef953ed4e3af6728ed008eda44 Mon Sep 17 00:00:00 2001 From: Xiwei Pan <90967972+isPANN@users.noreply.github.com> Date: Wed, 30 Sep 2026 03:11:14 -0700 Subject: [PATCH 13/22] Preserve scheduling semantics with compact ILP constructions --- docs/paper/reductions.typ | 137 +++--- problemreductions-cli/tests/cli_tests.rs | 35 +- src/models/misc/preemptive_scheduling.rs | 96 ++++ src/models/misc/production_planning.rs | 20 +- ...encing_with_release_times_and_deadlines.rs | 11 +- src/models/misc/timetable_design.rs | 181 ++----- src/rules/ilp_helpers.rs | 30 ++ .../ksatisfiability_preemptivescheduling.rs | 3 + src/rules/ksatisfiability_timetabledesign.rs | 3 + src/rules/mod.rs | 4 + src/rules/openshopscheduling_ilp.rs | 459 +++++++----------- src/rules/partition_productionplanning.rs | 5 +- src/rules/preemptivescheduling_ilp.rs | 203 ++++---- src/rules/productionplanning_ilp.rs | 116 +++++ ...ingtominimizeweightedcompletiontime_ilp.rs | 196 ++++---- ...uencingwithreleasetimesanddeadlines_ilp.rs | 238 ++++----- src/rules/threepartition_ilp.rs | 109 +++++ ..._sequencingwithreleasetimesanddeadlines.rs | 5 +- src/rules/timetabledesign_ilp.rs | 117 ++--- src/solvers/customized/solver.rs | 8 +- src/solvers/pipelines.rs | 13 +- .../models/misc/timetable_design.rs | 15 +- .../rules/openshopscheduling_ilp.rs | 69 ++- .../rules/preemptivescheduling_ilp.rs | 128 +++-- .../rules/productionplanning_ilp.rs | 71 +++ ...ingtominimizeweightedcompletiontime_ilp.rs | 64 ++- ...uencingwithreleasetimesanddeadlines_ilp.rs | 48 ++ src/unit_tests/rules/threepartition_ilp.rs | 34 ++ src/unit_tests/rules/timetabledesign_ilp.rs | 37 +- src/unit_tests/solvers/ilp/solver.rs | 45 ++ src/unit_tests/solvers/registry.rs | 4 +- 31 files changed, 1476 insertions(+), 1028 deletions(-) create mode 100644 src/rules/productionplanning_ilp.rs create mode 100644 src/rules/threepartition_ilp.rs create mode 100644 src/unit_tests/rules/productionplanning_ilp.rs create mode 100644 src/unit_tests/rules/threepartition_ilp.rs diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index 790b3e095..c100aa3b6 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -13844,37 +13844,17 @@ 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. + A strict linear order with exact prefix completion times preserves the permutation objective, including zero and signed processing times and weights. ][ - _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._ Let $p_j$ be task lengths, $w_j$ weights, $L=sum_j min(0,p_j)$ and $U=sum_j max(0,p_j)$. Use integer completion variables with bounds $p_j+L-min(0,p_j) <= C_j <= p_j+U-max(0,p_j)$, and binary $y_(i j)$ for $ij) p_i y_(j i) = sum_(i>=j) p_i. $ + Minimize $sum_j w_j C_j$. - _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$. + _Correctness._ ($arrow.r.double$) A feasible source permutation defines a transitive orientation satisfying every triangle and precedence row. Its prefix sums meet the stated bounds and completion equations, so the objective is unchanged. ($arrow.l.double$) Triangle inequalities exclude both orientations of every directed triangle. A tournament with no directed triangle is a strict total order. The completion equations give precisely the processing prefix sums in that order, even when times are zero or negative. Thus every feasible target decodes to a source permutation with identical objective. Cyclic precedences cannot be hidden by equal completion times. - _Bounds._ $l_j <= C_j <= M$ for every task $j$, and $y_(i j) in {0, 1}$. + _Solution extraction._ Validate target feasibility, count each task's predecessors from the order bits, and sort by this rank. No temporal tie-breaking or schedule repair is needed. - _Precedence constraints._ If $i prec.eq j$, require $C_j - C_i >= l_j$. - - _Single-machine disjunction._ For every pair $i < j$, require - $C_j - C_i + M (1 - y_(i j)) >= l_j$ - and - $C_i - C_j + M y_(i j) >= l_i$. - Exactly one of the two orderings is therefore active. - - _Objective._ Minimize $sum_j w_j C_j$. - - The ILP is: - $ - min quad & sum_j w_j C_j \ - "subject to" quad & l_j <= C_j <= M quad forall j \ - & C_j - C_i >= l_j quad forall i prec.eq j \ - & C_j - C_i + M (1 - y_(i j)) >= l_j quad forall i < j \ - & C_i - C_j + M y_(i j) >= l_i quad forall i < j \ - & y_(i j) in {0, 1}, C_j in ZZ_(>=0). - $ - - _Correctness._ ($arrow.r.double$) Any feasible schedule defines completion times and pairwise order values satisfying the bounds, precedence inequalities, and disjunctive machine constraints; its weighted completion time is exactly the ILP objective. ($arrow.l.double$) Any feasible ILP solution assigns a strict order to every task pair and forbids overlap, so the completion times correspond to a valid single-machine schedule that respects all precedences. Minimizing the ILP objective therefore minimizes the original weighted completion-time objective. - - _Solution extraction._ Sort tasks by their completion times $C_j$ and encode that order back into the source schedule representation. + _Size and arithmetic._ There are $n+n(n-1)/2$ variables and $n(n-1)(n-2)/3+n+e$ rows, with at most $n(n-1)(n-2)+n^2+e$ nonzeros. If $h$ is `max_processing_time_bits`, constraint magnitudes have at most $h+n$ bits. Signed sums and bounded constraint evaluation are checked before returning a target; arithmetic failure is distinct from infeasibility. Objective coefficients remain the source weights. ] #let hc_tsp = load-example("HamiltonianCircuit", "TravelingSalesman") @@ -15008,21 +14988,22 @@ The following reductions to Integer Linear Programming are straightforward formu ] #reduction-rule("PreemptiveScheduling", "ILP")[ - Minimize makespan for preemptive parallel scheduling with variable-length tasks and precedence constraints. + Sparse time-indexed activity with a certified horizon preserves the minimum makespan for positive task lengths and precedence constraints. ][ - _Construction._ Let $D = sum_t ell(t)$ be the horizon. Variables: binary $x_(t,u) in {0,1}$ (task $t$ processed at slot $u$) for $t in {0, dots, n-1}$, $u in {0, dots, D-1}$; integer $M in {0, dots, D}$ (makespan). The ILP is: + _Construction._ Let $D=sum_j p_j$. On the precedence DAG, compute earliest starts $E_j$ from predecessor paths and critical tails $B_j$ including task $j$. Construct a feasible nonpreemptive list schedule, prioritizing larger critical tails and then smaller task indices; process all simultaneous completions before dispatching ready tasks. Its makespan $H<=D$ certifies a horizon. Retain binary activity variables only for $E_j<=t= 1 quad "for each" (i prec j) quad "(precedence)" \ - & M - (u+1) dot x_(t,u) >= 0 quad forall t, u quad "(makespan)" \ - & x_(t,u) in {0, 1}, quad M in ZZ_(>= 0). + sum_t x_(j,t) &= p_j \ + sum_j x_(j,t) &<= min(m,n) \ + S_j+(H-t)x_(j,t) &<= H \ + C_j-(t+1)x_(j,t) &>= 0 \ + C_a &<= S_b quad forall (a,b) in P \ + C_j &<= M. $ + Minimize $M$. Bounds are variable domains, not redundant constraint rows. - _Correctness._ Work constraints enforce each task runs for exactly $ell(t)$ slots. Capacity limits at most $m$ tasks per slot. Precedences are enforced by weighted time indicators. Makespan lower bounds force $M >= u+1$ whenever task $t$ is active at slot $u$. + _Correctness._ ($arrow.r.double$) An optimal schedule has makespan at most the feasible list schedule's $H$. Every predecessor path forces its activity after $E_j$, and every successor path forces it before $H-B_j+p_j$. Its activity is therefore retained. Set endpoints to actual first and last activity and $M$ to its makespan. ($arrow.l.double$) Work and capacity rows ensure valid processing. Active-slot endpoint rows and $C_a<=S_b$ forbid successors from starting before predecessors finish, including interrupted tasks. The decoded makespan is at most $M$; tightening endpoints and $M$ to actual activity proves optimum equality. Positive durations make precedence cycles infeasible on both sides. - _Solution extraction._ Config$[t dot D + u] = x_(t,u)$ for all $t, u$. + _Solution extraction and size._ Validate, project retained activity to the original $n$ by $D$ matrix, and put zero in omitted slots. For $A$ retained slots and $e$ arc occurrences, the target has exactly $A+2n+1$ variables, $2A+2n+H+e$ rows, and $6A+2n+2e$ nonzeros. Source parameters `schedule_horizon` and `num_admissible_slots` come from the same deterministic calculation. Constraint magnitudes need at most `max_schedule_magnitude_bits`; endpoint arithmetic is checked before allocation. Empty instances have only $M=0$. ] #reduction-rule("SequencingWithinIntervals", "ILP")[ @@ -15342,25 +15323,15 @@ 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. + Tight start domains and pairwise order bits encode machine and job conflicts, with symmetry restricted to identical machines. ][ - _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 $H=sum_(j,i) p_(j,i)$, and bound each integer start by $0<=s_(j,i)<=H-p_(j,i)$. For every pair of operations $a,b$ sharing a job or machine, introduce a binary $z$ and impose $s_b-s_a-H z>=p_a-H$ and $s_a-s_b+H z>=p_b$. Append makespan $M in [0,H]$, impose $M-s_a>=p_a$ for every operation, and minimize $M$. - _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 \ - "subject to" quad - & s_(k,i) - s_(j,i) - M x_(j k i) >= p(j, i) - M quad forall j < k, i \ - & s_(j,i) - s_(k,i) + M x_(j k i) >= p(k, i) quad forall j < k, i \ - & s_(j,i') - s_(j,i) - M y_(j i i') >= p(j, i) - M quad forall j, i < i' \ - & s_(j,i) - s_(j,i') + M y_(j i i') >= p(j, i') quad forall j, i < i' \ - & C - s_(j,i) >= p(j, i) quad forall j, i \ - & x_(j k i), y_(j i i') in {0,1},; s_(j,i), C in ZZ_(>=0). - $ + _Symmetry._ Group machines whose processing columns are identical. For each group choose one anchor job of largest duration, breaking ties by job index. Require its operations to visit the group's machines in increasing machine-index order. This adds one row per consecutive pair in the group. Do not impose this order on other jobs. - _Correctness._ ($arrow.r.double$) Any feasible open-shop schedule with the given permutations $sigma_i$ induces valid ordering bits $x_{j k i}$ and $y_{j i i'}$ and start times satisfying all non-overlap constraints. ($arrow.l.double$) Any feasible ILP solution defines non-overlapping start times for all tasks, respecting both machine and job constraints. + _Correctness._ ($arrow.r.double$) A serial schedule proves the optimum is at most $H$. Permuting entire identical machine columns preserves every job and machine conflict and the makespan. Such a permutation orders the anchor's operations as required, independently in each group. Set each bit to the represented pair order. The inactive inequalities follow from the start domains; choose $M$ equal to makespan. ($arrow.l.double$) Each bit activates one finish-before-start condition, so every conflict is absent. This also matches the source overlap predicate for zero durations. Decreasing $M$ to the actual last finish proves optimum equality. - _Solution extraction._ Return the $n m$ start-time variables $s_{j,i}$ directly in job-major order. + _Extraction and size._ Validate and return the job-major start slice. Let $O=n(n-1)m/2+n m(m-1)/2$. The target has exactly $n m+O+1$ variables, at most $2O+n m+m$ rows and $6O+2n m+2m$ nonzeros. Magnitudes are bounded by `schedule_horizon_bits`. All count and normalized-row arithmetic is checked. ] #let doss_ilp = load-example("DecisionOpenShopScheduling", "ILP") @@ -15374,16 +15345,36 @@ The following reductions to Integer Linear Programming are straightforward formu "pred solve bundle.json", "pred evaluate schedule.json --config " + cli-config(doss_ilp.solutions.at(0).source_config), ) - The canonical instance has processing times #repr(doss_ilp.source.instance.inner.processing_times) and bound #doss_ilp.source.instance.bound. Add the makespan constraint with this bound and set the objective to zero. The stored feasible ILP assignment decodes to start times #fmt-values(doss_ilp.solutions.at(0).source_config), which satisfy the bound. The fixture stores one witness. + The bound is #doss_ilp.source.instance.bound. The stored witness decodes to start times #fmt-values(doss_ilp.solutions.at(0).source_config), which meet that bound. ], )[ - Impose the decision bound on the open-shop makespan variable. The optimization formulation gains one constraint and no variables. + Incorporate the decision bound in every operation's domain and disjunction, eliminating the makespan variable and its rows. +][ + _Construction._ For bound $B$, set $H=min(B,D)$, where $D$ is the serial processing sum. If $B<0$ or some duration exceeds $H$, emit $0=1$. Otherwise use the preceding conflict and identical-machine symmetry construction with this $H$, omit $M$ and its rows, and use zero objective. + + _Correctness._ ($arrow.r.double$) For $B<=D$, any satisfying source schedule already lies within $H$. For $B>D$, the serial schedule lies within $H=D$. Relabel identical machines as proved above and encode the pair orders. ($arrow.l.double$) Every target pair avoids overlap and all finishes are at most $H<=B$. The contradiction branch is source-infeasible because makespan is nonnegative and at least each operation's duration. + + _Extraction and size._ Validate and decode starts. There are at most $n m+O$ variables, $2O+m+1$ rows and $6O+2m$ nonzeros, including the contradiction branch. The magnitude bound remains `schedule_horizon_bits`. +] + +#reduction-rule("ThreePartition", "ILP")[ + Direct binary exact cover by legal indexed triples removes group-label symmetry. +][ + _Construction._ Enumerate all $i=0$. For production, setup and inventory costs $a_t,b_t,h_t$, impose $sum_t (a_t x_t+b_t z_t+h_t I_t)<=B$. Use zero objective. - _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. + _Correctness._ ($arrow.r.double$) A feasible source plan defines its cumulative inventories and $z_t=1$ exactly when production is positive; every row holds. ($arrow.l.double$) Conservation forces the true inventories, nonnegative inventory forbids backlog, and the two setup links force the same exact indicator. The budget row therefore equals source cost. Since costs are nonnegative, the final budget also enforces every source prefix-budget check. - _Solution extraction._ Check target feasibility, then use the existing job-major start-time decoder. Construction has the same asymptotic cost as the optimization formulation.#footnote[Complexity follows from the implementation; not independently verified from literature.] + _Extraction and arithmetic._ Validate and return the production coordinates. Checked cumulative demands and capacities, domain conversions, and normalized-row partial sums must fit the exact integer representation; failure is an arithmetic error rather than a NO answer. For $T$ periods there are $3T$ variables, $3T+1$ rows and at most $10T$ nonzeros. If $h$ bounds all input magnitudes in bits, $h+T$ also bounds cumulative inventory domains and every constraint magnitude. ] #reduction-rule("MinimumTardinessSequencing", "ILP")[ @@ -15518,37 +15509,25 @@ The following reductions to Integer Linear Programming are straightforward formu ] #reduction-rule("SequencingWithReleaseTimesAndDeadlines", "ILP")[ - A time-indexed formulation captures the admissible start window of each task and forbids overlap on the single machine. + Bounded starts and pairwise disjunctions encode a source permutation independently of horizon length, including zero-duration tasks. ][ - _Construction._ Variables: binary $x_(j,t)$ with $x_(j,t) = 1$ iff task $j$ starts at time $t$, where $p_j = ell(t_j)$ is the processing time (length) of task $j$. The ILP is: - $ - "find" quad & bold(x) \ - "subject to" quad & sum_(t = r_j)^(d_j - p_j) x_(j,t) = 1 quad forall j \ - & sum_(j, t : t <= tau < t + p_j) x_(j,t) <= 1 quad forall tau \ - & x_(j,t) in {0, 1}. - $ + _Construction._ Durations, releases and deadlines are nonnegative. If any window $[r_j,d_j-p_j]$ is empty, emit $0=1$. Otherwise use bounded integer starts in those windows and a binary $y_(i j)$ for every $i=p_i-A_(i j)$ and $s_i-s_j+B_(i j)y_(i j)>=p_j$. + For identical duration/release/deadline triples, fix $y_(i j)=1$. Relabeling tasks within each identical class into start order preserves all source conditions, including zero-duration ties, so this removes only equivalent representations. Use zero objective. The bounded-integer endpoint is direct; the binary endpoint applies the package's bounded-integer-to-binary encoding uniformly to this same construction. - _Correctness._ ($arrow.r.double$) Any feasible non-preemptive schedule chooses one valid start time per task and never overlaps two active jobs. ($arrow.l.double$) Any feasible ILP solution gives exactly such a start-time assignment, so executing the jobs in increasing start order solves the source instance. + _Correctness._ ($arrow.r.double$) A feasible source permutation's earliest-start schedule lies in the declared windows. Choose each bit according to its task order. The inactive inequalities follow from the window endpoints. ($arrow.l.double$) The disjunctions ensure that one task finishes before the other starts. Sort by start, then duration, then index; at equal starts this places zero-duration tasks before positive tasks. Positive tasks cannot surround a zero-duration task strictly in their interior. The resulting permutation respects all represented intervals, and its earliest-start execution can only finish earlier, meeting every deadline. An empty window certifies NO independently of any solver. - _Solution extraction._ Read each task's chosen start time, sort the tasks by that order, and encode the resulting permutation as Lehmer code. + _Size and extraction._ Validate and sort as above, returning the permutation. For $n$ tasks and horizon bit length $h$, the direct target has at most $n+n(n-1)/2$ variables, $n(n-1)+1$ rows, $3n(n-1)$ nonzeros and $h$ magnitude bits. The binary target has at most $n h+n(n-1)/2$ variables, the same row bound, and $n(n-1)(2h+1)$ nonzeros. Checked exact arithmetic rejects unrepresentable targets without declaring them infeasible. ] #reduction-rule("TimetableDesign", "ILP")[ - The source witness is a binary craftsman-task-period incidence table, and all feasibility conditions are already linear. + Keep only available craftsman-task-period assignments with positive pair demand. ][ - _Construction._ Variables: binary $x_(c,t,h)$ with $x_(c,t,h) = 1$ iff craftsman $c$ works on task $t$ in period $h$. The ILP is: - $ - "find" quad & bold(x) \ - "subject to" quad & x_(c,t,h) = 0 quad "whenever either side is unavailable" \ - & sum_t x_(c,t,h) <= 1 quad forall c, h \ - & sum_c x_(c,t,h) <= 1 quad forall t, h \ - & sum_h x_(c,t,h) = r_(c,t) quad forall c, t \ - & x_(c,t,h) in {0, 1}. - $ + _Construction._ Retain a binary variable for $(c,t,h)$ exactly when $r_(c,t)>0$ and both endpoints are available in period $h$. For each occupied craftsman-period and task-period emit a capacity row with upper bound one. For every nonzero pair demand emit its exact sum row, clipping the RHS to $[-1,H+1]$, where $H$ is the period count. Use zero objective; omit empty capacity rows and zero-demand pairs. - _Correctness._ ($arrow.r.double$) Any valid timetable satisfies availability, exclusivity, and exact requirement counts. ($arrow.l.double$) Any feasible ILP solution is exactly such a timetable because the variable layout matches the source configuration. + _Correctness._ ($arrow.r.double$) Availability and zero demand force every omitted entry of a feasible source tensor to zero. Projection satisfies every retained capacity and demand row. ($arrow.l.double$) Decode retained bits and zero-fill omitted entries. All availability and exclusivity conditions hold. Pair counts lie in $[0,H]$, where equality to the clipped demand is equivalent to equality to the original signed demand. Thus negative and oversized demands remain infeasible; no witness repair is needed. - _Solution extraction._ Output the flattened binary array $(x_(c,t,h))$ in source order. + _Size and extraction._ Validate target feasibility and reconstruct the original tensor. For $A$ available positive-demand assignments and $Q$ nonzero pair demands, there are exactly $A$ variables and $3A$ nonzeros, and at most $2A+Q$ rows. Constraint magnitude bits are at most `period_count_bits + 1`. These source-derived counts describe the emitted sparse matrix, including zero-variable infeasible instances. ] // Position/Assignment diff --git a/problemreductions-cli/tests/cli_tests.rs b/problemreductions-cli/tests/cli_tests.rs index 899189d80..ee8ec00c6 100644 --- a/problemreductions-cli/tests/cli_tests.rs +++ b/problemreductions-cli/tests/cli_tests.rs @@ -5539,35 +5539,28 @@ fn test_path_overall_unavailable_is_reported_per_field_without_internal_modes() .args([ "path", "ThreePartition", - "QUBO/i64", + "SequencingWithReleaseTimesAndDeadlines", "--limit", - "2", + "1", "--json", ]) .output() .unwrap(); assert!(output.status.success()); let envelope: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap(); - 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 overall = &envelope["paths"][0]["overall_parameters"]; let fields = overall["fields"].as_array().unwrap(); - assert!(!fields.is_empty()); - assert!(fields.iter().all(|field| { - field["relation"] == "unavailable" - && field["field"].is_string() - && field["reason"].is_string() - })); + let horizon = fields + .iter() + .find(|field| field["field"] == "time_horizon") + .unwrap(); + assert_eq!(horizon["relation"], "unavailable"); + assert!(horizon["reason"].is_string()); + let task_count = fields + .iter() + .find(|field| field["field"] == "num_tasks") + .unwrap(); + assert_eq!(task_count["relation"], "exact"); assert!(overall.get("exact_composition_error").is_none()); assert!(overall.get("bound_composition_error").is_none()); } diff --git a/src/models/misc/preemptive_scheduling.rs b/src/models/misc/preemptive_scheduling.rs index 119fae84d..cec027f5a 100644 --- a/src/models/misc/preemptive_scheduling.rs +++ b/src/models/misc/preemptive_scheduling.rs @@ -173,6 +173,99 @@ impl PreemptiveScheduling { &self.precedences } + /// Horizon of a feasible critical-path-priority list schedule; zero for cycles. + pub fn schedule_horizon(&self) -> usize { + self.scheduling_windows().0 + } + + /// Activity slots that can occur within the certified horizon. + pub fn num_admissible_slots(&self) -> usize { + self.scheduling_windows() + .1 + .iter() + .map(|window| window.len()) + .sum() + } + + /// Bit bound for work, capacity and endpoint coefficients. + pub fn max_schedule_magnitude_bits(&self) -> u64 { + crate::types::max_numeric_magnitude_bits([self.d_max(), self.num_processors()]) + } + + pub(crate) fn scheduling_windows(&self) -> (usize, Vec>) { + use std::cmp::Reverse; + use std::collections::BinaryHeap; + let n = self.num_tasks(); + let lengths: Vec<_> = self + .lengths + .iter() + .map(|&p| usize::try_from(p).expect("validated positive task length fits the horizon")) + .collect(); + let mut successors = vec![Vec::new(); n]; + let mut indegree = vec![0; n]; + for &(a, b) in &self.precedences { + successors[a].push(b); + indegree[b] += 1; + } + let mut pending = indegree.clone(); + let mut order: Vec<_> = (0..n).filter(|&j| pending[j] == 0).collect(); + let mut earliest = vec![0; n]; + let mut cursor = 0; + while cursor < order.len() { + let j = order[cursor]; + cursor += 1; + for &k in &successors[j] { + earliest[k] = earliest[k].max(earliest[j] + lengths[j]); + pending[k] -= 1; + if pending[k] == 0 { + order.push(k); + } + } + } + if order.len() != n { + return (0, vec![0..0; n]); + } + let mut tail = lengths.clone(); + for &j in order.iter().rev() { + tail[j] += successors[j].iter().map(|&k| tail[k]).max().unwrap_or(0); + } + let mut ready: BinaryHeap<_> = (0..n) + .filter(|&j| indegree[j] == 0) + .map(|j| (tail[j], Reverse(j))) + .collect(); + let mut running = BinaryHeap::new(); + let mut time = 0; + loop { + while running.len() < self.num_processors { + let Some((_, Reverse(j))) = ready.pop() else { + break; + }; + running.push(Reverse((time + lengths[j], j))); + } + let Some(&Reverse((finish, _))) = running.peek() else { + break; + }; + time = finish; + while running.peek().is_some_and(|Reverse((end, _))| *end == time) { + let Reverse((_, j)) = running.pop().expect("finish event exists"); + for &k in &successors[j] { + indegree[k] -= 1; + if indegree[k] == 0 { + ready.push((tail[k], Reverse(k))); + } + } + } + } + // Every addition above lies on a path or a feasible serial schedule, + // hence fits the constructor-validated sum of processing lengths. + ( + time, + (0..n) + .map(|j| earliest[j]..time - tail[j] + lengths[j]) + .collect(), + ) + } + /// Compute `D_max = sum of all task lengths` (worst-case makespan). pub fn d_max(&self) -> usize { let total = self @@ -209,6 +302,9 @@ impl Problem for PreemptiveScheduling { crate::problem_parameters![ ("d_max", d_max), + ("schedule_horizon", schedule_horizon), + ("num_admissible_slots", num_admissible_slots), + ("max_schedule_magnitude_bits", max_schedule_magnitude_bits), ("num_precedences", num_precedences), ("num_processors", num_processors), ("num_tasks", num_tasks), diff --git a/src/models/misc/production_planning.rs b/src/models/misc/production_planning.rs index 4d779bb26..31f55b4a5 100644 --- a/src/models/misc/production_planning.rs +++ b/src/models/misc/production_planning.rs @@ -173,6 +173,20 @@ impl ProductionPlanning { self.cost_bound } + /// Bit bound for every numeric construction input. + pub fn max_numeric_magnitude_bits(&self) -> u64 { + crate::types::max_numeric_magnitude_bits( + self.demands + .iter() + .chain(&self.capacities) + .chain(&self.setup_costs) + .chain(&self.production_costs) + .chain(&self.inventory_costs) + .copied() + .chain(std::iter::once(self.cost_bound)), + ) + } + pub fn max_capacity(&self) -> i64 { self.capacities.iter().copied().max().unwrap_or(0) } @@ -183,7 +197,11 @@ impl Problem for ProductionPlanning { type Solution = Vec; type Value = Or; - crate::problem_parameters![("max_capacity", max_capacity), ("num_periods", num_periods),]; + crate::problem_parameters![ + ("max_capacity", max_capacity), + ("num_periods", num_periods), + ("max_numeric_magnitude_bits", max_numeric_magnitude_bits), + ]; fn evaluate(&self, config: &Self::Solution) -> Result { Ok({ diff --git a/src/models/misc/sequencing_with_release_times_and_deadlines.rs b/src/models/misc/sequencing_with_release_times_and_deadlines.rs index c07c67451..eb49c9fb1 100644 --- a/src/models/misc/sequencing_with_release_times_and_deadlines.rs +++ b/src/models/misc/sequencing_with_release_times_and_deadlines.rs @@ -141,6 +141,11 @@ impl SequencingWithReleaseTimesAndDeadlines { pub fn time_horizon(&self) -> i64 { self.deadlines.iter().copied().max().unwrap_or(0) } + + /// Magnitude bits of the maximum deadline, with at least one bit. + pub fn time_horizon_bits(&self) -> u64 { + crate::types::max_numeric_magnitude_bits([self.time_horizon()]) + } } impl Problem for SequencingWithReleaseTimesAndDeadlines { @@ -148,7 +153,11 @@ impl Problem for SequencingWithReleaseTimesAndDeadlines { type Solution = Vec; type Value = crate::types::Or; - crate::problem_parameters![("num_tasks", num_tasks), ("time_horizon", time_horizon),]; + crate::problem_parameters![ + ("num_tasks", num_tasks), + ("time_horizon", time_horizon), + ("time_horizon_bits", time_horizon_bits), + ]; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] diff --git a/src/models/misc/timetable_design.rs b/src/models/misc/timetable_design.rs index d04d72892..c86e80e86 100644 --- a/src/models/misc/timetable_design.rs +++ b/src/models/misc/timetable_design.rs @@ -207,159 +207,39 @@ impl TimetableDesign { ((craftsman * self.num_tasks) + task) * self.num_periods + period } - pub(crate) fn solve_via_required_assignments(&self) -> Option>>> { - #[derive(Clone)] - struct PairRequirement { - craftsman: usize, - task: usize, - required: usize, - allowed_periods: Vec, - } - - let mut craftsman_demand = vec![0usize; self.num_craftsmen]; - let mut task_demand = vec![0usize; self.num_tasks]; - let mut pairs = Vec::new(); - - for (craftsman, requirement_row) in self.requirements.iter().enumerate() { - for (task, required_i64) in requirement_row.iter().enumerate() { - let required = usize::try_from(*required_i64).ok()?; - craftsman_demand[craftsman] += required; - task_demand[task] += required; - - if required == 0 { - continue; - } - - let allowed_periods = (0..self.num_periods) - .filter(|&period| { - self.craftsman_avail[craftsman][period] && self.task_avail[task][period] - }) - .collect::>(); - - if allowed_periods.len() < required { - return None; - } - - pairs.push(PairRequirement { - craftsman, - task, - required, - allowed_periods, - }); - } - } - - if craftsman_demand - .iter() - .zip(&self.craftsman_avail) - .any(|(demand, avail)| *demand > avail.iter().filter(|&&v| v).count()) - { - return None; - } - - if task_demand + /// Available triples belonging to pairs with positive demand. + pub fn num_available_assignments(&self) -> usize { + self.requirements .iter() - .zip(&self.task_avail) - .any(|(demand, avail)| *demand > avail.iter().filter(|&&v| v).count()) - { - return None; - } - - pairs.sort_by_key(|pair| (pair.allowed_periods.len(), pair.required)); - - struct SearchState<'a> { - problem: &'a TimetableDesign, - pairs: &'a [PairRequirement], - craftsman_busy: Vec>, - task_busy: Vec>, - config: Vec, - } - - impl SearchState<'_> { - fn search_pair( - &mut self, - pair_index: usize, - period_offset: usize, - remaining: usize, - ) -> bool { - if pair_index == self.pairs.len() { - return true; - } - - let pair = &self.pairs[pair_index]; - if remaining == 0 { - return self.search_pair( - pair_index + 1, - 0, - self.pairs - .get(pair_index + 1) - .map_or(0, |next| next.required), - ); - } - - let feasible_remaining = pair.allowed_periods[period_offset..] - .iter() - .filter(|&&period| { - !self.craftsman_busy[pair.craftsman][period] - && !self.task_busy[pair.task][period] + .enumerate() + .map(|(c, row)| { + row.iter() + .enumerate() + .filter(|(_, r)| **r > 0) + .map(|(t, _)| { + self.craftsman_avail[c] + .iter() + .zip(&self.task_avail[t]) + .filter(|(a, b)| **a && **b) + .count() }) - .count(); - if feasible_remaining < remaining { - return false; - } - - for candidate_index in period_offset..pair.allowed_periods.len() { - let period = pair.allowed_periods[candidate_index]; - if self.craftsman_busy[pair.craftsman][period] - || self.task_busy[pair.task][period] - { - continue; - } - - self.craftsman_busy[pair.craftsman][period] = true; - self.task_busy[pair.task][period] = true; - self.config[self.problem.index(pair.craftsman, pair.task, period)] = 1; - - if self.search_pair(pair_index, candidate_index + 1, remaining - 1) { - return true; - } - - self.config[self.problem.index(pair.craftsman, pair.task, period)] = 0; - self.task_busy[pair.task][period] = false; - self.craftsman_busy[pair.craftsman][period] = false; - } + .sum::() + }) + .sum() + } - false - } - } + /// Pair equations needed, including impossible negative demands. + pub fn num_nonzero_requirements(&self) -> usize { + self.requirements + .iter() + .flatten() + .filter(|&&r| r != 0) + .count() + } - let mut state = SearchState { - problem: self, - pairs: &pairs, - craftsman_busy: vec![vec![false; self.num_periods]; self.num_craftsmen], - task_busy: vec![vec![false; self.num_periods]; self.num_tasks], - config: vec![0; self.config_len()], - }; - - if state.search_pair(0, 0, pairs.first().map_or(0, |pair| pair.required)) { - Some( - (0..self.num_craftsmen) - .map(|craftsman| { - (0..self.num_tasks) - .map(|task| { - (0..self.num_periods) - .map(|period| { - state.config[self.index(craftsman, task, period)] == 1 - }) - .collect() - }) - .collect() - }) - .collect(), - ) - } else { - None - } + /// Bit length of the number of periods. + pub fn period_count_bits(&self) -> u64 { + crate::types::max_numeric_magnitude_bits([self.num_periods]) } } @@ -370,6 +250,9 @@ impl Problem for TimetableDesign { crate::problem_parameters![ ("num_craftsmen", num_craftsmen), + ("num_available_assignments", num_available_assignments), + ("num_nonzero_requirements", num_nonzero_requirements), + ("period_count_bits", period_count_bits), ("num_periods", num_periods), ("num_tasks", num_tasks), ]; diff --git a/src/rules/ilp_helpers.rs b/src/rules/ilp_helpers.rs index 22bf6e298..981d3922d 100644 --- a/src/rules/ilp_helpers.rs +++ b/src/rules/ilp_helpers.rs @@ -2,6 +2,36 @@ use crate::models::algebraic::LinearConstraint; +/// Ensure every normalized row's integer dot-product prefixes fit i64 over +/// the declared domains. This checks arithmetic, not mathematical feasibility. +pub(crate) fn validate_bounded_constraint_arithmetic( + target: &crate::models::algebraic::ILP, +) -> Result<(), crate::rules::ReductionError> { + for row in target.constraints() { + let mut lower = 0_i128; + let mut upper = 0_i128; + for &(variable, coefficient) in row.terms() { + let domain = &target.variables()[variable]; + let a = i128::from(coefficient) + * i128::from(domain.lower_bound().expect("bounded variable")); + let b = i128::from(coefficient) + * i128::from(domain.upper_bound().expect("bounded variable")); + // An i64*i64 product plus the preceding checked i64 prefix fits i128. + lower += a.min(b); + upper += a.max(b); + if i64::try_from(lower).is_err() || i64::try_from(upper).is_err() { + return Err(crate::rules::ReductionError::integer_overflow::< + S, + crate::models::algebraic::ILP, + >( + "bounding an integer constraint evaluation" + )); + } + } + } + Ok(()) +} + /// 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 diff --git a/src/rules/ksatisfiability_preemptivescheduling.rs b/src/rules/ksatisfiability_preemptivescheduling.rs index 1a61efea7..3b5353aff 100644 --- a/src/rules/ksatisfiability_preemptivescheduling.rs +++ b/src/rules/ksatisfiability_preemptivescheduling.rs @@ -372,6 +372,9 @@ impl crate::rules::AggregateReductionResult for Reduction3SATToPreemptiveSchedul 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)", + schedule_horizon = "(2 * num_vars + 3 + 6 * num_clauses) * (num_vars + 3)", + num_admissible_slots = "((2 * num_vars + 3 + 6 * num_clauses) * (num_vars + 3))^2", + max_schedule_magnitude_bits = "(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 { diff --git a/src/rules/ksatisfiability_timetabledesign.rs b/src/rules/ksatisfiability_timetabledesign.rs index b79da5e9e..295714b52 100644 --- a/src/rules/ksatisfiability_timetabledesign.rs +++ b/src/rules/ksatisfiability_timetabledesign.rs @@ -797,6 +797,9 @@ impl crate::rules::AggregateReductionResult for Reduction3SATToTimetableDesign { #[reduction( transform = upper_bound { num_periods = "4 * num_literals + 4", + period_count_bits = "4 * num_literals + 4", + num_available_assignments = "(24 * num_literals + num_clauses + 1)^2 * (4 * num_literals + 4)", + num_nonzero_requirements = "(24 * num_literals + num_clauses + 1)^2", num_craftsmen = "24 * num_literals + num_clauses + 1", num_tasks = "24 * num_literals + num_clauses + 1", } diff --git a/src/rules/mod.rs b/src/rules/mod.rs index 7e544af85..6d6e7f5b0 100644 --- a/src/rules/mod.rs +++ b/src/rules/mod.rs @@ -255,6 +255,7 @@ pub(crate) mod partitionintotriangles_ilp; pub(crate) mod pathconstrainednetworkflow_ilp; pub(crate) mod precedenceconstrainedscheduling_ilp; pub(crate) mod preemptivescheduling_ilp; +pub(crate) mod productionplanning_ilp; pub(crate) mod quadraticassignment_ilp; pub(crate) mod qubo_ilp; pub(crate) mod rectilinearpicturecompression_ilp; @@ -282,6 +283,7 @@ pub(crate) mod strongconnectivityaugmentation_ilp; pub(crate) mod subgraphisomorphism_ilp; pub(crate) mod sumofsquarespartition_ilp; pub(crate) mod threedimensionalmatching_ilp; +pub(crate) mod threepartition_ilp; pub(crate) mod timetabledesign_ilp; pub(crate) mod travelingsalesman_ilp; pub(crate) mod undirectedflowlowerbounds_ilp; @@ -565,6 +567,8 @@ pub(crate) fn canonical_rule_example_specs() -> Vec`. -//! -//! Disjunctive formulation with binary ordering variables and integer start times: -//! -//! **Variables:** -//! - `x_{j,k,i}` for j < k, all machines i: binary, 1 if job j precedes job k on machine i. -//! Index: pair index * m + i, where pair index = `j*(2n-j-1)/2 + (k-j-1)`. -//! Count: n*(n-1)/2 * m variables. -//! - `s_{j,i}` for all (j, i): integer start time of job j on machine i. -//! Index: num_order_vars + j * m + i. -//! Count: n * m variables. -//! - `C` (makespan): integer, index num_order_vars + n * m. -//! -//! **Constraints:** -//! 1. Binary bounds: 0 ≤ x_{j,k,i} ≤ 1 for all j < k, i. -//! 2. Machine non-overlap for each pair (j, k) and machine i: -//! - s_{k,i} ≥ s_{j,i} + p_{j,i} - M*(1 - x_{j,k,i}) → s_{k,i} - s_{j,i} + M*x_{j,k,i} ≥ p_{j,i} -//! - s_{j,i} ≥ s_{k,i} + p_{k,i} - M*x_{j,k,i} → s_{j,i} - s_{k,i} - M*x_{j,k,i} ≥ p_{k,i} - M -//! 3. Job non-overlap for each job j and each pair of machines (i, i'): -//! Uses separate binary variable y_{j,i,i'} for i < i' to decide which task runs first. -//! Variables y_{j,i,i'}: appended after s variables. -//! - s_{j,i'} ≥ s_{j,i} + p_{j,i} - M*(1 - y_{j,i,i'}) -//! - s_{j,i} ≥ s_{j,i'} + p_{j,i'} - M*y_{j,i,i'} -//! 4. Makespan: C ≥ s_{j,i} + p_{j,i} for all (j, i). -//! 5. Non-negativity of start times: s_{j,i} ≥ 0 (implied by ILP non-negativity). -//! -//! **Objective:** Minimize C. - +//! Bounded disjunctive open-shop scheduling with identical-machine symmetry. 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`. -/// -/// Variable layout: -/// - `x_{j,k,i}` at index `pair_idx(j,k) * m + i` (num_pairs * m vars) -/// - `s_{j,i}` at index `num_order_vars + j * m + i` (n * m vars) -/// - `y_{j,i,i'}` for i < i': at `num_order_vars + n*m + j * num_machine_pairs + machine_pair_idx(i,i')` -/// (n * m*(m-1)/2 vars) -/// - `C`: at index `num_order_vars + n * m + n * m*(m-1)/2` (1 var) #[derive(Debug, Clone)] pub struct ReductionOSSToILP { target: ILP, - num_jobs: usize, - num_machines: usize, - /// n*(n-1)/2 * m — start index of s_{j,i} variables - num_order_vars: usize, + start_offset: usize, + num_operations: usize, } impl ReductionOSSToILP { fn decode_schedule(&self, solution: &[i64]) -> crate::rules::ExtractionResult> { - let start = self.num_order_vars; - let end = start + self.num_jobs * self.num_machines; - crate::rules::ilp_helpers::decode_usize_values(&solution[start..end]) - } - - fn pair_idx(&self, j: usize, k: usize) -> usize { - debug_assert!(j < k); - let n = self.num_jobs; - j * (2 * n - j - 1) / 2 + (k - j - 1) - } - - fn x_var(&self, j: usize, k: usize, i: usize) -> usize { - self.pair_idx(j, k) * self.num_machines + i - } - - fn s_var(&self, j: usize, i: usize) -> usize { - self.num_order_vars + j * self.num_machines + i - } - - fn machine_pair_idx(&self, i: usize, ip: usize) -> usize { - debug_assert!(i < ip); - let m = self.num_machines; - i * (2 * m - i - 1) / 2 + (ip - i - 1) - } - - fn y_var(&self, j: usize, i: usize, ip: usize) -> usize { - let num_machine_pairs = self.num_machines * self.num_machines.saturating_sub(1) / 2; - self.num_order_vars - + self.num_jobs * self.num_machines - + j * num_machine_pairs - + self.machine_pair_idx(i, ip) - } -} - -impl ReductionResult for ReductionOSSToILP { - type Source = OpenShopScheduling; - type Target = ILP; - - fn target_problem(&self) -> &ILP { - &self.target - } - - /// Extract the job-major operation start times from the ILP solution. - fn extract_solution( - &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - self.decode_schedule(target_solution) + crate::rules::ilp_helpers::decode_usize_values( + &solution[self.start_offset..self.start_offset + self.num_operations], + ) } -} - -#[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", - }, - 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 { - type Result = ReductionOSSToILP; - - fn reduce_to(&self) -> Result { - let n = self.num_jobs(); - let m = self.num_machines(); - let p = self.processing_times(); - let num_pairs = n * n.saturating_sub(1) / 2; - let num_machine_pairs = m * m.saturating_sub(1) / 2; - - // Variable counts - let num_order_vars = num_pairs * m; // x_{j,k,i}: binary - let num_start_vars = n * m; // s_{j,i}: integer - let num_job_pair_vars = n * num_machine_pairs; // y_{j,i,i'}: binary - let num_vars = num_order_vars + num_start_vars + num_job_pair_vars + 1; // +1 for C - - let result = ReductionOSSToILP { - target: ILP::new(0, vec![], vec![], ObjectiveSense::Minimize) - .map_err(Self::target_construction)?, - num_jobs: n, - num_machines: m, - num_order_vars, + fn build( + source: &OpenShopScheduling, + bound: Option, + ) -> Result { + type Target = ILP; + let overflow = |operation| { + crate::rules::ReductionError::integer_overflow::(operation) }; - - // Big-M: sum of all processing times (loose upper bound on makespan) - let total_p = p - .iter() - .flat_map(|row| row.iter()) - .try_fold(0_i64, |total, &time| total.checked_add(time)) - .ok_or_else(|| { - crate::rules::ReductionError::integer_overflow::< - OpenShopScheduling, - ILP, - >("summing open-shop processing times") - })?; - let big_m = total_p; - let processing_times = p; - - let c_var = num_order_vars + num_start_vars + num_job_pair_vars; - - let mut constraints = Vec::new(); - - // 1. Binary bounds on x_{j,k,i}: 0 ≤ x ≤ 1 - for j in 0..n { - for k in (j + 1)..n { - for i in 0..m { - let x = result.x_var(j, k, i); - constraints.push(LinearConstraint::le(vec![(x, 1)], 1)); - } - } + let construction = >::target_construction; + let total = i64::try_from(source.schedule_horizon()) + .map_err(|_| overflow("converting the schedule horizon"))?; + let horizon = bound.map_or(total, |b| b.min(total)); + let p = source.processing_times(); + if horizon < 0 || p.iter().flatten().any(|&duration| duration > horizon) { + return Ok(Self { + target: ILP::with_variables( + vec![], + vec![LinearConstraint::eq(vec![], 1)], + vec![], + ObjectiveSense::Minimize, + ) + .map_err(construction)?, + start_offset: 0, + num_operations: 0, + }); } - - // Upper bounds on start time variables: s_{j,i} ≤ total_p - // (no task can start after all tasks have finished) - for j in 0..n { - for i in 0..m { - let sji = result.s_var(j, i); - constraints.push(LinearConstraint::le(vec![(sji, 1)], big_m)); + let n = source.num_jobs(); + let m = source.num_machines(); + let pairs = |k: usize| { + if k.is_multiple_of(2) { + (k / 2).checked_mul(k.saturating_sub(1)) + } else { + k.checked_mul(k / 2) } + }; + let machine_orders = if m == 0 { + 0 + } else { + pairs(n) + .and_then(|v| v.checked_mul(m)) + .ok_or_else(|| overflow("counting machine order variables"))? + }; + let job_orders = if n == 0 { + 0 + } else { + pairs(m) + .and_then(|v| v.checked_mul(n)) + .ok_or_else(|| overflow("counting job order variables"))? + }; + let operations = n + .checked_mul(m) + .ok_or_else(|| overflow("counting operations"))?; + let makespan = machine_orders + .checked_add(operations) + .and_then(|v| v.checked_add(job_orders)) + .ok_or_else(|| overflow("counting scheduling variables"))?; + let count = makespan + .checked_add(usize::from(bound.is_none())) + .ok_or_else(|| overflow("counting scheduling variables"))?; + let mut variables = vec![IntegerVariable::binary(); count]; + for (index, &duration) in p.iter().flatten().enumerate() { + variables[machine_orders + index] = + IntegerVariable::new(Some(0), Some(horizon - duration)).map_err(construction)?; } - - // Upper bound on makespan C ≤ total_p - constraints.push(LinearConstraint::le(vec![(c_var, 1)], big_m)); - - // 2. Machine non-overlap: for each pair (j,k) with j= p_{j,i} - M - constraints.push(LinearConstraint::ge( - vec![(sk, 1), (sj, -1), (x, -big_m)], - pji - big_m, - )); - - // (b) s_{j,i} - s_{k,i} + M*x_{j,k,i} >= p_{k,i} - constraints.push(LinearConstraint::ge( - vec![(sj, 1), (sk, -1), (x, big_m)], - pki, - )); + for k in j + 1..n { + for (i, (&left, &right)) in p[j].iter().zip(&p[k]).enumerate() { + disjunction(start(j, i), start(k, i), bit, left, right); + bit += 1; } } } - - // 3. Binary bounds on y_{j,i,i'}: 0 ≤ y ≤ 1 - for j in 0..n { + bit = machine_orders + operations; + for (j, durations) in p.iter().enumerate() { for i in 0..m { - for ip in (i + 1)..m { - let y = result.y_var(j, i, ip); - constraints.push(LinearConstraint::le(vec![(y, 1)], 1)); + for k in i + 1..m { + disjunction(start(j, i), start(j, k), bit, durations[i], durations[k]); + bit += 1; } } } - - // 4. Job non-overlap: for each job j and each pair (i, i') with i < i' - // y_{j,i,i'}=1 means machine i is scheduled before machine i' for job j: - // (a) s_{j,i'} ≥ s_{j,i} + p_{j,i} - M*(1-y) - // s_{j,i'} - s_{j,i} - M*y ≥ p_{j,i} - M - // (b) s_{j,i} ≥ s_{j,i'} + p_{j,i'} - M*y - // s_{j,i} - s_{j,i'} + M*y ≥ p_{j,i'} - for (j, pj) in processing_times.iter().enumerate() { + let mut objective = Vec::new(); + if bound.is_none() { + variables[makespan] = + IntegerVariable::new(Some(0), Some(horizon)).map_err(construction)?; + objective.push((makespan, 1)); + for (index, &duration) in p.iter().flatten().enumerate() { + rows.push(LinearConstraint::ge( + vec![(makespan, 1), (machine_orders + index, -1)], + duration, + )); + } + } + // Relabel identical machines so one anchor job visits them in index order. + // Ordering every job this way would incorrectly impose a flow shop. + if n > 0 { + let mut groups = std::collections::BTreeMap::, Vec>::new(); for i in 0..m { - for ip in (i + 1)..m { - let y = result.y_var(j, i, ip); - let sji = result.s_var(j, i); - let sjip = result.s_var(j, ip); - let pji = pj[i]; - let pjip = pj[ip]; - - // (a) s_{j,i'} - s_{j,i} - M*y >= p_{j,i} - M - constraints.push(LinearConstraint::ge( - vec![(sjip, 1), (sji, -1), (y, -big_m)], - pji - big_m, - )); - - // (b) s_{j,i} - s_{j,i'} + M*y >= p_{j,i'} - constraints.push(LinearConstraint::ge( - vec![(sji, 1), (sjip, -1), (y, big_m)], - pjip, + groups + .entry(p.iter().map(|job| job[i]).collect()) + .or_default() + .push(i); + } + for (column, machines) in groups { + let anchor = (0..n) + .max_by_key(|&j| (column[j], std::cmp::Reverse(j))) + .expect("nonempty jobs"); + for pair in machines.windows(2) { + rows.push(LinearConstraint::ge( + vec![(start(anchor, pair[1]), 1), (start(anchor, pair[0]), -1)], + column[anchor], )); } } } + let target = ILP::with_variables(variables, rows, objective, ObjectiveSense::Minimize) + .map_err(construction)?; + crate::rules::ilp_helpers::validate_bounded_constraint_arithmetic::( + &target, + )?; + Ok(Self { + target, + start_offset: machine_orders, + num_operations: operations, + }) + } +} - // 5. Makespan: C ≥ s_{j,i} + p_{j,i} ⟺ C - s_{j,i} ≥ p_{j,i} - for (j, pj) in processing_times.iter().enumerate() { - for (i, &pji) in pj.iter().enumerate() { - let sji = result.s_var(j, i); - constraints.push(LinearConstraint::ge(vec![(c_var, 1), (sji, -1)], pji)); - } - } - - // 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; +impl ReductionResult for ReductionOSSToILP { + type Source = OpenShopScheduling; + 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( + &self.target, + solution, + |value| value.value.is_some(), + "target ILP assignment is infeasible", + )?; + self.decode_schedule(solution) + } +} - Ok(ReductionOSSToILP { - target: ILP::with_variables( - variables, - constraints, - objective, - ObjectiveSense::Minimize, - ) - .map_err(Self::target_construction)?, - num_jobs: n, - num_machines: m, - num_order_vars, - }) +#[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", }, + upper_bound { + num_constraints = "num_jobs * (num_jobs - 1) * num_machines + num_jobs * num_machines * (num_machines - 1) + num_jobs * num_machines + num_machines", + num_nonzeros = "3 * num_jobs * (num_jobs - 1) * num_machines + 3 * num_jobs * num_machines * (num_machines - 1) + 2 * num_jobs * num_machines + 2 * num_machines", + max_constraint_magnitude_bits = "schedule_horizon_bits", + }, +})] +impl ReduceTo> for OpenShopScheduling { + type Result = ReductionOSSToILP; + fn reduce_to(&self) -> Result { + ReductionOSSToILP::build(self, None) } } -/// Feasibility encoding of the makespan bound, with the existing schedule decoder. #[derive(Debug, Clone)] pub struct ReductionDecisionOpenShopSchedulingToILP { inner: ReductionOSSToILP, } - impl ReductionResult for ReductionDecisionOpenShopSchedulingToILP { type Source = Decision; type Target = ILP; - fn target_problem(&self) -> &Self::Target { - self.inner.target_problem() + &self.inner.target } - fn extract_solution(&self, solution: &Vec) -> crate::rules::ExtractionResult> { crate::rules::traits::validate_target_witness( self.target_problem(), solution, |value| value.value.is_some(), - "ILP assignment does not satisfy the bounded scheduling constraints", + "target ILP assignment is infeasible", )?; self.inner.decode_schedule(solution) } } - #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionDecisionOpenShopSchedulingToILP {} - -#[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", - }, - 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)", - }, +#[reduction(transform = upper_bound { + num_vars = "num_jobs * (num_jobs - 1) / 2 * num_machines + num_jobs * num_machines + num_jobs * num_machines * (num_machines - 1) / 2", + num_constraints = "num_jobs * (num_jobs - 1) * num_machines + num_jobs * num_machines * (num_machines - 1) + num_machines + 1", + num_nonzeros = "3 * num_jobs * (num_jobs - 1) * num_machines + 3 * num_jobs * num_machines * (num_machines - 1) + 2 * num_machines", + max_constraint_magnitude_bits = "schedule_horizon_bits", })] impl ReduceTo> for Decision { type Result = ReductionDecisionOpenShopSchedulingToILP; - fn reduce_to(&self) -> Result { - 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()).clamp(-1, horizon), - )); - inner.target = ILP::with_variables( - inner.target.variables().to_vec(), - constraints, - vec![], - ObjectiveSense::Minimize, - ) - .map_err(>>::target_construction)?; - Ok(ReductionDecisionOpenShopSchedulingToILP { inner }) + Ok(ReductionDecisionOpenShopSchedulingToILP { + inner: ReductionOSSToILP::build(self.inner(), Some(*self.bound()))?, + }) } } diff --git a/src/rules/partition_productionplanning.rs b/src/rules/partition_productionplanning.rs index 137313d78..8ea41f028 100644 --- a/src/rules/partition_productionplanning.rs +++ b/src/rules/partition_productionplanning.rs @@ -39,8 +39,9 @@ impl ReductionResult for ReductionPartitionToProductionPlanning { impl crate::rules::AggregateReductionResult for ReductionPartitionToProductionPlanning {} #[reduction( - transform = exact { - num_periods = "num_elements + 1", + transform = { + exact { num_periods = "num_elements + 1", }, + upper_bound { max_numeric_magnitude_bits = "max_numeric_magnitude_bits + num_elements", }, }, unavailable = { max_capacity = "the exact target parameter is not represented by this reduction's symbolic transform", diff --git a/src/rules/preemptivescheduling_ilp.rs b/src/rules/preemptivescheduling_ilp.rs index f6e9e438f..6996e47b5 100644 --- a/src/rules/preemptivescheduling_ilp.rs +++ b/src/rules/preemptivescheduling_ilp.rs @@ -1,25 +1,12 @@ -//! Reduction from PreemptiveScheduling to `ILP`. +//! Time-indexed preemptive scheduling with bounded start and completion variables. //! -//! 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), -//! plus integer M (the makespan), indexed at position n*D_max. -//! - Variable index for x_{t,u}: t * D_max + u. -//! - Variable index for M: n * D_max. -//! - Constraints: -//! 1. Work: Σ_u x_{t,u} = l(t) for each task t -//! 2. Capacity: Σ_t x_{t,u} ≤ m for each time slot u -//! 3. Precedence: for each (pred, succ) and each slot u, -//! `l(pred) * x_{succ,u} ≤ Σ_{v=0}^{u-1} x_{pred,v}` -//! This ensures succ can only be active at slot u if pred has already -//! completed all l(pred) units of work in slots 0..u-1. -//! 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) -//! - Objective: Minimize M. +//! Binary x(t,u) records task activity. Start S(t) is no later than any active +//! slot; completion C(t) is later than every active slot. Each precedence +//! (a,b) requires C(a) <= S(b), so interrupted tasks still finish before their +//! successors start. Minimize M with M >= C(t) for every task. //! -//! Note: `ILP` treats all variables as non-negative integers. Binary constraints -//! on x_{t,u} are enforced by x_{t,u} ≤ 1. +//! Using task endpoints avoids repeating all earlier time slots for every +//! precedence edge: the matrix has 6*A + 2*p + 2*n for A admissible activity slots nonzeros. use crate::models::algebraic::{Bounded, IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::PreemptiveScheduling; @@ -27,17 +14,12 @@ use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; /// 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) -/// - M at index n * D_max (1 integer var) -/// -/// Total: n * D_max + 1 variables. #[derive(Debug, Clone)] pub struct ReductionPSToILP { target: ILP, num_tasks: usize, d_max: usize, + slots: Vec<(usize, usize)>, } impl ReductionResult for ReductionPSToILP { @@ -55,26 +37,29 @@ impl ReductionResult for ReductionPSToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - - Ok((0..self.num_tasks) - .map(|task| { - (0..self.d_max) - .map(|time| target_solution[task * self.d_max + time] == 1) - .collect() - }) - .collect()) + crate::rules::traits::validate_target_witness( + self.target_problem(), + target_solution, + |value| value.value.is_some(), + "target ILP assignment is infeasible", + )?; + + let mut schedule = vec![vec![false; self.d_max]; self.num_tasks]; + for (variable, &(task, time)) in self.slots.iter().enumerate() { + schedule[task][time] = target_solution[variable] == 1; + } + Ok(schedule) } } #[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", + num_vars = "num_admissible_slots + 2 * num_tasks + 1", + num_constraints = "2 * num_tasks + schedule_horizon + 2 * num_admissible_slots + num_precedences", + num_nonzeros = "6 * num_admissible_slots + 2 * num_precedences + 2 * num_tasks", }, 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)", + max_constraint_magnitude_bits = "max_schedule_magnitude_bits", }, })] impl ReduceTo> for PreemptiveScheduling { @@ -82,92 +67,102 @@ impl ReduceTo> for PreemptiveScheduling { fn reduce_to(&self) -> Result { let n = self.num_tasks(); - let d = self.d_max(); - let num_task_vars = n * d; + let (d, windows) = self.scheduling_windows(); + let num_task_vars: usize = windows.iter().map(|window| window.len()).sum(); let m_var = num_task_vars; // index of the makespan variable M - let num_vars = num_task_vars + 1; + let num_vars = n + .checked_mul(2) + .and_then(|endpoints| num_task_vars.checked_add(endpoints)) + .and_then(|total| total.checked_add(1)) + .ok_or_else(|| { + crate::rules::ReductionError::integer_overflow::>( + "counting scheduling variables", + ) + })?; + let horizon = Self::exact_i64(d, "bounding the scheduling horizon")?; let lengths = self.lengths(); - let processor_count = - Self::exact_i64(self.num_processors(), "encoding the processor capacity")?; - - let x = |t: usize, u: usize| t * d + u; + let processor_count = Self::exact_i64( + self.num_processors().min(n), + "encoding the processor capacity", + )?; + + // Check endpoint arithmetic before allocating the time-indexed matrix. + horizon.checked_mul(2).ok_or_else(|| { + crate::rules::ReductionError::integer_overflow::>( + "bounding endpoint constraint evaluation", + ) + })?; + let slots: Vec<_> = windows + .into_iter() + .enumerate() + .flat_map(|(task, window)| window.map(move |time| (task, time))) + .collect(); + let mut task_terms = vec![Vec::new(); n]; + let mut time_terms = vec![Vec::new(); d]; + for (variable, &(task, time)) in slots.iter().enumerate() { + task_terms[task].push((variable, 1)); + time_terms[time].push((variable, 1)); + } + let start = |t: usize| num_task_vars + 1 + t; + let completion = |t: usize| num_task_vars + 1 + n + t; let mut constraints = Vec::new(); - // 1. Work constraints: Σ_u x_{t,u} = l(t) for each task t - for (t, &length) in lengths.iter().enumerate() { - let terms: Vec<(usize, i64)> = (0..d).map(|u| (x(t, u), 1)).collect(); + for (terms, &length) in task_terms.into_iter().zip(lengths) { constraints.push(LinearConstraint::eq(terms, length)); } - - // 2. Capacity constraints: Σ_t x_{t,u} ≤ m for each time slot u - for u in 0..d { - let terms: Vec<(usize, i64)> = (0..n).map(|t| (x(t, u), 1)).collect(); + for terms in time_terms { constraints.push(LinearConstraint::le(terms, processor_count)); } - - // 3. Precedence constraints: for each (pred, succ) and each slot u: - // l(pred) * x_{succ,u} ≤ Σ_{v=0}^{u-1} x_{pred,v} - // i.e. l(pred) * x_{succ,u} - Σ_{v=0}^{u-1} x_{pred,v} ≤ 0 - // - // Interpretation: succ can only be active at slot u once pred has - // accumulated all l(pred) units of work in strictly earlier slots. - for &(pred, succ) in self.precedences() { - let l_pred = lengths[pred]; - for u in 0..d { - // Σ_{v=0}^{u-1} x_{pred,v} - l(pred)*x_{succ,u} ≥ 0 - // i.e. l(pred)*x_{succ,u} - Σ_{v = Vec::new(); - // Cumulative pred work up to u-1 - for v in 0..u { - terms.push((x(pred, v), -1)); - } - terms.push((x(succ, u), l_pred)); - constraints.push(LinearConstraint::le(terms, 0)); - } + for (variable, &(t, u)) in slots.iter().enumerate() { + constraints.push(LinearConstraint::le( + vec![ + (start(t), 1), + (variable, Self::exact_i64(d - u, "encoding a start bound")?), + ], + horizon, + )); + constraints.push(LinearConstraint::ge( + vec![ + (completion(t), 1), + ( + variable, + -Self::exact_i64(u + 1, "encoding a completion bound")?, + ), + ], + 0, + )); } - - // 4. Makespan lower bound: M - (u+1)*x_{t,u} ≥ 0 for all t,u - for t in 0..n { - for u in 0..d { - constraints.push(LinearConstraint::ge( - vec![ - (m_var, 1), - (x(t, u), -Self::exact_i64(u + 1, "encoding a time slot")?), - ], - 0, - )); - } + for &(pred, succ) in self.precedences() { + constraints.push(LinearConstraint::le( + vec![(completion(pred), 1), (start(succ), -1)], + 0, + )); } - - // 5. Binary upper bound: x_{t,u} ≤ 1 for all t,u for t in 0..n { - for u in 0..d { - constraints.push(LinearConstraint::le(vec![(x(t, u), 1)], 1)); - } + constraints.push(LinearConstraint::le( + vec![(completion(t), 1), (m_var, -1)], + 0, + )); } // Objective: minimize M let objective = vec![(m_var, 1)]; - // All task slots end by d; lowering the makespan to d preserves every feasible schedule. + // Slot domains enforce binary values without redundant bound rows. 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)?; + variables[num_task_vars..] + .fill(IntegerVariable::new(Some(0), Some(horizon)).map_err(Self::target_construction)?); + let target = + ILP::with_variables(variables, constraints, objective, ObjectiveSense::Minimize) + .map_err(Self::target_construction)?; + crate::rules::ilp_helpers::validate_bounded_constraint_arithmetic::(&target)?; Ok(ReductionPSToILP { - target: ILP::with_variables( - variables, - constraints, - objective, - ObjectiveSense::Minimize, - ) - .map_err(Self::target_construction)?, + target, num_tasks: n, - d_max: d, + d_max: self.d_max(), + slots, }) } } diff --git a/src/rules/productionplanning_ilp.rs b/src/rules/productionplanning_ilp.rs new file mode 100644 index 000000000..cd673c3d1 --- /dev/null +++ b/src/rules/productionplanning_ilp.rs @@ -0,0 +1,116 @@ +//! Inventory conservation with exact production setup indicators. +use crate::models::algebraic::{Bounded, IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; +use crate::models::misc::ProductionPlanning; +use crate::rules::traits::{ReduceTo, ReductionResult}; + +#[derive(Debug, Clone)] +pub struct ReductionProductionPlanningToILP { + target: ILP, + num_periods: usize, +} +impl ReductionResult for ReductionProductionPlanningToILP { + type Source = ProductionPlanning; + 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( + &self.target, + solution, + |value| value.value.is_some(), + "target ILP assignment is infeasible", + )?; + crate::rules::ilp_helpers::decode_usize_values(&solution[..self.num_periods]) + } +} +#[crate::aggregate_reduction(ilp_feasibility)] +impl crate::rules::AggregateReductionResult for ReductionProductionPlanningToILP {} +#[crate::reduction(transform = { + exact { num_vars = "3 * num_periods", num_constraints = "3 * num_periods + 1", }, + upper_bound { + num_nonzeros = "10 * num_periods", + max_constraint_magnitude_bits = "max_numeric_magnitude_bits + num_periods", + }, +})] +impl ReduceTo> for ProductionPlanning { + type Result = ReductionProductionPlanningToILP; + fn reduce_to(&self) -> Result { + let overflow = |operation| { + crate::rules::ReductionError::integer_overflow::>( + operation, + ) + }; + let n = self.num_periods(); + let count = n + .checked_mul(3) + .ok_or_else(|| overflow("counting planning variables"))?; + self.demands() + .iter() + .try_fold(0_i64, |sum, &d| sum.checked_add(d)) + .ok_or_else(|| overflow("summing planning demands"))?; + let mut variables = Vec::with_capacity(count); + for &capacity in self.capacities() { + variables.push( + IntegerVariable::new(Some(0), Some(capacity)).map_err(Self::target_construction)?, + ); + } + let mut total = 0_i64; + for &capacity in self.capacities() { + total = total + .checked_add(capacity) + .ok_or_else(|| overflow("summing planning capacities"))?; + variables.push( + IntegerVariable::new(Some(0), Some(total)).map_err(Self::target_construction)?, + ); + } + variables.extend(std::iter::repeat_n(IntegerVariable::binary(), n)); + let mut constraints = Vec::new(); + let mut costs = Vec::new(); + for t in 0..n { + let mut flow = vec![(t, 1), (n + t, -1)]; + if t > 0 { + flow.push((n + t - 1, 1)); + } + constraints.push(LinearConstraint::eq(flow, self.demands()[t])); + constraints.push(LinearConstraint::le( + vec![(t, 1), (2 * n + t, -self.capacities()[t])], + 0, + )); + constraints.push(LinearConstraint::ge(vec![(t, 1), (2 * n + t, -1)], 0)); + costs.extend([ + (t, self.production_costs()[t]), + (n + t, self.inventory_costs()[t]), + (2 * n + t, self.setup_costs()[t]), + ]); + } + constraints.push(LinearConstraint::le(costs, self.cost_bound())); + let target = ILP::with_variables(variables, constraints, vec![], ObjectiveSense::Minimize) + .map_err(Self::target_construction)?; + crate::rules::ilp_helpers::validate_bounded_constraint_arithmetic::(&target)?; + Ok(Self::Result { + target, + num_periods: n, + }) + } +} +#[cfg(feature = "example-db")] +pub(crate) fn canonical_rule_example_specs() -> Vec { + vec![crate::example_db::specs::RuleExampleSpec { + id: "productionplanning_to_ilp", + build: || { + crate::example_db::specs::rule_example_via_bounded_ilp::<_>(ProductionPlanning::new( + 3, + vec![1; 3], + vec![2, 0, 1], + vec![2; 3], + vec![1; 3], + vec![1; 3], + 8, + )) + }, + }] +} +#[cfg(test)] +#[path = "../unit_tests/rules/productionplanning_ilp.rs"] +mod tests; diff --git a/src/rules/sequencingtominimizeweightedcompletiontime_ilp.rs b/src/rules/sequencingtominimizeweightedcompletiontime_ilp.rs index 2df4c39af..ab50deb31 100644 --- a/src/rules/sequencingtominimizeweightedcompletiontime_ilp.rs +++ b/src/rules/sequencingtominimizeweightedcompletiontime_ilp.rs @@ -1,9 +1,5 @@ -//! 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`. -//! For each unordered pair `{i, j}`, a pair of big-M constraints forces one -//! task to finish before the other starts. +//! Strict linear ordering with exact signed completion-time equations. +//! Triangle inequalities rule out cyclic orders even for zero-duration jobs. use crate::models::algebraic::{Bounded, IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::SequencingToMinimizeWeightedCompletionTime; @@ -16,19 +12,6 @@ pub struct ReductionSTMWCTToILP { num_tasks: usize, } -impl ReductionSTMWCTToILP { - #[cfg(test)] - pub(crate) fn completion_var(&self, task: usize) -> usize { - task - } - - #[cfg(test)] - pub(crate) fn order_var(&self, i: usize, j: usize) -> usize { - assert!(i < j, "order_var expects i < j"); - self.num_tasks + i * (2 * self.num_tasks - i - 1) / 2 + (j - i - 1) - } -} - impl ReductionResult for ReductionSTMWCTToILP { type Source = SequencingToMinimizeWeightedCompletionTime; type Target = ILP; @@ -41,113 +24,116 @@ impl ReductionResult for ReductionSTMWCTToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - - Ok({ - let mut schedule: Vec = (0..self.num_tasks).collect(); - schedule.sort_by_key(|&task| (target_solution[task], task)); - schedule - }) + crate::rules::traits::validate_target_witness( + self.target_problem(), + target_solution, + |value| value.value.is_some(), + "target ILP assignment is infeasible", + )?; + + let mut ranks = vec![0; self.num_tasks]; + for i in 0..self.num_tasks { + for j in i + 1..self.num_tasks { + let pair = self.num_tasks + i * (2 * self.num_tasks - i - 1) / 2 + j - i - 1; + ranks[if target_solution[pair] == 1 { j } else { i }] += 1; + } + } + let mut order: Vec<_> = (0..self.num_tasks).collect(); + order.sort_by_key(|&j| ranks[j]); + Ok(order) } } #[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", + num_constraints = "num_tasks * (num_tasks - 1) * (num_tasks - 2) / 3 + num_tasks + 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)", + num_nonzeros = "num_tasks * (num_tasks - 1) * (num_tasks - 2) + num_tasks^2 + num_precedences", }, })] impl ReduceTo> for SequencingToMinimizeWeightedCompletionTime { type Result = ReductionSTMWCTToILP; fn reduce_to(&self) -> Result { - let num_tasks = self.num_tasks(); - - let total_processing_time = self.lengths().iter().try_fold(0i64, |total, &length| { - total.checked_add(length).ok_or_else(|| { - crate::rules::ReductionError::integer_overflow::< - SequencingToMinimizeWeightedCompletionTime, - ILP, - >("summing task processing times") - }) - })?; - let lengths = self.lengths(); - let weights = self.weights(); - let num_order_vars = num_tasks * (num_tasks.saturating_sub(1)) / 2; - let num_vars = num_tasks + num_order_vars; - - let order_var = |i: usize, j: usize| -> usize { - debug_assert!(i < j); - num_tasks + i * (2 * num_tasks - i - 1) / 2 + (j - i - 1) + let overflow = |operation: &str| { + crate::rules::ReductionError::integer_overflow::>( + operation, + ) }; - - let mut constraints = Vec::new(); - - for (task, &length) in lengths.iter().enumerate() { - constraints.push(LinearConstraint::ge(vec![(task, 1)], length)); - constraints.push(LinearConstraint::le(vec![(task, 1)], total_processing_time)); + let integer = |value: i128| { + i64::try_from(value).map_err(|_| overflow("representing an exact completion equation")) + }; + let n = self.num_tasks(); + let pairs = n + .checked_mul(n.saturating_sub(1)) + .ok_or_else(|| overflow("counting ordering variables"))? + / 2; + let num_vars = n + .checked_add(pairs) + .ok_or_else(|| overflow("counting sequencing variables"))?; + let rows = pairs + .checked_mul(n.saturating_sub(2)) + .and_then(|x| (x / 3).checked_mul(2)) + .and_then(|x| x.checked_add(n)) + .and_then(|x| x.checked_add(self.num_precedences())) + .ok_or_else(|| overflow("counting sequencing constraints"))?; + let lengths = self.lengths(); + // Any permutation prefix lies between the sums of negative and positive lengths. + let lower: i128 = lengths.iter().map(|&p| i128::from(p.min(0))).sum(); + let upper: i128 = lengths.iter().map(|&p| i128::from(p.max(0))).sum(); + integer(lower)?; + integer(upper)?; + let mut variables = Vec::with_capacity(num_vars); + for &p in lengths { + let low = integer(i128::from(p) + lower - i128::from(p.min(0)))?; + let high = integer(i128::from(p) + upper - i128::from(p.max(0)))?; + variables.push( + IntegerVariable::new(Some(low), Some(high)).map_err(Self::target_construction)?, + ); } - - for i in 0..num_tasks { - for j in (i + 1)..num_tasks { - let order = order_var(i, j); - let completion_i = i; - let completion_j = j; - let length_i = lengths[i]; - let length_j = lengths[j]; - - constraints.push(LinearConstraint::le(vec![(order, 1)], 1)); - - // If y_{i,j} = 1, then task i is before task j: C_j - C_i >= l_j. - constraints.push(LinearConstraint::ge( - vec![ - (completion_j, 1), - (completion_i, -1), - (order, -total_processing_time), - ], - length_j - total_processing_time, - )); - - // If y_{i,j} = 0, then task j is before task i: C_i - C_j >= l_i. - constraints.push(LinearConstraint::ge( - vec![ - (completion_i, 1), - (completion_j, -1), - (order, total_processing_time), - ], - length_i, - )); + variables.resize(num_vars, IntegerVariable::binary()); + let pair = |i: usize, j: usize| n + i * (2 * n - i - 1) / 2 + j - i - 1; + let mut constraints = Vec::with_capacity(rows); + for i in 0..n { + for j in i + 1..n { + for k in j + 1..n { + let terms = vec![(pair(i, j), 1), (pair(j, k), 1), (pair(i, k), -1)]; + constraints.push(LinearConstraint::ge(terms.clone(), 0)); + constraints.push(LinearConstraint::le(terms, 1)); + } } } - - for &(pred, succ) in self.precedences() { - constraints.push(LinearConstraint::ge( - vec![(succ, 1), (pred, -1)], - lengths[succ], - )); + for &(a, b) in self.precedences() { + let terms = if a == b { + vec![] + } else { + vec![(pair(a.min(b), a.max(b)), 1)] + }; + constraints.push(LinearConstraint::eq(terms, i64::from(a <= b))); } - - let objective = weights.iter().copied().enumerate().collect(); - - 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)?, - ); - + for j in 0..n { + let mut terms = vec![(j, 1)]; + for (i, &p) in lengths.iter().enumerate().take(j) { + terms.push((pair(i, j), integer(-i128::from(p))?)); + } + for (i, &p) in lengths.iter().enumerate().skip(j + 1) { + terms.push((pair(j, i), p)); + } + let rhs = integer(lengths[j..].iter().map(|&p| i128::from(p)).sum())?; + constraints.push(LinearConstraint::eq(terms, rhs)); + } + // Keep the source's coefficients and task-index accumulation order exactly. + let objective = self.weights().iter().copied().enumerate().collect(); + let target = + ILP::with_variables(variables, constraints, objective, ObjectiveSense::Minimize) + .map_err(Self::target_construction)?; + crate::rules::ilp_helpers::validate_bounded_constraint_arithmetic::(&target)?; Ok(Self::Result { - target: ILP::with_variables( - variables, - constraints, - objective, - ObjectiveSense::Minimize, - ) - .map_err(Self::target_construction)?, - num_tasks, + target, + num_tasks: n, }) } } diff --git a/src/rules/sequencingwithreleasetimesanddeadlines_ilp.rs b/src/rules/sequencingwithreleasetimesanddeadlines_ilp.rs index 48b960215..e9d9541db 100644 --- a/src/rules/sequencingwithreleasetimesanddeadlines_ilp.rs +++ b/src/rules/sequencingwithreleasetimesanddeadlines_ilp.rs @@ -1,152 +1,174 @@ -//! Reduction from SequencingWithReleaseTimesAndDeadlines to `ILP`. -//! -//! Time-indexed formulation: binary x_{j,t} = 1 iff task j starts at time t. -//! Each task starts within its admissible window [r_j, d_j - p_j]. -//! No two tasks may overlap on the single machine. +//! Exact release/deadline sequencing using bounded starts and pairwise order. +//! The binary endpoint uniformly composes the same construction with the +//! existing bounded-integer encoding; neither construction expands time slots. -use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{Bounded, IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::SequencingWithReleaseTimesAndDeadlines; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; -/// Result of reducing SequencingWithReleaseTimesAndDeadlines to `ILP`. -/// -/// Variable layout: x_{j,t} at index `j * T + t` for j in 0..n, t in 0..T, -/// where T = time_horizon (max deadline). #[derive(Debug, Clone)] -pub struct ReductionSWRTDToILP { - target: ILP, - num_tasks: usize, - time_horizon: usize, +pub struct ReductionSWRTDToBoundedILP { + target: ILP, + lengths: Vec, } -impl ReductionResult for ReductionSWRTDToILP { +impl ReductionResult for ReductionSWRTDToBoundedILP { type Source = SequencingWithReleaseTimesAndDeadlines; - type Target = ILP; + type Target = ILP; - fn target_problem(&self) -> &ILP { + fn target_problem(&self) -> &Self::Target { &self.target } - /// Extract by reading each task's start time and sorting tasks by start time. - fn extract_solution( - &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + fn extract_solution(&self, values: &Vec) -> crate::rules::ExtractionResult> { crate::rules::traits::validate_target_witness( self.target_problem(), - target_solution, + values, |value| value.value.is_some(), "target ILP assignment is infeasible", )?; - - Ok({ - let n = self.num_tasks; - let horizon = self.time_horizon; - // For each task, find the start time - let starts = - crate::rules::ilp_helpers::one_hot_decode_rows(target_solution, n, horizon, 0)?; - let mut start_times: Vec<_> = starts.into_iter().enumerate().collect(); - // Sort by start time (break ties by task index) - start_times.sort_by_key(|&(j, t)| (t, j)); - let schedule: Vec = start_times.iter().map(|&(j, _)| j).collect(); - schedule - }) + let mut order: Vec<_> = (0..self.lengths.len()).collect(); + // A zero-duration job at a positive job's start must come first. + order.sort_by_key(|&j| (values[j], self.lengths[j], j)); + Ok(order) } } #[crate::aggregate_reduction(ilp_feasibility)] -impl crate::rules::AggregateReductionResult for ReductionSWRTDToILP {} +impl crate::rules::AggregateReductionResult for ReductionSWRTDToBoundedILP {} #[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)", + num_vars = "num_tasks + num_tasks * (num_tasks - 1) / 2", + num_constraints = "num_tasks * (num_tasks - 1) + 1", + num_nonzeros = "3 * num_tasks * (num_tasks - 1)", + max_constraint_magnitude_bits = "time_horizon_bits", })] -impl ReduceTo> for SequencingWithReleaseTimesAndDeadlines { - type Result = ReductionSWRTDToILP; +impl ReduceTo> for SequencingWithReleaseTimesAndDeadlines { + type Result = ReductionSWRTDToBoundedILP; fn reduce_to(&self) -> Result { + let construct = >>::target_construction; let n = self.num_tasks(); - let horizon = self.time_horizon() as usize; - let num_vars = n * horizon; - - let var = |j: usize, t: usize| -> usize { j * horizon + t }; - let lengths = self.lengths(); - let release_times = self.release_times(); + let releases = self.release_times(); let deadlines = self.deadlines(); - - let mut constraints = Vec::new(); - - // 1. Each task starts exactly once within its admissible window: - // Σ_{t=r_j}^{d_j-p_j} x_{j,t} = 1 for all j. - // Also, x_{j,t} = 0 for t outside the window (handled implicitly - // by not including them; add explicit zero constraints for safety). + if (0..n).any(|j| releases[j] > deadlines[j] || lengths[j] > deadlines[j] - releases[j]) { + return Ok(Self::Result { + target: ILP::with_variables( + vec![], + vec![LinearConstraint::eq(vec![], 1)], + vec![], + ObjectiveSense::Minimize, + ) + .map_err(construct)?, + lengths: lengths.to_vec(), + }); + } + let pairs = n + .checked_mul(n.saturating_sub(1)) + .and_then(|count| n.checked_add(count / 2)) + .ok_or_else(|| { + crate::rules::ReductionError::integer_overflow::>( + "counting sequencing variables", + ) + })?; + let mut variables = Vec::with_capacity(pairs); for j in 0..n { - let r = release_times[j] as usize; - let last_start = deadlines[j] - .checked_sub(lengths[j]) - .and_then(|time| usize::try_from(time).ok()); - let terms: Vec<(usize, i64)> = last_start - .filter(|&last| r <= last) - .into_iter() - .flat_map(|last| r..=last) - .filter(|&t| t < horizon) - .map(|t| (var(j, t), 1)) - .collect(); - constraints.push(LinearConstraint::eq(terms, 1)); - - // Zero-fix variables outside the admissible window - for t in 0..horizon { - if t < r || last_start.is_none_or(|last| t > last) { - constraints.push(LinearConstraint::eq(vec![(var(j, t), 1)], 0)); - } - } + variables.push( + IntegerVariable::new(Some(releases[j]), Some(deadlines[j] - lengths[j])) + .map_err(construct)?, + ); } - - // 2. No overlap: for each time instant tau in 0..horizon, - // Σ_{j,t : t <= tau < t + p_j} x_{j,t} <= 1 - for tau in 0..horizon { - let mut terms: Vec<(usize, i64)> = Vec::new(); - for (j, &len_j) in lengths.iter().enumerate() { - let p = len_j as usize; - // Task j started at time t overlaps tau iff t <= tau < t + p_j - // i.e., tau - p_j + 1 <= t <= tau, where t >= 0 - let t_min = (tau + 1).saturating_sub(p); - let t_max = tau; - for t in t_min..=t_max { - if t < horizon { - terms.push((var(j, t), 1)); - } - } + let mut constraints = Vec::new(); + for i in 0..n { + for j in i + 1..n { + let y = variables.len(); + // Relabel identical tasks into index order; their source data are interchangeable. + let identical = (lengths[i], releases[i], deadlines[i]) + == (lengths[j], releases[j], deadlines[j]); + variables.push( + IntegerVariable::new(Some(i64::from(identical)), Some(1)).map_err(construct)?, + ); + let forward = (deadlines[i] - releases[j]).max(0); + let backward = (deadlines[j] - releases[i]).max(0); + constraints.push(LinearConstraint::ge( + vec![(j, 1), (i, -1), (y, -forward)], + lengths[i] - forward, + )); + constraints.push(LinearConstraint::ge( + vec![(i, 1), (j, -1), (y, backward)], + lengths[j], + )); } - constraints.push(LinearConstraint::le(terms, 1)); } - - Ok(ReductionSWRTDToILP { - target: ILP::new(num_vars, constraints, vec![], ObjectiveSense::Minimize) - .map_err(Self::target_construction)?, - num_tasks: n, - time_horizon: horizon, + let target = ILP::with_variables(variables, constraints, vec![], ObjectiveSense::Minimize) + .map_err(construct)?; + crate::rules::ilp_helpers::validate_bounded_constraint_arithmetic::(&target)?; + Ok(Self::Result { + target, + lengths: lengths.to_vec(), }) } } +#[derive(Debug, Clone)] +pub struct ReductionSWRTDToILP { + bounded: ReductionSWRTDToBoundedILP, + binary: crate::rules::ilp_i64_ilp_bool::ReductionIntILPToBinaryILP, +} + +impl ReductionResult for ReductionSWRTDToILP { + type Source = SequencingWithReleaseTimesAndDeadlines; + type Target = ILP; + + fn target_problem(&self) -> &Self::Target { + self.binary.target_problem() + } + + fn extract_solution(&self, values: &Vec) -> crate::rules::ExtractionResult> { + self.bounded + .extract_solution(&self.binary.extract_solution(values)?) + } +} + +#[crate::aggregate_reduction(ilp_feasibility)] +impl crate::rules::AggregateReductionResult for ReductionSWRTDToILP {} + +#[reduction(transform = upper_bound { + num_vars = "num_tasks * time_horizon_bits + num_tasks * (num_tasks - 1) / 2", + num_constraints = "num_tasks * (num_tasks - 1) + 1", + num_nonzeros = "num_tasks * (num_tasks - 1) * (2 * time_horizon_bits + 1)", + max_constraint_magnitude_bits = "time_horizon_bits", +})] +impl ReduceTo> for SequencingWithReleaseTimesAndDeadlines { + type Result = ReductionSWRTDToILP; + + fn reduce_to(&self) -> Result { + let bounded = ReduceTo::>::reduce_to(self)?; + let binary = ReduceTo::>::reduce_to(bounded.target_problem())?; + Ok(Self::Result { bounded, binary }) + } +} + #[cfg(feature = "example-db")] pub(crate) fn canonical_rule_example_specs() -> Vec { - vec![crate::example_db::specs::RuleExampleSpec { - id: "sequencingwithreleasetimesanddeadlines_to_ilp", - build: || { - let source = SequencingWithReleaseTimesAndDeadlines::new( - vec![1, 2, 1], - vec![0, 0, 2], - vec![3, 3, 4], - ); - crate::example_db::specs::rule_example_via_ilp::<_, bool>(source) + use crate::example_db::specs::{ + rule_example_via_bounded_ilp, rule_example_via_ilp, RuleExampleSpec, + }; + fn source() -> SequencingWithReleaseTimesAndDeadlines { + SequencingWithReleaseTimesAndDeadlines::new(vec![1, 2, 1], vec![0, 0, 2], vec![3, 3, 4]) + } + vec![ + RuleExampleSpec { + id: "sequencingwithreleasetimesanddeadlines_to_ilp", + build: || rule_example_via_ilp::<_, bool>(source()), + }, + RuleExampleSpec { + id: "sequencingwithreleasetimesanddeadlines_to_bounded_ilp", + build: || rule_example_via_bounded_ilp(source()), }, - }] + ] } #[cfg(test)] diff --git a/src/rules/threepartition_ilp.rs b/src/rules/threepartition_ilp.rs new file mode 100644 index 000000000..6f9c7e9fe --- /dev/null +++ b/src/rules/threepartition_ilp.rs @@ -0,0 +1,109 @@ +//! Exact cover of indexed items by triples whose sizes sum to the bound. +use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; +use crate::models::misc::ThreePartition; +use crate::rules::traits::{ReduceTo, ReductionResult}; + +#[derive(Debug, Clone)] +pub struct ReductionThreePartitionToILP { + target: ILP, + triples: Vec<[usize; 3]>, + num_elements: usize, +} + +impl ReductionResult for ReductionThreePartitionToILP { + type Source = ThreePartition; + 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( + &self.target, + solution, + |value| value.value.is_some(), + "target ILP assignment is infeasible", + )?; + let mut groups = vec![0; self.num_elements]; + for (group, triple) in self + .triples + .iter() + .zip(solution) + .filter(|(_, selected)| **selected == 1) + .map(|(triple, _)| triple) + .enumerate() + { + for &item in triple { + groups[item] = group; + } + } + Ok(groups) + } +} +#[crate::aggregate_reduction(ilp_feasibility)] +impl crate::rules::AggregateReductionResult for ReductionThreePartitionToILP {} + +#[crate::reduction(transform = { + exact { + num_constraints = "num_elements", + max_constraint_magnitude_bits = "1", + }, + upper_bound { + num_vars = "num_elements * (num_elements - 1) * (num_elements - 2) / 6", + num_nonzeros = "num_elements * (num_elements - 1) * (num_elements - 2) / 2", + }, +})] +impl ReduceTo> for ThreePartition { + type Result = ReductionThreePartitionToILP; + fn reduce_to(&self) -> Result { + let mut indices = std::collections::BTreeMap::>::new(); + for (item, &size) in self.sizes().iter().enumerate() { + indices.entry(size).or_default().push(item); + } + let mut triples = Vec::new(); + let mut rows = vec![Vec::new(); self.num_elements()]; + for (i, &a) in self.sizes().iter().enumerate() { + for (j, &b) in self.sizes().iter().enumerate().skip(i + 1) { + // Classical input bounds imply a+b < bound, without overflow. + if let Some(candidates) = indices.get(&(self.bound() - a - b)) { + for &k in &candidates[candidates.partition_point(|&k| k <= j)..] { + for item in [i, j, k] { + rows[item].push((triples.len(), 1)); + } + triples.push([i, j, k]); + } + } + } + } + let target: ILP = ILP::new( + triples.len(), + rows.into_iter() + .map(|terms| LinearConstraint::eq(terms, 1)) + .collect(), + vec![], + ObjectiveSense::Minimize, + ) + .map_err(>>::target_construction)?; + Ok(Self::Result { + target, + triples, + num_elements: self.num_elements(), + }) + } +} + +#[cfg(feature = "example-db")] +pub(crate) fn canonical_rule_example_specs() -> Vec { + vec![crate::example_db::specs::RuleExampleSpec { + id: "threepartition_to_ilp", + build: || { + crate::example_db::specs::rule_example_via_ilp::<_, bool>(ThreePartition::new( + vec![4, 5, 6, 4, 6, 5], + 15, + )) + }, + }] +} + +#[cfg(test)] +#[path = "../unit_tests/rules/threepartition_ilp.rs"] +mod tests; diff --git a/src/rules/threepartition_sequencingwithreleasetimesanddeadlines.rs b/src/rules/threepartition_sequencingwithreleasetimesanddeadlines.rs index a77aea096..06d5b7a27 100644 --- a/src/rules/threepartition_sequencingwithreleasetimesanddeadlines.rs +++ b/src/rules/threepartition_sequencingwithreleasetimesanddeadlines.rs @@ -95,8 +95,9 @@ impl ReductionResult for ReductionThreePartitionToSRTD { impl crate::rules::AggregateReductionResult for ReductionThreePartitionToSRTD {} #[reduction( - transform = exact { - num_tasks = "num_elements + num_groups - 1", + transform = { + exact { num_tasks = "num_elements + num_groups - 1", }, + upper_bound { time_horizon_bits = "max_numeric_magnitude_bits + num_groups + 1", }, }, unavailable = { time_horizon = "the exact target parameter is not represented by this reduction's symbolic transform", diff --git a/src/rules/timetabledesign_ilp.rs b/src/rules/timetabledesign_ilp.rs index 07a612bea..82572acbe 100644 --- a/src/rules/timetabledesign_ilp.rs +++ b/src/rules/timetabledesign_ilp.rs @@ -10,15 +10,13 @@ use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; /// Result of reducing TimetableDesign to `ILP`. -/// -/// Variable layout: x_{c,t,h} at index `((c * num_tasks) + t) * num_periods + h` -/// exactly matching the source configuration layout. #[derive(Debug, Clone)] pub struct ReductionTDToILP { target: ILP, num_craftsmen: usize, num_tasks: usize, num_periods: usize, + assignments: Vec<(usize, usize, usize)>, } impl ReductionResult for ReductionTDToILP { @@ -29,8 +27,6 @@ impl ReductionResult for ReductionTDToILP { &self.target } - /// Extract: direct identity mapping — the ILP variable layout matches the - /// source configuration layout exactly. fn extract_solution( &self, target_solution: &::Solution, @@ -42,36 +38,28 @@ impl ReductionResult for ReductionTDToILP { "target ILP assignment is infeasible", )?; - Ok((0..self.num_craftsmen) - .map(|craftsman| { - (0..self.num_tasks) - .map(|task| { - (0..self.num_periods) - .map(|period| { - let index = ((craftsman * self.num_tasks) + task) - * self.num_periods - + period; - target_solution[index] == 1 - }) - .collect() - }) - .collect() - }) - .collect()) + let mut timetable = + vec![vec![vec![false; self.num_periods]; self.num_tasks]; self.num_craftsmen]; + for (variable, &(craftsman, task, period)) in self.assignments.iter().enumerate() { + timetable[craftsman][task][period] = target_solution[variable] == 1; + } + Ok(timetable) } } #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionTDToILP {} -#[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)", +#[reduction(transform = { + exact { + num_vars = "num_available_assignments", + num_nonzeros = "3 * num_available_assignments", + }, + upper_bound { + num_constraints = "2 * num_available_assignments + num_nonzero_requirements", + max_constraint_magnitude_bits = "period_count_bits + 1", }, -)] +})] impl ReduceTo> for TimetableDesign { type Result = ReductionTDToILP; @@ -82,56 +70,51 @@ impl ReduceTo> for TimetableDesign { 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 }; - - let mut constraints = Vec::new(); - - // 1. Availability: x_{c,t,h} = 0 whenever craftsman c or task t is unavailable in h - for c in 0..nc { - for t in 0..nt { - for h in 0..nh { - if !self.craftsman_avail()[c][h] || !self.task_avail()[t][h] { - constraints.push(LinearConstraint::eq(vec![(var(c, t, h), 1)], 0)); - } - } - } - } - - // 2. Each craftsman works on at most one task per period: Σ_t x_{c,t,h} <= 1 for all c, h - for c in 0..nc { - for h in 0..nh { - let terms: Vec<(usize, i64)> = (0..nt).map(|t| (var(c, t, h), 1)).collect(); - constraints.push(LinearConstraint::le(terms, 1)); - } - } - - // 3. Each task worked on by at most one craftsman per period: Σ_c x_{c,t,h} <= 1 for all t, h - for t in 0..nt { - for h in 0..nh { - let terms: Vec<(usize, i64)> = (0..nc).map(|c| (var(c, t, h), 1)).collect(); - constraints.push(LinearConstraint::le(terms, 1)); - } - } - - // 4. Exact requirements: Σ_h x_{c,t,h} = r_{c,t} for all c, t + let mut assignments = Vec::new(); + let mut craftsmen = std::collections::BTreeMap::<_, Vec<_>>::new(); + let mut tasks = std::collections::BTreeMap::<_, Vec<_>>::new(); + let mut pairs = Vec::new(); 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( + if requirement == 0 { + continue; + } + let mut terms = Vec::new(); + if requirement > 0 { + for h in 0..nh { + if self.craftsman_avail()[c][h] && self.task_avail()[t][h] { + let term = (assignments.len(), 1); + assignments.push((c, t, h)); + terms.push(term); + craftsmen.entry((c, h)).or_default().push(term); + tasks.entry((t, h)).or_default().push(term); + } + } + } + pairs.push(LinearConstraint::eq( terms, requirement.clamp(-1, max_requirement), )); } } - + let mut constraints: Vec<_> = craftsmen + .into_values() + .chain(tasks.into_values()) + .map(|terms| LinearConstraint::le(terms, 1)) + .collect(); + constraints.extend(pairs); Ok(ReductionTDToILP { - target: ILP::new(num_vars, constraints, vec![], ObjectiveSense::Minimize) - .map_err(Self::target_construction)?, + target: ILP::new( + assignments.len(), + constraints, + vec![], + ObjectiveSense::Minimize, + ) + .map_err(Self::target_construction)?, num_craftsmen: nc, num_tasks: nt, num_periods: nh, + assignments, }) } } diff --git a/src/solvers/customized/solver.rs b/src/solvers/customized/solver.rs index 6d8bc66db..cca79253f 100644 --- a/src/solvers/customized/solver.rs +++ b/src/solvers/customized/solver.rs @@ -10,7 +10,7 @@ use crate::models::graph::{ }; use crate::models::misc::{ AdditionalKey, BoyceCoddNormalFormViolation, GroupingBySwapping, MinimumDecisionTree, - ShortestCommonSuperstring, TimetableDesign, + ShortestCommonSuperstring, }; use crate::models::set::{MinimumCardinalityKey, PrimeAttributeName}; use crate::solvers::registry::CustomizedSolverRegistration; @@ -86,12 +86,6 @@ register_customized_solver!( "maximal-clique-edge-cover", |problem| Ok(super::minimum_intersection_graph_basis::solve(problem)) ); -register_customized_solver!( - TimetableDesign, - "timetable-required-assignments", - |problem| Ok(TimetableDesign::solve_via_required_assignments(problem)) -); - register_customized_solver!( crate::models::algebraic::ClosestVectorProblem, "cvp-sphere-enumeration", diff --git a/src/solvers/pipelines.rs b/src/solvers/pipelines.rs index 6ab66ea38..c506ca029 100644 --- a/src/solvers/pipelines.rs +++ b/src/solvers/pipelines.rs @@ -766,7 +766,7 @@ register_ilp_pipeline! { register_ilp_pipeline! { ("SequencingWithReleaseTimesAndDeadlines", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), + ("ILP", [("variable", "i64"), ("coefficient", "i64"), ("bounds", "bounded")]), } register_ilp_pipeline! { @@ -843,7 +843,6 @@ register_ilp_pipeline! { register_ilp_pipeline! { ("ThreePartition", []), - ("ResourceConstrainedScheduling", []), ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } @@ -948,3 +947,13 @@ register_ilp_pipeline! { ("MinimumHittingSet", []), ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } + +register_ilp_pipeline! { + ("TimetableDesign", []), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), +} + +register_ilp_pipeline! { + ("ProductionPlanning", []), + ("ILP", [("variable", "i64"), ("coefficient", "i64"), ("bounds", "bounded")]), +} diff --git a/src/unit_tests/models/misc/timetable_design.rs b/src/unit_tests/models/misc/timetable_design.rs index 915a3eea9..12f660eaf 100644 --- a/src/unit_tests/models/misc/timetable_design.rs +++ b/src/unit_tests/models/misc/timetable_design.rs @@ -170,17 +170,17 @@ fn test_timetable_design_bruteforce_solver_finds_solution() { } #[test] -fn test_timetable_design_customized_solver_finds_feasible_solution() { +fn test_timetable_design_ilp_solver_finds_feasible_solution() { let problem = super::issue_example_problem(); - let solution = problem - .solve_via_required_assignments() - .expect("expected customized solver to find a satisfying timetable"); + let solution = crate::solvers::ILPSolver::new() + .solve(&problem) + .expect("expected ILP solver to find a satisfying timetable"); assert!(problem.evaluate(&solution).unwrap()); } #[test] -fn test_timetable_design_customized_solver_returns_none_for_infeasible_instance() { +fn test_timetable_design_ilp_solver_proves_infeasibility() { let problem = TimetableDesign::new( 1, 2, @@ -190,7 +190,10 @@ fn test_timetable_design_customized_solver_returns_none_for_infeasible_instance( vec![vec![1], vec![1]], ); - assert!(problem.solve_via_required_assignments().is_none()); + assert_eq!( + crate::solvers::ILPSolver::new().solve(&problem), + Err(crate::solvers::ILPSolveError::Infeasible) + ); } #[test] diff --git a/src/unit_tests/rules/openshopscheduling_ilp.rs b/src/unit_tests/rules/openshopscheduling_ilp.rs index f8a4c1641..b43b05a0c 100644 --- a/src/unit_tests/rules/openshopscheduling_ilp.rs +++ b/src/unit_tests/rules/openshopscheduling_ilp.rs @@ -13,7 +13,7 @@ fn small_instance() -> OpenShopScheduling { } #[test] -fn test_decision_openshopscheduling_to_ilp_bound_is_a_constraint() { +fn test_decision_openshopscheduling_to_ilp_preserves_makespan_threshold() { let inner = small_instance(); let optimization = ReduceTo::>::reduce_to(&inner).unwrap(); let solver = ILPSolver::new(); @@ -23,11 +23,8 @@ fn test_decision_openshopscheduling_to_ilp_bound_is_a_constraint() { 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()); - assert_eq!( - target.num_constraints(), - optimization.target_problem().num_constraints() + 1 - ); + assert!(target.num_vars() < optimization.target_problem().num_vars()); + crate::rules::test_helpers::assert_parameter_predictions(&source, &reduction); let result = solver.solve(&source); if bound < 3 { assert!(matches!( @@ -77,14 +74,8 @@ fn test_openshopscheduling_to_ilp_structure_small() { "expected 9 variables, got {}", ilp.num_vars() ); - // Constraint count: 2 bound_x + 4 s_upper + 1 c_upper + 4 machine_nooverlap - // + 2 bound_y + 4 job_nooverlap + 4 makespan = 21 - assert_eq!( - ilp.constraints().len(), - 21, - "expected 21 constraints, got {}", - ilp.constraints().len() - ); + // Four disjunction pairs need eight rows; four makespan rows remain. + assert_eq!(ilp.num_constraints(), 12); assert_eq!( ilp.objective(), vec![(8, 1)], @@ -205,3 +196,53 @@ fn test_decision_makespan_threshold_normalization() { } } } + +#[test] +fn decision_open_shop_uses_the_bound_without_a_makespan_variable() { + let source = Decision::new(OpenShopScheduling::new(2, vec![vec![1, 1], vec![1, 1]]), 2); + let reduced = ReduceTo::>::reduce_to(&source).unwrap(); + assert_eq!(reduced.target_problem().num_vars(), 8); + assert_eq!(reduced.target_problem().num_constraints(), 9); + assert_eq!(reduced.target_problem().num_nonzeros(), 26); + let solution = ILPSolver::new().solve(&source).unwrap(); + assert_eq!(source.evaluate(&solution).unwrap(), crate::types::Or(true)); +} + +#[test] +fn symmetry_preserves_zero_duration_and_asymmetric_open_shops() { + use crate::solvers::{BruteForce, ILPSolveError}; + for times in [ + vec![], + vec![vec![]], + vec![vec![0, 0], vec![0, 0]], + vec![vec![0, 1], vec![1, 0]], + vec![vec![1, 1], vec![1, 1]], + ] { + let machines = times.first().map_or(0, Vec::len); + let source = OpenShopScheduling::new(machines, times); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + crate::rules::test_helpers::assert_parameter_predictions(&source, &reduction); + let expected = BruteForce::new().solve(&source).unwrap().unwrap(); + let actual = ILPSolver::new().solve(&source).unwrap(); + assert_eq!(source.evaluate(&actual), source.evaluate(&expected)); + for bound in [-1, 0, 1, 2] { + let decision = Decision::new(source.clone(), bound); + let reduced = ReduceTo::>::reduce_to(&decision).unwrap(); + crate::rules::test_helpers::assert_parameter_predictions(&decision, &reduced); + match ILPSolver::new().solve(&decision) { + Ok(witness) => assert!(decision.evaluate(&witness).unwrap().0), + Err(ILPSolveError::Infeasible) => assert!(!decision.evaluate(&expected).unwrap().0), + other => panic!("unexpected solve: {other:?}"), + } + } + } +} + +#[test] +fn open_shop_reports_unrepresentable_constraint_arithmetic() { + let source = OpenShopScheduling::new(1, vec![vec![i64::MAX / 2], vec![i64::MAX / 2]]); + assert!(matches!( + ReduceTo::>::reduce_to(&source), + Err(crate::rules::ReductionError::IntegerOverflow { .. }) + )); +} diff --git a/src/unit_tests/rules/preemptivescheduling_ilp.rs b/src/unit_tests/rules/preemptivescheduling_ilp.rs index 4f14b120b..568fc2f5f 100644 --- a/src/unit_tests/rules/preemptivescheduling_ilp.rs +++ b/src/unit_tests/rules/preemptivescheduling_ilp.rs @@ -24,20 +24,11 @@ fn medium_instance() -> PreemptiveScheduling { #[test] 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"); - let ilp = reduction.target_problem(); - assert_eq!(ilp.num_vars(), 5, "expected n*D_max+1 = 5 variables"); - assert_eq!( - ilp.objective(), - vec![(4, 1)], - "objective: minimize M at index 4" - ); - - // Constraints: - // 2 work + 2 capacity + 1 prec*(D_max=2 slots) + 2*2 makespan + 2*2 binary = 2+2+2+4+4 = 14 - assert_eq!(ilp.constraints().len(), 14); + let reduction = ReduceTo::>::reduce_to(&p).unwrap(); + // Precedence leaves only task 0 at slot 0 and task 1 at slot 1. + assert_eq!(reduction.target_problem().num_vars(), 7); + assert_eq!(reduction.target_problem().num_constraints(), 11); + crate::rules::test_helpers::assert_parameter_predictions(&p, &reduction); } // ─── closed-loop ─────────────────────────────────────────────────────────── @@ -88,34 +79,103 @@ fn test_preemptivescheduling_to_ilp_medium_closed_loop() { ); } -// ─── infeasible ──────────────────────────────────────────────────────────── - -#[test] -fn test_preemptivescheduling_to_ilp_infeasible() { - // 1 processor, tasks t0→t1→t0 would be a cycle — let's just make a - // tight instance: 1 processor, 1 task of length 1, always feasible. - // Actually, let's check that a huge task on 1 tiny processor is fine - // (it's always feasible; makespan is just larger). - // 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"); - 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"); -} - // ─── extract_solution ────────────────────────────────────────────────────── #[test] fn test_preemptivescheduling_to_ilp_extract_solution() { - // small_instance: n=2, D_max=2, m_var=4 - // 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"); - let ilp_solution = vec![1, 0, 0, 1, 2]; // last element is M + let ilp_solution = vec![1, 1, 2, 0, 1, 1, 2]; // x, M, S, C let extracted = reduction.extract_solution(&ilp_solution).unwrap(); assert_eq!(extracted, vec![vec![true, false], vec![false, true]]); assert_eq!(p.evaluate(&extracted).unwrap(), Min(Some(2))); } + +#[test] +fn precedence_encoding_has_constant_size_per_edge() { + let edges: Vec<_> = (0..12) + .flat_map(|a| (a + 1..12).map(move |b| (a, b))) + .collect(); + let independent = PreemptiveScheduling::new(vec![3; 12], 4, vec![]).unwrap(); + let ordered = PreemptiveScheduling::new(vec![3; 12], 4, edges.clone()).unwrap(); + let base = ReduceTo::>::reduce_to(&independent).unwrap(); + let constrained = ReduceTo::>::reduce_to(&ordered).unwrap(); + // Finish-before-start needs just two task endpoints per edge, regardless of horizon. + assert!( + constrained.target_problem().num_nonzeros() + <= base.target_problem().num_nonzeros() + 2 * edges.len() + ); + crate::rules::test_helpers::assert_parameter_predictions(&ordered, &constrained); +} + +#[test] +fn scheduling_ilp_preserves_optima_and_rejects_precedence_cycles() { + use crate::solvers::{BruteForce, ILPSolveError}; + + for (lengths, processors, precedences) in [ + (vec![], 1, vec![]), + (vec![2, 1], 2, vec![]), + (vec![2, 1], 2, vec![(0, 1)]), + (vec![1, 1], 2, vec![(0, 1), (0, 1)]), + (vec![1], 1, vec![(0, 0)]), + (vec![1, 1], 2, vec![(0, 1), (1, 0)]), + (vec![1, 1, 1], 2, vec![(0, 1), (1, 2), (2, 0)]), + ] { + let source = PreemptiveScheduling::new(lengths, processors, precedences).unwrap(); + let expected = BruteForce::new().solve(&source).unwrap(); + let reduced = ReduceTo::>::reduce_to(&source).unwrap(); + crate::rules::test_helpers::assert_parameter_predictions(&source, &reduced); + match (expected, ILPSolver::new().solve(reduced.target_problem())) { + (Some(expected), Ok(actual)) => { + let decoded = reduced.extract_solution(&actual).unwrap(); + assert_eq!(source.evaluate(&decoded), source.evaluate(&expected)); + } + (None, Err(ILPSolveError::Infeasible)) => {} + other => panic!("source and target disagree: {other:?}"), + } + } +} + +#[test] +fn endpoint_encoding_allows_interruptions_but_rejects_early_successors() { + let source = PreemptiveScheduling::new(vec![2, 1, 1, 3], 2, vec![(0, 2)]).unwrap(); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + // Task 0 runs at 0 and 2; its successor may start at 3, not at 1. + let mut witness = vec![ + 1, 0, 1, 0, 1, 0, 0, 0, 1, 1, 1, 1, 0, 4, 0, 1, 3, 0, 3, 2, 4, 3, + ]; + let decoded = reduction.extract_solution(&witness).unwrap(); + assert_eq!(source.evaluate(&decoded).unwrap(), Min(Some(4))); + witness[7] = 1; + witness[8] = 0; + witness[16] = 2; + witness[20] = 3; + assert!(!reduction.target_problem().is_feasible(&witness).unwrap()); + assert!(reduction.extract_solution(&witness).is_err()); +} + +#[test] +fn scheduling_arithmetic_overflow_is_reported_before_allocation() { + let horizon = i64::try_from(usize::MAX / 2).unwrap(); + // One case exceeds the variable count, the other endpoint-row arithmetic. + for lengths in [vec![horizon - 1, 1], vec![horizon]] { + let source = PreemptiveScheduling::new(lengths, 1, vec![]).unwrap(); + assert!(matches!( + ReduceTo::>::reduce_to(&source), + Err(crate::rules::ReductionError::IntegerOverflow { .. }) + )); + } +} + +#[test] +fn parallel_work_uses_a_certified_horizon_instead_of_the_serial_horizon() { + let source = PreemptiveScheduling::new(vec![1; 4], 2, vec![]).unwrap(); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + // Two slots suffice; eight activity bits plus two endpoints per task and M. + assert!(reduction.target_problem().num_vars() <= 17); + let values = ILPSolver::new().solve(reduction.target_problem()).unwrap(); + let decoded = reduction.extract_solution(&values).unwrap(); + assert_eq!(source.evaluate(&decoded).unwrap(), Min(Some(2))); + assert!(decoded.iter().all(|task| task.len() == 4)); +} diff --git a/src/unit_tests/rules/productionplanning_ilp.rs b/src/unit_tests/rules/productionplanning_ilp.rs new file mode 100644 index 000000000..1413e8d3e --- /dev/null +++ b/src/unit_tests/rules/productionplanning_ilp.rs @@ -0,0 +1,71 @@ +use super::*; +use crate::{ + solvers::{BruteForce, ILPSolveError, ILPSolver}, + Problem, +}; + +#[test] +fn inventory_and_setup_formulation_matches_exhaustive_production_plans() { + for capacities in [vec![2, 0, 1], vec![0, 0, 0], vec![2, 2, 2]] { + for demands in [vec![1, 1, 1], vec![0, 0, 0], vec![0, 1, 2]] { + for budget in [0, 3, 7, 8] { + let source = ProductionPlanning::new( + 3, + demands.clone(), + capacities.clone(), + vec![2; 3], + vec![1; 3], + vec![1; 3], + budget, + ); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + crate::rules::test_helpers::assert_parameter_predictions(&source, &reduction); + match ( + BruteForce::new().solve(&source).unwrap(), + ILPSolver::new().solve(reduction.target_problem()), + ) { + (Some(_), Ok(witness)) => assert!( + source + .evaluate(&reduction.extract_solution(&witness).unwrap()) + .unwrap() + .0 + ), + (None, Err(ILPSolveError::Infeasible)) => {} + other => panic!("planning disagreement: {other:?}"), + } + assert!(reduction.extract_solution(&vec![]).is_err()); + assert!(reduction.extract_solution(&vec![-1; 9]).is_err()); + } + } + } +} + +#[test] +fn unrepresentable_planning_arithmetic_is_an_error_not_infeasibility() { + for source in [ + ProductionPlanning::new( + 2, + vec![i64::MAX, 1], + vec![0; 2], + vec![0; 2], + vec![0; 2], + vec![0; 2], + 0, + ), + ProductionPlanning::new( + 2, + vec![0; 2], + vec![i64::MAX, 1], + vec![0; 2], + vec![0; 2], + vec![0; 2], + 0, + ), + ProductionPlanning::new(1, vec![0], vec![2], vec![0], vec![i64::MAX], vec![0], 0), + ] { + assert!(matches!( + ReduceTo::>::reduce_to(&source), + Err(crate::rules::ReductionError::IntegerOverflow { .. }) + )); + } +} diff --git a/src/unit_tests/rules/sequencingtominimizeweightedcompletiontime_ilp.rs b/src/unit_tests/rules/sequencingtominimizeweightedcompletiontime_ilp.rs index 9a4340b8a..4f49c9b50 100644 --- a/src/unit_tests/rules/sequencingtominimizeweightedcompletiontime_ilp.rs +++ b/src/unit_tests/rules/sequencingtominimizeweightedcompletiontime_ilp.rs @@ -12,11 +12,11 @@ fn test_reduction_creates_expected_ilp_shape() { ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); - // 2 completion variables + 1 pair-order variable. + // Two exact completion variables and one order bit. assert_eq!(ilp.num_vars(), 3); - // 2 lower bounds + 2 upper bounds + 1 binary upper bound + 2 disjunctive constraints. - assert_eq!(ilp.constraints().len(), 7); + // Each job has one exact completion equation; two jobs need no triangles. + assert_eq!(ilp.constraints().len(), 2); assert_eq!(ilp.sense(), ObjectiveSense::Minimize); // Objective is w_0 * C_0 + w_1 * C_1. @@ -24,21 +24,7 @@ fn test_reduction_creates_expected_ilp_shape() { } #[test] -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"); - - assert_eq!(reduction.completion_var(0), 0); - assert_eq!(reduction.completion_var(2), 2); - assert_eq!(reduction.order_var(0, 1), 3); - assert_eq!(reduction.order_var(0, 2), 4); - assert_eq!(reduction.order_var(1, 2), 5); -} - -#[test] -fn test_extract_solution_encodes_schedule_as_lehmer_code() { +fn test_extract_solution_preserves_the_order_and_objective() { let problem = SequencingToMinimizeWeightedCompletionTime::new(vec![2, 1], vec![3, 5], vec![]); let reduction: ReductionSTMWCTToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); @@ -51,7 +37,7 @@ fn test_extract_solution_encodes_schedule_as_lehmer_code() { } #[test] -fn test_issue_example_closed_loop() { +fn test_precedence_weighted_completion_closed_loop() { let problem = SequencingToMinimizeWeightedCompletionTime::new( vec![2, 1, 3, 1, 2], vec![3, 5, 1, 4, 2], @@ -167,3 +153,43 @@ fn test_sequencingtominimizeweightedcompletiontime_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 zero_duration_jobs_keep_strict_precedence() { + let source = + SequencingToMinimizeWeightedCompletionTime::new(vec![0, 1], vec![1, 1], vec![(1, 0)]); + let witness = ILPSolver::new().solve(&source).unwrap(); + assert_eq!(witness, vec![1, 0]); + assert_eq!(source.evaluate(&witness).unwrap(), Min(Some(2))); +} + +#[test] +fn zero_duration_precedence_cycles_are_infeasible() { + let source = SequencingToMinimizeWeightedCompletionTime::new( + vec![0, 0], + vec![1, 1], + vec![(0, 1), (1, 0)], + ); + assert_eq!(BruteForce::new().solve(&source).unwrap(), None); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + assert!(reduction.extract_solution(&vec![0, 0, 0]).is_err()); + assert!(matches!( + ILPSolver::new().solve(&source), + Err(crate::solvers::ILPSolveError::Infeasible) + )); +} + +#[test] +fn signed_processing_times_preserve_the_permutation_objective() { + let source = SequencingToMinimizeWeightedCompletionTime::new( + vec![-1, 2, 0], + vec![-2, 3, -1], + vec![(2, 0)], + ); + let expected = BruteForce::new().solve(&source).unwrap().unwrap(); + let actual = ILPSolver::new().solve(&source).unwrap(); + assert_eq!( + source.evaluate(&actual).unwrap(), + source.evaluate(&expected).unwrap() + ); +} diff --git a/src/unit_tests/rules/sequencingwithreleasetimesanddeadlines_ilp.rs b/src/unit_tests/rules/sequencingwithreleasetimesanddeadlines_ilp.rs index 2598ef274..d3b6b4b0e 100644 --- a/src/unit_tests/rules/sequencingwithreleasetimesanddeadlines_ilp.rs +++ b/src/unit_tests/rules/sequencingwithreleasetimesanddeadlines_ilp.rs @@ -67,3 +67,51 @@ fn test_sequencingwithreleasetimesanddeadlines_to_ilp_single_task() { let extracted = reduction.extract_solution(&ilp_solution).unwrap(); assert_eq!(problem.evaluate(&extracted).unwrap(), Or(true)); } + +#[test] +fn zero_duration_tasks_obey_the_source_permutation_semantics() { + for (lengths, releases, deadlines, feasible) in [ + (vec![0], vec![0], vec![0], true), + (vec![2, 0], vec![0, 1], vec![2, 1], false), + (vec![1, 0], vec![0, 0], vec![1, 0], true), + ] { + let source = SequencingWithReleaseTimesAndDeadlines::new(lengths, releases, deadlines); + assert_eq!( + BruteForce::new().solve(&source).unwrap().is_some(), + feasible + ); + match ILPSolver::new().solve(&source) { + Ok(witness) => { + assert!(feasible); + assert_eq!(source.evaluate(&witness).unwrap(), Or(true)); + } + Err(error) => { + assert!(!feasible, "{error}"); + assert!(matches!(error, crate::solvers::ILPSolveError::Infeasible)); + } + } + } +} + +#[test] +fn identical_tasks_share_a_canonical_temporal_order() { + use crate::models::algebraic::Bounded; + let source = SequencingWithReleaseTimesAndDeadlines::new(vec![1; 3], vec![0; 3], vec![3; 3]); + assert_eq!(source.evaluate(&vec![2, 1, 0]).unwrap(), Or(true)); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + assert!(reduction + .target_problem() + .is_feasible(&[0, 1, 2, 1, 1, 1]) + .unwrap()); + // Reversing indistinguishable task labels must not create another target order. + assert!(!reduction + .target_problem() + .is_feasible(&[2, 1, 0, 0, 0, 0]) + .unwrap()); + assert_eq!( + source + .evaluate(&ILPSolver::new().solve(&source).unwrap()) + .unwrap(), + Or(true) + ); +} diff --git a/src/unit_tests/rules/threepartition_ilp.rs b/src/unit_tests/rules/threepartition_ilp.rs new file mode 100644 index 000000000..14f0ea7ea --- /dev/null +++ b/src/unit_tests/rules/threepartition_ilp.rs @@ -0,0 +1,34 @@ +use super::*; +use crate::{ + solvers::{BruteForce, ILPSolveError, ILPSolver}, + Problem, +}; + +#[test] +fn indexed_triples_preserve_duplicate_items_and_impossibility() { + for sizes in [vec![4, 5, 6, 4, 6, 5], vec![4, 4, 4, 6, 6, 6], vec![5; 6]] { + let source = ThreePartition::new(sizes, 15); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + crate::rules::test_helpers::assert_parameter_predictions(&source, &reduction); + let expected = BruteForce::new().solve(&source).unwrap(); + match (expected, ILPSolver::new().solve(reduction.target_problem())) { + (Some(_), Ok(witness)) => { + assert!( + source + .evaluate(&reduction.extract_solution(&witness).unwrap()) + .unwrap() + .0 + ); + assert!(reduction.extract_solution(&vec![0; witness.len()]).is_err()); + assert!(reduction.extract_solution(&vec![1; witness.len()]).is_err()); + } + (None, Err(ILPSolveError::Infeasible)) => { + assert!(reduction.extract_solution(&vec![]).is_err()) + } + other => panic!("partition disagreement: {other:?}"), + } + assert!(reduction + .extract_solution(&vec![0; reduction.target_problem().num_vars() + 1]) + .is_err()); + } +} diff --git a/src/unit_tests/rules/timetabledesign_ilp.rs b/src/unit_tests/rules/timetabledesign_ilp.rs index a37656505..a6b4d1925 100644 --- a/src/unit_tests/rules/timetabledesign_ilp.rs +++ b/src/unit_tests/rules/timetabledesign_ilp.rs @@ -58,7 +58,7 @@ fn test_timetabledesign_to_ilp_infeasible() { } #[test] -fn test_timetabledesign_to_ilp_identity_extraction() { +fn test_timetabledesign_to_ilp_sparse_extraction() { let problem = TimetableDesign::new( 2, 2, @@ -74,18 +74,9 @@ fn test_timetabledesign_to_ilp_identity_extraction() { .expect("ILP should be solvable"); let extracted = reduction.extract_solution(&ilp_solution).unwrap(); - assert_eq!( - extracted - .iter() - .flatten() - .flatten() - .copied() - .collect::>(), - ilp_solution - .iter() - .map(|&value| value != 0) - .collect::>() - ); + assert_eq!(extracted[0][1], vec![false; 2]); + assert_eq!(extracted[1][0], vec![false; 2]); + crate::rules::test_helpers::assert_parameter_predictions(&problem, &reduction); assert_eq!(problem.evaluate(&extracted).unwrap(), Or(true)); } @@ -119,3 +110,23 @@ fn test_timetable_threshold_normalization() { } } } + +#[test] +fn timetable_allocates_only_required_available_assignments() { + let source = TimetableDesign::new( + 2, + 2, + 2, + vec![vec![true, false], vec![false, true]], + vec![vec![true, true], vec![true, true]], + vec![vec![1, 0], vec![0, 1]], + ); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + assert_eq!(reduction.target_problem().num_vars(), 2); + let expected = vec![ + vec![vec![true, false], vec![false, false]], + vec![vec![false, false], vec![false, true]], + ]; + assert_eq!(source.evaluate(&expected).unwrap(), Or(true)); + assert_eq!(ILPSolver::new().solve(&source).unwrap(), expected); +} diff --git a/src/unit_tests/solvers/ilp/solver.rs b/src/unit_tests/solvers/ilp/solver.rs index 4b4b6fe6b..9af8ad377 100644 --- a/src/unit_tests/solvers/ilp/solver.rs +++ b/src/unit_tests/solvers/ilp/solver.rs @@ -399,3 +399,48 @@ fn test_ilp_solver_rejects_source_objective_overflow() { Err(ILPSolveError::InvalidSolution(_)) )); } + +#[test] +fn production_planning_pipeline_accounts_for_inventory_and_setups() { + use crate::models::misc::ProductionPlanning; + use crate::Problem; + for budget in [7, 8] { + let source = ProductionPlanning::new( + 3, + vec![1, 1, 1], + vec![2, 0, 1], + vec![2; 3], + vec![1; 3], + vec![1; 3], + budget, + ); + let expected = crate::solvers::BruteForce::new().solve(&source).unwrap(); + match (expected, crate::solvers::ILPSolver::new().solve(&source)) { + (Some(witness), Ok(actual)) => { + assert_eq!(actual, vec![2, 0, 1]); + assert_eq!(source.evaluate(&actual), source.evaluate(&witness)); + } + (None, Err(crate::solvers::ILPSolveError::Infeasible)) => {} + other => panic!("production pipeline disagrees: {other:?}"), + } + } +} + +#[test] +fn three_partition_pipeline_selects_triples_directly() { + let key = crate::solvers::registry::ExactProblemKey::new("ThreePartition", Default::default()); + let capabilities = crate::solvers::registry::solver_capabilities(&key).unwrap(); + let pipeline = capabilities.ilp.unwrap(); + assert_eq!(pipeline.path_labels().len(), 2); + for sizes in [vec![4, 5, 6, 4, 6, 5], vec![4, 4, 4, 6, 6, 6]] { + let source = crate::models::misc::ThreePartition::new(sizes, 15); + let expected = crate::solvers::BruteForce::new().solve(&source).unwrap(); + match (expected, crate::solvers::ILPSolver::new().solve(&source)) { + (Some(_), Ok(actual)) => { + assert_eq!(source.evaluate(&actual).unwrap(), crate::types::Or(true)) + } + (None, Err(crate::solvers::ILPSolveError::Infeasible)) => {} + other => panic!("partition pipeline disagrees: {other:?}"), + } + } +} diff --git a/src/unit_tests/solvers/registry.rs b/src/unit_tests/solvers/registry.rs index 230871390..535fab3cc 100644 --- a/src/unit_tests/solvers/registry.rs +++ b/src/unit_tests/solvers/registry.rs @@ -453,10 +453,10 @@ fn solver_capability_registry_exposes_representative_capability_classes() { ) }; - let customized_only = solver_capabilities(&key("TimetableDesign", &[])).unwrap(); + let customized_only = solver_capabilities(&key("MinimumDecisionTree", &[])).unwrap(); assert_eq!( customized_only.customized.unwrap().implementation, - "timetable-required-assignments" + "subset-dp" ); assert!(customized_only.ilp.is_none()); From cf454d9e9aba5c4a59f4e544635325194f130341 Mon Sep 17 00:00:00 2001 From: Xiwei Pan <90967972+isPANN@users.noreply.github.com> Date: Wed, 30 Sep 2026 03:29:54 -0700 Subject: [PATCH 14/22] Simplify scheduling solution extraction --- src/rules/openshopscheduling_ilp.rs | 18 ++++-------------- src/rules/preemptivescheduling_ilp.rs | 5 +++-- ...cingtominimizeweightedcompletiontime_ilp.rs | 3 ++- 3 files changed, 9 insertions(+), 17 deletions(-) diff --git a/src/rules/openshopscheduling_ilp.rs b/src/rules/openshopscheduling_ilp.rs index bb40d6be9..a36e46f61 100644 --- a/src/rules/openshopscheduling_ilp.rs +++ b/src/rules/openshopscheduling_ilp.rs @@ -13,12 +13,6 @@ pub struct ReductionOSSToILP { } impl ReductionOSSToILP { - fn decode_schedule(&self, solution: &[i64]) -> crate::rules::ExtractionResult> { - crate::rules::ilp_helpers::decode_usize_values( - &solution[self.start_offset..self.start_offset + self.num_operations], - ) - } - fn build( source: &OpenShopScheduling, bound: Option, @@ -173,7 +167,9 @@ impl ReductionResult for ReductionOSSToILP { |value| value.value.is_some(), "target ILP assignment is infeasible", )?; - self.decode_schedule(solution) + crate::rules::ilp_helpers::decode_usize_values( + &solution[self.start_offset..self.start_offset + self.num_operations], + ) } } @@ -203,13 +199,7 @@ impl ReductionResult for ReductionDecisionOpenShopSchedulingToILP { &self.inner.target } fn extract_solution(&self, solution: &Vec) -> crate::rules::ExtractionResult> { - crate::rules::traits::validate_target_witness( - self.target_problem(), - solution, - |value| value.value.is_some(), - "target ILP assignment is infeasible", - )?; - self.inner.decode_schedule(solution) + self.inner.extract_solution(solution) } } #[crate::aggregate_reduction(ilp_feasibility)] diff --git a/src/rules/preemptivescheduling_ilp.rs b/src/rules/preemptivescheduling_ilp.rs index 6996e47b5..f4db4af38 100644 --- a/src/rules/preemptivescheduling_ilp.rs +++ b/src/rules/preemptivescheduling_ilp.rs @@ -6,7 +6,8 @@ //! successors start. Minimize M with M >= C(t) for every task. //! //! Using task endpoints avoids repeating all earlier time slots for every -//! precedence edge: the matrix has 6*A + 2*p + 2*n for A admissible activity slots nonzeros. +//! precedence edge: the matrix has 6*A + 2*p + 2*n nonzeros for A admissible +//! activity slots, p precedence edges, and n tasks. use crate::models::algebraic::{Bounded, IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::PreemptiveScheduling; @@ -32,7 +33,7 @@ impl ReductionResult for ReductionPSToILP { /// Extract schedule from ILP solution. /// - /// Returns a binary config of length n * D_max: `config[t * D_max + u] = x_{t,u}`. + /// Returns the task-by-time activity matrix, restoring omitted slots to false. fn extract_solution( &self, target_solution: &::Solution, diff --git a/src/rules/sequencingtominimizeweightedcompletiontime_ilp.rs b/src/rules/sequencingtominimizeweightedcompletiontime_ilp.rs index ab50deb31..949c467a1 100644 --- a/src/rules/sequencingtominimizeweightedcompletiontime_ilp.rs +++ b/src/rules/sequencingtominimizeweightedcompletiontime_ilp.rs @@ -32,10 +32,11 @@ impl ReductionResult for ReductionSTMWCTToILP { )?; let mut ranks = vec![0; self.num_tasks]; + let mut pair = self.num_tasks; for i in 0..self.num_tasks { for j in i + 1..self.num_tasks { - let pair = self.num_tasks + i * (2 * self.num_tasks - i - 1) / 2 + j - i - 1; ranks[if target_solution[pair] == 1 { j } else { i }] += 1; + pair += 1; } } let mut order: Vec<_> = (0..self.num_tasks).collect(); From 5e501e8cdc6959f03a17724b778c9a3cb3ffe109 Mon Sep 17 00:00:00 2001 From: Xiwei Pan <90967972+isPANN@users.noreply.github.com> Date: Wed, 30 Sep 2026 03:35:41 -0700 Subject: [PATCH 15/22] Tighten ILP nonzero bounds using construction counts --- src/rules/biconnectivityaugmentation_ilp.rs | 5 +- src/rules/eulerianpath_ilp.rs | 4 +- src/rules/factoring_ilp.rs | 5 +- .../strongconnectivityaugmentation_ilp.rs | 5 +- .../parameter_formula_validation.rs | 61 +++++++++++++++++++ 5 files changed, 76 insertions(+), 4 deletions(-) diff --git a/src/rules/biconnectivityaugmentation_ilp.rs b/src/rules/biconnectivityaugmentation_ilp.rs index 9c5bcf819..f8bc8e07f 100644 --- a/src/rules/biconnectivityaugmentation_ilp.rs +++ b/src/rules/biconnectivityaugmentation_ilp.rs @@ -75,6 +75,9 @@ impl ReductionResult for ReductionBiconnAugToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionBiconnAugToILP {} +// Budget uses at most p terms. Each of n(n+1) commodities contributes at most +// 4m base-flow terms and 8p candidate-flow/activation terms. Deleted-edge pins +// replace those terms; trivial commodities and normalization only reduce them. #[reduction(transform = { exact { max_constraint_magnitude_bits = "max_numeric_magnitude_bits", @@ -82,7 +85,7 @@ impl crate::rules::AggregateReductionResult for ReductionBiconnAugToILP {} 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))", + num_nonzeros = "num_potential_edges + num_vertices * (num_vertices + 1) * (4 * num_edges + 8 * num_potential_edges)", }, })] impl ReduceTo> for BiconnectivityAugmentation { diff --git a/src/rules/eulerianpath_ilp.rs b/src/rules/eulerianpath_ilp.rs index 367b7efba..e6ed9125c 100644 --- a/src/rules/eulerianpath_ilp.rs +++ b/src/rules/eulerianpath_ilp.rs @@ -147,11 +147,13 @@ fn compatible_pairs(arcs: &[(usize, usize)]) -> Vec<(usize, usize)> { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionEulerianPathToILP {} +// For m arcs and P compatible distinct ordered pairs, row counts give exactly +// 7m+6P nonzeros when m>0. P<=m(m-1), and the empty construction has zero. #[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)", + num_nonzeros = "num_arcs + 6 * num_arcs * num_arcs", })] impl ReduceTo> for EulerianPath { type Result = ReductionEulerianPathToILP; diff --git a/src/rules/factoring_ilp.rs b/src/rules/factoring_ilp.rs index d3cabd998..3b86900ae 100644 --- a/src/rules/factoring_ilp.rs +++ b/src/rules/factoring_ilp.rs @@ -113,11 +113,14 @@ impl ReductionResult for ReductionFactoringToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionFactoringToILP {} +// With L=max(m+n,target_bits), McCormick rows use 7mn terms; bit equations, +// final carry, factor bounds and carry bounds use mn+m+n+4L more. +// Substitute L <= m+n+target_bits to obtain a bound using source parameters. #[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)", + num_nonzeros = "8 * num_bits_first * num_bits_second + 5 * num_bits_first + 5 * num_bits_second + 4 * target_bits", })] impl ReduceTo> for Factoring { type Result = ReductionFactoringToILP; diff --git a/src/rules/strongconnectivityaugmentation_ilp.rs b/src/rules/strongconnectivityaugmentation_ilp.rs index c2eb6d16d..debe00ffb 100644 --- a/src/rules/strongconnectivityaugmentation_ilp.rs +++ b/src/rules/strongconnectivityaugmentation_ilp.rs @@ -44,6 +44,9 @@ impl ReductionResult for ReductionSCAToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionSCAToILP {} +// Candidate bounds and budget use at most 2p terms. Each commodity has +// 4(m+p) forward/backward conservation terms and 4p activation terms; dummy +// commodity pins use only 2(m+p). Loop cancellation only reduces this count. #[reduction(transform = { exact { max_constraint_magnitude_bits = "max_numeric_magnitude_bits", @@ -51,7 +54,7 @@ impl crate::rules::AggregateReductionResult for ReductionSCAToILP {} 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)", + num_nonzeros = "2 * num_potential_arcs + num_vertices * (4 * num_arcs + 8 * num_potential_arcs)", }, })] impl ReduceTo> for StrongConnectivityAugmentation { diff --git a/src/unit_tests/parameter_formula_validation.rs b/src/unit_tests/parameter_formula_validation.rs index 117fbfa5a..bc09eb81e 100644 --- a/src/unit_tests/parameter_formula_validation.rs +++ b/src/unit_tests/parameter_formula_validation.rs @@ -228,3 +228,64 @@ fn every_executable_parameter_formula_matches_a_constructed_target() { assert!(failures.is_empty(), "{}", failures.join("\n")); assert_eq!(checked, expected, "some formula fields were not checked"); } + +#[test] +fn sparse_ilp_predictions_use_construction_counts() { + use crate::models::{BiconnectivityAugmentation, Factoring, StrongConnectivityAugmentation}; + use crate::topology::{DirectedGraph, SimpleGraph}; + use crate::traits::Problem; + + use crate::models::graph::EulerianPath; + + // Independent row-count budgets for these small constructions, before cancellations: + // biconnectivity: 3 budget terms + 20 commodities * (12 base + 24 candidate terms); + // strong connectivity: 4 candidate/budget terms + 3 commodities * (8 base + 16 candidate); + // factoring: 48 product terms + 5 factor bounds + at most 36 carry-related terms; + // Eulerian path: 28 arc terms + at most 12 ordered pairs * 6 terms. + let cases = [ + ( + "BiconnectivityAugmentation", + BiconnectivityAugmentation::new( + SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]), + vec![(0, 2, 1_i64), (0, 3, 2), (1, 3, 1)], + 3, + ) + .parameters(), + 723_u64, + ), + ( + "StrongConnectivityAugmentation", + StrongConnectivityAugmentation::new( + DirectedGraph::new(3, vec![(0, 1), (1, 2)]), + vec![(2, 0, 1_i64), (1, 0, 2)], + 2, + ) + .parameters(), + 76, + ), + ( + "Factoring", + Factoring::with_factor_bits(15, 2, 3).parameters(), + 89, + ), + ( + "EulerianPath", + EulerianPath::new(DirectedGraph::new(3, vec![(0, 1), (0, 1), (1, 2), (2, 0)])) + .parameters(), + 100, + ), + ]; + let entries = crate::rules::registry::reduction_entries(); + for (name, parameters, budget) in cases { + let entry = entries + .iter() + .find(|e| e.source_name == name && e.target_name == "ILP") + .unwrap(); + let contract = entry.parameter_contract().unwrap(); + let prediction = contract.transform().unwrap().evaluate(¶meters).unwrap(); + assert!( + prediction.get("num_nonzeros").unwrap() <= budget, + "{name}: sparse count budget exceeded" + ); + } +} From 463c03604eee446b4ea087621dbcd106ac477dd5 Mon Sep 17 00:00:00 2001 From: Xiwei Pan <90967972+isPANN@users.noreply.github.com> Date: Wed, 30 Sep 2026 10:46:30 -0700 Subject: [PATCH 16/22] Compact exact reductions and register missing solver pipelines Replace oversized formulations with compact constructions for partition, register, matrix, graph, and ordering problems. Add direct bounded ILP pipelines and reuse exact customized subset-sum and clique-cover solvers. Preserve signed weights and costs, avoid artificial partition-bound overflow, and align overhead contracts, rule targets, tests, and proofs with the resulting constructions. Validated with make check, make paper, independent reduction audits, and CLI solver and extraction round trips. --- docs/paper/reductions.typ | 397 ++++++++++++------ src/rules/acyclicpartition_ilp.rs | 148 +++---- src/rules/betweenness_ilp.rs | 87 ++++ src/rules/bmf_ilp.rs | 122 +++--- src/rules/boundeddiameterspanningtree_ilp.rs | 153 +++++++ .../consecutiveonesmatrixaugmentation_ilp.rs | 230 +++------- src/rules/cyclicordering_ilp.rs | 101 +++++ src/rules/feasibleregisterassignment_ilp.rs | 138 +++--- src/rules/hamiltonianpath_ilp.rs | 95 ++--- src/rules/ilp_helpers.rs | 27 ++ ...mumcodegenerationunlimitedregisters_ilp.rs | 115 +++++ src/rules/minimumweightandorgraph_ilp.rs | 117 ++++++ src/rules/mod.rs | 14 + .../partitionintoperfectmatchings_ilp.rs | 117 ++++++ src/rules/registersufficiency_ilp.rs | 215 ++++------ .../twodimensionalconsecutivesets_ilp.rs | 86 ++++ .../minimum_intersection_graph_basis.rs | 77 +++- src/solvers/customized/mod.rs | 1 + src/solvers/customized/solver.rs | 22 + src/solvers/customized/subset_sum.rs | 53 +++ src/solvers/pipelines.rs | 41 +- src/unit_tests/ilp_overhead.rs | 32 +- src/unit_tests/rules/acyclicpartition_ilp.rs | 20 +- src/unit_tests/rules/betweenness_ilp.rs | 39 ++ src/unit_tests/rules/bmf_ilp.rs | 57 ++- .../rules/boundeddiameterspanningtree_ilp.rs | 78 ++++ .../consecutiveonesmatrixaugmentation_ilp.rs | 80 +++- src/unit_tests/rules/cyclicordering_ilp.rs | 37 ++ .../rules/feasibleregisterassignment_ilp.rs | 22 +- src/unit_tests/rules/hamiltonianpath_ilp.rs | 6 +- ...mumcodegenerationunlimitedregisters_ilp.rs | 54 +++ .../rules/minimumweightandorgraph_ilp.rs | 113 +++++ .../partitionintoperfectmatchings_ilp.rs | 50 +++ .../rules/registersufficiency_ilp.rs | 78 +++- .../twodimensionalconsecutivesets_ilp.rs | 36 ++ .../minimum_intersection_graph_basis.rs | 13 + src/unit_tests/solvers/customized/solver.rs | 87 ++++ src/unit_tests/solvers/ilp/solver.rs | 149 +++++++ src/unit_tests/solvers/resolver.rs | 6 - .../symbolic_parameter_contracts.rs | 19 +- 40 files changed, 2508 insertions(+), 824 deletions(-) create mode 100644 src/rules/betweenness_ilp.rs create mode 100644 src/rules/boundeddiameterspanningtree_ilp.rs create mode 100644 src/rules/cyclicordering_ilp.rs create mode 100644 src/rules/minimumcodegenerationunlimitedregisters_ilp.rs create mode 100644 src/rules/minimumweightandorgraph_ilp.rs create mode 100644 src/rules/partitionintoperfectmatchings_ilp.rs create mode 100644 src/rules/twodimensionalconsecutivesets_ilp.rs create mode 100644 src/solvers/customized/subset_sum.rs create mode 100644 src/unit_tests/rules/betweenness_ilp.rs create mode 100644 src/unit_tests/rules/boundeddiameterspanningtree_ilp.rs create mode 100644 src/unit_tests/rules/cyclicordering_ilp.rs create mode 100644 src/unit_tests/rules/minimumcodegenerationunlimitedregisters_ilp.rs create mode 100644 src/unit_tests/rules/minimumweightandorgraph_ilp.rs create mode 100644 src/unit_tests/rules/partitionintoperfectmatchings_ilp.rs create mode 100644 src/unit_tests/rules/twodimensionalconsecutivesets_ilp.rs diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index c100aa3b6..916fbfdb6 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -14286,6 +14286,116 @@ The following reductions to Integer Linear Programming are straightforward formu _Solution extraction._ $cal(C) = {T_j : x_j = 1}$. ] +#reduction-rule("MinimumCodeGenerationUnlimitedRegisters", "ILP", example: true)[ + For the $k$ internal operations, use integer ranks $p_v in {0, dots, k-1}$, + binary copy indicators $y_v$, and a fixed variable $z=1$. For each internal + operand $u$ of $v$, impose $p_v-p_u >= 1$. For every distinct other operation + $u$ using the left operand of $v$, impose $p_v-p_u+k y_v >= 1$. + Minimize $k z + sum_v y_v$ and extract positions by sorting ranks, breaking ties + by internal vertex index. With $n$ input vertices, there are at most $2n+1$ + variables, $n^2+n$ rows, and $3n^2+n$ nonzeros. +][ + A valid order supplies ranks and its actual copy indicators, with equal cost. + Conversely, strict dependency comparisons survive sorting tied ranks. When + $y_v=0$, every other user of its left operand precedes $v$, so no copy is + required; otherwise the extracted order charges at most one copy. Its cost is + therefore at most the target cost, proving equal optima together with the + forward construction. Cyclic dependencies make both problems infeasible. + Repeated left/right uses by one operation count as one other user. +] + +#reduction-rule("PartitionIntoPerfectMatchings", "ILP", example: true)[ + For each vertex use a group label $c_v in {0, dots, K-1}$. Ignore loops and + repeated adjacencies. Each remaining edge $e=(u,v)$ has binary variables $s_e$ + and $y_e$. Require degree one in the selected $s$ edges. Writing $d=c_v-c_u$, + impose $d+(K-1)s_e <= K-1$, $-d+(K-1)s_e <= K-1$, + $d-K y_e+s_e >= 1-K$, and $-d+K y_e+s_e >= 1$. + The objective is zero; extract the group labels. With $n$ vertices and $m$ + stored edges, at most $n+2m$ variables, $n+4m$ rows, and $16m$ nonzeros suffice. +][ + A valid partition sets $s_e=1$ exactly for equal-label endpoints and chooses + the direction of unequal labels with $y_e$. Conversely, the first two rows + force equal labels when $s_e=1$, while the last two force unequal labels when + $s_e=0$. Degree one therefore holds exactly within each induced group. Every + nonempty group is a disjoint union of edges and hence a perfect matching. + The argument includes $K=1$, empty groups, and isolated vertices. +] + +#reduction-rule("MinimumWeightAndOrGraph", "ILP", example: true)[ + Use binary arc selections $x_e$, binary reached flags $z_v$, and integer + flows $f_e in {0,dots,n-1}$ on gate arcs (zero flow on leaf arcs). Mark the + source. Selected tails, and selected gate-arc heads, must be marked. Reached + AND gates select every outgoing arc; reached OR gates select at least one. + Require $f_e <= (n-1)x_e$ on gate arcs and incoming flow minus outgoing flow + at least $z_v$ at every nonsource vertex. Minimize the original selected-arc + weight. This uses $n+2m$ variables, at most $2n+4m$ rows and $2n+10m$ nonzeros. +][ + A valid source solution supplies its reached flags and sends one flow unit + to every reached nonsource vertex along a discovery tree. Conversely, summing + flow balances over any unreachable set rules out marked vertices there. + The local gate constraints then certify the source selection, with identical + cost. This includes cycles, signed weights, shared descendants, and the model's + nonpropagating leaf arcs. Integer objective prefixes agree in original arc + order, including typed overflow errors. +] + +#reduction-rule("BoundedDiameterSpanningTree", "ILP", example: true)[ + Clamp the diameter bound to $D' = min(D,n-1)$ and put $q=floor(D'/2)$. + Use selected-edge bits, two parent orientations per edge, root flags, and depths + in $0, dots, q$. Each nonroot has exactly one incoming parent, roots have depth + zero, and a selected parent strictly increases depth using coefficient $q+1$. + For even $D'$ choose one root. For odd $D'$ choose one center edge and make its + endpoints the two roots. An edge is selected exactly when used as a parent or + center edge. Enforce the original weight budget; loops are fixed unselected. + The empty graph maps to an empty feasible ILP. At most $4m+2n$ variables, + $3m+3n+2$ rows, and $16m+4n$ nonzeros suffice. +][ + Every bounded-diameter tree has a midpoint vertex or midpoint edge whose + rooted components have depth at most $q$. If its diameter is even but $D'$ is + odd, any edge incident to a midpoint vertex supplies the two centers. + Conversely, strict depth growth prevents parent cycles, and the parent counts + produce one rooted tree or two trees joined by the center edge. Any vertex + lies at most $q$ edges from its root, giving diameter at most $D'$. + Extraction takes the selected edges, whose weight satisfies the budget. +] + +#reduction-rule("CyclicOrdering", "ILP", example: true)[ + A sparse bounded-rank formulation uses $n+3t$ variables, $7t$ rows and $21t$ nonzeros for $n$ elements and $t$ cyclic triples. Its zero objective encodes feasibility. +][ + _Construction._ Assign each element an integer rank $p_v in [0,n-1]$. For each triple $(a,b,c)$ introduce three binary comparisons $y_(a b),y_(b c),y_(c a)$. For each directed pair $(u,v)$ enforce + $p_v-p_u-n y_(u v) >= 1-n, quad p_u-p_v+n y_(u v) >= 1.$ + Add $y_(a b)+y_(b c)+y_(c a)=2$. + + _Correctness._ ($arrow.r.double$) A valid source permutation sets the three bits to its comparison truth values. Each permitted cyclic orientation has exactly two true comparisons. ($arrow.l.double$) The inequalities force each bit to represent its strict comparison. Their sum restricts each triple to one of the three permitted orientations. Sorting all elements by rank, breaking ties by index, preserves every required strict comparison. No triple contains tied elements, so this produces a valid permutation. + + _Solution extraction._ Return the inverse of that sorted list: each element receives its new position. Unconstrained elements may have equal target ranks. +] + +#reduction-rule("Betweenness", "ILP", example: true)[ + A bounded-rank formulation uses $n+t$ variables, $4t$ rows and $12t$ nonzeros for $n$ elements and $t$ betweenness triples. +][ + _Construction._ Use integer ranks $p_v in [0,n-1]$. For each $(a,b,c)$ introduce one binary $y$ and, for both $(u,v)=(a,b)$ and $(b,c)$, add + $p_v-p_u-n y >= 1-n, quad p_u-p_v+n y >= 1.$ + The objective is zero. + + _Correctness._ ($arrow.r.double$) Copy the positions of a valid source permutation and set $y=1$ for $a= 1-n, quad p_u-p_v+n y >= 1,$ + $p_v-p_u <= k-1, quad p_u-p_v <= k-1.$ + Distinct subsets have separate selectors, including repeated subsets. The objective is zero. + + _Correctness._ ($arrow.r.double$) Compress a valid source assignment to dense group labels. Every subset occupies $k$ consecutive labels, so its pairs are distinct and differ by at most $k-1$; choose selectors according to their order. ($arrow.l.double$) The target gives every subset $k$ distinct integer labels with total span at most $k-1$, hence exactly $k$ consecutive labels. Global compression cannot remove a label inside such an interval because the subset itself occupies every label in it. Unrelated elements may share groups. + + _Solution extraction._ Return the first $n$ target labels. The model applies its usual global label compression. +] + #reduction-rule("OneInThreeSatisfiability", "ILP", example: true)[ Encode exact-one clauses directly with binary variables and one equality per clause. The elementary encoding below is derived directly from the literal semantics. @@ -15533,23 +15643,27 @@ The following reductions to Integer Linear Programming are straightforward formu // Position/Assignment #reduction-rule("HamiltonianPath", "ILP")[ - Place each vertex in exactly one path position and use auxiliary variables for consecutive pairs so only graph edges may appear between adjacent positions. + A binary permutation matrix with direct adjacency rows, using $n^2$ variables. ][ - _Construction._ Variables: binary $x_(v,p)$ with $x_(v,p) = 1$ iff vertex $v$ is placed at position $p$, and binary $z_((u,v),p)$ linearizing $x_(u,p) x_(v,p+1)$. The ILP is: - $ - "find" quad & bold(x) \ - "subject to" quad & sum_p x_(v,p) = 1 quad forall v \ - & sum_v x_(v,p) = 1 quad forall p \ - & z_((u,v),p) <= x_(u,p) quad forall (u, v), p \ - & z_((u,v),p) <= x_(v,p+1) quad forall (u, v), p \ - & z_((u,v),p) >= x_(u,p) + x_(v,p+1) - 1 quad forall (u, v), p \ - & sum_((u,v) in E) z_((u,v),p) = 1 quad forall p \ - & x_(v,p), z_((u,v),p) in {0, 1}. - $ + _Construction._ Let $x_(v,p)$ indicate vertex $v$ at position $p$. + Require each vertex and each position to occur exactly once. For every + nonfinal position $p$ and vertex $v$, impose + $x_(v,p) <= sum_(w in N(v)) x_(w,p+1)$, where $N(v)$ contains distinct + non-self neighbors. Minimize zero. + + _Correctness._ A Hamiltonian path supplies a permutation matrix satisfying + every adjacency row. Conversely, the assignment rows define a permutation. + At each nonfinal position its selected vertex forces the next vertex to be + a neighbor. Thus every feasible target decodes to a Hamiltonian path. + Duplicate edges and loops do not change adjacency between distinct vertices. + Empty and singleton paths require no adjacency rows. - _Correctness._ ($arrow.r.double$) A Hamiltonian path defines a permutation of the vertices and therefore a feasible assignment matrix with one admissible graph edge between every consecutive pair. ($arrow.l.double$) Any feasible ILP solution is a vertex permutation whose consecutive pairs are graph edges, hence a Hamiltonian path. + _Overhead._ With $q=max(n-1,0)$, there are exactly $n^2$ variables and + $2n+n q$ rows. If $e$ is the number of distinct non-loop edges, nonzeros + equal $2n^2+q(n+2e)$, bounded by $2n^2+q(n+2m)$ for the stored edge count + $m$. Coefficients and right-hand sides have magnitude at most one. - _Solution extraction._ For each position $p$, output the unique vertex $v$ with $x_(v,p) = 1$. + _Solution extraction._ Return the unique selected vertex at each position. ] #reduction-rule("DirectedHamiltonianPath", "ILP")[ @@ -15833,45 +15947,38 @@ 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 - $"idx"_x(v,c) = v n + c$, - $"idx"_s(t,c) = n^2 + t n + c$, - and $"idx"_y(t) = n^2 + m n + t$. - There are $n^2 + m n + m$ variables. - - Here $x_(v,c) in {0, 1}$ means vertex $v$ is assigned to topological class label $c$, $s_(t,c) in {0, 1}$ means both endpoints of arc $a_t$ lie in class $c$, and $y_t in {0, 1}$ marks that arc $a_t$ crosses between two different classes. - - The constraints are: - $sum_(c = 0)^(n - 1) x_(v,c) = 1$ for every vertex $v$; - $sum_v w_v x_(v,c) <= B$ for every class $c$; - $s_(t,c) <= x_(u_t,c)$, $s_(t,c) <= x_(v_t,c)$, and $s_(t,c) >= x_(u_t,c) + x_(v_t,c) - 1$ for every arc $a_t$ and class $c$; - $y_t + sum_(c = 0)^(n - 1) s_(t,c) = 1$ for every arc $a_t$, so $y_t = 1$ exactly for crossing arcs; - $sum_(t = 0)^(m - 1) "cost"(a_t) y_t <= K$; - and $sum_(c = 0)^(n - 1) c x_(u_t,c) <= sum_(c = 0)^(n - 1) c x_(v_t,c)$ for every arc $a_t = (u_t -> v_t)$. - The last inequality directly requires every original arc to go from a lower or equal class label to a higher or equal label. Equality represents an internal arc; a crossing arc has distinct labels and therefore strictly increases along the corresponding quotient arc. - - The ILP is: - $ - "find" quad & bold(x) \ - "subject to" quad & sum_(c = 0)^(n - 1) x_(v,c) = 1 quad forall v in V \ - & sum_v w_v x_(v,c) <= B quad forall c in {0, dots, n - 1} \ - & s_(t,c) <= x_(u_t,c), s_(t,c) <= x_(v_t,c) quad forall t, c \ - & s_(t,c) >= x_(u_t,c) + x_(v_t,c) - 1 quad forall t, c \ - & y_t + sum_(c = 0)^(n - 1) s_(t,c) = 1 quad forall t in {0, dots, m - 1} \ - & sum_(t = 0)^(m - 1) "cost"(a_t) y_t <= K \ - & sum_(c = 0)^(n - 1) c x_(u_t,c) <= sum_(c = 0)^(n - 1) c x_(v_t,c) quad forall t in {0, dots, m - 1} \ - & x_(v,c), s_(t,c), y_t in {0, 1}. - $ - - _Correctness._ ($arrow.r.double$) Given a valid acyclic partition, choose a topological ordering of its quotient digraph and relabel each used class by its position in that ordering. This relabeling preserves class membership, class weights, and crossing cost. Every internal arc has equal endpoint labels, while every quotient arc goes to a strictly larger label, so the direct ordering inequalities hold. ($arrow.l.double$) Any feasible ILP solution partitions the vertices, keeps every class within the weight bound, and charges exactly the inter-class arcs. Along every quotient arc the endpoint classes have distinct, nondecreasing labels and hence the label strictly increases. A directed quotient cycle would require a strict increase around the cycle back to its starting label, which is impossible; therefore the quotient digraph is acyclic. - - _Solution extraction._ For each vertex $v$, output the unique class label $c$ with $x_(v,c) = 1$. + Bounded topological part labels and one exact crossing indicator per stored arc, + retaining one-hot membership for signed part-weight constraints. +][ + _Construction._ Introduce binary memberships $x_(v,j)$ and emptiness bits $e_j$, + integer labels $p_v in {0,dots,n-1}$, and binary crossing bits $y_a$. + Require $sum_j x_(v,j)=1$ and $p_v=sum_j j x_(v,j)$. + For each part impose $x_(v,j)+e_j <= 1$, $sum_v x_(v,j)+e_j >= 1$, and + $sum_v w_v x_(v,j)+min(B,0)e_j <= B$. Thus $e_j=1$ exactly for empty + parts, which satisfy $min(B,0) <= B$; occupied parts meet their original + weight bound without negating or subtracting $B$. + For arc $a=(u,v)$, set $d=p_v-p_u$ and + impose $d >= y_a$ and $d <= (n-1)y_a$. Finally require + $sum_a c_a y_a <= K$, retaining this row even for an empty graph. + + _Correctness._ A feasible source quotient is a DAG, so its occupied parts + can be relabeled in topological order without changing weights or crossing + costs. This supplies a feasible target. Conversely, a target selects one + part per vertex, checks each occupied weight, and forces $y_a=1$ exactly + when an arc crosses. Every crossing strictly increases its label, excluding + quotient cycles. The cost row therefore measures the exact signed crossing + sum; negative costs cannot be exploited by a false crossing indicator. + + _Overhead._ There are exactly $n^2+2n+m$ variables and + $n^2+4n+2m+1$ rows. The one-hot, label, emptiness implication, emptiness sum, + weight, crossing, and cost blocks contribute at most + $n^2+n^2+2n^2+(n^2+n)+(n^2+n)+6m+m=6n^2+2n+7m$ nonzeros. + Loops and zero coefficients can reduce nonzeros after normalization. + With source numeric magnitude bound $h$, target magnitudes need at most + $h+n+1$ bits. All integer variables have explicit finite bounds. + + _Solution extraction._ Decode the selected part of each one-hot row; + the label equalities give the same result from $p$. ] #reduction-rule("BalancedCompleteBipartiteSubgraph", "ILP")[ @@ -16037,23 +16144,21 @@ The following reductions to Integer Linear Programming are straightforward formu // Matrix/encoding #reduction-rule("BMF", "ILP")[ - Split the witness into binary factor matrices $B$ and $C$, reconstruct their Boolean product with McCormick auxiliaries, pin each reconstructed entry to the target, and minimize the total factor weight. -][ - _Construction._ Variables: binary $b_(i,r)$, binary $c_(r,j)$, binary $p_(i,r,j)$ linearizing $b_(i,r) c_(r,j)$, and binary $w_(i,j)$ for the reconstructed entry. The ILP is: - $ - min quad & sum_(i,r) b_(i,r) + sum_(r,j) c_(r,j) \ - "subject to" quad & p_(i,r,j) <= b_(i,r) quad forall i, r, j \ - & p_(i,r,j) <= c_(r,j) quad forall i, r, j \ - & p_(i,r,j) >= b_(i,r) + c_(r,j) - 1 quad forall i, r, j \ - & w_(i,j) >= p_(i,r,j) quad forall i, r, j \ - & w_(i,j) <= sum_r p_(i,r,j) quad forall i, j \ - & w_(i,j) = A_(i,j) quad forall i, j \ - & b_(i,r), c_(r,j), p_(i,r,j), w_(i,j) in {0, 1}. - $ - - _Correctness._ ($arrow.r.double$) Any exact factorization $B circle.tiny C = A$ gives a feasible ILP solution with objective equal to $|B|_1 + |C|_1$. ($arrow.l.double$) The McCormick constraints force $p_(i,r,j) = b_(i,r) dot c_(r,j)$; the $w$ constraints then force $w_(i,j) = or.big_r p_(i,r,j)$, so the equality $w_(i,j) = A_(i,j)$ is feasible exactly when $B circle.tiny C = A$. If no exact rank-$k$ factorization exists the ILP is infeasible, matching BMF's infeasibility signal. - - _Solution extraction._ Output the flattened bits of $B$ followed by the flattened bits of $C$, discarding the reconstruction auxiliaries. + Retain the binary factor matrices $B,C$. For each zero entry of $A$, require + $b_(i,r)+c_(r,j) <= 1$ for every rank $r$. For each one entry introduce coverage + bits $p_(i,j,r) <= b_(i,r)$ and $p_(i,j,r) <= c_(r,j)$, with + $sum_r p_(i,j,r) >= 1$. Minimize the total factor weight. If $t$ entries are + one, this uses $k(m+n)+k t$ variables, $k(m n-t)+(2k+1)t$ rows, and + $2k(m n-t)+5k t$ nonzeros. Existing source parameters yield upper bounds by + substituting $t <= m n$. +][ + An exact factorization supplies valid coverage bits from its true products. + Conversely, zero-entry rows exclude every product there, and each one entry + has a selected coverage bit that forces both factor memberships. The extracted + matrices therefore reconstruct $A$ exactly, with identical objective for every + feasible target witness. Coverage bits need not equal every true product. + Rank zero yields an empty contradictory row for each one entry and remains + feasible for zero matrices; empty dimensions preserve their factor shapes. ] #reduction-rule("BMF", "BicliqueCover")[ @@ -16097,58 +16202,34 @@ The following reductions to Integer Linear Programming are straightforward formu ] #reduction-rule("ConsecutiveOnesMatrixAugmentation", "ILP")[ - Choose a column permutation and, for each row, choose the interval that will become its consecutive block of 1s; flips are needed only for zeros inside that interval. -][ - _Construction._ Let the matrix have $m$ rows and $n$ columns, and let $A_(r,c) in {0, 1}$ be the given entry. For each row define the constant - $beta_r = 1$ if row $r$ contains at least one 1, and $beta_r = 0$ otherwise. - Use `ILP` with variable order - $(x_(c,p))_(c,p), (a_(r,p))_(r,p), (ell_(r,p))_(r,p), (u_(r,p))_(r,p), (h_(r,p))_(r,p), (f_(r,p))_(r,p)$. - The indices are - $"idx"_x(c,p) = c n + p$, - $"idx"_a(r,p) = n^2 + r n + p$, - $"idx"_ell(r,p) = n^2 + m n + r n + p$, - $"idx"_u(r,p) = n^2 + 2 m n + r n + p$, - $"idx"_h(r,p) = n^2 + 3 m n + r n + p$, - and $ "idx"_f(r,p) = n^2 + 4 m n + r n + p$. - There are $n^2 + 5 m n$ binary variables. - - Here $x_(c,p) = 1$ means original column $c$ is placed at position $p$ of the permutation, $a_(r,p)$ is the value seen in row $r$ at permuted position $p$, $ell_(r,p)$ and $u_(r,p)$ choose the left and right interval boundaries of row $r$, $h_(r,p)$ indicates that position $p$ lies inside that chosen interval, and $f_(r,p)$ indicates that row $r$ flips a 0 to a 1 at position $p$. - - The constraints are: - $sum_p x_(c,p) = 1$ for every column $c$; - $sum_c x_(c,p) = 1$ for every position $p$; - $a_(r,p) = sum_c A_(r,c) x_(c,p)$ for every row $r$ and position $p$; - $sum_p ell_(r,p) = beta_r$ and $sum_p u_(r,p) = beta_r$ for every row $r$; - $sum_p p ell_(r,p) <= sum_p p u_(r,p) + (n - 1) (1 - beta_r)$ for every row $r$, which forces the left boundary not to exceed the right boundary when the row is nonzero; - for every row $r$ and position $p$, - $h_(r,p) <= sum_(q = 0)^p ell_(r,q)$, - $h_(r,p) <= sum_(q = p)^(n - 1) u_(r,q)$, - and - $h_(r,p) >= sum_(q = 0)^p ell_(r,q) + sum_(q = p)^(n - 1) u_(r,q) - 1$; - $a_(r,p) <= h_(r,p)$ for every $r, p$, so every original 1 lies inside the chosen interval; - $h_(r,p) <= a_(r,p) + f_(r,p)$, $f_(r,p) <= h_(r,p)$, and $f_(r,p) + a_(r,p) <= 1$ for every $r, p$, so $f_(r,p) = 1$ exactly when the position lies inside the interval but the original matrix has a 0 there; - and the augmentation budget - $sum_(r = 0)^(m - 1) sum_(p = 0)^(n - 1) f_(r,p) <= K$. - These are the exact consecutive-ones constraints: after permutation, row $r$ is 1 exactly on the positions with $h_(r,p) = 1$, and the only modifications charged are the zero-to-one flips recorded by $f$. - - The ILP is: - $ - "find" quad & bold(x) \ - "subject to" quad & sum_p x_(c,p) = 1 quad forall c \ - & sum_c x_(c,p) = 1 quad forall p \ - & a_(r,p) = sum_c A_(r,c) x_(c,p) quad forall r, p \ - & sum_p ell_(r,p) = beta_r, sum_p u_(r,p) = beta_r quad forall r \ - & sum_p p ell_(r,p) <= sum_p p u_(r,p) + (n - 1) (1 - beta_r) quad forall r \ - & h_(r,p) <= sum_(q = 0)^p ell_(r,q), h_(r,p) <= sum_(q = p)^(n - 1) u_(r,q) quad forall r, p \ - & h_(r,p) >= sum_(q = 0)^p ell_(r,q) + sum_(q = p)^(n - 1) u_(r,q) - 1 quad forall r, p \ - & a_(r,p) <= h_(r,p); h_(r,p) <= a_(r,p) + f_(r,p); f_(r,p) <= h_(r,p); f_(r,p) + a_(r,p) <= 1 quad forall r, p \ - & sum_(r = 0)^(m - 1) sum_(p = 0)^(n - 1) f_(r,p) <= K \ - & x_(c,p), a_(r,p), ell_(r,p), u_(r,p), h_(r,p), f_(r,p) in {0, 1}. - $ - - _Correctness._ ($arrow.r.double$) A feasible augmentation chooses a permutation and flips exactly the zeros lying inside each row's final consecutive-ones interval. ($arrow.l.double$) Any feasible ILP solution yields a permuted matrix whose rows become consecutive-ones after the encoded zero-to-one augmentations, with total augmentation cost at most $K$. - - _Solution extraction._ Decode the column permutation from $x_(c,p)$ and discard the auxiliary flip variables. + A column permutation and two bounded interval endpoints per row, using + $n^2+2m$ variables for an $m$ by $n$ Boolean matrix. +][ + _Construction._ Binary $x_(c,p)$ places column $c$ at position $p$. + Assign every column exactly once with capacity one per position. + For each nonempty row $r$, introduce $L_r,R_r in {0,dots,n-1}$ and + require every original one-position $sum_p p x_(c,p)$ to lie between them. + Empty rows have both endpoints fixed to zero. Let $T$ be the number of + ones, $a$ the number of nonempty rows, and $k=min(K,m n-T)$. + Require $sum_(r: t_r>0)(R_r-L_r) <= k+T-a$ and minimize zero. + + _Correctness._ A valid ordering supplies its true first and last one positions; + the augmentation cost is exactly $sum_(r:t_r>0)(R_r-L_r+1-t_r)$. + Conversely, any feasible target gives a column permutation whose true one + spans lie inside the chosen intervals. Filling those spans costs no more + than the budgeted interval lengths. Enlarging an interval can only increase + the cost. All-zero rows contribute zero. Clipping $K$ to the number of + zero entries preserves feasibility and prevents artificial budget overflow. + + _Overhead._ Variables equal $n^2+2m$; rows equal $2n+2T+1$, bounded by + $2n+2m n+1$. Nonzeros equal $2n^2+2n T+2a$, bounded by + $2n^2+2m n^2+2m$. Coefficients and endpoints are at most $n$, and the + budget right-hand side is at most $m n$, covered by $2m n+n+1$ magnitude + bits. Empty-column matrices retain their fixed-zero endpoints and one + empty budget row. + + _Solution extraction._ Return the selected column at each position after + validating target feasibility. ] #reduction-rule("ConsecutiveOnesSubmatrix", "ILP")[ @@ -17589,25 +17670,69 @@ The following table shows concrete target-variable counts for example instances, #reduction-rule("FeasibleRegisterAssignment", "ILP", example: false, )[ - Direct ILP formulation of the feasible register assignment problem: binary permutation matrix variables, topological ordering constraints, and register-conflict constraints via shared-register ordering indicators. -][ - _Construction._ Binary variables $x_(v,t) in {0,1}$ (vertex $v$ at position $t$). Permutation: each row and column sums to $1$. Topological: for arc $(u,v)$, $sum_(t) t dot x_(v,t) < sum_(t) t dot x_(u,t)$. Register conflict: for vertices $v,w$ sharing a register, an ordering indicator $b_(v,w)$ with big-$M$ constraints ensures all dependents of the first-computed vertex complete before the second uses the register. Feasibility objective (Value $=$ Or). - - _Correctness._ The ILP is feasible iff a valid evaluation ordering respecting the register assignment exists. - - _Solution extraction._ Read vertex positions from the permutation matrix. + Bounded ranks and one binary order selector per pair sharing a register. + With $n$ vertices, $m$ arcs, and $S$ such pairs, the construction has + $n+S$ variables, at most $n m+2S$ rows, and at most $3n m+6S$ nonzeros. +][ + _Construction._ Give each vertex a rank $p_v in {0, dots, n-1}$. + An arc $(w,u)$ means $w$ consumes $u$; impose $p_w-p_u >= 1$. + For a same-register pair $u= 1-n$ and $p_u-p_v+n y >= 1$. + For every consumer $w$ of $u$ except $v$, add $p_v-p_w-n y >= 1-n$. + For every consumer $w$ of $v$ except $u$, add $p_u-p_w+n y >= 1$. + The objective is zero. The empty source gives an empty feasible target. + + _Correctness._ A valid source ordering supplies the ranks and pair orientations. + The later writer may consume the earlier value itself, but every other + consumer must precede it, so all rows hold. Conversely, the pair rows choose + a strict orientation. Every dependency and every required consumer-before-overwrite + relation is strict; sorting by rank, with vertex index breaking ties, preserves + all these relations. Thus every feasible target extracts a valid source ordering. + Unrelated vertices can share ranks without hiding an overwrite conflict. + + _Overhead._ Each arc contributes one dependency row and at most $n-1$ + conditional rows, in addition to two rows per same-register pair. + These have respectively two and three nonzeros. Hence rows are at most + $n m+2S$ and nonzeros at most $3n m+6S$. Coefficients and bounds have + magnitude at most $max(n,1)$, covered by $n+1$ magnitude bits. + + _Solution extraction._ Sort the vertices by $(p_v,v)$ and return their inverse + permutation as evaluation positions. ] #reduction-rule("RegisterSufficiency", "ILP", example: false, )[ - Direct ILP formulation of Register Sufficiency: integer evaluation times, latest-use times, binary pair-order selectors, and per-step live-value indicators. For a DAG with $n$ vertices, $m$ arcs, and $s$ sinks, the ILP has $(7n^2 + 3n)/2$ variables and $(21n^2 + 3n)/2 + 2m + s$ constraints. -][ - _Construction._ Let the source DAG use the repository convention that an arc $(v, u)$ means vertex $v$ depends on vertex $u$. Introduce integer variables $t_v in {0, dots, n-1}$ for evaluation positions and $l_v in {0, dots, n}$ for latest-use positions. For every unordered vertex pair ${u, v}$, add a binary selector $b_(u,v)$ with big-$M$ constraints forcing either $t_u < t_v$ or $t_v < t_u$; since all $t_v$ lie in the interval ${0, dots, n-1}$, the positions form a permutation. For every dependency arc $(v, u)$, enforce $t_v >= t_u + 1$ and $l_u >= t_v$. For every sink vertex (no dependents), set $l_u = n$. For each vertex-step pair $(u, s)$ with $s in {0, dots, n-1}$, add binary threshold variables $p_(u,s)$ and $q_(u,s)$ satisfying $p_(u,s) = 1$ iff $t_u <= s$ and $q_(u,s) = 1$ iff $l_u > s$, plus a binary live indicator $h_(u,s) = p_(u,s) and q_(u,s)$. Finally impose $sum_u h_(u,s) <= K$ for every step $s$. - - _Correctness._ ($arrow.r.double$) Any valid computation ordering of the source DAG yields a feasible ILP solution: assign each $t_v$ to the vertex position in the ordering, each $l_v$ to the latest dependent position (or $n$ for sinks), and derive the binary threshold/live variables from those integers. The dependency constraints hold by topological validity, and the live-count inequalities hold because the source witness uses at most $K$ registers. ($arrow.l.double$) Any feasible ILP solution gives distinct positions $t_v$, hence a permutation of the vertices, and the arc constraints make that permutation topological. The live indicators $h_(u,s)$ certify exactly which values remain live after step $s$, so the step constraints prove that no more than $K$ values are simultaneously live. Therefore the extracted ordering is a valid Register Sufficiency witness. - - _Solution extraction._ Return the first $n$ ILP coordinates $(t_0, dots, t_(n-1))$ as the vertex evaluation positions. + A binary cumulative schedule with live-value indicators. For $n$ vertices, + $m$ stored arcs, and $s$ sinks, the ILP has $2n^2-n s$ variables, + $n^2+2n m+n$ rows, and $4n^2-2n+5n m-m$ nonzeros. +][ + _Construction._ A binary $x_(v,t)$ says vertex $v$ has been computed by the + end of step $t$. Require $x_(v,t) <= x_(v,t+1)$ and + $sum_v x_(v,t)=t+1$. For every arc $(w,u)$ (consumer, dependency), require + $x_(w,t) <= x_(u,t-1)$, treating $x_(u,-1)=0$. + For each non-sink $u$, introduce binary $h_(u,t)$ and impose + $h_(u,t) >= x_(u,t)-x_(w,t)$ for every consumer $w$. + At each step, the sum of these live indicators and computed sink indicators + must be at most $min(K,n)$. Sinks remain live until the computation ends. + Minimize zero; the empty source yields an empty feasible target. + + _Correctness._ A valid source permutation supplies its cumulative computed + bits and actual live bits, satisfying every row. Conversely, monotonicity + and column sums force exactly one vertex to become computed at each step. + The dependency inequalities make this a topological order. Whenever a + computed non-sink has an uncomputed consumer, its live indicator must be + one. Extra live indicators only increase the capacity sum. Consequently + every feasible target bounds the actual source register usage by $K$. + + _Overhead._ The blocks have $n(n-1)$, $n$, $n m$, $n m$, and $n$ rows, + respectively, and $2n(n-1)$, $n^2$, $(2n-1)m$, $3n m$, and $n^2$ + nonzeros. Self-arcs are rejected by the source, so no terms cancel; + duplicate arcs retain their rows. Coefficients have magnitude one and + right-hand sides at most $n$, covered by $n+1$ magnitude bits. + + _Solution extraction._ Return the first step $t$ with $x_(v,t)=1$ for each + vertex $v$, after validating target feasibility. ] // Removed: Partition → SequencingWithinIntervals (unsound reduction, #1006) diff --git a/src/rules/acyclicpartition_ilp.rs b/src/rules/acyclicpartition_ilp.rs index cfdbfd1dd..5d8fb254e 100644 --- a/src/rules/acyclicpartition_ilp.rs +++ b/src/rules/acyclicpartition_ilp.rs @@ -1,26 +1,21 @@ -//! 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. -//! See the paper entry for the full formulation. +//! Bounded partition labels with exact crossing flags and occupied-part budgets. -use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{Bounded, IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::AcyclicPartition; use crate::reduction; -use crate::rules::ilp_helpers::mccormick_product; 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 } @@ -45,15 +40,15 @@ impl crate::rules::AggregateReductionResult for ReductionAcyclicPartitionToILP { #[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", + num_vars = "num_vertices^2 + 2 * num_vertices + num_arcs", + num_constraints = "num_vertices^2 + 4 * 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)", + num_nonzeros = "6 * num_vertices^2 + 2 * num_vertices + 7 * num_arcs", }, })] -impl ReduceTo> for AcyclicPartition { +impl ReduceTo> for AcyclicPartition { type Result = ReductionAcyclicPartitionToILP; fn reduce_to(&self) -> Result { @@ -61,72 +56,79 @@ impl ReduceTo> for AcyclicPartition { let arcs = self.graph().arcs(); let m = arcs.len(); - // Variable indices: - // x_{v,c} : v*n + c [0, n^2) - // s_{t,c} : n^2 + t*n + c [n^2, n^2 + m*n) - // y_t : n^2 + m*n + t [n^2 + m*n, n^2 + m*n + m) - let x_idx = |v: usize, c: usize| -> usize { v * n + c }; - let s_idx = |t: usize, c: usize| -> usize { n * n + t * n + c }; - let y_idx = |t: usize| -> usize { n * n + m * n + t }; - let used_idx = |c: usize| -> usize { n * n + m * n + m + c }; - let num_vars = n * n + m * n + m + n; + let overflow = || { + crate::rules::ReductionError::integer_overflow::>( + "counting acyclic partition variables", + ) + }; + let square = n.checked_mul(n).ok_or_else(overflow)?; + let labels = square.checked_add(n).ok_or_else(overflow)?; + let crossing = labels.checked_add(n).ok_or_else(overflow)?; + let num_vars = crossing.checked_add(m).ok_or_else(overflow)?; + let last_label = Self::exact_i64(n.saturating_sub(1), "bounding partition labels")?; + let x_idx = |v: usize, c: usize| v * n + c; + let empty_idx = |c: usize| square + c; + let label_idx = |v: usize| labels + v; + let y_idx = |t: usize| crossing + t; let mut constraints = Vec::new(); let vertex_weights = self.vertex_weights(); let arc_costs = self.arc_costs(); let weight_bound = *self.weight_bound(); let cost_bound = *self.cost_bound(); - // 1) Assignment: Σ_c x_{v,c} = 1 for each vertex v + // Assignment: Σ_c x_{v,c} = 1 for each vertex v. for v in 0..n { let terms: Vec<(usize, i64)> = (0..n).map(|c| (x_idx(v, c), 1)).collect(); constraints.push(LinearConstraint::eq(terms, 1)); + let mut label = vec![(label_idx(v), 1)]; + for c in 1..n { + label.push(( + x_idx(v, c), + -Self::exact_i64(c, "representing a partition label")?, + )); + } + constraints.push(LinearConstraint::eq(label, 0)); } - // 2) Only occupied classes must meet the weight bound, which can be negative. + // Only occupied classes must meet the weight bound, which can be negative. for c in 0..n { - constraints.push(LinearConstraint::le(vec![(used_idx(c), 1)], 1)); - let mut occupied = vec![(used_idx(c), -1)]; + let mut membership = vec![(empty_idx(c), 1)]; for v in 0..n { constraints.push(LinearConstraint::le( - vec![(x_idx(v, c), 1), (used_idx(c), -1)], - 0, + vec![(x_idx(v, c), 1), (empty_idx(c), 1)], + 1, )); - occupied.push((x_idx(v, c), 1)); + membership.push((x_idx(v, c), 1)); } - constraints.push(LinearConstraint::ge(occupied, 0)); + constraints.push(LinearConstraint::ge(membership, 1)); let mut terms: Vec<(usize, i64)> = vertex_weights .iter() .enumerate() .map(|(vertex, &weight)| (x_idx(vertex, c), weight)) .collect(); - terms.push(( - used_idx(c), - weight_bound.checked_neg().ok_or_else(|| { - crate::rules::ReductionError::integer_overflow::>( - "negating the partition weight bound", - ) - })?, - )); - constraints.push(LinearConstraint::le(terms, 0)); + // Keep the bound on the RHS to preserve representable source sums. + terms.push((empty_idx(c), weight_bound.min(0))); + constraints.push(LinearConstraint::le(terms, weight_bound)); } - // 3) McCormick: s_{t,c} = x_{u_t,c} * x_{v_t,c} + // A crossing arc increases its part label by at least one; an internal + // arc has equal labels. This equivalence also handles negative costs. for (t, &(u, v)) in arcs.iter().enumerate() { - for c in 0..n { - constraints.extend(mccormick_product(s_idx(t, c), x_idx(u, c), x_idx(v, c))); - } - } - - // 4) Crossing: y_t + Σ_c s_{t,c} = 1 - for t in 0..m { - let mut terms: Vec<(usize, i64)> = vec![(y_idx(t), 1)]; - for c in 0..n { - terms.push((s_idx(t, c), 1)); - } - constraints.push(LinearConstraint::eq(terms, 1)); + constraints.push(LinearConstraint::ge( + vec![(label_idx(v), 1), (label_idx(u), -1), (y_idx(t), -1)], + 0, + )); + constraints.push(LinearConstraint::le( + vec![ + (label_idx(v), 1), + (label_idx(u), -1), + (y_idx(t), -last_label), + ], + 0, + )); } - // 5) Cost bound: Σ_t cost(a_t) * y_t ≤ K + // Cost bound: Σ_t cost(a_t) * y_t ≤ K. let cost_terms: Vec<(usize, i64)> = arc_costs .iter() .enumerate() @@ -134,20 +136,11 @@ impl ReduceTo> for AcyclicPartition { .collect(); constraints.push(LinearConstraint::le(cost_terms, cost_bound)); - // 6) Topological labels: every arc goes from a lower or equal class to a - // higher or equal class. Equal labels are internal arcs; strict - // increases are quotient arcs. - for (u, v) in arcs { - let mut terms = Vec::with_capacity(2 * n.saturating_sub(1)); - for c in 1..n { - let label = Self::exact_i64(c, "representing a partition label in ILP rows")?; - terms.push((x_idx(u, c), label)); - terms.push((x_idx(v, c), -label)); - } - constraints.push(LinearConstraint::le(terms, 0)); - } - - let target = ILP::new(num_vars, constraints, vec![], ObjectiveSense::Minimize) + let mut variables = vec![IntegerVariable::binary(); num_vars]; + variables[labels..crossing].fill( + IntegerVariable::new(Some(0), Some(last_label)).map_err(Self::target_construction)?, + ); + let target = ILP::with_variables(variables, constraints, vec![], ObjectiveSense::Minimize) .map_err(Self::target_construction)?; Ok(ReductionAcyclicPartitionToILP { target, n }) @@ -156,32 +149,17 @@ impl ReduceTo> for AcyclicPartition { #[cfg(feature = "example-db")] pub(crate) fn canonical_rule_example_specs() -> Vec { - use crate::export::SolutionPair; use crate::topology::DirectedGraph; vec![crate::example_db::specs::RuleExampleSpec { id: "acyclicpartition_to_ilp", build: || { - let source = AcyclicPartition::new( + crate::example_db::specs::rule_example_via_bounded_ilp(AcyclicPartition::new( DirectedGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]), vec![1, 1, 1, 1], vec![1, 1, 1], 3, 2, - ); - let reduction: ReductionAcyclicPartitionToILP = - 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>( - source, - SolutionPair { - source_config: serde_json::json!(extracted), - target_config: serde_json::json!(ilp_sol), - }, - ) + )) }, }] } diff --git a/src/rules/betweenness_ilp.rs b/src/rules/betweenness_ilp.rs new file mode 100644 index 000000000..a2cff2767 --- /dev/null +++ b/src/rules/betweenness_ilp.rs @@ -0,0 +1,87 @@ +//! One orientation selector per betweenness triple, with bounded element ranks. + +use crate::models::algebraic::{Bounded, IntegerVariable, ObjectiveSense, ILP}; +use crate::models::misc::Betweenness; +use crate::rules::ilp_helpers::{bounded_order_comparison, ranks_to_positions}; +use crate::rules::traits::{ReduceTo, ReductionResult}; + +#[derive(Debug, Clone)] +pub struct ReductionBetweennessToILP { + target: ILP, + num_elements: usize, +} + +impl ReductionResult for ReductionBetweennessToILP { + type Source = Betweenness; + 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( + &self.target, + solution, + |value| value.value.is_some(), + "target ILP assignment is infeasible", + )?; + Ok(ranks_to_positions(&solution[..self.num_elements])) + } +} +#[crate::aggregate_reduction(ilp_feasibility)] +impl crate::rules::AggregateReductionResult for ReductionBetweennessToILP {} + +#[crate::reduction(transform = { + exact { + num_vars = "num_elements + num_triples", + num_constraints = "4 * num_triples", + num_nonzeros = "12 * num_triples", + }, + upper_bound { max_constraint_magnitude_bits = "num_elements", }, +})] +impl ReduceTo> for Betweenness { + type Result = ReductionBetweennessToILP; + fn reduce_to(&self) -> Result { + let n = self.num_elements(); + let count = n.checked_add(self.num_triples()).ok_or_else(|| { + crate::rules::ReductionError::integer_overflow::>( + "counting betweenness variables", + ) + })?; + let bound = Self::exact_i64(n, "bounding ordering ranks")?; + let mut variables = vec![ + IntegerVariable::new(Some(0), Some(bound - 1)) + .map_err(Self::target_construction)?; + n + ]; + variables.resize(count, IntegerVariable::binary()); + let mut constraints = Vec::new(); + for (index, &(a, b, c)) in self.triples().iter().enumerate() { + constraints.extend(bounded_order_comparison(a, b, n + index, bound)); + constraints.extend(bounded_order_comparison(b, c, n + index, bound)); + } + let target = ILP::with_variables(variables, constraints, vec![], ObjectiveSense::Minimize) + .map_err(Self::target_construction)?; + crate::rules::ilp_helpers::validate_bounded_constraint_arithmetic::(&target)?; + Ok(Self::Result { + target, + num_elements: n, + }) + } +} + +#[cfg(feature = "example-db")] +pub(crate) fn canonical_rule_example_specs() -> Vec { + vec![crate::example_db::specs::RuleExampleSpec { + id: "betweenness_to_ilp", + build: || { + crate::example_db::specs::rule_example_via_bounded_ilp(Betweenness::new( + 3, + vec![(0, 1, 2)], + )) + }, + }] +} + +#[cfg(test)] +#[path = "../unit_tests/rules/betweenness_ilp.rs"] +mod tests; diff --git a/src/rules/bmf_ilp.rs b/src/rules/bmf_ilp.rs index 2d36b0180..242cf1e89 100644 --- a/src/rules/bmf_ilp.rs +++ b/src/rules/bmf_ilp.rs @@ -1,12 +1,7 @@ -//! Reduction from BMF (Boolean Matrix Factorization) to ILP. -//! -//! Variables: binary b_{i,r}, c_{r,j}, McCormick product p_{i,r,j} = b_{i,r} * c_{r,j}, -//! reconstructed entry w_{i,j} = OR_r p_{i,r,j}. Pin w_{i,j} = A_{i,j} (exact factorization) -//! and minimize sum_{i,r} b_{i,r} + sum_{r,j} c_{r,j} (total factor size). +//! Exact Boolean factorization with coverage indicators only for true entries. use crate::models::algebraic::{LinearConstraint, ObjectiveSense, BMF, ILP}; use crate::reduction; -use crate::rules::ilp_helpers::mccormick_product; use crate::rules::traits::{ReduceTo, ReductionResult}; #[derive(Debug, Clone)] @@ -29,7 +24,12 @@ impl ReductionResult for ReductionBMFToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + crate::rules::traits::validate_target_witness( + self.target_problem(), + target_solution, + |v| v.value.is_some(), + "target ILP assignment does not reconstruct the Boolean matrix", + )?; let b = (0..self.m) .map(|i| { @@ -50,14 +50,14 @@ 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", - } -)] +#[reduction(transform = { + exact { max_constraint_magnitude_bits = "1", }, + upper_bound { + num_vars = "rows * rank + rank * cols + rows * rank * cols", + num_constraints = "(2 * rank + 1) * rows * cols", + num_nonzeros = "5 * rows * rank * cols", + }, +})] impl ReduceTo> for BMF { type Result = ReductionBMFToILP; @@ -65,57 +65,55 @@ impl ReduceTo> for BMF { let m = self.rows(); let n = self.cols(); let k = self.rank(); - - // Variable layout: - // b_{i,r}: m*k variables at indices [0, m*k) - // c_{r,j}: k*n variables at indices [m*k, m*k + k*n) - // p_{i,r,j}: m*k*n variables at indices [m*k + k*n, m*k + k*n + m*k*n) - // w_{i,j}: m*n variables at indices [m*k + k*n + m*k*n, m*k + k*n + m*k*n + m*n) - let b_offset = 0; - let c_offset = m * k; - let p_offset = m * k + k * n; - let w_offset = p_offset + m * k * n; - let num_vars = w_offset + m * n; - + let overflow = || { + crate::rules::ReductionError::integer_overflow::>( + "counting Boolean factorization variables", + ) + }; + let c_offset = m.checked_mul(k).ok_or_else(overflow)?; + let factor_count = k + .checked_mul(n) + .and_then(|v| v.checked_add(c_offset)) + .ok_or_else(overflow)?; + >>::exact_i64(factor_count, "bounding Boolean factor size")?; + let ones = self + .matrix() + .iter() + .flatten() + .filter(|&&value| value) + .count(); + let num_vars = ones + .checked_mul(k) + .and_then(|v| v.checked_add(factor_count)) + .ok_or_else(overflow)?; + let mut next = factor_count; let mut constraints = Vec::new(); - - for i in 0..m { - for j in 0..n { - for r in 0..k { - let p_idx = p_offset + i * k * n + r * n + j; - let b_idx = b_offset + i * k + r; - let c_idx = c_offset + r * n + j; - - // McCormick: p_{i,r,j} = b_{i,r} * c_{r,j} - constraints.extend(mccormick_product(p_idx, b_idx, c_idx)); + for (i, row) in self.matrix().iter().enumerate() { + for (j, &value) in row.iter().enumerate() { + if value { + let mut coverage = Vec::new(); + for r in 0..k { + let bit = next; + next += 1; + constraints.push(LinearConstraint::le(vec![(bit, 1), (i * k + r, -1)], 0)); + constraints.push(LinearConstraint::le( + vec![(bit, 1), (c_offset + r * n + j, -1)], + 0, + )); + coverage.push((bit, 1)); + } + constraints.push(LinearConstraint::ge(coverage, 1)); + } else { + for r in 0..k { + constraints.push(LinearConstraint::le( + vec![(i * k + r, 1), (c_offset + r * n + j, 1)], + 1, + )); + } } - - let w_idx = w_offset + i * n + j; - - // w_{i,j} >= p_{i,r,j} for all r - for r in 0..k { - let p_idx = p_offset + i * k * n + r * n + j; - constraints.push(LinearConstraint::ge(vec![(w_idx, 1), (p_idx, -1)], 0)); - } - - // w_{i,j} <= sum_r p_{i,r,j} - let mut w_upper_terms = vec![(w_idx, 1)]; - for r in 0..k { - let p_idx = p_offset + i * k * n + r * n + j; - w_upper_terms.push((p_idx, -1)); - } - constraints.push(LinearConstraint::le(w_upper_terms, 0)); - - // Exact factorization: w_{i,j} = A_{i,j} - let a_val = if self.matrix()[i][j] { 1 } else { 0 }; - constraints.push(LinearConstraint::eq(vec![(w_idx, 1)], a_val)); } } - - // Objective: minimize sum_{i,r} b_{i,r} + sum_{r,j} c_{r,j} (total factor size) - let mut objective: Vec<(usize, i64)> = (0..m * k).map(|idx| (b_offset + idx, 1)).collect(); - objective.extend((0..k * n).map(|idx| (c_offset + idx, 1))); - + let objective = (0..factor_count).map(|i| (i, 1)).collect(); let target = ILP::new(num_vars, constraints, objective, ObjectiveSense::Minimize) .map_err(>>::target_construction)?; Ok(ReductionBMFToILP { target, m, n, k }) diff --git a/src/rules/boundeddiameterspanningtree_ilp.rs b/src/rules/boundeddiameterspanningtree_ilp.rs new file mode 100644 index 000000000..6d3cb0baf --- /dev/null +++ b/src/rules/boundeddiameterspanningtree_ilp.rs @@ -0,0 +1,153 @@ +//! A bounded-depth tree rooted at a vertex or the two ends of a center edge. +use crate::models::algebraic::{Bounded, IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; +use crate::models::graph::BoundedDiameterSpanningTree; +use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::topology::SimpleGraph; + +#[derive(Debug, Clone)] +pub struct ReductionBoundedDiameterSpanningTreeToILP { + target: ILP, + num_edges: usize, +} +impl ReductionResult for ReductionBoundedDiameterSpanningTreeToILP { + type Source = BoundedDiameterSpanningTree; + 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( + &self.target, + solution, + |v| v.value.is_some(), + "target ILP assignment is infeasible", + )?; + Ok(solution[..self.num_edges].iter().map(|&v| v == 1).collect()) + } +} +#[crate::aggregate_reduction(ilp_feasibility)] +impl crate::rules::AggregateReductionResult for ReductionBoundedDiameterSpanningTreeToILP {} +#[crate::reduction(transform = upper_bound { + num_vars = "4 * num_edges + 2 * num_vertices", + num_constraints = "3 * num_edges + 3 * num_vertices + 2", + num_nonzeros = "16 * num_edges + 4 * num_vertices", + max_constraint_magnitude_bits = "64", +})] +impl ReduceTo> for BoundedDiameterSpanningTree { + type Result = ReductionBoundedDiameterSpanningTreeToILP; + fn reduce_to(&self) -> Result { + let n = self.num_vertices(); + let m = self.num_edges(); + if n == 0 { + return Ok(Self::Result { + target: ILP::empty(), + num_edges: 0, + }); + } + let diameter = self.diameter_bound().min(n - 1); + let odd = diameter % 2 == 1; + let q = Self::exact_i64(diameter / 2, "bounding tree depth")?; + let big_m = q + 1; + let count = m + .checked_mul(if odd { 4 } else { 3 }) + .and_then(|v| n.checked_mul(2).and_then(|w| v.checked_add(w))) + .ok_or_else(|| { + crate::rules::ReductionError::integer_overflow::>( + "counting tree variables", + ) + })?; + let root = 3 * m; + let depth = root + n; + let center = depth + n; + let mut variables = vec![IntegerVariable::binary(); count]; + variables[depth..center] + .fill(IntegerVariable::new(Some(0), Some(q)).map_err(Self::target_construction)?); + let mut incoming: Vec<_> = (0..n).map(|v| vec![(root + v, 1)]).collect(); + let mut centers: Vec<_> = (0..n).map(|v| vec![(root + v, 1)]).collect(); + let mut rows = Vec::new(); + for (e, &(u, v)) in self.edge_list().iter().enumerate() { + let forward = m + 2 * e; + let reverse = forward + 1; + if u == v { + for i in [e, forward, reverse] + .into_iter() + .chain(odd.then_some(center + e)) + { + variables[i] = IntegerVariable::new(Some(0), Some(0)) + .map_err(Self::target_construction)?; + } + } + let mut terms = vec![(e, 1), (forward, -1), (reverse, -1)]; + if odd { + terms.push((center + e, -1)); + } + rows.push(LinearConstraint::eq(terms, 0)); + incoming[v].push((forward, 1)); + incoming[u].push((reverse, 1)); + rows.push(LinearConstraint::ge( + vec![(depth + v, 1), (depth + u, -1), (forward, -big_m)], + 1 - big_m, + )); + rows.push(LinearConstraint::ge( + vec![(depth + u, 1), (depth + v, -1), (reverse, -big_m)], + 1 - big_m, + )); + if odd { + centers[u].push((center + e, -1)); + if u != v { + centers[v].push((center + e, -1)); + } + } + } + for (v, terms) in incoming.into_iter().enumerate() { + rows.push(LinearConstraint::eq(terms, 1)); + rows.push(LinearConstraint::le(vec![(depth + v, 1), (root + v, q)], q)); + } + if odd { + rows.push(LinearConstraint::eq( + (0..m).map(|e| (center + e, 1)).collect(), + 1, + )); + rows.extend( + centers + .into_iter() + .map(|terms| LinearConstraint::eq(terms, 0)), + ); + } else { + rows.push(LinearConstraint::eq( + (0..n).map(|v| (root + v, 1)).collect(), + 1, + )); + } + rows.push(LinearConstraint::le( + self.edge_weights().iter().copied().enumerate().collect(), + *self.weight_bound(), + )); + let target = ILP::with_variables(variables, rows, vec![], ObjectiveSense::Minimize) + .map_err(Self::target_construction)?; + crate::rules::ilp_helpers::validate_bounded_constraint_arithmetic::(&target)?; + Ok(Self::Result { + target, + num_edges: m, + }) + } +} +#[cfg(feature = "example-db")] +pub(crate) fn canonical_rule_example_specs() -> Vec { + vec![crate::example_db::specs::RuleExampleSpec { + id: "boundeddiameterspanningtree_to_ilp", + build: || { + crate::example_db::specs::rule_example_via_bounded_ilp( + BoundedDiameterSpanningTree::new( + SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]), + vec![1; 3], + 3, + 3, + ), + ) + }, + }] +} +#[cfg(test)] +#[path = "../unit_tests/rules/boundeddiameterspanningtree_ilp.rs"] +mod tests; diff --git a/src/rules/consecutiveonesmatrixaugmentation_ilp.rs b/src/rules/consecutiveonesmatrixaugmentation_ilp.rs index da906ea83..83eea00d1 100644 --- a/src/rules/consecutiveonesmatrixaugmentation_ilp.rs +++ b/src/rules/consecutiveonesmatrixaugmentation_ilp.rs @@ -1,11 +1,8 @@ -//! Reduction from ConsecutiveOnesMatrixAugmentation to ILP. -//! -//! Choose a column permutation and, for each row, choose an interval that will -//! become its consecutive block of 1s. Flips are needed only for zeros inside -//! that interval. +//! Bound each row’s span in a column permutation using two integer endpoints. use crate::models::algebraic::{ - ConsecutiveOnesMatrixAugmentation, LinearConstraint, ObjectiveSense, ILP, + Bounded, ConsecutiveOnesMatrixAugmentation, IntegerVariable, LinearConstraint, ObjectiveSense, + ILP, }; use crate::reduction; use crate::rules::ilp_helpers::{one_hot_assignment_constraints, one_hot_decode}; @@ -13,15 +10,15 @@ use crate::rules::traits::{ReduceTo, ReductionResult}; #[derive(Debug, Clone)] pub struct ReductionCOMAToILP { - target: ILP, + target: ILP, num_cols: usize, } impl ReductionResult for ReductionCOMAToILP { type Source = ConsecutiveOnesMatrixAugmentation; - type Target = ILP; + type Target = ILP; - fn target_problem(&self) -> &ILP { + fn target_problem(&self) -> &ILP { &self.target } @@ -44,152 +41,75 @@ impl ReductionResult for ReductionCOMAToILP { impl crate::rules::AggregateReductionResult for ReductionCOMAToILP {} #[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", - }, + exact { num_vars = "num_cols^2 + 2 * num_rows", }, 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)", + num_constraints = "2 * num_cols + 2 * num_rows * num_cols + 1", + num_nonzeros = "2 * num_cols^2 + 2 * num_rows * num_cols^2 + 2 * num_rows", + max_constraint_magnitude_bits = "2 * num_rows * num_cols + num_cols + 1", }, })] -impl ReduceTo> for ConsecutiveOnesMatrixAugmentation { +impl ReduceTo> for ConsecutiveOnesMatrixAugmentation { type Result = ReductionCOMAToILP; fn reduce_to(&self) -> Result { let m = self.num_rows(); let n = self.num_cols(); - // Variable layout (all binary): - // x_{c,p}: n^2 at [0, n^2) - // a_{r,p}: m*n at [n^2, n^2 + m*n) - // l_{r,p}: m*n at [n^2 + m*n, n^2 + 2*m*n) - // u_{r,p}: m*n at [n^2 + 2*m*n, n^2 + 3*m*n) - // h_{r,p}: m*n at [n^2 + 3*m*n, n^2 + 4*m*n) - // f_{r,p}: m*n at [n^2 + 4*m*n, n^2 + 5*m*n) - let x_off = 0; - let a_off = n * n; - let l_off = n * n + m * n; - let u_off = n * n + 2 * m * n; - let h_off = n * n + 3 * m * n; - let f_off = n * n + 4 * m * n; - let num_vars = n * n + 5 * m * n; - - let mut constraints = Vec::new(); - - // One-hot permutation assignment - constraints.extend(one_hot_assignment_constraints(n, n, x_off)); - - // a_{r,p} = sum_c A_{r,c} * x_{c,p} - for r in 0..m { - for p in 0..n { - let a_idx = a_off + r * n + p; - let mut terms = vec![(a_idx, 1)]; - for c in 0..n { - if self.matrix()[r][c] { - terms.push((x_off + c * n + p, -1)); - } - } - constraints.push(LinearConstraint::eq(terms, 0)); - } - } - - // Per-row interval constraints - for r in 0..m { - let beta_r: i64 = if self.matrix()[r].iter().any(|&v| v) { - 1 - } else { - 0 - }; - - // sum_p l_{r,p} = beta_r - let l_terms: Vec<(usize, i64)> = (0..n).map(|p| (l_off + r * n + p, 1)).collect(); - constraints.push(LinearConstraint::eq(l_terms, beta_r)); - - // sum_p u_{r,p} = beta_r - let u_terms: Vec<(usize, i64)> = (0..n).map(|p| (u_off + r * n + p, 1)).collect(); - constraints.push(LinearConstraint::eq(u_terms, beta_r)); - - // sum_p p*l_{r,p} <= sum_p p*u_{r,p} + (n-1)*(1 - beta_r) - // => sum_p p*l_{r,p} - sum_p p*u_{r,p} <= (n-1)*(1 - beta_r) - let mut order_terms = Vec::new(); - for p in 0..n { - let p_i64 = >>::exact_i64( - p, - "encoding a matrix column position", - )?; - order_terms.push((l_off + r * n + p, p_i64)); - order_terms.push((u_off + r * n + p, -p_i64)); - } - let last_position = >>::exact_i64( - n.saturating_sub(1), - "encoding the final matrix column position", - )?; - constraints.push(LinearConstraint::le( - order_terms, - last_position * (1 - beta_r), - )); - - for p in 0..n { - let h_idx = h_off + r * n + p; - let a_idx = a_off + r * n + p; - let f_idx = f_off + r * n + p; - - // h_{r,p} <= sum_{q=0}^{p} l_{r,q} - let l_prefix: Vec<(usize, i64)> = - (0..=p).map(|q| (l_off + r * n + q, -1)).collect(); - let mut h_le_l = vec![(h_idx, 1)]; - h_le_l.extend(l_prefix); - constraints.push(LinearConstraint::le(h_le_l, 0)); - - // h_{r,p} <= sum_{q=p}^{n-1} u_{r,q} - let u_suffix: Vec<(usize, i64)> = (p..n).map(|q| (u_off + r * n + q, -1)).collect(); - let mut h_le_u = vec![(h_idx, 1)]; - h_le_u.extend(u_suffix); - constraints.push(LinearConstraint::le(h_le_u, 0)); - - // h_{r,p} >= sum_{q=0}^{p} l_{r,q} + sum_{q=p}^{n-1} u_{r,q} - 1 - let mut h_ge_terms = vec![(h_idx, 1)]; - for q in 0..=p { - h_ge_terms.push((l_off + r * n + q, -1)); + let overflow = || { + crate::rules::ReductionError::integer_overflow::>( + "counting matrix interval variables", + ) + }; + let square = n.checked_mul(n).ok_or_else(overflow)?; + let num_vars = m + .checked_mul(2) + .and_then(|count| square.checked_add(count)) + .ok_or_else(overflow)?; + Self::exact_i64(square, "bounding permutation row arithmetic")?; + let cells = Self::exact_i64( + m.checked_mul(n).ok_or_else(overflow)?, + "counting matrix cells", + )?; + let last_position = Self::exact_i64(n.saturating_sub(1), "bounding interval endpoints")?; + let mut variables = vec![IntegerVariable::binary(); square]; + let mut constraints = one_hot_assignment_constraints(n, n, 0); + let mut budget = Vec::new(); + let mut total_ones = 0_i64; + let mut active_rows = 0_i64; + for (r, row) in self.matrix().iter().enumerate() { + let nonempty = row.iter().any(|&value| value); + let endpoint = + IntegerVariable::new(Some(0), Some(if nonempty { last_position } else { 0 })) + .map_err(Self::target_construction)?; + variables.extend([endpoint, endpoint]); + let left = square + 2 * r; + let right = left + 1; + for (c, &one) in row.iter().enumerate() { + if !one { + continue; } - for q in p..n { - h_ge_terms.push((u_off + r * n + q, -1)); + total_ones += 1; + let mut after_left = vec![(left, -1)]; + let mut before_right = vec![(right, 1)]; + for p in 1..n { + let coefficient = Self::exact_i64(p, "encoding a column position")?; + after_left.push((c * n + p, coefficient)); + before_right.push((c * n + p, -coefficient)); } - constraints.push(LinearConstraint::ge(h_ge_terms, -1)); - - // a_{r,p} <= h_{r,p} - constraints.push(LinearConstraint::le(vec![(a_idx, 1), (h_idx, -1)], 0)); - - // h_{r,p} <= a_{r,p} + f_{r,p} - constraints.push(LinearConstraint::le( - vec![(h_idx, 1), (a_idx, -1), (f_idx, -1)], - 0, - )); - - // f_{r,p} <= h_{r,p} - constraints.push(LinearConstraint::le(vec![(f_idx, 1), (h_idx, -1)], 0)); - - // f_{r,p} + a_{r,p} <= 1 - constraints.push(LinearConstraint::le(vec![(f_idx, 1), (a_idx, 1)], 1)); + constraints.push(LinearConstraint::ge(after_left, 0)); + constraints.push(LinearConstraint::ge(before_right, 0)); } - } - - // Augmentation budget: sum f_{r,p} <= K - let mut budget_terms = Vec::new(); - for r in 0..m { - for p in 0..n { - budget_terms.push((f_off + r * n + p, 1)); + if nonempty { + active_rows += 1; + budget.extend([(left, -1), (right, 1)]); } } - 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) + // No ordering can fill more than the matrix's total number of zeros. + // Clipping before addition keeps even i64::MAX budgets representable. + let rhs = self.bound().min(cells - total_ones) + total_ones - active_rows; + constraints.push(LinearConstraint::le(budget, rhs)); + debug_assert_eq!(variables.len(), num_vars); + let target = ILP::with_variables(variables, constraints, vec![], ObjectiveSense::Minimize) .map_err(Self::target_construction)?; Ok(ReductionCOMAToILP { target, @@ -200,32 +120,14 @@ impl ReduceTo> for ConsecutiveOnesMatrixAugmentation { #[cfg(feature = "example-db")] pub(crate) fn canonical_rule_example_specs() -> Vec { - use crate::export::SolutionPair; vec![crate::example_db::specs::RuleExampleSpec { id: "consecutiveonesmatrixaugmentation_to_ilp", build: || { - let source = ConsecutiveOnesMatrixAugmentation::new( - vec![vec![true, false, true], vec![false, true, true]], - 1, - ); - // Identity permutation [0,1,2]: - // Row 0: [1,0,1] needs 1 flip (the middle 0), cost=1 - // Row 1: [0,1,1] needs 0 flips, cost=0 - // Total = 1 <= 1 - let reduction: ReductionCOMAToILP = - ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); - let ilp_solver = crate::solvers::ILPSolver::new(); - let target_config = ilp_solver - .solve(reduction.target_problem()) - .expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&target_config).unwrap(); - crate::example_db::specs::rule_example_with_witness::<_, ILP>( - source, - SolutionPair { - source_config: serde_json::json!(extracted), - target_config: serde_json::to_value(target_config) - .expect("solution serialization must succeed"), - }, + crate::example_db::specs::rule_example_via_bounded_ilp( + ConsecutiveOnesMatrixAugmentation::new( + vec![vec![true, false, true], vec![false, true, true]], + 1, + ), ) }, }] diff --git a/src/rules/cyclicordering_ilp.rs b/src/rules/cyclicordering_ilp.rs new file mode 100644 index 000000000..1a8520a29 --- /dev/null +++ b/src/rules/cyclicordering_ilp.rs @@ -0,0 +1,101 @@ +//! Sparse rank comparisons for cyclic ordering triples. + +use crate::models::algebraic::{Bounded, IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; +use crate::models::misc::CyclicOrdering; +use crate::rules::ilp_helpers::{bounded_order_comparison, ranks_to_positions}; +use crate::rules::traits::{ReduceTo, ReductionResult}; + +#[derive(Debug, Clone)] +pub struct ReductionCyclicOrderingToILP { + target: ILP, + num_elements: usize, +} + +impl ReductionResult for ReductionCyclicOrderingToILP { + type Source = CyclicOrdering; + 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( + &self.target, + solution, + |value| value.value.is_some(), + "target ILP assignment is infeasible", + )?; + Ok(ranks_to_positions(&solution[..self.num_elements])) + } +} + +#[crate::aggregate_reduction(ilp_feasibility)] +impl crate::rules::AggregateReductionResult for ReductionCyclicOrderingToILP {} + +#[crate::reduction(transform = { + exact { + num_vars = "num_elements + 3 * num_triples", + num_constraints = "7 * num_triples", + num_nonzeros = "21 * num_triples", + }, + upper_bound { max_constraint_magnitude_bits = "num_elements", }, +})] +impl ReduceTo> for CyclicOrdering { + type Result = ReductionCyclicOrderingToILP; + + fn reduce_to(&self) -> Result { + let n = self.num_elements(); + let count = self + .num_triples() + .checked_mul(3) + .and_then(|v| n.checked_add(v)) + .ok_or_else(|| { + crate::rules::ReductionError::integer_overflow::>( + "counting cyclic-ordering variables", + ) + })?; + let bound = Self::exact_i64(n, "bounding ordering ranks")?; + let mut variables = vec![ + IntegerVariable::new(Some(0), Some(bound - 1)) + .map_err(Self::target_construction)?; + n + ]; + variables.resize(count, IntegerVariable::binary()); + let mut constraints = Vec::new(); + for (index, &(a, b, c)) in self.triples().iter().enumerate() { + let base = n + 3 * index; + for (offset, (u, v)) in [(a, b), (b, c), (c, a)].into_iter().enumerate() { + constraints.extend(bounded_order_comparison(u, v, base + offset, bound)); + } + constraints.push(LinearConstraint::eq( + (base..base + 3).map(|i| (i, 1)).collect(), + 2, + )); + } + let target = ILP::with_variables(variables, constraints, vec![], ObjectiveSense::Minimize) + .map_err(Self::target_construction)?; + crate::rules::ilp_helpers::validate_bounded_constraint_arithmetic::(&target)?; + Ok(Self::Result { + target, + num_elements: n, + }) + } +} + +#[cfg(feature = "example-db")] +pub(crate) fn canonical_rule_example_specs() -> Vec { + vec![crate::example_db::specs::RuleExampleSpec { + id: "cyclicordering_to_ilp", + build: || { + crate::example_db::specs::rule_example_via_bounded_ilp(CyclicOrdering::new( + 3, + vec![(0, 1, 2)], + )) + }, + }] +} + +#[cfg(test)] +#[path = "../unit_tests/rules/cyclicordering_ilp.rs"] +mod tests; diff --git a/src/rules/feasibleregisterassignment_ilp.rs b/src/rules/feasibleregisterassignment_ilp.rs index d5815f763..73cf309ed 100644 --- a/src/rules/feasibleregisterassignment_ilp.rs +++ b/src/rules/feasibleregisterassignment_ilp.rs @@ -1,18 +1,9 @@ -//! Reduction from Feasible Register Assignment to ILP (Integer Linear Programming). -//! -//! The formulation uses non-negative integer variables: -//! - `t_v`: evaluation position of vertex `v` -//! - `L_v`: latest position among `v` and all dependents of `v` -//! - `z_uv`: binary order selector for each unordered pair `{u, v}` -//! -//! The pair-order constraints force the `t_v` values to form a permutation of -//! `{0, ..., n-1}`. For same-register pairs, the extra constraints enforce -//! interval non-overlap: if `u` is before `v`, then `v` must be scheduled no -//! earlier than the latest dependent of `u`. +//! Bounded ranks with order selectors only for vertices sharing a register. use crate::models::algebraic::{Bounded, IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::FeasibleRegisterAssignment; use crate::reduction; +use crate::rules::ilp_helpers::{bounded_order_comparison, ranks_to_positions}; use crate::rules::traits::{ReduceTo, ReductionResult}; #[derive(Debug, Clone)] @@ -40,7 +31,7 @@ impl ReductionResult for ReductionFeasibleRegisterAssignmentToILP { "target ILP assignment is infeasible", )?; - crate::rules::ilp_helpers::decode_usize_values(&target_solution[..self.num_vertices]) + Ok(ranks_to_positions(&target_solution[..self.num_vertices])) } } @@ -49,11 +40,11 @@ impl crate::rules::AggregateReductionResult for ReductionFeasibleRegisterAssignm #[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", + num_vars = "num_vertices + num_same_register_pairs", }, upper_bound { + num_constraints = "num_vertices * num_arcs + 2 * num_same_register_pairs", + num_nonzeros = "3 * num_vertices * num_arcs + 6 * num_same_register_pairs", max_constraint_magnitude_bits = "num_vertices + 1", }, })] @@ -62,91 +53,60 @@ impl ReduceTo> for FeasibleRegisterAssignment { fn reduce_to(&self) -> Result { let n = self.num_vertices(); - let pair_list: Vec<(usize, usize)> = (0..n) + let pairs: Vec<_> = (0..n) .flat_map(|u| ((u + 1)..n).map(move |v| (u, v))) + .filter(|&(u, v)| self.assignment()[u] == self.assignment()[v]) .collect(); - let same_register_pairs: Vec<(usize, usize, usize)> = pair_list - .iter() - .copied() - .enumerate() - .filter(|(_, (u, v))| self.assignment()[*u] == self.assignment()[*v]) - .map(|(pair_idx, (u, v))| (u, v, pair_idx)) - .collect(); - - let num_pair_vars = pair_list.len(); - let num_vars = 2 * n + num_pair_vars; + let num_vars = n.checked_add(pairs.len()).ok_or_else(|| { + crate::rules::ReductionError::integer_overflow::>( + "counting register assignment variables", + ) + })?; let big_m = Self::exact_i64(n, "encoding the schedule order")?; - let last_position = - Self::exact_i64(n.saturating_sub(1), "encoding the final schedule position")?; - - let time_idx = |vertex: usize| -> usize { vertex }; - let latest_idx = |vertex: usize| -> usize { n + vertex }; - let order_idx = |pair_idx: usize| -> usize { 2 * n + pair_idx }; - - let mut constraints = Vec::with_capacity( - 3 * num_pair_vars + 3 * n + 2 * self.num_arcs() + 2 * same_register_pairs.len(), - ); - - for vertex in 0..n { - constraints.push(LinearConstraint::le( - vec![(time_idx(vertex), 1)], - last_position, - )); - constraints.push(LinearConstraint::le( - vec![(latest_idx(vertex), 1)], - last_position, - )); - constraints.push(LinearConstraint::ge( - vec![(latest_idx(vertex), 1), (time_idx(vertex), -1)], - 0, - )); - } - + let last_position = big_m.saturating_sub(1).max(0); + let mut variables = vec![ + IntegerVariable::new(Some(0), Some(last_position)) + .map_err(Self::target_construction)?; + n + ]; + variables.resize(num_vars, IntegerVariable::binary()); + let mut constraints = Vec::new(); + let mut dependents = vec![Vec::new(); n]; for &(dependent, dependency) in self.arcs() { constraints.push(LinearConstraint::ge( - vec![(time_idx(dependent), 1), (time_idx(dependency), -1)], - 1, - )); - constraints.push(LinearConstraint::ge( - vec![(latest_idx(dependency), 1), (time_idx(dependent), -1)], - 0, - )); - } - - for (pair_idx, &(u, v)) in pair_list.iter().enumerate() { - let order_var = order_idx(pair_idx); - constraints.push(LinearConstraint::le(vec![(order_var, 1)], 1)); - constraints.push(LinearConstraint::ge( - vec![(time_idx(v), 1), (time_idx(u), -1), (order_var, -big_m)], - 1 - big_m, - )); - constraints.push(LinearConstraint::ge( - vec![(time_idx(u), 1), (time_idx(v), -1), (order_var, big_m)], + vec![(dependent, 1), (dependency, -1)], 1, )); + dependents[dependency].push(dependent); } - - for &(u, v, pair_idx) in &same_register_pairs { - let order_var = order_idx(pair_idx); - constraints.push(LinearConstraint::ge( - vec![(time_idx(v), 1), (latest_idx(u), -1), (order_var, -big_m)], - -big_m, - )); - constraints.push(LinearConstraint::ge( - vec![(time_idx(u), 1), (latest_idx(v), -1), (order_var, big_m)], - 0, - )); + for (index, &(u, v)) in pairs.iter().enumerate() { + let selector = n + index; + constraints.extend(bounded_order_comparison(u, v, selector, big_m)); + // An overwriter may consume the old value in that same operation. + // Every other consumer must finish strictly before the overwrite. + for &w in &dependents[u] { + if w != v { + constraints.push(LinearConstraint::ge( + vec![(v, 1), (w, -1), (selector, -big_m)], + 1 - big_m, + )); + } + } + for &w in &dependents[v] { + if w != u { + constraints.push(LinearConstraint::ge( + vec![(u, 1), (w, -1), (selector, big_m)], + 1, + )); + } + } } - - let mut variables = vec![IntegerVariable::binary(); num_vars]; - variables[..2 * n].fill( - IntegerVariable::new(Some(0), Some(last_position)) - .map_err(Self::target_construction)?, - ); + let target = ILP::with_variables(variables, constraints, vec![], ObjectiveSense::Minimize) + .map_err(Self::target_construction)?; + crate::rules::ilp_helpers::validate_bounded_constraint_arithmetic::(&target)?; Ok(ReductionFeasibleRegisterAssignmentToILP { - target: ILP::with_variables(variables, constraints, vec![], ObjectiveSense::Minimize) - .map_err(Self::target_construction)?, + target, num_vertices: n, }) } diff --git a/src/rules/hamiltonianpath_ilp.rs b/src/rules/hamiltonianpath_ilp.rs index d6ba3a77c..c2eedd5c6 100644 --- a/src/rules/hamiltonianpath_ilp.rs +++ b/src/rules/hamiltonianpath_ilp.rs @@ -1,26 +1,13 @@ -//! Reduction from HamiltonianPath to ILP (Integer Linear Programming). -//! -//! Position-assignment formulation: -//! - Binary x_{v,p}: vertex v at position p -//! - Binary z_{(u,v),p,dir}: linearized product for edge (u,v) at consecutive positions -//! - Assignment: each vertex in exactly one position, each position exactly one vertex -//! - Adjacency: at least one graph edge between consecutive positions +//! A permutation matrix with direct consecutive-position adjacency constraints. use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::HamiltonianPath; use crate::reduction; -use crate::rules::ilp_helpers::{ - mccormick_product, one_hot_assignment_constraints, one_hot_decode, -}; +use crate::rules::ilp_helpers::{one_hot_assignment_constraints, one_hot_decode}; use crate::rules::traits::{ReduceTo, ReductionResult}; use crate::topology::{Graph, SimpleGraph}; -/// Result of reducing HamiltonianPath to ILP. -/// -/// Variable layout (all binary): -/// - `x_{v,p}` at index `v * n + p` for `v, p in 0..n` -/// - `z_{e,p,dir}` at index `n^2 + 2*(e*n_pos + p) + dir` for edge `e`, position `p`, -/// direction `dir in {0=forward, 1=reverse}` +/// Binary `x[v,p]` at index `v * n + p` selects vertex v at path position p. #[derive(Debug, Clone)] pub struct ReductionHamiltonianPathToILP { target: ILP, @@ -53,63 +40,43 @@ impl ReductionResult for ReductionHamiltonianPathToILP { #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionHamiltonianPathToILP {} -#[reduction( - transform = exact { +#[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", - } -)] + num_vars = "num_vertices^2", + num_constraints = "2 * num_vertices + num_vertices * num_consecutive_positions", + }, + upper_bound { + num_nonzeros = "2 * num_vertices^2 + num_consecutive_positions * (num_vertices + 2 * num_edges)", + }, +})] impl ReduceTo> for HamiltonianPath { type Result = ReductionHamiltonianPathToILP; fn reduce_to(&self) -> Result { let n = self.num_vertices(); - let graph = self.graph(); - let edges = graph.edges(); - let m = edges.len(); - let n_pos = self.num_consecutive_positions(); - - let num_x = n * n; - let num_z = 2 * m * n_pos; - let num_vars = num_x + num_z; - - let x_idx = |v: usize, p: usize| -> usize { v * n + p }; - let z_fwd_idx = |e: usize, p: usize| -> usize { num_x + 2 * (e * n_pos + p) }; - let z_rev_idx = |e: usize, p: usize| -> usize { num_x + 2 * (e * n_pos + p) + 1 }; - - let mut constraints = Vec::new(); - - // Assignment: one-hot for vertices and positions - constraints.extend(one_hot_assignment_constraints(n, n, 0)); - - // McCormick linearization for both directions - for (e, &(u, v)) in edges.iter().enumerate() { - for p in 0..n_pos { - // Forward: z_fwd = x_{u,p} * x_{v,p+1} - constraints.extend(mccormick_product( - z_fwd_idx(e, p), - x_idx(u, p), - x_idx(v, p + 1), - )); - // Reverse: z_rev = x_{v,p} * x_{u,p+1} - constraints.extend(mccormick_product( - z_rev_idx(e, p), - x_idx(v, p), - x_idx(u, p + 1), - )); + let num_vars = n.checked_mul(n).ok_or_else(|| { + crate::rules::ReductionError::integer_overflow::>( + "counting Hamiltonian permutation variables", + ) + })?; + >>::exact_i64(n, "bounding adjacency row sums")?; + let mut neighbors = vec![Vec::new(); n]; + for (u, v) in self.graph().edges() { + if u != v { + neighbors[u].push(v); + neighbors[v].push(u); } } - - // At least one connecting edge; parallel edges may contribute more than one. - for p in 0..n_pos { - let mut terms = Vec::new(); - for e in 0..m { - terms.push((z_fwd_idx(e, p), 1)); - terms.push((z_rev_idx(e, p), 1)); + let mut constraints = one_hot_assignment_constraints(n, n, 0); + for (v, adjacent) in neighbors.iter_mut().enumerate() { + adjacent.sort_unstable(); + adjacent.dedup(); + for p in 0..self.num_consecutive_positions() { + let mut terms = vec![(v * n + p, 1)]; + terms.extend(adjacent.iter().map(|&w| (w * n + p + 1, -1))); + constraints.push(LinearConstraint::le(terms, 0)); } - constraints.push(LinearConstraint::ge(terms, 1)); } // Feasibility: no objective diff --git a/src/rules/ilp_helpers.rs b/src/rules/ilp_helpers.rs index 981d3922d..d9cbd5a88 100644 --- a/src/rules/ilp_helpers.rs +++ b/src/rules/ilp_helpers.rs @@ -157,6 +157,33 @@ pub fn permutation_to_lehmer(permutation: &[usize]) -> Vec { .collect() } +/// Compare ranks in `0..num_positions`: selector one means first precedes second. +pub(crate) fn bounded_order_comparison( + first: usize, + second: usize, + selector: usize, + num_positions: i64, +) -> [LinearConstraint; 2] { + [ + LinearConstraint::ge( + vec![(second, 1), (first, -1), (selector, -num_positions)], + 1 - num_positions, + ), + LinearConstraint::ge(vec![(first, 1), (second, -1), (selector, num_positions)], 1), + ] +} + +/// Break rank ties by element index, preserving every strict comparison. +pub(crate) fn ranks_to_positions(ranks: &[i64]) -> Vec { + let mut elements: Vec<_> = (0..ranks.len()).collect(); + elements.sort_by_key(|&element| (ranks[element], element)); + let mut positions = vec![0; ranks.len()]; + for (position, element) in elements.into_iter().enumerate() { + positions[element] = position; + } + positions +} + /// Constrain each item to exactly one slot and each slot to at most one item. pub fn one_hot_assignment_constraints( num_items: usize, diff --git a/src/rules/minimumcodegenerationunlimitedregisters_ilp.rs b/src/rules/minimumcodegenerationunlimitedregisters_ilp.rs new file mode 100644 index 000000000..3f6f4a988 --- /dev/null +++ b/src/rules/minimumcodegenerationunlimitedregisters_ilp.rs @@ -0,0 +1,115 @@ +//! Rank dependencies and copy indicators for two-address operations. +use crate::models::algebraic::{Bounded, IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; +use crate::models::misc::MinimumCodeGenerationUnlimitedRegisters; +use crate::rules::ilp_helpers::ranks_to_positions; +use crate::rules::traits::{ReduceTo, ReductionResult}; + +#[derive(Debug, Clone)] +pub struct ReductionMinimumCodeGenerationUnlimitedRegistersToILP { + target: ILP, + num_operations: usize, +} +impl ReductionResult for ReductionMinimumCodeGenerationUnlimitedRegistersToILP { + type Source = MinimumCodeGenerationUnlimitedRegisters; + 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( + &self.target, + solution, + |value| value.value.is_some(), + "target ILP assignment is infeasible", + )?; + Ok(ranks_to_positions(&solution[..self.num_operations])) + } +} +#[crate::reduction(transform = upper_bound { + num_vars = "2 * num_vertices + 1", + num_constraints = "num_vertices * num_vertices + num_vertices", + num_nonzeros = "3 * num_vertices * num_vertices + num_vertices", + max_constraint_magnitude_bits = "num_vertices + 1", +})] +impl ReduceTo> for MinimumCodeGenerationUnlimitedRegisters { + type Result = ReductionMinimumCodeGenerationUnlimitedRegistersToILP; + fn reduce_to(&self) -> Result { + // Every internal vertex has exactly one left operand. + let mut operations = self.left_arcs().to_vec(); + operations.sort_unstable(); + let k = operations.len(); + let count = k + .checked_mul(2) + .and_then(|v| v.checked_add(1)) + .ok_or_else(|| { + crate::rules::ReductionError::integer_overflow::>( + "counting code-generation variables", + ) + })?; + let bound = Self::exact_i64(k, "bounding operation ranks")?; + Self::exact_i64(count, "bounding the instruction count")?; + let mut variables = Vec::with_capacity(count); + for _ in 0..k { + variables.push( + IntegerVariable::new(Some(0), Some(bound - 1)) + .map_err(Self::target_construction)?, + ); + } + variables.extend(std::iter::repeat_n(IntegerVariable::binary(), k)); + variables.push(IntegerVariable::new(Some(1), Some(1)).map_err(Self::target_construction)?); + let mut index = vec![None; self.num_vertices()]; + let mut users = vec![Vec::new(); self.num_vertices()]; + for (i, &(v, _)) in operations.iter().enumerate() { + index[v] = Some(i); + } + let mut rows = Vec::new(); + for &(v, child) in self.left_arcs().iter().chain(self.right_arcs()) { + let parent = index[v].expect("every operation has a left operand"); + users[child].push(parent); + if let Some(child) = index[child] { + rows.push(LinearConstraint::ge(vec![(parent, 1), (child, -1)], 1)); + } + } + for users in &mut users { + users.sort_unstable(); + users.dedup(); + } + for (v, &(_, left)) in operations.iter().enumerate() { + for &u in &users[left] { + if u != v { + rows.push(LinearConstraint::ge( + vec![(v, 1), (u, -1), (k + v, bound)], + 1, + )); + } + } + } + let mut objective: Vec<_> = (k..2 * k).map(|i| (i, 1)).collect(); + objective.push((2 * k, bound)); + let target = ILP::with_variables(variables, rows, objective, ObjectiveSense::Minimize) + .map_err(Self::target_construction)?; + crate::rules::ilp_helpers::validate_bounded_constraint_arithmetic::(&target)?; + Ok(Self::Result { + target, + num_operations: k, + }) + } +} +#[cfg(feature = "example-db")] +pub(crate) fn canonical_rule_example_specs() -> Vec { + vec![crate::example_db::specs::RuleExampleSpec { + id: "minimumcodegenerationunlimitedregisters_to_ilp", + build: || { + crate::example_db::specs::rule_example_via_bounded_ilp( + MinimumCodeGenerationUnlimitedRegisters::new( + 5, + vec![(1, 3), (2, 3), (0, 1)], + vec![(1, 4), (2, 4), (0, 2)], + ), + ) + }, + }] +} +#[cfg(test)] +#[path = "../unit_tests/rules/minimumcodegenerationunlimitedregisters_ilp.rs"] +mod tests; diff --git a/src/rules/minimumweightandorgraph_ilp.rs b/src/rules/minimumweightandorgraph_ilp.rs new file mode 100644 index 000000000..ad633e089 --- /dev/null +++ b/src/rules/minimumweightandorgraph_ilp.rs @@ -0,0 +1,117 @@ +//! Gate selection and source reachability, including shared or cyclic subgraphs. +use crate::models::algebraic::{Bounded, IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; +use crate::models::misc::MinimumWeightAndOrGraph; +use crate::rules::traits::{ReduceTo, ReductionResult}; + +#[derive(Debug, Clone)] +pub struct ReductionMinimumWeightAndOrGraphToILP { + target: ILP, + num_arcs: usize, +} +impl ReductionResult for ReductionMinimumWeightAndOrGraphToILP { + type Source = MinimumWeightAndOrGraph; + 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( + &self.target, + solution, + |v| v.value.is_some(), + "target ILP assignment is infeasible", + )?; + Ok(solution[..self.num_arcs].iter().map(|&v| v == 1).collect()) + } +} +#[crate::reduction(transform = { + exact { num_vars = "num_vertices + 2 * num_arcs", }, + upper_bound { + num_constraints = "2 * num_vertices + 4 * num_arcs", + num_nonzeros = "2 * num_vertices + 10 * num_arcs", + max_constraint_magnitude_bits = "num_vertices", + }, +})] +impl ReduceTo> for MinimumWeightAndOrGraph { + type Result = ReductionMinimumWeightAndOrGraphToILP; + fn reduce_to(&self) -> Result { + let n = self.num_vertices(); + let m = self.num_arcs(); + let count = m + .checked_mul(2) + .and_then(|v| v.checked_add(n)) + .ok_or_else(|| { + crate::rules::ReductionError::integer_overflow::>( + "counting AND/OR flow variables", + ) + })?; + let capacity = Self::exact_i64(n - 1, "bounding reachability flow")?; + let flow = m + n; + let mut variables = Vec::with_capacity(count); + variables.resize(flow, IntegerVariable::binary()); + let mut outgoing = vec![Vec::new(); n]; + let mut balance: Vec<_> = (0..n).map(|v| vec![(m + v, -1)]).collect(); + let mut rows = vec![LinearConstraint::eq(vec![(m + self.source(), 1)], 1)]; + for (e, &(u, v)) in self.arcs().iter().enumerate() { + let gate = self.gate_types()[u].is_some(); + variables.push( + IntegerVariable::new(Some(0), Some(if gate { capacity } else { 0 })) + .map_err(Self::target_construction)?, + ); + outgoing[u].push((e, 1)); + rows.push(LinearConstraint::le(vec![(e, 1), (m + u, -1)], 0)); + if gate { + rows.push(LinearConstraint::le(vec![(e, 1), (m + v, -1)], 0)); + rows.push(LinearConstraint::le(vec![(flow + e, 1), (e, -capacity)], 0)); + balance[v].push((flow + e, 1)); + balance[u].push((flow + e, -1)); + } + } + for (u, terms) in outgoing.into_iter().enumerate() { + match self.gate_types()[u] { + Some(true) => rows.extend( + terms + .into_iter() + .map(|(e, _)| LinearConstraint::le(vec![(m + u, 1), (e, -1)], 0)), + ), + Some(false) => { + let mut terms = terms; + terms.push((m + u, -1)); + rows.push(LinearConstraint::ge(terms, 0)); + } + None => {} + } + } + for (v, terms) in balance.into_iter().enumerate() { + if v != self.source() { + rows.push(LinearConstraint::ge(terms, 0)); + } + } + let objective = self.arc_weights().iter().copied().enumerate().collect(); + let target = ILP::with_variables(variables, rows, objective, ObjectiveSense::Minimize) + .map_err(Self::target_construction)?; + crate::rules::ilp_helpers::validate_bounded_constraint_arithmetic::(&target)?; + Ok(Self::Result { + target, + num_arcs: m, + }) + } +} +#[cfg(feature = "example-db")] +pub(crate) fn canonical_rule_example_specs() -> Vec { + vec![crate::example_db::specs::RuleExampleSpec { + id: "minimumweightandorgraph_to_ilp", + build: || { + crate::example_db::specs::rule_example_via_bounded_ilp(MinimumWeightAndOrGraph::new( + 3, + vec![(0, 1), (0, 2), (1, 2)], + 0, + vec![Some(true), Some(false), None], + vec![1, 2, -4], + )) + }, + }] +} +#[cfg(test)] +#[path = "../unit_tests/rules/minimumweightandorgraph_ilp.rs"] +mod tests; diff --git a/src/rules/mod.rs b/src/rules/mod.rs index 6d6e7f5b0..e2762b988 100644 --- a/src/rules/mod.rs +++ b/src/rules/mod.rs @@ -9,12 +9,15 @@ pub use registry::{ #[doc(hidden)] pub use traits::aggregate_view; +pub(crate) mod betweenness_ilp; pub(crate) mod bicliquecover_bmf; pub(crate) mod bmf_bicliquecover; +pub(crate) mod boundeddiameterspanningtree_ilp; pub(crate) mod circuit_sat; pub(crate) mod circuit_spinglass; mod closestvectorproblem_qubo; pub(crate) mod coloring_qubo; +pub(crate) mod cyclicordering_ilp; pub(crate) mod decisionmaximumindependentset_integralflowbundles; pub(crate) mod decisionminimumdominatingset_minimumsummulticenter; pub(crate) mod decisionminimumdominatingset_minmaxmulticenter; @@ -29,6 +32,9 @@ pub(crate) mod exactcoverby3sets_staffscheduling; pub(crate) mod exactcoverby3sets_subsetproduct; pub(crate) mod factoring_circuit; mod graph; +pub(crate) mod minimumweightandorgraph_ilp; +pub(crate) mod partitionintoperfectmatchings_ilp; +pub(crate) mod twodimensionalconsecutivesets_ilp; pub(crate) use graph::{recover_completed_result, RecoveryStep}; pub(crate) mod graph_helpers; pub(crate) mod graphpartitioning_maxcut; @@ -89,6 +95,7 @@ mod maximumindependentset_triangular; pub(crate) mod maximummatching_maximumsetpacking; mod maximumsetpacking_casts; pub(crate) mod maximumsetpacking_qubo; +pub(crate) mod minimumcodegenerationunlimitedregisters_ilp; pub(crate) mod minimumcostmaximumflow_minimumcostcirculation; pub(crate) mod minimumcoveringbycliques_minimumintersectiongraphbasis; pub(crate) mod minimumdiscreteplanarinversekinematics_qubo; @@ -305,6 +312,13 @@ pub use traits::{ #[cfg(feature = "example-db")] pub(crate) fn canonical_rule_example_specs() -> Vec { let mut specs = Vec::new(); + specs.extend(partitionintoperfectmatchings_ilp::canonical_rule_example_specs()); + specs.extend(minimumweightandorgraph_ilp::canonical_rule_example_specs()); + specs.extend(boundeddiameterspanningtree_ilp::canonical_rule_example_specs()); + specs.extend(betweenness_ilp::canonical_rule_example_specs()); + specs.extend(minimumcodegenerationunlimitedregisters_ilp::canonical_rule_example_specs()); + specs.extend(cyclicordering_ilp::canonical_rule_example_specs()); + specs.extend(twodimensionalconsecutivesets_ilp::canonical_rule_example_specs()); specs.extend(bicliquecover_bmf::canonical_rule_example_specs()); specs.extend(bmf_bicliquecover::canonical_rule_example_specs()); specs.extend(circuit_sat::canonical_rule_example_specs()); diff --git a/src/rules/partitionintoperfectmatchings_ilp.rs b/src/rules/partitionintoperfectmatchings_ilp.rs new file mode 100644 index 000000000..95add09a6 --- /dev/null +++ b/src/rules/partitionintoperfectmatchings_ilp.rs @@ -0,0 +1,117 @@ +//! Group labels with exact same-group edge indicators and degree one. +use crate::models::algebraic::{Bounded, IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; +use crate::models::graph::PartitionIntoPerfectMatchings; +use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::topology::{Graph, SimpleGraph}; + +#[derive(Debug, Clone)] +pub struct ReductionPartitionIntoPerfectMatchingsToILP { + target: ILP, + num_vertices: usize, +} +impl ReductionResult for ReductionPartitionIntoPerfectMatchingsToILP { + type Source = PartitionIntoPerfectMatchings; + 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( + &self.target, + solution, + |v| v.value.is_some(), + "target ILP assignment is infeasible", + )?; + crate::rules::ilp_helpers::decode_usize_values(&solution[..self.num_vertices]) + } +} +#[crate::aggregate_reduction(ilp_feasibility)] +impl crate::rules::AggregateReductionResult for ReductionPartitionIntoPerfectMatchingsToILP {} +#[crate::reduction(transform = upper_bound { + num_vars = "num_vertices + 2 * num_edges", + num_constraints = "num_vertices + 4 * num_edges", + num_nonzeros = "16 * num_edges", + max_constraint_magnitude_bits = "num_matchings", +})] +impl ReduceTo> for PartitionIntoPerfectMatchings { + type Result = ReductionPartitionIntoPerfectMatchingsToILP; + fn reduce_to(&self) -> Result { + let n = self.num_vertices(); + let k = Self::exact_i64(self.num_matchings(), "bounding matching-group labels")?; + // The source counts distinct other neighbors, ignoring loops and multiplicity. + let mut edges: Vec<_> = self + .graph() + .edges() + .into_iter() + .filter(|(u, v)| u != v) + .map(|(u, v)| (u.min(v), u.max(v))) + .collect(); + edges.sort_unstable(); + edges.dedup(); + let m = edges.len(); + let count = m + .checked_mul(2) + .and_then(|v| v.checked_add(n)) + .ok_or_else(|| { + crate::rules::ReductionError::integer_overflow::>( + "counting matching-partition variables", + ) + })?; + let mut variables = + vec![IntegerVariable::new(Some(0), Some(k - 1)).map_err(Self::target_construction)?; n]; + variables.resize(count, IntegerVariable::binary()); + let mut degree = vec![Vec::new(); n]; + let mut rows = Vec::new(); + for (i, &(u, v)) in edges.iter().enumerate() { + let same = n + i; + let direction = n + m + i; + degree[u].push((same, 1)); + degree[v].push((same, 1)); + rows.push(LinearConstraint::le( + vec![(v, 1), (u, -1), (same, k - 1)], + k - 1, + )); + rows.push(LinearConstraint::le( + vec![(u, 1), (v, -1), (same, k - 1)], + k - 1, + )); + rows.push(LinearConstraint::ge( + vec![(v, 1), (u, -1), (direction, -k), (same, 1)], + 1 - k, + )); + rows.push(LinearConstraint::ge( + vec![(u, 1), (v, -1), (direction, k), (same, 1)], + 1, + )); + } + rows.extend( + degree + .into_iter() + .map(|terms| LinearConstraint::eq(terms, 1)), + ); + let target = ILP::with_variables(variables, rows, vec![], ObjectiveSense::Minimize) + .map_err(Self::target_construction)?; + crate::rules::ilp_helpers::validate_bounded_constraint_arithmetic::(&target)?; + Ok(Self::Result { + target, + num_vertices: n, + }) + } +} +#[cfg(feature = "example-db")] +pub(crate) fn canonical_rule_example_specs() -> Vec { + vec![crate::example_db::specs::RuleExampleSpec { + id: "partitionintoperfectmatchings_to_ilp", + build: || { + crate::example_db::specs::rule_example_via_bounded_ilp( + PartitionIntoPerfectMatchings::new( + SimpleGraph::new(4, vec![(0, 1), (0, 2), (1, 3), (2, 3)]), + 2, + ), + ) + }, + }] +} +#[cfg(test)] +#[path = "../unit_tests/rules/partitionintoperfectmatchings_ilp.rs"] +mod tests; diff --git a/src/rules/registersufficiency_ilp.rs b/src/rules/registersufficiency_ilp.rs index c126fe801..4e6f6dee5 100644 --- a/src/rules/registersufficiency_ilp.rs +++ b/src/rules/registersufficiency_ilp.rs @@ -1,28 +1,21 @@ -//! Reduction from RegisterSufficiency to `ILP`. -//! -//! The formulation uses: -//! - integer `t_v` variables for evaluation positions -//! - integer `l_v` variables for latest-use positions -//! - binary pair-order selectors to force a permutation of `0..n-1` -//! - binary threshold/live indicators to count how many values are live after -//! each evaluation step +//! Binary cumulative evaluation and live-value indicators for register sufficiency. -use crate::models::algebraic::{Bounded, IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; +use crate::models::algebraic::{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 } @@ -37,7 +30,17 @@ impl ReductionResult for ReductionRegisterSufficiencyToILP { "target ILP assignment is infeasible", )?; - crate::rules::ilp_helpers::decode_usize_values(&target_solution[..self.num_vertices]) + let n = self.num_vertices; + (0..n) + .map(|v| { + target_solution[v * n..(v + 1) * n] + .iter() + .position(|&computed| computed != 0) + .ok_or_else(|| { + crate::rules::ExtractionError::invalid("vertex is never computed") + }) + }) + .collect() } } @@ -46,157 +49,87 @@ impl crate::rules::AggregateReductionResult for ReductionRegisterSufficiencyToIL #[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", + num_vars = "2 * num_vertices^2 - num_vertices * num_sinks", + num_constraints = "num_vertices^2 + 2 * num_vertices * num_arcs + num_vertices", + num_nonzeros = "4 * num_vertices^2 - 2 * num_vertices + 5 * num_vertices * num_arcs - num_arcs", }, upper_bound { - max_constraint_magnitude_bits = "2 * num_vertices + bound + 1", + max_constraint_magnitude_bits = "num_vertices + 1", }, })] -impl ReduceTo> for RegisterSufficiency { +impl ReduceTo> for RegisterSufficiency { type Result = ReductionRegisterSufficiencyToILP; fn reduce_to(&self) -> Result { let n = self.num_vertices(); - let pair_list: Vec<(usize, usize)> = (0..n) - .flat_map(|u| ((u + 1)..n).map(move |v| (u, v))) - .collect(); - let num_pair_vars = pair_list.len(); - - let time_offset = 0; - let latest_offset = n; - let order_offset = 2 * n; - let before_offset = order_offset + num_pair_vars; - let after_offset = before_offset + n * n; - let live_offset = after_offset + n * n; - let num_vars = live_offset + n * n; - - let time_idx = |vertex: usize| -> usize { time_offset + vertex }; - let latest_idx = |vertex: usize| -> usize { latest_offset + vertex }; - let order_idx = |pair_idx: usize| -> usize { order_offset + pair_idx }; - let before_idx = - |vertex: usize, step: usize| -> usize { before_offset + vertex * n + step }; - let after_idx = |vertex: usize, step: usize| -> usize { after_offset + vertex * n + step }; - let live_idx = |vertex: usize, step: usize| -> usize { live_offset + vertex * n + step }; - - let big_m = Self::exact_i64(n, "representing the schedule length in ILP rows")?; - let latest_time = big_m; - let maximum_time = Self::exact_i64( - n.saturating_sub(1), - "representing the maximum schedule time in ILP rows", - )?; + let overflow = || { + crate::rules::ReductionError::integer_overflow::>( + "counting cumulative scheduling variables", + ) + }; + let square = n.checked_mul(n).ok_or_else(overflow)?; let mut has_dependent = vec![false; n]; - let mut constraints = Vec::new(); - - for vertex in 0..n { - constraints.push(LinearConstraint::le( - vec![(time_idx(vertex), 1)], - maximum_time, - )); - constraints.push(LinearConstraint::le( - vec![(latest_idx(vertex), 1)], - latest_time, - )); + for &(_, u) in self.arcs() { + has_dependent[u] = true; } - - for (pair_idx, &(u, v)) in pair_list.iter().enumerate() { - let order_var = order_idx(pair_idx); - constraints.push(LinearConstraint::le(vec![(order_var, 1)], 1)); - constraints.push(LinearConstraint::ge( - vec![(time_idx(v), 1), (time_idx(u), -1), (order_var, -big_m)], - 1 - big_m, - )); - constraints.push(LinearConstraint::ge( - vec![(time_idx(u), 1), (time_idx(v), -1), (order_var, big_m)], - 1, - )); - } - - for &(dependent, dependency) in self.arcs() { - has_dependent[dependency] = true; - constraints.push(LinearConstraint::ge( - vec![(time_idx(dependent), 1), (time_idx(dependency), -1)], - 1, - )); - constraints.push(LinearConstraint::ge( - vec![(latest_idx(dependency), 1), (time_idx(dependent), -1)], - 0, - )); - } - - for (vertex, &has_child) in has_dependent.iter().enumerate() { - if !has_child { - constraints.push(LinearConstraint::eq( - vec![(latest_idx(vertex), 1)], - latest_time, - )); + let non_sinks = has_dependent.iter().filter(|&&value| value).count(); + let num_vars = n + .checked_mul(non_sinks) + .and_then(|count| square.checked_add(count)) + .ok_or_else(overflow)?; + let horizon = Self::exact_i64(n, "bounding schedule row sums")?; + let bound = Self::exact_i64(self.bound(), "representing the register bound")?.min(horizon); + let computed = |v: usize, t: usize| v * n + t; + let mut live_offset: Vec<_> = (0..n).map(|v| v * n).collect(); + let mut next = square; + for (v, &needed) in has_dependent.iter().enumerate() { + if needed { + live_offset[v] = next; + next += n; } } - - for vertex in 0..n { - for step in 0..n { - let step_value = Self::exact_i64(step, "representing a schedule step in ILP rows")?; - let before_var = before_idx(vertex, step); - constraints.push(LinearConstraint::le(vec![(before_var, 1)], 1)); - constraints.push(LinearConstraint::le( - vec![(time_idx(vertex), 1), (before_var, big_m)], - step_value + big_m, - )); - constraints.push(LinearConstraint::ge( - vec![(time_idx(vertex), 1), (before_var, big_m)], - step_value + 1, - )); - - let after_var = after_idx(vertex, step); - constraints.push(LinearConstraint::le(vec![(after_var, 1)], 1)); - constraints.push(LinearConstraint::ge( - vec![(latest_idx(vertex), 1), (after_var, -big_m)], - step_value + 1 - big_m, - )); - constraints.push(LinearConstraint::le( - vec![(latest_idx(vertex), 1), (after_var, -big_m)], - step_value, - )); - - let live_var = live_idx(vertex, step); - constraints.push(LinearConstraint::le( - vec![(live_var, 1), (before_var, -1)], - 0, - )); + let mut constraints = Vec::new(); + for v in 0..n { + for t in 0..n.saturating_sub(1) { constraints.push(LinearConstraint::le( - vec![(live_var, 1), (after_var, -1)], + vec![(computed(v, t), 1), (computed(v, t + 1), -1)], 0, )); + } + } + for t in 0..n { + constraints.push(LinearConstraint::eq( + (0..n).map(|v| (computed(v, t), 1)).collect(), + Self::exact_i64(t + 1, "counting completed vertices")?, + )); + } + for &(w, u) in self.arcs() { + for t in 0..n { + let mut precedence = vec![(computed(w, t), 1)]; + if t > 0 { + precedence.push((computed(u, t - 1), -1)); + } + constraints.push(LinearConstraint::le(precedence, 0)); constraints.push(LinearConstraint::ge( - vec![(live_var, 1), (before_var, -1), (after_var, -1)], - -1, + vec![ + (live_offset[u] + t, 1), + (computed(u, t), -1), + (computed(w, t), 1), + ], + 0, )); } } - - for step in 0..n { - let live_terms: Vec<(usize, i64)> = - (0..n).map(|vertex| (live_idx(vertex, step), 1)).collect(); + for t in 0..n { + // For sinks, computed bits are also their exact live indicators. constraints.push(LinearConstraint::le( - live_terms, - Self::exact_i64( - self.bound(), - "representing the register bound in an ILP row", - )?, + live_offset.iter().map(|&offset| (offset + t, 1)).collect(), + bound, )); } - 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::with_variables(variables, constraints, vec![], ObjectiveSense::Minimize) + target: ILP::new(num_vars, constraints, vec![], ObjectiveSense::Minimize) .map_err(Self::target_construction)?, num_vertices: n, }) @@ -222,7 +155,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/twodimensionalconsecutivesets_ilp.rs b/src/rules/twodimensionalconsecutivesets_ilp.rs new file mode 100644 index 000000000..e5a21f4d9 --- /dev/null +++ b/src/rules/twodimensionalconsecutivesets_ilp.rs @@ -0,0 +1,86 @@ +//! Distinct labels within a subset occupy an interval of that subset's size. + +use crate::models::algebraic::{Bounded, IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; +use crate::models::set::TwoDimensionalConsecutiveSets; +use crate::rules::ilp_helpers::{bounded_order_comparison, decode_usize_values}; +use crate::rules::traits::{ReduceTo, ReductionResult}; + +#[derive(Debug, Clone)] +pub struct ReductionTwoDimensionalConsecutiveSetsToILP { + target: ILP, + alphabet_size: usize, +} +impl ReductionResult for ReductionTwoDimensionalConsecutiveSetsToILP { + type Source = TwoDimensionalConsecutiveSets; + 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( + &self.target, + solution, + |value| value.value.is_some(), + "target ILP assignment is infeasible", + )?; + decode_usize_values(&solution[..self.alphabet_size]) + } +} +#[crate::aggregate_reduction(ilp_feasibility)] +impl crate::rules::AggregateReductionResult for ReductionTwoDimensionalConsecutiveSetsToILP {} + +// P=sum_S choose(|S|,2) <= num_subsets * choose(alphabet_size,2). +#[crate::reduction(transform = upper_bound { + num_vars = "alphabet_size + num_subsets * alphabet_size * (alphabet_size - 1) / 2", + num_constraints = "2 * num_subsets * alphabet_size * (alphabet_size - 1)", + num_nonzeros = "5 * num_subsets * alphabet_size * (alphabet_size - 1)", + max_constraint_magnitude_bits = "alphabet_size", +})] +impl ReduceTo> for TwoDimensionalConsecutiveSets { + type Result = ReductionTwoDimensionalConsecutiveSetsToILP; + fn reduce_to(&self) -> Result { + let n = self.alphabet_size(); + let bound = Self::exact_i64(n, "bounding group labels")?; + let mut variables = vec![ + IntegerVariable::new(Some(0), Some(bound - 1)) + .map_err(Self::target_construction)?; + n + ]; + let mut constraints = Vec::new(); + for subset in self.subsets() { + let width = Self::exact_i64(subset.len().saturating_sub(1), "bounding subset span")?; + for (i, &u) in subset.iter().enumerate() { + for &v in &subset[i + 1..] { + let selector = variables.len(); + variables.push(IntegerVariable::binary()); + constraints.extend(bounded_order_comparison(u, v, selector, bound)); + constraints.push(LinearConstraint::le(vec![(v, 1), (u, -1)], width)); + constraints.push(LinearConstraint::le(vec![(u, 1), (v, -1)], width)); + } + } + } + let target = ILP::with_variables(variables, constraints, vec![], ObjectiveSense::Minimize) + .map_err(Self::target_construction)?; + crate::rules::ilp_helpers::validate_bounded_constraint_arithmetic::(&target)?; + Ok(Self::Result { + target, + alphabet_size: n, + }) + } +} + +#[cfg(feature = "example-db")] +pub(crate) fn canonical_rule_example_specs() -> Vec { + vec![crate::example_db::specs::RuleExampleSpec { + id: "twodimensionalconsecutivesets_to_ilp", + build: || { + crate::example_db::specs::rule_example_via_bounded_ilp( + TwoDimensionalConsecutiveSets::new(3, vec![vec![0, 1], vec![1, 2]]), + ) + }, + }] +} + +#[cfg(test)] +#[path = "../unit_tests/rules/twodimensionalconsecutivesets_ilp.rs"] +mod tests; diff --git a/src/solvers/customized/minimum_intersection_graph_basis.rs b/src/solvers/customized/minimum_intersection_graph_basis.rs index d2d183d4d..d440bf211 100644 --- a/src/solvers/customized/minimum_intersection_graph_basis.rs +++ b/src/solvers/customized/minimum_intersection_graph_basis.rs @@ -1,22 +1,46 @@ //! Exact intersection-basis solver via maximal cliques and edge-cover branch and bound. -use crate::models::graph::MinimumIntersectionGraphBasis; +use crate::models::graph::{MinimumCoveringByCliques, MinimumIntersectionGraphBasis}; use crate::topology::{Graph, SimpleGraph}; pub(crate) fn solve( problem: &MinimumIntersectionGraphBasis, ) -> Option>> { - let n = problem.num_vertices(); - let edges = problem.graph().edges(); - if edges.is_empty() { - return Some(vec![Vec::new(); n]); + let cliques = minimum_covering_cliques(problem.graph()); + let mut solution = vec![vec![false; problem.num_edges()]; problem.num_vertices()]; + for (element, clique) in cliques.into_iter().enumerate() { + for vertex in clique { + solution[vertex][element] = true; + } } + Some(solution) +} + +pub(crate) fn solve_cover(problem: &MinimumCoveringByCliques) -> Vec { + let cliques = minimum_covering_cliques(problem.graph()); + problem + .graph() + .edges() + .iter() + .map(|&(u, v)| { + cliques + .iter() + .position(|clique| clique.contains(&u) && clique.contains(&v)) + .expect("the chosen cliques cover every edge") + }) + .collect() +} +fn minimum_covering_cliques(graph: &SimpleGraph) -> Vec> { + let edges = graph.edges(); + if edges.is_empty() { + return Vec::new(); + } let mut cliques = Vec::new(); maximal_cliques( - problem.graph(), + graph, Vec::new(), - (0..n).collect(), + (0..graph.num_vertices()).collect(), Vec::new(), &mut cliques, ); @@ -29,10 +53,14 @@ pub(crate) fn solve( .collect::>() }) .collect::>(); - - // One maximal clique per edge is a feasible initial cover with at most |E| cliques. + // Every edge, including a loop, lies in a nonempty maximal clique. let mut chosen = (0..edges.len()) - .map(|edge| covers.iter().position(|cover| cover[edge]).unwrap()) + .map(|edge| { + covers + .iter() + .position(|cover| cover[edge]) + .expect("every edge belongs to a maximal clique") + }) .collect::>(); chosen.sort_unstable(); chosen.dedup(); @@ -42,13 +70,7 @@ pub(crate) fn solve( &mut Vec::new(), &mut chosen, ); - let mut solution = vec![vec![false; edges.len()]; n]; - for (element, clique) in chosen.into_iter().enumerate() { - for &vertex in &cliques[clique] { - solution[vertex][element] = true; - } - } - Some(solution) + chosen.into_iter().map(|i| cliques[i].clone()).collect() } fn minimum_cover( @@ -86,13 +108,30 @@ fn maximal_cliques( output: &mut Vec>, ) { if candidates.is_empty() && excluded.is_empty() { - if clique.len() >= 2 { + if !clique.is_empty() { output.push(clique); } return; } - while let Some(vertex) = candidates.pop() { + let pivot = candidates + .iter() + .chain(&excluded) + .copied() + .max_by_key(|&v| { + candidates + .iter() + .filter(|&&u| u != v && graph.has_edge(u, v)) + .count() + }) + .expect("a nonterminal clique has a candidate or excluded vertex"); + let branches: Vec<_> = candidates + .iter() + .copied() + .filter(|&v| v == pivot || !graph.has_edge(v, pivot)) + .collect(); + for vertex in branches { + candidates.retain(|&v| v != vertex); let mut next_clique = clique.clone(); next_clique.push(vertex); maximal_cliques( diff --git a/src/solvers/customized/mod.rs b/src/solvers/customized/mod.rs index 1eee49c47..fb939b952 100644 --- a/src/solvers/customized/mod.rs +++ b/src/solvers/customized/mod.rs @@ -15,3 +15,4 @@ pub(crate) mod partial_feedback_edge_set; pub(crate) mod rooted_tree_arrangement; pub(crate) mod shortest_common_superstring; mod solver; +pub(crate) mod subset_sum; diff --git a/src/solvers/customized/solver.rs b/src/solvers/customized/solver.rs index cca79253f..2325120e7 100644 --- a/src/solvers/customized/solver.rs +++ b/src/solvers/customized/solver.rs @@ -73,6 +73,11 @@ register_customized_solver!(GroupingBySwapping, "symbol-block-order", |problem| register_customized_solver!(ShortestCommonSuperstring, "subset-dp", |problem| { super::shortest_common_superstring::solve(problem).map(Some) }); +register_customized_solver!( + crate::models::misc::SubsetSum, + "meet-in-the-middle", + super::subset_sum::solve +); register_customized_solver!(MinimumDecisionTree, "subset-dp", |problem| { super::minimum_decision_tree::solve(problem).map(Some) }); @@ -86,6 +91,23 @@ register_customized_solver!( "maximal-clique-edge-cover", |problem| Ok(super::minimum_intersection_graph_basis::solve(problem)) ); +register_customized_solver!( + crate::models::graph::MinimumCoveringByCliques, + "maximal-clique-edge-cover", + |problem| Ok(Some(super::minimum_intersection_graph_basis::solve_cover( + problem + ))) +); +register_customized_solver!( + crate::models::Decision>, + "maximal-clique-edge-cover", + |problem: &crate::models::Decision< + crate::models::graph::MinimumCoveringByCliques, + >| { + let solution = super::minimum_intersection_graph_basis::solve_cover(problem.inner()); + Ok(problem.evaluate(&solution)?.0.then_some(solution)) + } +); register_customized_solver!( crate::models::algebraic::ClosestVectorProblem, "cvp-sphere-enumeration", diff --git a/src/solvers/customized/subset_sum.rs b/src/solvers/customized/subset_sum.rs new file mode 100644 index 000000000..aed68c491 --- /dev/null +++ b/src/solvers/customized/subset_sum.rs @@ -0,0 +1,53 @@ +//! Exact meet-in-the-middle subset sum with arbitrary-precision sums and masks. +//! Horowitz and Sahni, JACM 21(2), 1974, pp. 277–292. + +use crate::models::misc::SubsetSum; +use crate::solvers::SolveError; +use num_bigint::BigUint; +use num_traits::{One, Zero}; + +fn half_sums(sizes: &[BigUint], target: &BigUint) -> Result, SolveError> { + let mut states = vec![(BigUint::zero(), BigUint::zero())]; + for (bit, size) in sizes.iter().enumerate() { + let previous = states.len(); + states.try_reserve(previous)?; + let bit_mask = BigUint::one() << bit; + for index in 0..previous { + let sum = &states[index].0 + size; + if sum > *target { + break; // Existing sums are sorted and sizes are positive. + } + let mask = &states[index].1 | &bit_mask; + states.push((sum, mask)); + } + states.sort_unstable_by(|a, b| a.0.cmp(&b.0)); + states.dedup_by(|a, b| a.0 == b.0); + } + Ok(states) +} + +pub(crate) fn solve(problem: &SubsetSum) -> Result>, SolveError> { + let split = problem.num_elements() / 2; + let left = half_sums(&problem.sizes()[..split], problem.target())?; + let right = half_sums(&problem.sizes()[split..], problem.target())?; + let mut i = 0; + let mut j = right.len(); + while i < left.len() && j > 0 { + match (&left[i].0 + &right[j - 1].0).cmp(problem.target()) { + std::cmp::Ordering::Less => i += 1, + std::cmp::Ordering::Greater => j -= 1, + std::cmp::Ordering::Equal => { + let bytes = (&left[i].1 | (&right[j - 1].1 << split)).to_bytes_le(); + let witness = (0..problem.num_elements()) + .map(|bit| { + bytes + .get(bit / 8) + .is_some_and(|byte| byte & (1 << (bit % 8)) != 0) + }) + .collect(); + return Ok(Some(witness)); + } + } + } + Ok(None) +} diff --git a/src/solvers/pipelines.rs b/src/solvers/pipelines.rs index c506ca029..17e52907e 100644 --- a/src/solvers/pipelines.rs +++ b/src/solvers/pipelines.rs @@ -43,7 +43,7 @@ register_ilp_pipeline! { register_ilp_pipeline! { ("AcyclicPartition", [("weight", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), + ("ILP", [("variable", "i64"), ("coefficient", "i64"), ("bounds", "bounded")]), } register_ilp_pipeline! { @@ -124,7 +124,7 @@ register_ilp_pipeline! { register_ilp_pipeline! { ("ConsecutiveOnesMatrixAugmentation", []), - ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), + ("ILP", [("variable", "i64"), ("coefficient", "i64"), ("bounds", "bounded")]), } register_ilp_pipeline! { @@ -705,7 +705,7 @@ register_ilp_pipeline! { register_ilp_pipeline! { ("RegisterSufficiency", []), - ("ILP", [("variable", "i64"), ("coefficient", "i64"), ("bounds", "bounded")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), } register_ilp_pipeline! { @@ -957,3 +957,38 @@ register_ilp_pipeline! { ("ProductionPlanning", []), ("ILP", [("variable", "i64"), ("coefficient", "i64"), ("bounds", "bounded")]), } + +register_ilp_pipeline! { + ("CyclicOrdering", []), + ("ILP", [("variable", "i64"), ("coefficient", "i64"), ("bounds", "bounded")]), +} + +register_ilp_pipeline! { + ("Betweenness", []), + ("ILP", [("variable", "i64"), ("coefficient", "i64"), ("bounds", "bounded")]), +} + +register_ilp_pipeline! { + ("TwoDimensionalConsecutiveSets", []), + ("ILP", [("variable", "i64"), ("coefficient", "i64"), ("bounds", "bounded")]), +} + +register_ilp_pipeline! { + ("MinimumCodeGenerationUnlimitedRegisters", []), + ("ILP", [("variable", "i64"), ("coefficient", "i64"), ("bounds", "bounded")]), +} + +register_ilp_pipeline! { + ("PartitionIntoPerfectMatchings", [("graph", "SimpleGraph")]), + ("ILP", [("variable", "i64"), ("coefficient", "i64"), ("bounds", "bounded")]), +} + +register_ilp_pipeline! { + ("MinimumWeightAndOrGraph", []), + ("ILP", [("variable", "i64"), ("coefficient", "i64"), ("bounds", "bounded")]), +} + +register_ilp_pipeline! { + ("BoundedDiameterSpanningTree", [("graph", "SimpleGraph"), ("weight", "i64")]), + ("ILP", [("variable", "i64"), ("coefficient", "i64"), ("bounds", "bounded")]), +} diff --git a/src/unit_tests/ilp_overhead.rs b/src/unit_tests/ilp_overhead.rs index 320afb524..70fc6bed5 100644 --- a/src/unit_tests/ilp_overhead.rs +++ b/src/unit_tests/ilp_overhead.rs @@ -362,15 +362,39 @@ fn bounded_forest_magnitude_and_incoming_qubo() { #[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)] { + for (weight, bound, bits) in [ + (0, 1, 1), + (8, 1, 4), + (1, -8, 4), + (i64::MIN, 1, 64), + (i64::MIN, i64::MIN, 64), + (i64::MAX, -1, 63), + ] { 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>(&source); + let reduction = ReduceTo::::reduce_to(&source).unwrap(); + let mut feasible = 0; + // One vertex forces its membership and label; enumerate the auxiliary bit. + for auxiliary in 0..=1 { + let target = vec![1, auxiliary, 0]; + if reduction + .target_problem() + .evaluate(&target) + .unwrap() + .value + .is_some() + { + assert_eq!(reduction.extract_solution(&target).unwrap(), vec![0]); + feasible += 1; + } + } + assert_eq!(feasible, usize::from(source.evaluate(&vec![0]).unwrap().0)); } - check_contract::<_, ILP>(&AcyclicPartition::new( + check_contract::<_, ILP>(&AcyclicPartition::new( DirectedGraph::new(1, vec![(0, 0)]), vec![1], vec![1], @@ -378,7 +402,7 @@ fn acyclic_partition_magnitude_and_qubo() { 1, )); for bound in [1, 3] { - check_qubo::<_, ILP>(AcyclicPartition::new( + check_qubo::<_, ILP>(AcyclicPartition::new( DirectedGraph::new(2, vec![(0, 1)]), vec![1, 2], vec![1], diff --git a/src/unit_tests/rules/acyclicpartition_ilp.rs b/src/unit_tests/rules/acyclicpartition_ilp.rs index c09f19a9d..9bc534825 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,10 +44,10 @@ 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); + assert_eq!(ilp.num_vars(), 27); + assert_eq!(ilp.num_constraints(), 39); } #[test] @@ -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/betweenness_ilp.rs b/src/unit_tests/rules/betweenness_ilp.rs new file mode 100644 index 000000000..23c1080d1 --- /dev/null +++ b/src/unit_tests/rules/betweenness_ilp.rs @@ -0,0 +1,39 @@ +use super::*; +use crate::solvers::{BruteForce, ILPSolver}; +use crate::traits::Problem; + +#[test] +fn betweenness_closed_loop_and_counts() { + for (n, triples) in [ + (1, vec![]), + (3, vec![(0, 1, 2)]), + (3, vec![(0, 1, 2); 2]), + (3, vec![(0, 1, 2), (0, 2, 1)]), + ] { + let source = Betweenness::new(n, triples); + let reduced = ReduceTo::>::reduce_to(&source).unwrap(); + crate::rules::test_helpers::assert_parameter_predictions(&source, &reduced); + let expected = BruteForce::new().solve(&source).unwrap(); + match (expected, ILPSolver::new().solve(&source)) { + (Some(_), Ok(witness)) => assert!(source.evaluate(&witness).unwrap().0), + (None, Err(crate::solvers::ILPSolveError::Infeasible)) => {} + other => panic!("betweenness disagrees: {other:?}"), + } + } +} + +#[test] +fn betweenness_decodes_both_directions_without_distinct_unrelated_ranks() { + let source = Betweenness::new(4, vec![(0, 1, 2)]); + let reduced = ReduceTo::>::reduce_to(&source).unwrap(); + for target in [vec![0, 1, 2, 1, 1], vec![2, 1, 0, 1, 0]] { + let witness = reduced.extract_solution(&target).unwrap(); + assert!(source.evaluate(&witness).unwrap().0); + let mut sorted = witness; + sorted.sort(); + assert_eq!(sorted, vec![0, 1, 2, 3]); + } + for invalid in [vec![], vec![0; 5], vec![0, 1, 2, 1, 0], vec![0, 1, 4, 1, 1]] { + assert!(reduced.extract_solution(&invalid).is_err()); + } +} diff --git a/src/unit_tests/rules/bmf_ilp.rs b/src/unit_tests/rules/bmf_ilp.rs index d09a728fe..22720c913 100644 --- a/src/unit_tests/rules/bmf_ilp.rs +++ b/src/unit_tests/rules/bmf_ilp.rs @@ -10,8 +10,10 @@ fn test_bmf_to_ilp_structure() { let reduction: ReductionBMFToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); - // b: 2*1=2, c: 1*2=2, p: 2*1*2=4, w: 2*2=4 => 12 (no error variables) - assert_eq!(ilp.num_vars(), 12); + // Four factor bits and one coverage bit per true diagonal entry. + assert_eq!(ilp.num_vars(), 6); + assert_eq!(ilp.num_constraints(), 8); + assert_eq!(ilp.num_nonzeros(), 14); assert_eq!(ilp.sense(), ObjectiveSense::Minimize); } @@ -41,6 +43,53 @@ fn test_bmf_to_ilp_trivial() { let reduction: ReductionBMFToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); - // b: 1, c: 1, p: 1, w: 1 => 4 (no error variables) - assert_eq!(ilp.num_vars(), 4); + // Two factor bits and one coverage bit. + assert_eq!(ilp.num_vars(), 3); +} + +#[test] +fn bmf_extraction_requires_exact_reconstruction() { + let source = BMF::new(vec![vec![true]], 1); + let reduced = ReduceTo::>::reduce_to(&source).unwrap(); + assert!(reduced + .extract_solution(&vec![0; reduced.target_problem().num_vars()]) + .is_err()); +} + +#[test] +fn bmf_sparse_coverage_preserves_values_and_zero_dimensions() { + use crate::solvers::{ILPSolveError, ILPSolver}; + use crate::traits::Problem; + use crate::types::Min; + for (matrix, k, expected) in [ + (vec![], 0, Some(0)), + (vec![], 2, Some(0)), + (vec![vec![], vec![]], 2, Some(0)), + (vec![vec![false; 2]; 2], 0, Some(0)), + (vec![vec![true]], 0, None), + (vec![vec![true, false], vec![false, true]], 1, None), + (vec![vec![true, false], vec![false, true]], 2, Some(4)), + (vec![vec![true; 3]; 2], 2, Some(5)), + ] { + let source = BMF::new(matrix, k); + let reduced = ReduceTo::>::reduce_to(&source).unwrap(); + crate::rules::test_helpers::assert_parameter_predictions(&source, &reduced); + match (expected, ILPSolver::new().solve(&source)) { + (Some(value), Ok(w)) => assert_eq!(source.evaluate(&w).unwrap(), Min(Some(value))), + (None, Err(ILPSolveError::Infeasible)) => {} + other => panic!("Boolean factorization disagrees: {other:?}"), + } + } + let source = BMF::new(vec![vec![true]], 2); + let reduced = ReduceTo::>::reduce_to(&source).unwrap(); + // Both rank products are true; selecting just one coverage indicator is valid. + let witness = reduced.extract_solution(&vec![1, 1, 1, 1, 1, 0]).unwrap(); + assert_eq!(source.evaluate(&witness).unwrap(), Min(Some(4))); + assert!(reduced.extract_solution(&vec![1, 1, 1, 1, 0, 0]).is_err()); + assert!(reduced.extract_solution(&vec![]).is_err()); + let huge = BMF::new(vec![vec![false]], usize::MAX); + assert!(matches!( + ReduceTo::>::reduce_to(&huge), + Err(crate::rules::ReductionError::IntegerOverflow { .. }) + )); } diff --git a/src/unit_tests/rules/boundeddiameterspanningtree_ilp.rs b/src/unit_tests/rules/boundeddiameterspanningtree_ilp.rs new file mode 100644 index 000000000..34c6c9efc --- /dev/null +++ b/src/unit_tests/rules/boundeddiameterspanningtree_ilp.rs @@ -0,0 +1,78 @@ +use super::*; +use crate::solvers::{BruteForce, ILPSolveError, ILPSolver}; +use crate::traits::Problem; +#[test] +fn test_boundeddiameterspanningtree_to_ilp_closed_loop() { + for (n, edges, weights) in [ + (0, vec![], vec![]), + (1, vec![(0, 0)], vec![3]), + (3, vec![(0, 1)], vec![1]), + (4, vec![(0, 1), (1, 2), (2, 3)], vec![1, 2, 1]), + ( + 4, + vec![(0, 1), (0, 2), (0, 3), (1, 2), (2, 3)], + vec![3, 1, 2, 1, 1], + ), + (3, vec![(0, 0), (0, 1), (1, 0), (1, 2)], vec![1, 4, 1, 1]), + ] { + for diameter in 1..=5 { + for budget in [1, 2, 4, 8] { + let source = BoundedDiameterSpanningTree::new( + SimpleGraph::new(n, edges.clone()), + weights.clone(), + budget, + diameter, + ); + let reduced = ReduceTo::>::reduce_to(&source).unwrap(); + crate::rules::test_helpers::assert_parameter_predictions(&source, &reduced); + match ( + BruteForce::new().solve(&source).unwrap(), + ILPSolver::new().solve(&source), + ) { + (Some(_), Ok(w)) => assert!(source.evaluate(&w).unwrap().0), + (None, Err(ILPSolveError::Infeasible)) => {} + other => panic!("bounded tree disagrees: {other:?}"), + } + } + } + } +} +#[test] +fn bounded_tree_extracts_center_edge_and_rejects_invalid_certificates() { + let source = BoundedDiameterSpanningTree::new( + SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]), + vec![1; 3], + 3, + 3, + ); + let reduced = ReduceTo::>::reduce_to(&source).unwrap(); + let target = vec![1, 1, 1, 0, 1, 0, 0, 1, 0, 0, 1, 1, 0, 1, 0, 0, 1, 0, 1, 0]; + assert_eq!(reduced.extract_solution(&target).unwrap(), vec![true; 3]); + for invalid in [ + vec![], + vec![0; 20], + { + let mut x = target.clone(); + x[18] = 0; + x + }, + { + let mut x = target; + x[13] = 2; + x + }, + ] { + assert!(reduced.extract_solution(&invalid).is_err()); + } + let huge = BoundedDiameterSpanningTree::new( + SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]), + vec![i64::MAX, 1, 1], + 2, + 2, + ); + assert!(huge.evaluate(&vec![false, true, true]).unwrap().0); + assert!(matches!( + ReduceTo::>::reduce_to(&huge), + Err(crate::rules::ReductionError::IntegerOverflow { .. }) + )); +} diff --git a/src/unit_tests/rules/consecutiveonesmatrixaugmentation_ilp.rs b/src/unit_tests/rules/consecutiveonesmatrixaugmentation_ilp.rs index 6c53a903d..180073d8e 100644 --- a/src/unit_tests/rules/consecutiveonesmatrixaugmentation_ilp.rs +++ b/src/unit_tests/rules/consecutiveonesmatrixaugmentation_ilp.rs @@ -12,10 +12,11 @@ fn test_coma_to_ilp_structure() { 1, ); let reduction: ReductionCOMAToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); - // x: 3*3=9, a+l+u+h+f: 5*2*3=30 => 39 - assert_eq!(ilp.num_vars(), 39); + assert_eq!(ilp.num_vars(), 13); + assert_eq!(ilp.num_constraints(), 15); + assert_eq!(ilp.num_nonzeros(), 46); assert_eq!(ilp.sense(), ObjectiveSense::Minimize); } @@ -26,9 +27,8 @@ fn test_coma_to_ilp_closed_loop() { 1, ); let reduction: ReductionCOMAToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); - // Use ILP solver instead of brute-force on the target (39 binary vars too large) let ilp_solver = ILPSolver::new(); let ilp_solution = ilp_solver .solve(reduction.target_problem()) @@ -49,7 +49,7 @@ fn test_coma_to_ilp_bf_vs_ilp() { 1, ); let reduction: ReductionCOMAToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let bf = BruteForce::new(); let bf_witness = bf.solve(&problem).unwrap().expect("should be feasible"); @@ -68,17 +68,16 @@ fn test_coma_to_ilp_trivial() { // 1x1 matrix, bound 0 — already consecutive let problem = ConsecutiveOnesMatrixAugmentation::new(vec![vec![true]], 0); let reduction: ReductionCOMAToILP = - ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); + ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); - // x: 1, a+l+u+h+f: 5*1=5 => 6 - assert_eq!(ilp.num_vars(), 6); + assert_eq!(ilp.num_vars(), 3); } #[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(); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); assert_eq!( reduction.target_problem().max_constraint_magnitude_bits(), 1 @@ -92,3 +91,64 @@ fn test_augmentation_threshold_normalization() { ); } } + +#[test] +fn interval_endpoints_preserve_zero_rows_and_exact_budget() { + for (matrix, bound, feasible) in [ + (vec![], 0, true), + (vec![vec![], vec![]], 0, true), + (vec![vec![false; 3]], 0, true), + (vec![vec![true; 3]], 0, true), + ( + vec![ + vec![true, true, false], + vec![true, false, true], + vec![false, true, true], + ], + 0, + false, + ), + ( + vec![ + vec![true, true, false], + vec![true, false, true], + vec![false, true, true], + ], + 1, + true, + ), + ] { + let source = ConsecutiveOnesMatrixAugmentation::new(matrix, bound); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + match ILPSolver::new().solve(reduction.target_problem()) { + Ok(solution) => { + assert!(feasible); + assert_eq!( + source + .evaluate(&reduction.extract_solution(&solution).unwrap()) + .unwrap(), + Or(true) + ); + } + Err(crate::solvers::ILPSolveError::Infeasible) => assert!(!feasible), + Err(error) => panic!("unexpected solver failure: {error}"), + } + } +} + +#[test] +fn loose_intervals_spend_budget_and_infeasible_targets_are_rejected() { + let solution = vec![1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 2]; + for bound in [1, 2] { + let source = ConsecutiveOnesMatrixAugmentation::new(vec![vec![false, true, false]], bound); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + if bound == 2 { + assert_eq!( + reduction.extract_solution(&solution).unwrap(), + vec![0, 1, 2] + ); + } else { + assert!(reduction.extract_solution(&solution).is_err()); + } + } +} diff --git a/src/unit_tests/rules/cyclicordering_ilp.rs b/src/unit_tests/rules/cyclicordering_ilp.rs new file mode 100644 index 000000000..b50b836d9 --- /dev/null +++ b/src/unit_tests/rules/cyclicordering_ilp.rs @@ -0,0 +1,37 @@ +use super::*; +use crate::solvers::{BruteForce, ILPSolver}; +use crate::traits::Problem; + +#[test] +fn cyclic_ordering_closed_loop_and_counts() { + for (n, triples) in [ + (1, vec![]), + (3, vec![(0, 1, 2)]), + (3, vec![(0, 1, 2); 2]), + (3, vec![(0, 1, 2), (0, 2, 1)]), + ] { + let source = CyclicOrdering::new(n, triples); + let reduced = ReduceTo::>::reduce_to(&source).unwrap(); + crate::rules::test_helpers::assert_parameter_predictions(&source, &reduced); + let expected = BruteForce::new().solve(&source).unwrap(); + match (expected, ILPSolver::new().solve(&source)) { + (Some(_), Ok(witness)) => assert!(source.evaluate(&witness).unwrap().0), + (None, Err(crate::solvers::ILPSolveError::Infeasible)) => {} + other => panic!("cyclic ordering disagrees: {other:?}"), + } + } +} + +#[test] +fn cyclic_ordering_decodes_inverse_positions_and_unrelated_ties() { + let source = CyclicOrdering::new(4, vec![(0, 1, 2)]); + let reduced = ReduceTo::>::reduce_to(&source).unwrap(); + let witness = reduced + .extract_solution(&vec![1, 2, 0, 1, 1, 0, 1]) + .unwrap(); + assert_eq!(witness, vec![1, 3, 0, 2]); + assert!(source.evaluate(&witness).unwrap().0); + for invalid in [vec![], vec![0; 7], vec![1, 2, 0, 1, 1, 0, 2]] { + assert!(reduced.extract_solution(&invalid).is_err()); + } +} diff --git a/src/unit_tests/rules/feasibleregisterassignment_ilp.rs b/src/unit_tests/rules/feasibleregisterassignment_ilp.rs index 3f296cc80..df7445b76 100644 --- a/src/unit_tests/rules/feasibleregisterassignment_ilp.rs +++ b/src/unit_tests/rules/feasibleregisterassignment_ilp.rs @@ -15,8 +15,9 @@ fn test_feasible_register_assignment_to_ilp_structure() { ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let ilp = reduction.target_problem(); - assert_eq!(ilp.num_vars(), 14); - assert_eq!(ilp.constraints().len(), 42); + assert_eq!(ilp.num_vars(), 7); + assert_eq!(ilp.constraints().len(), 12); + assert_eq!(ilp.num_nonzeros(), 33); assert_eq!(ilp.objective(), vec![]); assert_eq!(ilp.sense(), ObjectiveSense::Minimize); } @@ -57,3 +58,20 @@ fn test_feasible_register_assignment_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 register_assignment_decodes_unrelated_ties_without_hiding_overwrites() { + let source = FeasibleRegisterAssignment::new(2, vec![], 2, vec![0, 1]); + let reduced = ReduceTo::>::reduce_to(&source).unwrap(); + assert_eq!(reduced.extract_solution(&vec![0, 0]).unwrap(), vec![0, 1]); + let conflict = FeasibleRegisterAssignment::new( + 4, + vec![(2, 0), (2, 1), (3, 0), (3, 1)], + 2, + vec![0, 1, 0, 1], + ); + assert_eq!( + ILPSolver::new().solve(&conflict), + Err(crate::solvers::ILPSolveError::Infeasible) + ); +} diff --git a/src/unit_tests/rules/hamiltonianpath_ilp.rs b/src/unit_tests/rules/hamiltonianpath_ilp.rs index 66a2a897e..5a516dae6 100644 --- a/src/unit_tests/rules/hamiltonianpath_ilp.rs +++ b/src/unit_tests/rules/hamiltonianpath_ilp.rs @@ -11,9 +11,9 @@ fn test_reduction_creates_valid_ilp() { let reduction: ReductionHamiltonianPathToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); - // n=3, m=2, n_pos=2 - // num_x = 9, num_z = 2*2*2 = 8, total = 17 - assert_eq!(ilp.num_vars(), 17); + assert_eq!(ilp.num_vars(), 9); + assert_eq!(ilp.num_constraints(), 12); + assert_eq!(ilp.num_nonzeros(), 32); assert_eq!(ilp.sense(), ObjectiveSense::Minimize); } diff --git a/src/unit_tests/rules/minimumcodegenerationunlimitedregisters_ilp.rs b/src/unit_tests/rules/minimumcodegenerationunlimitedregisters_ilp.rs new file mode 100644 index 000000000..ca5de27aa --- /dev/null +++ b/src/unit_tests/rules/minimumcodegenerationunlimitedregisters_ilp.rs @@ -0,0 +1,54 @@ +use super::*; +use crate::solvers::{BruteForce, ILPSolveError, ILPSolver}; +use crate::traits::Problem; + +#[test] +fn test_minimumcodegenerationunlimitedregisters_to_ilp_closed_loop() { + for (n, left, right) in [ + (0, vec![], vec![]), + (3, vec![], vec![]), + (3, vec![(0, 1), (1, 2)], vec![]), + (3, vec![(0, 2), (1, 2)], vec![(0, 2), (1, 2)]), + ( + 5, + vec![(1, 3), (2, 3), (0, 1)], + vec![(1, 4), (2, 4), (0, 2)], + ), + (3, vec![(0, 1), (1, 2), (2, 0)], vec![]), + ] { + let source = MinimumCodeGenerationUnlimitedRegisters::new(n, left, right); + let reduced = ReduceTo::>::reduce_to(&source).unwrap(); + crate::rules::test_helpers::assert_parameter_predictions(&source, &reduced); + match ( + BruteForce::new().solve(&source).unwrap(), + ILPSolver::new().solve(&source), + ) { + (Some(expected), Ok(actual)) => assert_eq!( + source.evaluate(&expected).unwrap(), + source.evaluate(&actual).unwrap() + ), + (None, Err(ILPSolveError::Infeasible)) => {} + other => panic!("code generation disagrees: {other:?}"), + } + } +} +#[test] +fn code_generation_extracts_tied_ranks_and_validates_target() { + let source = MinimumCodeGenerationUnlimitedRegisters::new(4, vec![(0, 2), (1, 3)], vec![]); + let reduced = ReduceTo::>::reduce_to(&source).unwrap(); + // Unrelated operations may tie; nonoptimal copy bits still decode feasibly. + assert_eq!( + reduced.extract_solution(&vec![0, 0, 1, 1, 1]).unwrap(), + vec![0, 1] + ); + for invalid in [vec![], vec![0, 0, 0, 0, 0], vec![2, 0, 0, 0, 1]] { + assert!(reduced.extract_solution(&invalid).is_err()); + } + let reused = MinimumCodeGenerationUnlimitedRegisters::new(3, vec![(0, 2), (1, 2)], vec![]); + let reduced = ReduceTo::>::reduce_to(&reused).unwrap(); + assert!(reduced.extract_solution(&vec![0, 1, 0, 0, 1]).is_err()); + assert_eq!( + reduced.extract_solution(&vec![0, 1, 1, 0, 1]).unwrap(), + vec![0, 1] + ); +} diff --git a/src/unit_tests/rules/minimumweightandorgraph_ilp.rs b/src/unit_tests/rules/minimumweightandorgraph_ilp.rs new file mode 100644 index 000000000..f10eb012b --- /dev/null +++ b/src/unit_tests/rules/minimumweightandorgraph_ilp.rs @@ -0,0 +1,113 @@ +use super::*; +use crate::solvers::{BruteForce, ILPSolveError, ILPSolver}; +use crate::traits::Problem; +use crate::types::Min; +#[test] +fn test_minimumweightandorgraph_to_ilp_closed_loop() { + for (n, arcs, gates, weights, expected) in [ + ( + 3, + vec![(0, 1), (0, 2), (1, 2)], + vec![Some(true), Some(false), None], + vec![1, 2, -4], + Some(-1), + ), + ( + 3, + vec![(0, 1)], + vec![Some(true), Some(false), None], + vec![1], + None, + ), + ( + 4, + vec![(0, 1), (2, 3), (3, 2)], + vec![Some(true), None, Some(false), Some(false)], + vec![1, -8, -8], + Some(1), + ), + ( + 3, + vec![(0, 1), (1, 2), (2, 1)], + vec![Some(true), Some(false), Some(false)], + vec![1, -2, -2], + Some(-3), + ), + ( + 3, + vec![(0, 1), (1, 2)], + vec![Some(true), None, Some(false)], + vec![1, -5], + Some(-4), + ), + ( + 1, + vec![(0, 0), (0, 0)], + vec![Some(false)], + vec![-1, 2], + Some(-1), + ), + (1, vec![], vec![Some(true)], vec![], Some(0)), + (1, vec![], vec![Some(false)], vec![], None), + ] { + let source = MinimumWeightAndOrGraph::new(n, arcs, 0, gates, weights); + let reduced = ReduceTo::>::reduce_to(&source).unwrap(); + crate::rules::test_helpers::assert_parameter_predictions(&source, &reduced); + let brute = BruteForce::new().solve(&source).unwrap(); + assert_eq!( + brute.as_ref().map(|w| source.evaluate(w).unwrap()), + expected.map(|v| Min(Some(v))) + ); + match (expected, ILPSolver::new().solve(&source)) { + (Some(v), Ok(w)) => assert_eq!(source.evaluate(&w).unwrap(), Min(Some(v))), + (None, Err(ILPSolveError::Infeasible)) => {} + other => panic!("AND/OR graph disagrees: {other:?}"), + } + } +} +#[test] +fn and_or_extraction_rejects_unreachable_selections_and_preserves_overflow() { + let source = MinimumWeightAndOrGraph::new( + 3, + vec![(0, 1), (2, 2)], + 0, + vec![Some(true), None, Some(true)], + vec![1, -8], + ); + let reduced = ReduceTo::>::reduce_to(&source).unwrap(); + assert_eq!( + reduced + .extract_solution(&vec![1, 0, 1, 1, 0, 1, 0]) + .unwrap(), + vec![true, false] + ); + for invalid in [vec![], vec![1, 1, 1, 1, 1, 1, 0], vec![2, 0, 1, 1, 0, 1, 0]] { + assert!(reduced.extract_solution(&invalid).is_err()); + } + for weights in [ + vec![i64::MAX, 1, -1], + vec![i64::MIN, -1, 1], + vec![i64::MAX, -1, 1], + ] { + let source = MinimumWeightAndOrGraph::new(1, vec![(0, 0); 3], 0, vec![Some(true)], weights); + let reduced = ReduceTo::>::reduce_to(&source).unwrap(); + let target = vec![1, 1, 1, 1, 0, 0, 0]; + match source.evaluate(&vec![true; 3]) { + Ok(Min(Some(value))) => { + assert_eq!( + reduced.target_problem().evaluate(&target).unwrap().value, + Some(value) + ); + assert_eq!(reduced.extract_solution(&target).unwrap(), vec![true; 3]); + } + Err(crate::traits::EvaluationError::IntegerOverflow(_)) => { + assert!(matches!( + reduced.target_problem().evaluate(&target), + Err(crate::traits::EvaluationError::IntegerOverflow(_)) + )); + assert!(reduced.extract_solution(&target).is_err()); + } + other => panic!("unexpected source evaluation: {other:?}"), + } + } +} diff --git a/src/unit_tests/rules/partitionintoperfectmatchings_ilp.rs b/src/unit_tests/rules/partitionintoperfectmatchings_ilp.rs new file mode 100644 index 000000000..958753644 --- /dev/null +++ b/src/unit_tests/rules/partitionintoperfectmatchings_ilp.rs @@ -0,0 +1,50 @@ +use super::*; +use crate::solvers::{BruteForce, ILPSolveError, ILPSolver}; +use crate::traits::Problem; +#[test] +fn test_partitionintoperfectmatchings_to_ilp_closed_loop() { + for n in 1..=4 { + let pairs: Vec<_> = (0..n) + .flat_map(|u| (u + 1..n).map(move |v| (u, v))) + .collect(); + for mask in 0..1 << pairs.len() { + let edges = pairs + .iter() + .enumerate() + .filter_map(|(i, &e)| (mask >> i & 1 == 1).then_some(e)) + .collect(); + let graph = SimpleGraph::new(n, edges); + for k in 1..=n { + let source = PartitionIntoPerfectMatchings::new(graph.clone(), k); + let reduced = ReduceTo::>::reduce_to(&source).unwrap(); + crate::rules::test_helpers::assert_parameter_predictions(&source, &reduced); + match ( + BruteForce::new().solve(&source).unwrap(), + ILPSolver::new().solve(&source), + ) { + (Some(_), Ok(w)) => assert!(source.evaluate(&w).unwrap().0), + (None, Err(ILPSolveError::Infeasible)) => {} + other => panic!("matching partition disagrees: {other:?}"), + } + } + } + } +} +#[test] +fn matching_partition_ignores_loops_and_repeated_adjacencies() { + let source = PartitionIntoPerfectMatchings::new( + SimpleGraph::new(4, vec![(0, 0), (0, 1), (1, 0), (0, 1), (2, 3)]), + 1, + ); + let reduced = ReduceTo::>::reduce_to(&source).unwrap(); + crate::rules::test_helpers::assert_parameter_predictions(&source, &reduced); + for directions in [vec![0, 0], vec![1, 1]] { + let mut target = vec![0, 0, 0, 0, 1, 1]; + target.extend(directions); + let witness = reduced.extract_solution(&target).unwrap(); + assert!(source.evaluate(&witness).unwrap().0); + } + for invalid in [vec![], vec![0; 8], vec![1, 0, 0, 0, 1, 1, 0, 0]] { + assert!(reduced.extract_solution(&invalid).is_err()); + } +} diff --git a/src/unit_tests/rules/registersufficiency_ilp.rs b/src/unit_tests/rules/registersufficiency_ilp.rs index c2b08e675..9b50c2864 100644 --- a/src/unit_tests/rules/registersufficiency_ilp.rs +++ b/src/unit_tests/rules/registersufficiency_ilp.rs @@ -1,5 +1,4 @@ use super::*; -use crate::models::algebraic::Bounded; use crate::models::misc::RegisterSufficiency; use crate::solvers::ILPSolver; use crate::traits::Problem; @@ -34,12 +33,11 @@ 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); - assert_eq!(ilp.constraints().len(), 180); + assert_eq!(ilp.num_vars(), 24); + assert_eq!(ilp.constraints().len(), 36); assert_eq!(ilp.objective(), vec![]); assert_eq!(ilp.sense(), ObjectiveSense::Minimize); } @@ -47,8 +45,7 @@ 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()) @@ -64,8 +61,7 @@ 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(), @@ -76,8 +72,7 @@ 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); } @@ -100,20 +95,19 @@ fn test_register_sufficiency_to_ilp_canonical_example_spec() { .as_array() .unwrap() .len(), - 182 + 91 ); assert_eq!( example.target.instance["constraints"] .as_array() .unwrap() .len(), - 542 + 168 ); 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(); @@ -123,3 +117,57 @@ fn test_register_sufficiency_to_ilp_canonical_example_spec() { source_config ); } + +#[test] +fn cumulative_schedule_preserves_live_values_and_rejects_invalid_targets() { + for (source, feasible) in [ + (RegisterSufficiency::new(0, vec![], 0), true), + (RegisterSufficiency::new(3, vec![(1, 0), (2, 1)], 1), true), + (RegisterSufficiency::new(3, vec![(2, 0), (2, 1)], 1), false), + (RegisterSufficiency::new(3, vec![(2, 0), (2, 1)], 2), true), + (RegisterSufficiency::new(3, vec![], 2), false), + (RegisterSufficiency::new(3, vec![], 3), true), + (RegisterSufficiency::new(2, vec![(1, 0), (0, 1)], 2), false), + (RegisterSufficiency::new(2, vec![(1, 0), (1, 0)], 1), true), + (RegisterSufficiency::new(1, vec![], 0), false), + ] { + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + match ILPSolver::new().solve(reduction.target_problem()) { + Ok(solution) => { + assert!(feasible); + assert_eq!( + source + .evaluate(&reduction.extract_solution(&solution).unwrap()) + .unwrap(), + Or(true) + ); + } + Err(crate::solvers::ILPSolveError::Infeasible) => assert!(!feasible), + Err(error) => panic!("unexpected solver failure: {error}"), + } + if source.num_vertices() > 0 { + assert!(reduction + .extract_solution(&vec![0; reduction.target_problem().num_vars()]) + .is_err()); + } + } +} + +#[test] +fn cumulative_schedule_decodes_every_feasible_target_with_surplus_live_bits() { + let source = RegisterSufficiency::new(2, vec![(1, 0)], 2); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + let target = reduction.target_problem(); + let mut feasible = 0; + for bits in 0..1 << target.num_vars() { + let solution: Vec<_> = (0..target.num_vars()) + .map(|i| i64::from(bits & (1 << i) != 0)) + .collect(); + if target.evaluate(&solution).unwrap().value.is_some() { + assert_eq!(reduction.extract_solution(&solution).unwrap(), vec![0, 1]); + feasible += 1; + } + } + // The first live bit is forced; the final one may be either value. + assert_eq!(feasible, 2); +} diff --git a/src/unit_tests/rules/twodimensionalconsecutivesets_ilp.rs b/src/unit_tests/rules/twodimensionalconsecutivesets_ilp.rs new file mode 100644 index 000000000..5b7bbf746 --- /dev/null +++ b/src/unit_tests/rules/twodimensionalconsecutivesets_ilp.rs @@ -0,0 +1,36 @@ +use super::*; +use crate::solvers::{BruteForce, ILPSolver}; +use crate::traits::Problem; + +#[test] +fn consecutive_sets_closed_loop_and_counts() { + for subsets in [ + vec![], + vec![vec![], vec![0]], + vec![vec![0, 1], vec![1, 2]], + vec![vec![0, 1]; 2], + vec![vec![0, 1], vec![1, 2], vec![0, 2]], + ] { + let source = TwoDimensionalConsecutiveSets::new(3, subsets); + let reduced = ReduceTo::>::reduce_to(&source).unwrap(); + crate::rules::test_helpers::assert_parameter_predictions(&source, &reduced); + let expected = BruteForce::new().solve(&source).unwrap(); + match (expected, ILPSolver::new().solve(&source)) { + (Some(_), Ok(witness)) => assert!(source.evaluate(&witness).unwrap().0), + (None, Err(crate::solvers::ILPSolveError::Infeasible)) => {} + other => panic!("consecutive sets disagree: {other:?}"), + } + } +} + +#[test] +fn consecutive_sets_preserve_shared_groups_and_unused_labels() { + let source = TwoDimensionalConsecutiveSets::new(4, vec![vec![0, 1], vec![1, 2]]); + let reduced = ReduceTo::>::reduce_to(&source).unwrap(); + let witness = reduced.extract_solution(&vec![1, 2, 1, 0, 1, 0]).unwrap(); + assert_eq!(witness, vec![1, 2, 1, 0]); + assert!(source.evaluate(&witness).unwrap().0); + for invalid in [vec![], vec![0; 6], vec![0, 2, 3, 1, 1, 1]] { + assert!(reduced.extract_solution(&invalid).is_err()); + } +} diff --git a/src/unit_tests/solvers/customized/minimum_intersection_graph_basis.rs b/src/unit_tests/solvers/customized/minimum_intersection_graph_basis.rs index 74335d39c..102b1357e 100644 --- a/src/unit_tests/solvers/customized/minimum_intersection_graph_basis.rs +++ b/src/unit_tests/solvers/customized/minimum_intersection_graph_basis.rs @@ -55,3 +55,16 @@ fn test_minimum_cover_improves_a_redundant_initial_cover() { assert_eq!(best.len(), 2); assert!((0..3).all(|edge| best.iter().any(|&clique| covers[clique][edge]))); } + +#[test] +fn intersection_basis_covers_isolated_loops_and_parallel_edges() { + for (n, edges, expected) in [ + (1, vec![(0, 0)], 1), + (3, vec![(0, 0), (1, 2), (2, 1)], 2), + (2, vec![(0, 0), (0, 1), (1, 1)], 1), + ] { + let source = MinimumIntersectionGraphBasis::new(SimpleGraph::new(n, edges)); + let witness = solve(&source).unwrap(); + assert_eq!(source.evaluate(&witness).unwrap().0, Some(expected)); + } +} diff --git a/src/unit_tests/solvers/customized/solver.rs b/src/unit_tests/solvers/customized/solver.rs index 59be07853..80aa6c967 100644 --- a/src/unit_tests/solvers/customized/solver.rs +++ b/src/unit_tests/solvers/customized/solver.rs @@ -53,6 +53,53 @@ fn all_simple_graphs(num_vertices: usize) -> impl Iterator { }) } +#[test] +fn subset_sum_customized_search_matches_exhaustive_truth() { + use crate::models::misc::SubsetSum; + for n in 0..=5 { + for encoded in 0..3_usize.pow(n) { + let mut digits = encoded; + let sizes: Vec = (0..n) + .map(|_| { + let value = u32::try_from(digits % 3 + 1).unwrap(); + digits /= 3; + value + }) + .collect(); + for target in 0..=sizes.iter().sum::() + 1 { + let source = SubsetSum::new(sizes.clone(), target); + let expected = crate::solvers::BruteForce::new().solve(&source).unwrap(); + let actual = CustomizedTestSolver::new().solve_dyn(&source); + assert_eq!( + actual.is_some(), + expected.is_some(), + "{sizes:?}, target={target}" + ); + if let Some(witness) = actual { + assert!(source.evaluate(&witness).unwrap().0); + } + } + } + } +} + +#[test] +fn subset_sum_customized_search_preserves_big_integers_and_long_masks() { + use crate::models::misc::SubsetSum; + use num_bigint::BigUint; + let huge = BigUint::from(1_u32) << 200_usize; + let mut sizes = vec![&huge * 3_u32; 132]; + sizes[65] = huge.clone(); + sizes[131] = &huge + 1_u32; + let source = SubsetSum::new(sizes, &huge * 2_u32 + 1_u32); + let witness = CustomizedTestSolver::new() + .solve_dyn(&source) + .expect("exact sum must be found"); + assert!(source.evaluate(&witness).unwrap().0); + assert_eq!(witness.iter().filter(|&&selected| selected).count(), 2); + assert!(witness[65] && witness[131]); +} + #[test] fn test_customized_two_coloring_matches_brute_force() { use crate::models::graph::KColoring; @@ -561,3 +608,43 @@ fn test_solve_two_coloring_direct_graph_cases() { } } } + +#[test] +fn customized_clique_cover_and_decision_preserve_optimum() { + use crate::models::graph::MinimumCoveringByCliques; + use crate::models::Decision; + for graph in all_simple_graphs(4).chain([ + SimpleGraph::new(0, vec![]), + SimpleGraph::new(3, vec![(0, 0), (1, 2), (2, 1)]), + ]) { + let source = MinimumCoveringByCliques::new(graph); + let expected = crate::solvers::BruteForce::new() + .solve(&source) + .unwrap() + .unwrap(); + let actual = CustomizedTestSolver::new() + .solve_dyn(&source) + .expect("registered clique-cover solver"); + let value = source.evaluate(&expected).unwrap(); + assert_eq!(source.evaluate(&actual).unwrap(), value); + let optimum = value.0.unwrap(); + for bound in [optimum - 1, optimum, optimum + 1] { + let decision = Decision::new(source.clone(), bound); + let actual = CustomizedTestSolver::new().solve_dyn(&decision); + assert_eq!(actual.is_some(), bound >= optimum); + if let Some(w) = actual { + assert!(decision.evaluate(&w).unwrap().0); + } + } + } + // Dense graphs need pivoting; all vertices form one maximal clique. + let graph = SimpleGraph::new( + 80, + (0..80) + .flat_map(|u| (u + 1..80).map(move |v| (u, v))) + .collect(), + ); + let source = MinimumCoveringByCliques::new(graph); + let witness = CustomizedTestSolver::new().solve_dyn(&source).unwrap(); + assert_eq!(source.evaluate(&witness).unwrap().0, Some(1)); +} diff --git a/src/unit_tests/solvers/ilp/solver.rs b/src/unit_tests/solvers/ilp/solver.rs index 9af8ad377..d8c4c9ed6 100644 --- a/src/unit_tests/solvers/ilp/solver.rs +++ b/src/unit_tests/solvers/ilp/solver.rs @@ -444,3 +444,152 @@ fn three_partition_pipeline_selects_triples_directly() { } } } + +#[test] +fn cyclic_ordering_pipeline_preserves_orientations() { + use crate::models::misc::CyclicOrdering; + for (triples, feasible) in [(vec![(0, 1, 2)], true), (vec![(0, 1, 2), (0, 2, 1)], false)] { + let source = CyclicOrdering::new(4, triples); + match ILPSolver::new().solve(&source) { + Ok(witness) => { + assert!(feasible); + assert!(source.evaluate(&witness).unwrap().0); + } + Err(error) => { + assert!(!feasible); + assert_eq!(error, ILPSolveError::Infeasible); + } + } + } +} + +#[test] +fn betweenness_pipeline_preserves_middle_elements() { + use crate::models::misc::Betweenness; + for (triples, feasible) in [(vec![(0, 1, 2)], true), (vec![(0, 1, 2), (0, 2, 1)], false)] { + let source = Betweenness::new(4, triples); + match ILPSolver::new().solve(&source) { + Ok(witness) => { + assert!(feasible); + assert!(source.evaluate(&witness).unwrap().0); + } + Err(error) => { + assert!(!feasible); + assert_eq!(error, ILPSolveError::Infeasible); + } + } + } +} + +#[test] +fn consecutive_sets_pipeline_preserves_group_adjacency() { + use crate::models::set::TwoDimensionalConsecutiveSets; + for (subsets, feasible) in [ + (vec![vec![], vec![0, 1], vec![1, 2]], true), + (vec![vec![0, 1], vec![1, 2], vec![0, 2]], false), + ] { + let source = TwoDimensionalConsecutiveSets::new(4, subsets); + match ILPSolver::new().solve(&source) { + Ok(witness) => { + assert!(feasible); + assert!(source.evaluate(&witness).unwrap().0); + } + Err(error) => { + assert!(!feasible); + assert_eq!(error, ILPSolveError::Infeasible); + } + } + } +} + +#[test] +fn unlimited_register_pipeline_minimizes_copies_and_rejects_cycles() { + use crate::models::misc::MinimumCodeGenerationUnlimitedRegisters; + use crate::types::Min; + let source = MinimumCodeGenerationUnlimitedRegisters::new( + 5, + vec![(1, 3), (2, 3), (0, 1)], + vec![(1, 4), (2, 4), (0, 2)], + ); + let witness = ILPSolver::new().solve(&source).unwrap(); + assert_eq!(source.evaluate(&witness).unwrap(), Min(Some(4))); + let cycle = + MinimumCodeGenerationUnlimitedRegisters::new(3, vec![(0, 1), (1, 2), (2, 0)], vec![]); + assert_eq!( + ILPSolver::new().solve(&cycle), + Err(ILPSolveError::Infeasible) + ); +} + +#[test] +fn matching_partition_pipeline_checks_induced_degrees() { + use crate::models::graph::PartitionIntoPerfectMatchings; + use crate::topology::SimpleGraph; + for (n, edges, k, feasible) in [ + (4, vec![(0, 1), (0, 2), (1, 3), (2, 3)], 2, true), + (4, vec![(0, 1), (0, 2), (1, 3), (2, 3)], 1, false), + (3, vec![(0, 1), (0, 2), (1, 2)], 3, false), + ] { + let source = PartitionIntoPerfectMatchings::new(SimpleGraph::new(n, edges), k); + match ILPSolver::new().solve(&source) { + Ok(witness) => { + assert!(feasible); + assert!(source.evaluate(&witness).unwrap().0); + } + Err(error) => { + assert!(!feasible); + assert_eq!(error, ILPSolveError::Infeasible); + } + } + } +} + +#[test] +fn and_or_pipeline_preserves_reachability_and_signed_cost() { + use crate::models::misc::MinimumWeightAndOrGraph; + use crate::types::Min; + let source = MinimumWeightAndOrGraph::new( + 3, + vec![(0, 1), (0, 2), (1, 2)], + 0, + vec![Some(true), Some(false), None], + vec![1, 2, -4], + ); + let witness = ILPSolver::new().solve(&source).unwrap(); + assert_eq!(source.evaluate(&witness).unwrap(), Min(Some(-1))); + let source = MinimumWeightAndOrGraph::new( + 3, + vec![(0, 1)], + 0, + vec![Some(true), Some(false), None], + vec![1], + ); + assert_eq!( + ILPSolver::new().solve(&source), + Err(ILPSolveError::Infeasible) + ); +} + +#[test] +fn bounded_tree_pipeline_enforces_diameter_and_weight() { + use crate::models::graph::BoundedDiameterSpanningTree; + use crate::topology::SimpleGraph; + for (diameter, budget, feasible) in [(3, 3, true), (2, 3, false), (4, 2, false)] { + let source = BoundedDiameterSpanningTree::new( + SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]), + vec![1; 3], + budget, + diameter, + ); + match ILPSolver::new().solve(&source) { + Ok(w) => { + assert!(feasible); + assert!(source.evaluate(&w).unwrap().0); + } + Err(e) => { + assert!(!feasible); + assert_eq!(e, ILPSolveError::Infeasible); + } + } + } +} diff --git a/src/unit_tests/solvers/resolver.rs b/src/unit_tests/solvers/resolver.rs index 9d40551e9..cf5159707 100644 --- a/src/unit_tests/solvers/resolver.rs +++ b/src/unit_tests/solvers/resolver.rs @@ -71,12 +71,6 @@ fn decision_ilp_paths_respect_bounds_and_return_valid_witnesses() { serde_json::json!({"graph": graph, "weights": [1,1,1]}), 1, ), - ( - "DecisionMinimumCoveringByCliques", - BTreeMap::from([("graph".into(), "SimpleGraph".into())]), - serde_json::json!({"graph": graph}), - 2, - ), ( "DecisionOpenShopScheduling", BTreeMap::new(), diff --git a/src/unit_tests/symbolic_parameter_contracts.rs b/src/unit_tests/symbolic_parameter_contracts.rs index 3dd3c3cee..3fb048073 100644 --- a/src/unit_tests/symbolic_parameter_contracts.rs +++ b/src/unit_tests/symbolic_parameter_contracts.rs @@ -261,7 +261,7 @@ fn parameter_relations_match_reduced_instances() { check_reduced_parameters::<_, ILP>( BMF::new(vec![vec![true, false], vec![false, true]], 2), &["num_nonzeros"], - exact, + ParameterRelation::UpperBound, ); check_reduced_parameters::<_, ILP>( ClosestString::new(2, vec![vec![0, 1], vec![1, 0]]), @@ -286,7 +286,7 @@ fn parameter_relations_match_reduced_instances() { check_reduced_parameters::<_, ILP>( FeasibleRegisterAssignment::new(4, vec![(0, 1), (0, 2), (1, 3)], 2, vec![0, 1, 0, 0]), &["num_nonzeros"], - exact, + ParameterRelation::UpperBound, ); check_reduced_parameters::<_, ILP>( IntegerKnapsack::new(vec![3, 4], vec![5, 6], 7).unwrap(), @@ -313,7 +313,7 @@ fn parameter_relations_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, @@ -335,9 +335,14 @@ fn parameter_relations_match_reduced_instances() { ); check_reduced_parameters::<_, ILP>( HamiltonianPath::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)])), - &["num_vars", "num_constraints", "num_nonzeros"], + &["num_vars", "num_constraints"], exact, ); + check_reduced_parameters::<_, ILP>( + HamiltonianPath::new(SimpleGraph::new(3, vec![(0, 1), (0, 1), (1, 1)])), + &["num_nonzeros"], + ParameterRelation::UpperBound, + ); check_reduced_parameters::<_, BicliqueCover>( BMF::new(vec![vec![true, false], vec![false, true]], 1), &["num_vertices", "left_size", "right_size", "rank"], @@ -406,7 +411,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, @@ -423,12 +428,12 @@ fn exact_parameter_formulas_cover_sparse_and_boundary_instances() { ); check_reduced_parameters::<_, ILP>( HamiltonianPath::new(SimpleGraph::new(0, vec![])), - &["num_vars", "num_constraints", "num_nonzeros"], + &["num_vars", "num_constraints"], exact, ); check_reduced_parameters::<_, ILP>( HamiltonianPath::new(SimpleGraph::new(1, vec![])), - &["num_vars", "num_constraints", "num_nonzeros"], + &["num_vars", "num_constraints"], exact, ); check_reduced_parameters::<_, HamiltonianPath>( From 2515bb59a5ce36e89bc5da1abf2c12ef8ce43101 Mon Sep 17 00:00:00 2001 From: Xiwei Pan <90967972+isPANN@users.noreply.github.com> Date: Wed, 30 Sep 2026 11:51:06 -0700 Subject: [PATCH 17/22] Remove redundant reduction parameters and derive bounds from model inputs --- docs/paper/reductions.typ | 9 ++++--- src/models/graph/hamiltonian_path.rs | 11 +------- src/models/misc/preemptive_scheduling.rs | 16 ------------ .../hamiltoniancircuit_hamiltonianpath.rs | 1 - src/rules/hamiltonianpath_ilp.rs | 6 ++--- .../ksatisfiability_preemptivescheduling.rs | 2 -- src/rules/preemptivescheduling_ilp.rs | 14 ++++------- .../symbolic_parameter_contracts.rs | 25 ++++++++++++++----- 8 files changed, 33 insertions(+), 51 deletions(-) diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index 916fbfdb6..16a05eebf 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -15113,7 +15113,7 @@ The following reductions to Integer Linear Programming are straightforward formu _Correctness._ ($arrow.r.double$) An optimal schedule has makespan at most the feasible list schedule's $H$. Every predecessor path forces its activity after $E_j$, and every successor path forces it before $H-B_j+p_j$. Its activity is therefore retained. Set endpoints to actual first and last activity and $M$ to its makespan. ($arrow.l.double$) Work and capacity rows ensure valid processing. Active-slot endpoint rows and $C_a<=S_b$ forbid successors from starting before predecessors finish, including interrupted tasks. The decoded makespan is at most $M$; tightening endpoints and $M$ to actual activity proves optimum equality. Positive durations make precedence cycles infeasible on both sides. - _Solution extraction and size._ Validate, project retained activity to the original $n$ by $D$ matrix, and put zero in omitted slots. For $A$ retained slots and $e$ arc occurrences, the target has exactly $A+2n+1$ variables, $2A+2n+H+e$ rows, and $6A+2n+2e$ nonzeros. Source parameters `schedule_horizon` and `num_admissible_slots` come from the same deterministic calculation. Constraint magnitudes need at most `max_schedule_magnitude_bits`; endpoint arithmetic is checked before allocation. Empty instances have only $M=0$. + _Solution extraction and size._ Validate, project retained activity to the original $n$ by $D$ matrix, and put zero in omitted slots. For $A$ retained slots and $e$ arc occurrences, the target has exactly $A+2n+1$ variables, $2A+2n+H+e$ rows, and $6A+2n+2e$ nonzeros. Since $H <= D$ and $A <= n D$, the registered upper bounds are $n D+2n+1$ variables, $2n+D+2n D+e$ rows, and $6n D+2n+2e$ nonzeros. Constraint magnitudes need at most `max_schedule_magnitude_bits`; endpoint arithmetic is checked before allocation. Empty instances have only $M=0$. ] #reduction-rule("SequencingWithinIntervals", "ILP")[ @@ -15659,9 +15659,10 @@ The following reductions to Integer Linear Programming are straightforward formu Empty and singleton paths require no adjacency rows. _Overhead._ With $q=max(n-1,0)$, there are exactly $n^2$ variables and - $2n+n q$ rows. If $e$ is the number of distinct non-loop edges, nonzeros - equal $2n^2+q(n+2e)$, bounded by $2n^2+q(n+2m)$ for the stored edge count - $m$. Coefficients and right-hand sides have magnitude at most one. + $n^2+n$ rows. If $e$ is the number of distinct non-loop edges, nonzeros + equal $2n^2+q(n+2e)$. Since $q <= n$ and $e <= m$ for the stored edge + count $m$, the registered upper bound is $3n^2+2n m$. + Coefficients and right-hand sides have magnitude at most one. _Solution extraction._ Return the unique selected vertex at each position. ] diff --git a/src/models/graph/hamiltonian_path.rs b/src/models/graph/hamiltonian_path.rs index bf64a90b4..b31d6d2ff 100644 --- a/src/models/graph/hamiltonian_path.rs +++ b/src/models/graph/hamiltonian_path.rs @@ -89,11 +89,6 @@ 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) @@ -108,11 +103,7 @@ where type Solution = Vec; type Value = crate::types::Or; - crate::problem_parameters![ - ("num_edges", num_edges), - ("num_vertices", num_vertices), - ("num_consecutive_positions", num_consecutive_positions), - ]; + crate::problem_parameters![("num_edges", num_edges), ("num_vertices", num_vertices),]; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![G] diff --git a/src/models/misc/preemptive_scheduling.rs b/src/models/misc/preemptive_scheduling.rs index cec027f5a..d214b9f35 100644 --- a/src/models/misc/preemptive_scheduling.rs +++ b/src/models/misc/preemptive_scheduling.rs @@ -173,20 +173,6 @@ impl PreemptiveScheduling { &self.precedences } - /// Horizon of a feasible critical-path-priority list schedule; zero for cycles. - pub fn schedule_horizon(&self) -> usize { - self.scheduling_windows().0 - } - - /// Activity slots that can occur within the certified horizon. - pub fn num_admissible_slots(&self) -> usize { - self.scheduling_windows() - .1 - .iter() - .map(|window| window.len()) - .sum() - } - /// Bit bound for work, capacity and endpoint coefficients. pub fn max_schedule_magnitude_bits(&self) -> u64 { crate::types::max_numeric_magnitude_bits([self.d_max(), self.num_processors()]) @@ -302,8 +288,6 @@ impl Problem for PreemptiveScheduling { crate::problem_parameters![ ("d_max", d_max), - ("schedule_horizon", schedule_horizon), - ("num_admissible_slots", num_admissible_slots), ("max_schedule_magnitude_bits", max_schedule_magnitude_bits), ("num_precedences", num_precedences), ("num_processors", num_processors), diff --git a/src/rules/hamiltoniancircuit_hamiltonianpath.rs b/src/rules/hamiltoniancircuit_hamiltonianpath.rs index 1520088e3..8563fade6 100644 --- a/src/rules/hamiltoniancircuit_hamiltonianpath.rs +++ b/src/rules/hamiltoniancircuit_hamiltonianpath.rs @@ -91,7 +91,6 @@ impl crate::rules::AggregateReductionResult for ReductionHamiltonianCircuitToHam #[reduction(transform = { exact { num_vertices = "num_vertices + 3", - num_consecutive_positions = "num_vertices + 2", }, upper_bound { num_edges = "2 * num_edges + 2", diff --git a/src/rules/hamiltonianpath_ilp.rs b/src/rules/hamiltonianpath_ilp.rs index c2eedd5c6..046d0fe15 100644 --- a/src/rules/hamiltonianpath_ilp.rs +++ b/src/rules/hamiltonianpath_ilp.rs @@ -44,10 +44,10 @@ impl crate::rules::AggregateReductionResult for ReductionHamiltonianPathToILP {} exact { max_constraint_magnitude_bits = "1", num_vars = "num_vertices^2", - num_constraints = "2 * num_vertices + num_vertices * num_consecutive_positions", + num_constraints = "num_vertices^2 + num_vertices", }, upper_bound { - num_nonzeros = "2 * num_vertices^2 + num_consecutive_positions * (num_vertices + 2 * num_edges)", + num_nonzeros = "3 * num_vertices^2 + 2 * num_vertices * num_edges", }, })] impl ReduceTo> for HamiltonianPath { @@ -72,7 +72,7 @@ impl ReduceTo> for HamiltonianPath { for (v, adjacent) in neighbors.iter_mut().enumerate() { adjacent.sort_unstable(); adjacent.dedup(); - for p in 0..self.num_consecutive_positions() { + for p in 0..n.saturating_sub(1) { let mut terms = vec![(v * n + p, 1)]; terms.extend(adjacent.iter().map(|&w| (w * n + p + 1, -1))); constraints.push(LinearConstraint::le(terms, 0)); diff --git a/src/rules/ksatisfiability_preemptivescheduling.rs b/src/rules/ksatisfiability_preemptivescheduling.rs index 3b5353aff..78aa1bc0f 100644 --- a/src/rules/ksatisfiability_preemptivescheduling.rs +++ b/src/rules/ksatisfiability_preemptivescheduling.rs @@ -372,8 +372,6 @@ impl crate::rules::AggregateReductionResult for Reduction3SATToPreemptiveSchedul 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)", - schedule_horizon = "(2 * num_vars + 3 + 6 * num_clauses) * (num_vars + 3)", - num_admissible_slots = "((2 * num_vars + 3 + 6 * num_clauses) * (num_vars + 3))^2", max_schedule_magnitude_bits = "(2 * num_vars + 3 + 6 * num_clauses) * (num_vars + 3)", num_precedences = "((2 * num_vars + 3 + 6 * num_clauses) * (num_vars + 3))^2", })] diff --git a/src/rules/preemptivescheduling_ilp.rs b/src/rules/preemptivescheduling_ilp.rs index f4db4af38..4878d45d7 100644 --- a/src/rules/preemptivescheduling_ilp.rs +++ b/src/rules/preemptivescheduling_ilp.rs @@ -53,15 +53,11 @@ impl ReductionResult for ReductionPSToILP { } } -#[reduction(transform = { - exact { - num_vars = "num_admissible_slots + 2 * num_tasks + 1", - num_constraints = "2 * num_tasks + schedule_horizon + 2 * num_admissible_slots + num_precedences", - num_nonzeros = "6 * num_admissible_slots + 2 * num_precedences + 2 * num_tasks", - }, - upper_bound { - max_constraint_magnitude_bits = "max_schedule_magnitude_bits", - }, +#[reduction(transform = upper_bound { + num_vars = "num_tasks * d_max + 2 * num_tasks + 1", + num_constraints = "2 * num_tasks + d_max + 2 * num_tasks * d_max + num_precedences", + num_nonzeros = "6 * num_tasks * d_max + 2 * num_precedences + 2 * num_tasks", + max_constraint_magnitude_bits = "max_schedule_magnitude_bits", })] impl ReduceTo> for PreemptiveScheduling { type Result = ReductionPSToILP; diff --git a/src/unit_tests/symbolic_parameter_contracts.rs b/src/unit_tests/symbolic_parameter_contracts.rs index 3fb048073..986f09136 100644 --- a/src/unit_tests/symbolic_parameter_contracts.rs +++ b/src/unit_tests/symbolic_parameter_contracts.rs @@ -21,10 +21,6 @@ fn parameter_schemas_keep_distinct_counts_without_synonymous_aliases() { "MinimumCodeGenerationOneRegister", &["num_vertices", "num_leaves", "num_internal"], ), - ( - "HamiltonianPath", - &["num_vertices", "num_consecutive_positions"], - ), ( "LongestCommonSubsequence", &["max_length", "num_transitions"], @@ -39,6 +35,23 @@ fn parameter_schemas_keep_distinct_counts_without_synonymous_aliases() { .parameter_names("ExactCoverBy3Sets") .iter() .any(|field| field == "num_sets")); + for (model, expected) in [ + ("HamiltonianPath", &["num_edges", "num_vertices"][..]), + ( + "PreemptiveScheduling", + &[ + "d_max", + "max_schedule_magnitude_bits", + "num_precedences", + "num_processors", + "num_tasks", + ], + ), + ] { + let mut fields = graph.parameter_names(model); + fields.sort(); + assert_eq!(fields, expected, "{model} public parameter schema"); + } } #[test] @@ -438,12 +451,12 @@ fn exact_parameter_formulas_cover_sparse_and_boundary_instances() { ); check_reduced_parameters::<_, HamiltonianPath>( HamiltonianCircuit::new(SimpleGraph::new(0, vec![])), - &["num_vertices", "num_consecutive_positions"], + &["num_vertices"], exact, ); check_reduced_parameters::<_, HamiltonianPath>( HamiltonianCircuit::new(SimpleGraph::new(3, vec![(0, 1), (1, 2), (2, 0)])), - &["num_vertices", "num_consecutive_positions"], + &["num_vertices"], exact, ); check_reduced_parameters::<_, LongestCommonSubsequence>( From 54647230c491185e1807aeeeec00ef45fa8a85bb Mon Sep 17 00:00:00 2001 From: Xiwei Pan <90967972+isPANN@users.noreply.github.com> Date: Thu, 1 Oct 2026 00:31:59 -0700 Subject: [PATCH 18/22] Fix exact verification bottlenecks in reduction targets --- docs/paper/reductions.typ | 125 ++++++---- src/models/misc/register_sufficiency.rs | 112 ++++++++- src/rules/acyclicpartition_ilp.rs | 226 ++++++++++++++++-- src/rules/bmf_ilp.rs | 80 +++++-- src/rules/closestvectorproblem_qubo.rs | 167 ++++++++----- src/rules/hamiltoniancircuit_ilp.rs | 126 ++++++++++ src/rules/ksatisfiability_acyclicpartition.rs | 26 +- src/rules/mod.rs | 2 + .../customized/ensemble_computation.rs | 188 ++++++++++----- src/solvers/customized/mod.rs | 2 +- .../customized/quadratic_congruences.rs | 202 ++++++++++++++++ src/solvers/customized/solver.rs | 22 ++ src/solvers/pipelines.rs | 3 +- src/unit_tests/rules/acyclicpartition_ilp.rs | 84 +++++++ src/unit_tests/rules/bmf_ilp.rs | 38 ++- .../rules/closestvectorproblem_qubo.rs | 78 ++++-- .../rules/hamiltoniancircuit_ilp.rs | 71 ++++++ .../rules/ksatisfiability_acyclicpartition.rs | 32 ++- .../customized/ensemble_computation.rs | 21 +- src/unit_tests/solvers/registry.rs | 23 +- src/unit_tests/solvers/resolver.rs | 145 +++++++++++ 21 files changed, 1497 insertions(+), 276 deletions(-) create mode 100644 src/rules/hamiltoniancircuit_ilp.rs create mode 100644 src/solvers/customized/quadratic_congruences.rs create mode 100644 src/unit_tests/rules/hamiltoniancircuit_ilp.rs diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index 16a05eebf..44256b45b 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -11967,7 +11967,14 @@ the displayed rule, extracted from the corresponding `pred path` entry. matrix.at(i).at(j) = value } let bits = cvp_qubo_sol.target_config - let lower = (-23, -14) + let determinant = basis.at(0).at(0) * basis.at(1).at(1) - basis.at(1).at(0) * basis.at(0).at(1) + let adjugate = ((basis.at(1).at(1), -basis.at(1).at(0)), (-basis.at(0).at(1), basis.at(0).at(0))) + let center = adjugate.map(row => row.zip(target).fold(0, (acc, pair) => acc + pair.at(0) * pair.at(1)) / determinant) + let candidate = center.map(calc.round) + let radius-sq = range(target.len()).fold(0, (acc, d) => acc + calc.pow(candidate.enumerate().fold(0, (sum, pair) => sum + pair.at(1) * basis.at(pair.at(0)).at(d)) - target.at(d), 2)) + let radii = adjugate.map(row => calc.floor(calc.sqrt(row.fold(0, (acc, x) => acc + x*x) * radius-sq))) + let lower = center.enumerate().map(((i,x)) => calc.ceil(x - radii.at(i) / calc.abs(determinant))) + let upper = center.enumerate().map(((i,x)) => calc.floor(x + radii.at(i) / calc.abs(determinant))) let anchor = range(target.len()).map(d => lower.enumerate().fold(0.0, (acc, (i, x)) => acc + x * basis.at(i).at(d))) let constant = range(target.len()).fold(0.0, (acc, d) => acc + calc.pow(anchor.at(d) - target.at(d), 2)) let qubo-value = range(bits.len()).fold(0.0, (acc, i) => acc + if bits.at(i) == false { 0.0 } else { @@ -11990,30 +11997,32 @@ the displayed rule, extracted from the corresponding `pred path` entry. ) *Step 1 -- Source instance.* The canonical CVP example has basis columns $bold(b)_1=#fmt-vec(basis.at(0))$ and $bold(b)_2=#fmt-vec(basis.at(1))$ and target $bold(t)=#fmt-vec(target)$. The source model supplies no coefficient bounds. - *Step 2 -- Derive a safe box.* Here $A=((2,1),(0,2))$, $norm(bold(t))_1=5$, and the selected-row bounds are $bold(C)=(8,7)$. Since $op("adj")(A)=((2,-1),(0,2))$, the reduction obtains $M_1=23$ and $M_2=14$. + *Step 2 -- Derive a safe box.* The inverse-basis center is #fmt-vec(center). Rounding gives #fmt-vec(candidate), whose squared residual is #radius-sq. Exact Cauchy--Schwarz bounds give coefficient intervals from #fmt-vec(lower) to #fmt-vec(upper). - *Step 3 -- Encode and expand.* The exact-range weights are $(1,2,4,8,16,15)$ for $x_1+23 in [0,46]$ and $(1,2,4,8,13)$ for $x_2+14 in [0,28]$, giving #cvp_qubo.target.instance.num_vars variables. With $G=B^top B=((4,2),(2,5))$ and $h=B^top bold(t)=(6,7)^top$, representative coefficients are $Q_(0,0)=#matrix.at(0).at(0)$, $Q_(0,1)=#matrix.at(0).at(1)$, $Q_(0,6)=#matrix.at(0).at(6)$, and $Q_(6,6)=#matrix.at(6).at(6)$. + *Step 3 -- Encode and expand.* Encode each interval's offset using powers of two and one capped final weight. Fixed coordinates need no bits; this target uses #cvp_qubo.target.instance.num_vars variable, with coefficient $Q_(0,0)=#matrix.at(0).at(0)$. - *Step 4 -- Verify a solution.* The fixture stores $bold(z)=(#fmt-values(bits))$, which decodes to $bold(x)=(#fmt-values(coords))$. The QUBO value is #rounded-qubo; adding the dropped constant #rounded-constant gives squared CVP distance #rounded-distance-sq, so $B bold(x)=bold(t)$ #sym.checkmark. - - *Multiplicity.* Residual final weights make some offsets have multiple encodings, so the fixture stores one canonical bit vector although other optimal QUBO witnesses can decode to the same $bold(x)$. + *Step 4 -- Verify a solution.* The fixture stores $bold(z)=(#fmt-values(bits))$, which decodes to $bold(x)=(#fmt-values(coords))$. The QUBO value is #rounded-qubo; adding the dropped constant #rounded-constant gives squared CVP distance #rounded-distance-sq #sym.checkmark. ], )[ Following the quadratic formulation of Canale, Qureshi, and Viola @canale2023qubo, this rule derives a finite box containing a global minimizer of standard CVP, then encodes that box and expands the squared-distance objective. ][ - _Construction._ Let $B in ZZ^(m times n)$ have full column rank and $bold(t) in ZZ^m$. Select $n$ rows forming an invertible matrix $A$, with source row indices $r_j$. Since zero is a candidate, every minimizer $bold(x)^*$ satisfies $norm(B bold(x)^*-bold(t))_2 <= norm(bold(t))_2$. For $bold(y)=A bold(x)^*$, define $C_j=abs(t_(r_j))+norm(bold(t))_1$. Then $abs(y_j)<=C_j$, and $bold(x)^*=op("adj")(A)bold(y)/det(A)$ gives - $ abs(x_i^*) <= M_i = sum_j abs(op("adj")(A)_(i,j)) C_j $ - because the nonzero integer determinant has magnitude at least one. + _Construction._ Let $B in ZZ^(m times n)$ have full column rank and $bold(t) in ZZ^m$. Select $n$ independent rows forming $A$. Write $D=abs(det(A))$ and $H=op("sign")(det(A))op("adj")(A)$, so $A^(-1)=H/D$. Let $s_i=sum_j H_(i,j)t_(r_j)$. Round $s_i/D$ to integer coefficients $q_i$, and set $R^2=min(norm(B bold(q)-bold(t))_2^2,norm(bold(t))_2^2)$. Both comparison points belong to the lattice, so every minimizer has squared residual at most $R^2$. + + Cauchy--Schwarz applied to the selected residual coordinates gives + $ abs(D x_i^*-s_i)^2 <= (sum_j H_(i,j)^2) R^2. $ + Set $rho_i=floor(sqrt((sum_j H_(i,j)^2)R^2))$. Because $D x_i^*-s_i$ is integer, all minimizing coefficients lie in + $ ell_i=ceil((s_i-rho_i)/D) <= x_i^* <= u_i=floor((s_i+rho_i)/D). $ + All divisions and square roots in the implementation use exact integer arithmetic. - 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 + Encode $x_i-ell_i in [0,u_i-ell_i]$ with powers of two and one capped final weight. Fixed coordinates contribute no bits. If $W$ maps bits to offsets, $G=B^top B$ and $h=B^top bold(t)$, 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. + Drop the constant. - _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. + _Size bound._ The new intervals lie within the coarse box $[-M_i,M_i]$, where $M_i=sum_j abs(H_(i,j))(abs(t_(r_j))+norm(bold(t))_1)$: the comparison radius never exceeds the zero-vector radius. 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. - _Solution extraction._ Sum the selected weights for each coefficient and subtract $M_i$. + _Solution extraction._ Sum the selected weights for each coefficient and add $ell_i$. ] ] } @@ -15947,9 +15956,38 @@ The following reductions to Integer Linear Programming are straightforward formu _Solution extraction._ Return the vertex-assignment prefix $(x_0, dots, x_(n-1))$. ] +#let hc_ilp = load-example("HamiltonianCircuit", "ILP") +#reduction-rule("HamiltonianCircuit", "ILP", + example: true, + example-caption: [One flow certifies connectivity of the selected spanning cycle], + extra: [ + #pred-commands( + "pred create --example " + rule-spec(hc_ilp) + " -o circuit.json", + "pred reduce circuit.json --via route.json -o bundle.json", + "pred solve bundle.json", + "pred evaluate circuit.json --config " + cli-config(hc_ilp.solutions.at(0).source_config), + ) + The source has #hc_ilp.source.instance.graph.num_vertices vertices and + #hc_ilp.source.instance.graph.edges.len() edges. Its target has + #hc_ilp.target.instance.variables.len() bounded integer variables. + The extracted order is #fmt-values(hc_ilp.solutions.at(0).source_config): + every source vertex is visited once and the final edge returns to the first. + ], +)[ + Select a spanning degree-two subgraph and enforce connectivity with one bounded integral flow. +][ + _Construction._ Discard loops and duplicate undirected edges. For each remaining edge $e$, use a binary selection $y_e$ and two directed flows bounded in $[0,n-1]$. Require degree two at every vertex and $f_(e,eta)<=(n-1)y_e$. Vertex zero supplies $n-1$ units; every other vertex consumes one. Graphs with fewer than three vertices receive a contradictory row, matching the source's cycle definition. All variables belong to the bounded integer ILP variant. + + _Correctness._ ($arrow.r.double$) A Hamiltonian cycle has degree two. Send one unit to each other vertex along cycle paths from zero; every directed flow fits the capacity. ($arrow.l.double$) A selected component excluding zero cannot consume one unit per vertex without a selected edge crossing its boundary. Thus the selected graph is connected. A connected simple graph of degree two is one spanning cycle, and the contradictory row excludes the empty and two-vertex cases. + + _Overhead._ With $m'$ distinct non-loop edges, there are $3m'<=3m$ variables, $2n+2m'$ rows plus at most one contradictory row, and at most $10m'$ nonzeros. The largest magnitude is at most $max(n-1,2)$. + + _Solution extraction._ Walk the selected cycle from zero, returning the visited vertex order. Reject an invalid target or a walk that repeats a vertex before returning to zero after all vertices. +] + #reduction-rule("AcyclicPartition", "ILP")[ - Bounded topological part labels and one exact crossing indicator per stored arc, - retaining one-hot membership for signed part-weight constraints. + Binary labels for a certified two-part domain; otherwise bounded topological + labels, exact crossing indicators, and one-hot membership for signed weights. ][ _Construction._ Introduce binary memberships $x_(v,j)$ and emptiness bits $e_j$, integer labels $p_v in {0,dots,n-1}$, and binary crossing bits $y_a$. @@ -15970,16 +16008,21 @@ The following reductions to Integer Linear Programming are straightforward formu quotient cycles. The cost row therefore measures the exact signed crossing sum; negative costs cannot be exploited by a false crossing indicator. - _Overhead._ There are exactly $n^2+2n+m$ variables and - $n^2+4n+2m+1$ rows. The one-hot, label, emptiness implication, emptiness sum, + _Part bound._ For nonnegative weights and costs, two vertices $r,s$ with $w_r+w_s>B$ must occupy different parts. Suppose every other vertex $v$ has arcs $r arrow.r v arrow.r s$. Let $a_v,b_v$ be the total costs of the respective parallel arcs, and let $d$ be the total direct $r arrow.r s$ cost. Every feasible partition costs at least $L=d+sum_v min(a_v,b_v)$. If $v$ occupies neither anchor part, both path arcs cross, adding at least $max(a_v,b_v)$ above that baseline. Therefore, when $K-L=3$; smaller instances use the unmodified general construction. All integer variables have explicit finite bounds. - _Solution extraction._ Decode the selected part of each one-hot row; - the label equalities give the same result from $p$. + _Solution extraction._ Return the binary labels in the certified two-part + domain; otherwise decode the selected part of each one-hot row. ] #reduction-rule("BalancedCompleteBipartiteSubgraph", "ILP")[ @@ -16145,21 +16188,15 @@ The following reductions to Integer Linear Programming are straightforward formu // Matrix/encoding #reduction-rule("BMF", "ILP")[ - Retain the binary factor matrices $B,C$. For each zero entry of $A$, require - $b_(i,r)+c_(r,j) <= 1$ for every rank $r$. For each one entry introduce coverage - bits $p_(i,j,r) <= b_(i,r)$ and $p_(i,j,r) <= c_(r,j)$, with - $sum_r p_(i,j,r) >= 1$. Minimize the total factor weight. If $t$ entries are - one, this uses $k(m+n)+k t$ variables, $k(m n-t)+(2k+1)t$ rows, and - $2k(m n-t)+5k t$ nonzeros. Existing source parameters yield upper bounds by - substituting $t <= m n$. -][ - An exact factorization supplies valid coverage bits from its true products. - Conversely, zero-entry rows exclude every product there, and each one entry - has a selected coverage bit that forces both factor memberships. The extracted - matrices therefore reconstruct $A$ exactly, with identical objective for every - feasible target witness. Coverage bits need not equal every true product. - Rank zero yields an empty contradictory row for each one entry and remains - feasible for zero matrices; empty dimensions preserve their factor shapes. + Binary factor memberships and sparse coverage witnesses, with factors named by pairwise incompatible matrix entries. +][ + _Construction._ Retain $B,C$. Choose up to $k$ one entries $(i,j)$ whose pairs $(i,j),(u,v)$ satisfy $A_(i,v)=0$ or $A_(u,j)=0$. Name a distinct factor for each entry, fixing its endpoint memberships to one and its non-neighbors to zero. Skip rows and coverage variables already decided by these fixed memberships. For remaining zeros require $b_(i,r)+c_(r,j)<=1$; for remaining ones use coverage bits $p_(i,j,r)<=b_(i,r),c_(r,j)$ and $sum_r p_(i,j,r)>=1$. Minimize factor weight. + + _Correctness._ Incompatible entries cannot lie in one all-ones rectangle. Any exact factorization therefore covers the chosen entries with distinct factors, which can be permuted into the named positions without changing its weight. Fixing the endpoint memberships and excluding their non-neighbors preserves that permuted factorization. Every omitted row is implied by these fixed values. Remaining coverage rows force exact reconstruction, and extraction retains the identical factor-weight objective. Coverage bits need not equal every true product. Rank zero and empty dimensions retain their original semantics. + + _Overhead._ There are at most $k(m+n)+k m n$ variables. At most $k(m+n)$ membership pins supplement the original bounds $(2k+1)m n$ rows and $5k m n$ nonzeros. Pruning can only reduce these counts. + + _Solution extraction._ Read the factor-membership prefix in its original layout. ] #reduction-rule("BMF", "BicliqueCover")[ @@ -17967,26 +18004,26 @@ The following table shows concrete target-variable counts for example instances, #let ksat_ap_sol = ksat_ap.solutions.at(0) #reduction-rule("KSatisfiability", "AcyclicPartition", example: true, - example-caption: [3-SAT to a partition with two heavy anchors and unit incidence items], + example-caption: [3-SAT to a partition with heavy anchors, unit-weight vertices, and zero-weight edge items], extra: [ - The canonical formula $(x_1 or x_1 or x_1)$ gives a clique graph with four vertices, three edges, and threshold two. The incidence construction has seven items and two anchors, capacity $c=3$, magnitude $M=15$, weight bound $B=21$, and cost bound $K=101$. Anchor weights are 18 and 14. The stored partition $(#fmt-values(ksat_ap_sol.target_config))$ puts two clique vertices and their edge item with the source anchor. Its cut cost is $105-2-2=101$. Formal extraction gives $(#fmt-values(ksat_ap_sol.source_config))$. + The canonical formula $(x_1 or x_1 or x_1)$ gives a four-vertex clique graph with threshold two. Its partition target has #ksat_ap.target.instance.graph.num_vertices vertices and #ksat_ap.target.instance.graph.arcs.len() arcs, weight bound #ksat_ap.target.instance.weight_bound and cost bound #ksat_ap.target.instance.cost_bound. The final two vertex weights are #fmt-values(ksat_ap.target.instance.vertex_weights.slice(-2)). The stored partition $(#fmt-values(ksat_ap_sol.target_config))$ puts two clique vertices and their edge item with the source anchor; edge items consume no capacity. Formal extraction gives $(#fmt-values(ksat_ap_sol.source_config))$. ], )[ - 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. + Compose the literal-compatibility clique construction @karp1972 with the incidence construction below. Vertex weights are nonnegative integers 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$. - 2. Add anchors $s,t$ with weights $B-c$ and $B-L$. For every item $i$, add $(s,i)$ of cost $M$ and $(i,t)$ of cost $M-d_i$, where $d_i=deg_H(i)+1$ for vertex items and zero for edge items. - 3. For every edge $a={u,v}$ add $(u,a)$ and $(v,a)$, each of cost one. Set the weight bound to $B$ and cut-cost bound to $K=M L-k^2$. + 1. Create one unit-weight item per vertex and one zero-weight item per edge of $H$. Set $M=2L+1$ and $B=2(L+k)+1$. + 2. Add anchors $s,t$ with weights $B-k$ and $B-(h-k)$. For every item $i$, add $(s,i)$ of cost $M$ and $(i,t)$ of cost $M-d_i$, where $d_i=deg_H(i)$ for vertex items and zero for edge items. + 3. For every edge $a={u,v}$ add $(u,a)$ and $(v,a)$, each of cost one. Set the weight bound to $B$ and cut-cost bound to $K=M L-k(k-1)$. - _Forward direction._ Given a clique of at least $k$ vertices, choose exactly $k$ of them. Put these vertex items and their $k(k-1)/2$ edge items with $s$, and all remaining items with $t$. The source block contains exactly $c$ unit items; the sink block contains at most $L$. No dependency arc goes from the sink block to the source block, so the quotient is acyclic. Spoke costs are $M L-sum_(v " selected")(deg_H(v)+1)$, and dependency costs are $sum_(v " selected") deg_H(v)-k(k-1)$. Their sum is $M L-k^2=K$. + _Forward direction._ Choose exactly $k$ vertices of a source clique. Put their vertex items and all $k(k-1)/2$ internal edge items with $s$, and the remaining items with $t$. The respective ordinary vertex weights are $k$ and $h-k$, so both blocks meet their capacity exactly. Edge items add no weight. No dependency points from the sink block to the source block. Spoke costs are $M L-sum_(v " selected") deg_H(v)$, and dependency costs are $sum_(v " selected") deg_H(v)-k(k-1)$. The total is $K$. - _Backward direction._ Each anchor weighs more than $B/2$, so they occupy distinct blocks. If $r>=1$ items lie outside those blocks, spoke costs alone are at least $M(L+r)-(h+2e)>M L>K$, since $h+2e<=2L=1$. A dependency in the opposite direction would create a cycle, so an edge item in the source block has both endpoints there. + _Backward direction._ Each anchor weighs more than $B/2$, so they occupy distinct blocks. If $r>=1$ items lie outside those blocks, spoke costs alone are at least $M(L+r)-2e>M L>=K$, since $2e<=2L=k^2$. If $pk$, then $p+2q<=2c-p=k(k-1)/2$. Equality follows, and the selected $k$ vertices form a clique. The explicit vertex cardinality avoids the weaker mixed vertex-plus-edge capacity inequality. _Solution extraction._ Validate the target partition, select precisely those vertex items sharing the label of $s$, and invoke the formal clique-to-SAT extractor. This mapping is independent of the numerical names of partition blocks. diff --git a/src/models/misc/register_sufficiency.rs b/src/models/misc/register_sufficiency.rs index a6df3493e..554784d54 100644 --- a/src/models/misc/register_sufficiency.rs +++ b/src/models/misc/register_sufficiency.rs @@ -239,9 +239,9 @@ impl RegisterSufficiency { /// most `self.bound` registers, or returns `None` if no such ordering /// exists. Uses heuristic candidate ordering (prefer vertices that free /// the most registers) so that YES instances typically resolve on the - /// first greedy path without backtracking. For NO instances the full - /// search tree must be explored, so prefer the ILP solver path for - /// infeasibility proofs. + /// first greedy path without backtracking. Identical dependency/consumer + /// groups are scheduled consecutively, and ready operations that do not + /// increase the live set can be performed immediately. /// /// NOTE: a greedy topological sort is *not* exact — it can miss valid /// orderings. This method is exact because it backtracks when the @@ -254,12 +254,15 @@ impl RegisterSufficiency { let mut dependents: Vec> = vec![vec![]; n]; let mut dependencies: Vec> = vec![vec![]; n]; - let mut in_degree = vec![0u32; n]; + let mut in_degree = vec![0usize; n]; for &(v, u) in &self.arcs { in_degree[v] += 1; dependents[u].push(v); dependencies[v].push(u); } + for neighbors in dependents.iter_mut().chain(dependencies.iter_mut()) { + neighbors.sort_unstable(); + } let mut state = BnBState { n, @@ -267,6 +270,8 @@ impl RegisterSufficiency { config: vec![0usize; n], live: vec![false; n], live_count: 0, + computed: vec![false; n], + failed: std::collections::HashSet::new(), remaining_in_degree: in_degree.clone(), remaining_deps: dependents.iter().map(|d| d.len()).collect(), ready: (0..n).filter(|&v| in_degree[v] == 0).collect(), @@ -275,7 +280,20 @@ impl RegisterSufficiency { }; state.ready.sort_unstable(); - if state.backtrack(0) { + // If every next operation requires all input vertices, their order + // cannot matter: no operation can run before the last input and each + // input adds one live value. Avoid exploring their permutations. + let input_block = if state + .dependencies + .iter() + .filter(|deps| !deps.is_empty() && deps.iter().all(|&v| in_degree[v] == 0)) + .all(|deps| state.ready.iter().all(|v| deps.contains(v))) + { + state.ready.clone() + } else { + Vec::new() + }; + if state.backtrack(0, &input_block) { Some(state.config) } else { None @@ -289,7 +307,9 @@ struct BnBState { config: Vec, live: Vec, live_count: usize, - remaining_in_degree: Vec, + computed: Vec, + failed: std::collections::HashSet>, + remaining_in_degree: Vec, remaining_deps: Vec, ready: Vec, dependents: Vec>, @@ -297,23 +317,89 @@ struct BnBState { } impl BnBState { - fn backtrack(&mut self, step: usize) -> bool { + fn backtrack(&mut self, step: usize, block: &[usize]) -> bool { if step == self.n { return true; } + // Liveness and available operations depend only on the computed set, + // so a failed continuation never needs searching under another order. + if block.is_empty() && self.failed.contains(&self.computed) { + return false; + } // Heuristic: prefer vertices that free the most registers. - let mut candidates = self.ready.clone(); - candidates.sort_by_key(|&v| { + let mut candidates = if block.is_empty() { + self.ready.clone() + } else { + vec![block[0]] + }; + candidates.sort_by_cached_key(|&v| { let frees = self.dependencies[v] .iter() .filter(|&&dep| self.remaining_deps[dep] == 1 && self.live[dep]) .count(); - std::cmp::Reverse(frees) + let group_size = self + .ready + .iter() + .filter(|&&w| { + self.dependencies[v] == self.dependencies[w] + && self.dependents[v] == self.dependents[w] + }) + .count(); + let unblocks = self.dependents[v].iter().any(|&consumer| { + self.remaining_in_degree[consumer] + == group_size + * self.dependents[v] + .iter() + .filter(|&&w| w == consumer) + .count() + }); + // Prefer work that releases values or makes its consumer ready. + ( + std::cmp::Reverse(frees), + std::cmp::Reverse(unblocks), + group_size, + v, + ) }); + if block.is_empty() { + candidates.retain(|&v| { + !self.ready.iter().any(|&w| { + w < v + && self.dependencies[v] == self.dependencies[w] + && self.dependents[v] == self.dependents[w] + }) + }); + // Moving a ready operation that frees at least one value earlier + // replaces those live values by one result and cannot raise a peak. + if candidates.first().is_some_and(|&v| { + self.dependencies[v] + .iter() + .any(|&dep| self.remaining_deps[dep] == 1 && self.live[dep]) + }) { + candidates.truncate(1); + } + } for &vertex in &candidates { + // No consumer of identical vertices can run until all are done. + // Delay partial computation until the last member: a consecutive + // block never keeps more values live than the original ordering. + let following = if block.is_empty() { + self.ready + .iter() + .copied() + .filter(|&v| { + v != vertex + && self.dependencies[v] == self.dependencies[vertex] + && self.dependents[v] == self.dependents[vertex] + }) + .collect::>() + } else { + block[1..].to_vec() + }; self.config[vertex] = step; + self.computed[vertex] = true; let was_live = self.live[vertex]; if !was_live { @@ -342,7 +428,7 @@ impl BnBState { } } - if self.backtrack(step + 1) { + if self.backtrack(step + 1, &following) { return true; } @@ -368,8 +454,12 @@ impl BnBState { self.live[vertex] = false; self.live_count -= 1; } + self.computed[vertex] = false; } + if block.is_empty() { + self.failed.insert(self.computed.clone()); + } false } } diff --git a/src/rules/acyclicpartition_ilp.rs b/src/rules/acyclicpartition_ilp.rs index 5d8fb254e..6cbe9fd59 100644 --- a/src/rules/acyclicpartition_ilp.rs +++ b/src/rules/acyclicpartition_ilp.rs @@ -9,6 +9,7 @@ use crate::rules::traits::{ReduceTo, ReductionResult}; pub struct ReductionAcyclicPartitionToILP { target: ILP, n: usize, + parts: Option, } impl ReductionResult for ReductionAcyclicPartitionToILP { @@ -31,22 +32,203 @@ impl ReductionResult for ReductionAcyclicPartitionToILP { "target ILP assignment is infeasible", )?; - crate::rules::ilp_helpers::one_hot_decode_rows(target_solution, self.n, self.n, 0) + match self.parts { + Some(parts) => { + crate::rules::ilp_helpers::one_hot_decode_rows(target_solution, self.n, parts, 0) + } + None => crate::rules::ilp_helpers::decode_usize_values(&target_solution[..self.n]), + } } } #[crate::aggregate_reduction(ilp_feasibility)] impl crate::rules::AggregateReductionResult for ReductionAcyclicPartitionToILP {} -#[reduction(transform = { - exact { - num_vars = "num_vertices^2 + 2 * num_vertices + num_arcs", - num_constraints = "num_vertices^2 + 4 * num_vertices + 2 * num_arcs + 1", - }, - upper_bound { - max_constraint_magnitude_bits = "max_numeric_magnitude_bits + num_vertices + 1", - num_nonzeros = "6 * num_vertices^2 + 2 * num_vertices + 7 * num_arcs", - }, +// Two separated heavy anchors, with a two-arc path through every other +// vertex, give a lower bound on crossing cost. A third part would cross both +// arcs of one path, rather than at least one, and pay the indicated extra. +fn two_part_anchors(problem: &AcyclicPartition) -> Option<(usize, usize)> { + let n = problem.num_vertices(); + if n < 3 + || *problem.weight_bound() < 0 + || problem.vertex_weights().iter().any(|&weight| weight < 0) + || problem.arc_costs().iter().any(|&cost| cost < 0) + { + return None; + } + let arcs = problem.graph().arcs(); + let mut incoming = vec![false; n]; + let mut outgoing = vec![false; n]; + for &(u, v) in &arcs { + if u != v { + outgoing[u] = true; + incoming[v] = true; + } + } + for root in (0..n).filter(|&v| !incoming[v]) { + for sink in (0..n).filter(|&v| v != root && !outgoing[v]) { + if i128::from(problem.vertex_weights()[root]) + + i128::from(problem.vertex_weights()[sink]) + <= i128::from(*problem.weight_bound()) + { + continue; + } + let mut left = vec![None; n]; + let mut right = vec![None; n]; + let mut baseline = 0_i128; + for (&(u, v), &cost) in arcs.iter().zip(problem.arc_costs()) { + let cost = i128::from(cost); + if u == root && v == sink { + baseline += cost; + } else if u == root && v != root { + left[v] = Some(left[v].unwrap_or(0) + cost); + } else if v == sink && u != sink { + right[u] = Some(right[u].unwrap_or(0) + cost); + } + } + let mut extra = i128::MAX; + for v in (0..n).filter(|&v| v != root && v != sink) { + if let (Some(a), Some(b)) = (left[v], right[v]) { + baseline += a.min(b); + extra = extra.min(a.max(b)); + } else { + extra = 0; + break; + } + } + if extra > 0 && i128::from(*problem.cost_bound()) - baseline < extra { + return Some((root, sink)); + } + } + } + None +} + +// With two certified occupied parts, one binary label per vertex suffices. +// Substitute fixed anchor labels before summing weight and crossing rows. +fn two_part_reduction( + problem: &AcyclicPartition, + root: usize, + sink: usize, +) -> Result { + type Source = AcyclicPartition; + type Target = ILP; + let n = problem.num_vertices(); + let exact = |value, operation| { + i64::try_from(value).map_err(|_| { + crate::rules::ReductionError::integer_overflow::(operation) + }) + }; + let mut variables = vec![IntegerVariable::binary(); n]; + variables[root] = IntegerVariable::new(Some(0), Some(0)) + .map_err(>::target_construction)?; + variables[sink] = IntegerVariable::new(Some(1), Some(1)) + .map_err(>::target_construction)?; + let mut constraints = Vec::new(); + let mut costs = vec![0_i128; n]; + for (&(u, v), &cost) in problem.graph().arcs().iter().zip(problem.arc_costs()) { + constraints.push(LinearConstraint::le(vec![(u, 1), (v, -1)], 0)); + costs[u] -= i128::from(cost); + costs[v] += i128::from(cost); + } + let interior = |v: &usize| *v != root && *v != sink; + let weights: Vec<_> = (0..n) + .filter(interior) + .map(|v| (v, problem.vertex_weights()[v])) + .collect(); + let total_weight: i128 = weights.iter().map(|&(_, weight)| i128::from(weight)).sum(); + constraints.push(LinearConstraint::le( + weights.clone(), + exact( + i128::from(*problem.weight_bound()) - i128::from(problem.vertex_weights()[sink]), + "bounding the sink part weight", + )?, + )); + constraints.push(LinearConstraint::ge( + weights, + exact( + total_weight - i128::from(*problem.weight_bound()) + + i128::from(problem.vertex_weights()[root]), + "bounding the root part weight", + )?, + )); + let cost_terms = (0..n) + .filter(interior) + .map(|v| Ok((v, exact(costs[v], "summing crossing cost coefficients")?))) + .collect::>()?; + constraints.push(LinearConstraint::le( + cost_terms, + exact( + i128::from(*problem.cost_bound()) - costs[sink], + "substituting the sink crossing cost", + )?, + )); + let selected = i128::from(*problem.weight_bound()) - i128::from(problem.vertex_weights()[root]); + let unselected = + i128::from(*problem.weight_bound()) - i128::from(problem.vertex_weights()[sink]); + if selected > 0 + && selected + unselected == total_weight + && (0..n) + .filter(interior) + .all(|v| problem.vertex_weights()[v] <= 1) + { + // Exactly k unit-weight vertices belong to the root part. A zero-weight + // item with two such parents can be selected only with both parents. + // Thus each selected parent has at most k-1 distinct selected neighbors. + let mut parents = vec![Vec::new(); n]; + for (u, v) in problem.graph().arcs() { + if interior(&u) && problem.vertex_weights()[u] == 1 { + parents[v].push(u); + } + } + let mut pairs = std::collections::BTreeMap::new(); + for v in (0..n).filter(interior) { + parents[v].sort_unstable(); + parents[v].dedup(); + if problem.vertex_weights()[v] == 0 && parents[v].len() == 2 { + pairs.entry((parents[v][0], parents[v][1])).or_insert(v); + } + } + let mut incident = vec![Vec::new(); n]; + for ((u, v), item) in pairs { + incident[u].push(item); + incident[v].push(item); + } + let limit = exact(selected - 1, "bounding distinct selected neighbors")?; + for (v, neighbors) in incident + .into_iter() + .enumerate() + .filter(|(_, neighbors)| !neighbors.is_empty()) + { + let degree = >::exact_i64( + neighbors.len(), + "counting distinct neighbors", + )?; + let mut terms: Vec<_> = neighbors.into_iter().map(|item| (item, 1)).collect(); + terms.push((v, -limit)); + constraints.push(LinearConstraint::ge( + terms, + exact( + i128::from(degree) - i128::from(limit), + "bounding selected incidence items", + )?, + )); + } + } + let target = Target::with_variables(variables, constraints, vec![], ObjectiveSense::Minimize) + .map_err(>::target_construction)?; + Ok(ReductionAcyclicPartitionToILP { + target, + n, + parts: None, + }) +} + +#[reduction(transform = upper_bound { + num_vars = "num_vertices^2 + 2 * num_vertices + num_arcs", + num_constraints = "num_vertices^2 + 4 * num_vertices + 2 * num_arcs + 1", + max_constraint_magnitude_bits = "max_numeric_magnitude_bits + num_vertices + num_arcs + 1", + num_nonzeros = "6 * num_vertices^2 + 2 * num_vertices + 7 * num_arcs", })] impl ReduceTo> for AcyclicPartition { type Result = ReductionAcyclicPartitionToILP; @@ -55,18 +237,22 @@ impl ReduceTo> for AcyclicPartition { let n = self.num_vertices(); let arcs = self.graph().arcs(); let m = arcs.len(); + if let Some((root, sink)) = two_part_anchors(self) { + return two_part_reduction(self, root, sink); + } + let parts = n; let overflow = || { crate::rules::ReductionError::integer_overflow::>( "counting acyclic partition variables", ) }; - let square = n.checked_mul(n).ok_or_else(overflow)?; - let labels = square.checked_add(n).ok_or_else(overflow)?; + let square = n.checked_mul(parts).ok_or_else(overflow)?; + let labels = square.checked_add(parts).ok_or_else(overflow)?; let crossing = labels.checked_add(n).ok_or_else(overflow)?; let num_vars = crossing.checked_add(m).ok_or_else(overflow)?; - let last_label = Self::exact_i64(n.saturating_sub(1), "bounding partition labels")?; - let x_idx = |v: usize, c: usize| v * n + c; + let last_label = Self::exact_i64(parts.saturating_sub(1), "bounding partition labels")?; + let x_idx = |v: usize, c: usize| v * parts + c; let empty_idx = |c: usize| square + c; let label_idx = |v: usize| labels + v; let y_idx = |t: usize| crossing + t; @@ -78,10 +264,10 @@ impl ReduceTo> for AcyclicPartition { // Assignment: Σ_c x_{v,c} = 1 for each vertex v. for v in 0..n { - let terms: Vec<(usize, i64)> = (0..n).map(|c| (x_idx(v, c), 1)).collect(); + let terms: Vec<(usize, i64)> = (0..parts).map(|c| (x_idx(v, c), 1)).collect(); constraints.push(LinearConstraint::eq(terms, 1)); let mut label = vec![(label_idx(v), 1)]; - for c in 1..n { + for c in 1..parts { label.push(( x_idx(v, c), -Self::exact_i64(c, "representing a partition label")?, @@ -91,7 +277,7 @@ impl ReduceTo> for AcyclicPartition { } // Only occupied classes must meet the weight bound, which can be negative. - for c in 0..n { + for c in 0..parts { let mut membership = vec![(empty_idx(c), 1)]; for v in 0..n { constraints.push(LinearConstraint::le( @@ -143,7 +329,11 @@ impl ReduceTo> for AcyclicPartition { let target = ILP::with_variables(variables, constraints, vec![], ObjectiveSense::Minimize) .map_err(Self::target_construction)?; - Ok(ReductionAcyclicPartitionToILP { target, n }) + Ok(ReductionAcyclicPartitionToILP { + target, + n, + parts: Some(parts), + }) } } diff --git a/src/rules/bmf_ilp.rs b/src/rules/bmf_ilp.rs index 242cf1e89..28ca4bfc1 100644 --- a/src/rules/bmf_ilp.rs +++ b/src/rules/bmf_ilp.rs @@ -54,8 +54,8 @@ impl ReductionResult for ReductionBMFToILP { exact { max_constraint_magnitude_bits = "1", }, upper_bound { num_vars = "rows * rank + rank * cols + rows * rank * cols", - num_constraints = "(2 * rank + 1) * rows * cols", - num_nonzeros = "5 * rows * rank * cols", + num_constraints = "(2 * rank + 1) * rows * cols + rank * (rows + cols)", + num_nonzeros = "5 * rows * rank * cols + rank * (rows + cols)", }, })] impl ReduceTo> for BMF { @@ -76,25 +76,74 @@ impl ReduceTo> for BMF { .and_then(|v| v.checked_add(c_offset)) .ok_or_else(overflow)?; >>::exact_i64(factor_count, "bounding Boolean factor size")?; - let ones = self - .matrix() + // Pairwise incompatible edges must use distinct factors. Name those + // factors first: permuting B columns and C rows preserves every cover + // and its objective. Anchor endpoints then exclude non-neighbors. + let matrix = self.matrix(); + let row_degrees: Vec<_> = matrix .iter() - .flatten() - .filter(|&&value| value) - .count(); - let num_vars = ones - .checked_mul(k) - .and_then(|v| v.checked_add(factor_count)) - .ok_or_else(overflow)?; + .map(|row| row.iter().filter(|&&v| v).count()) + .collect(); + let col_degrees: Vec<_> = (0..n) + .map(|j| matrix.iter().filter(|row| row[j]).count()) + .collect(); + let mut edges: Vec<_> = matrix + .iter() + .enumerate() + .flat_map(|(i, row)| { + row.iter() + .enumerate() + .filter_map(move |(j, &v)| v.then_some((i, j))) + }) + .collect(); + edges.sort_by_key(|&(i, j)| (row_degrees[i], col_degrees[j])); + let mut anchors: Vec<(usize, usize)> = Vec::new(); + for (i, j) in edges { + if anchors.len() == k { + break; + } + if anchors.iter().all(|&(u, v)| !matrix[i][v] || !matrix[u][j]) { + anchors.push((i, j)); + } + } + let mut fixed = vec![None; factor_count]; + for (r, &(u, v)) in anchors.iter().enumerate() { + for i in 0..m { + if !matrix[i][v] { + fixed[i * k + r] = Some(0); + } + } + for j in 0..n { + if !matrix[u][j] { + fixed[c_offset + r * n + j] = Some(0); + } + } + fixed[u * k + r] = Some(1); + fixed[c_offset + r * n + v] = Some(1); + } let mut next = factor_count; - let mut constraints = Vec::new(); + let mut constraints: Vec<_> = fixed + .iter() + .enumerate() + .filter_map(|(index, &value)| { + value.map(|value| LinearConstraint::eq(vec![(index, 1)], value)) + }) + .collect(); for (i, row) in self.matrix().iter().enumerate() { for (j, &value) in row.iter().enumerate() { if value { + if (0..k).any(|r| { + fixed[i * k + r] == Some(1) && fixed[c_offset + r * n + j] == Some(1) + }) { + continue; + } let mut coverage = Vec::new(); for r in 0..k { + if fixed[i * k + r] == Some(0) || fixed[c_offset + r * n + j] == Some(0) { + continue; + } let bit = next; - next += 1; + next = next.checked_add(1).ok_or_else(overflow)?; constraints.push(LinearConstraint::le(vec![(bit, 1), (i * k + r, -1)], 0)); constraints.push(LinearConstraint::le( vec![(bit, 1), (c_offset + r * n + j, -1)], @@ -105,6 +154,9 @@ impl ReduceTo> for BMF { constraints.push(LinearConstraint::ge(coverage, 1)); } else { for r in 0..k { + if fixed[i * k + r] == Some(0) || fixed[c_offset + r * n + j] == Some(0) { + continue; + } constraints.push(LinearConstraint::le( vec![(i * k + r, 1), (c_offset + r * n + j, 1)], 1, @@ -114,7 +166,7 @@ impl ReduceTo> for BMF { } } let objective = (0..factor_count).map(|i| (i, 1)).collect(); - let target = ILP::new(num_vars, constraints, objective, ObjectiveSense::Minimize) + let target = ILP::new(next, constraints, objective, ObjectiveSense::Minimize) .map_err(>>::target_construction)?; Ok(ReductionBMFToILP { target, m, n, k }) } diff --git a/src/rules/closestvectorproblem_qubo.rs b/src/rules/closestvectorproblem_qubo.rs index f29dde9fa..cfe739304 100644 --- a/src/rules/closestvectorproblem_qubo.rs +++ b/src/rules/closestvectorproblem_qubo.rs @@ -10,7 +10,7 @@ use crate::models::algebraic::{ClosestVectorProblem, QUBO}; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; use num_bigint::BigInt; -use num_traits::Zero; +use num_traits::{Signed, Zero}; type Source = ClosestVectorProblem; type Target = QUBO; @@ -109,7 +109,7 @@ fn determinant(matrix: &[Vec]) -> Result .map_err(|_| overflow("computing a closest-vector determinant")) } -fn coefficient_bounds(problem: &Source) -> Result, crate::rules::ReductionError> { +fn coefficient_bounds(problem: &Source) -> Result, crate::rules::ReductionError> { let rows = problem .independent_rows() .map_err(crate::rules::ReductionError::construction::)?; @@ -117,66 +117,109 @@ fn coefficient_bounds(problem: &Source) -> Result, crate::rules::Reduct if size == 0 { return Ok(Vec::new()); } - - let matrix = rows + let matrix: Vec> = rows .iter() - .map(|&row| { - problem - .basis() - .iter() - .map(|column| column[row]) - .collect::>() - }) - .collect::>(); - if determinant(&matrix)? == 0 { + .map(|&row| problem.basis().iter().map(|column| column[row]).collect()) + .collect(); + let determinant = determinant(&matrix)?; + if determinant == 0 { return Err( crate::rules::ReductionError::invalid_target::( "selected closest-vector rows are not independent", ), ); } - - let target_norm = problem.target().iter().try_fold(0_i64, |total, &value| { - total - .checked_add( - value - .checked_abs() - .ok_or_else(|| overflow("taking a closest-vector target absolute value"))?, - ) - .ok_or_else(|| overflow("computing the closest-vector target one-norm")) - })?; - let row_bounds = rows + let denominator = BigInt::from(determinant).abs(); + let mut adjugate = Vec::new(); + for coefficient in 0..size { + let mut row = Vec::new(); + for selected_row in 0..size { + let minor: Vec> = (0..size) + .filter(|&row| row != selected_row) + .map(|row| { + (0..size) + .filter(|&column| column != coefficient) + .map(|column| matrix[row][column]) + .collect() + }) + .collect(); + let mut entry = BigInt::from(self::determinant(&minor)?); + if (coefficient + selected_row) % 2 == 1 { + entry = -entry; + } + if determinant < 0 { + entry = -entry; + } + row.push(entry); + } + adjugate.push(row); + } + // Cramer's rule gives a rational center. A rounded center supplies a + // feasible lattice point, so its squared residual bounds the optimum. + let centers: Vec = adjugate .iter() - .map(|&row| { - problem.target()[row] - .checked_abs() - .and_then(|value| value.checked_add(target_norm)) - .ok_or_else(|| overflow("computing a closest-vector selected-row bound")) + .map(|row| { + row.iter() + .zip(&rows) + .map(|(entry, &coordinate)| entry * problem.target()[coordinate]) + .sum() }) - .collect::, _>>()?; - - (0..size) - .map(|coefficient| { - (0..size).try_fold(0_i64, |bound, selected_row| { - let minor = (0..size) - .filter(|&row| row != selected_row) - .map(|row| { - (0..size) - .filter(|&column| column != coefficient) - .map(|column| matrix[row][column]) - .collect::>() - }) - .collect::>(); - let adjugate_magnitude = determinant(&minor)? - .checked_abs() - .ok_or_else(|| overflow("taking a closest-vector cofactor absolute value"))?; - let term = adjugate_magnitude - .checked_mul(row_bounds[selected_row]) - .ok_or_else(|| overflow("computing a closest-vector coefficient bound"))?; - bound - .checked_add(term) - .ok_or_else(|| overflow("computing a closest-vector coefficient bound")) - }) + .collect(); + let candidate: Vec = centers + .iter() + .map(|center| { + let mut quotient = center / &denominator; + let remainder = center % &denominator; + if remainder.abs() * 2 >= denominator { + quotient += remainder.signum(); + } + quotient + }) + .collect(); + let mut radius_squared = BigInt::zero(); + let mut zero_distance = BigInt::zero(); + for coordinate in 0..problem.ambient_dimension() { + let target = BigInt::from(problem.target()[coordinate]); + let lattice: BigInt = problem + .basis() + .iter() + .zip(&candidate) + .map(|(column, coefficient)| coefficient * column[coordinate]) + .sum(); + let residual = lattice - ⌖ + radius_squared += &residual * &residual; + zero_distance += &target * ⌖ + } + radius_squared = radius_squared.min(zero_distance); + let floor = |numerator: &BigInt| { + let quotient = numerator / &denominator; + if numerator.is_negative() && !(numerator % &denominator).is_zero() { + quotient - 1 + } else { + quotient + } + }; + centers + .iter() + .zip(&adjugate) + .map(|(center, row)| { + // Cauchy-Schwarz: |det(A) z_i - center_i|² <= ||adj_i||² R². + // The left side is integer, so the integer square root is exact here. + let norm: BigInt = row.iter().map(|entry| entry * entry).sum(); + let radius = BigInt::from( + (norm * &radius_squared) + .to_biguint() + .expect("squared radius is nonnegative") + .sqrt(), + ); + let lower = -floor(&(&radius - center)); + let upper = floor(&(center + radius)); + Ok(( + i64::try_from(lower) + .map_err(|_| overflow("bounding closest-vector coefficients"))?, + i64::try_from(upper) + .map_err(|_| overflow("bounding closest-vector coefficients"))?, + )) }) .collect() } @@ -198,19 +241,21 @@ fn exact_range_weights(maximum: i64) -> Result, crate::rules::Reduction Ok(weights) } -fn encoding_spans(bounds: &[i64]) -> Result, crate::rules::ReductionError> { +fn encoding_spans( + bounds: &[(i64, i64)], +) -> Result, crate::rules::ReductionError> { let mut start = 0usize; bounds .iter() - .map(|&bound| { - let maximum = bound - .checked_mul(2) + .map(|&(lower, upper)| { + let maximum = upper + .checked_sub(lower) .ok_or_else(|| overflow("computing a closest-vector encoding range"))?; let weights = exact_range_weights(maximum)?; let span = EncodingSpan { start, weights, - lower: -bound, + lower, }; start = start .checked_add(span.weights.len()) @@ -331,7 +376,7 @@ impl ReduceTo> for ClosestVectorProblem { #[cfg(feature = "example-db")] fn canonical_cvp_instance() -> Source { - ClosestVectorProblem::new(vec![vec![2, 0], vec![1, 2]], vec![3_i64, 2]) + ClosestVectorProblem::new(vec![vec![2, 0], vec![1, 2]], vec![3_i64, 1]) .expect("canonical closest-vector instance must be valid") } @@ -344,9 +389,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec; + +#[derive(Debug, Clone)] +pub struct ReductionHamiltonianCircuitToILP { + target: Target, + edges: Vec<(usize, usize)>, + n: usize, +} + +impl ReductionResult for ReductionHamiltonianCircuitToILP { + type Source = HamiltonianCircuit; + type Target = Target; + + fn target_problem(&self) -> &Target { + &self.target + } + + fn extract_solution(&self, solution: &Vec) -> crate::rules::ExtractionResult> { + crate::rules::traits::validate_target_witness( + self.target_problem(), + solution, + |value| value.value.is_some(), + "target ILP assignment is infeasible", + )?; + let selected: Vec<_> = solution[..self.edges.len()] + .iter() + .map(|&value| value == 1) + .collect(); + crate::rules::graph_helpers::edges_to_cycle_order( + &SimpleGraph::new(self.n, self.edges.clone()), + &selected, + ) + } +} + +#[crate::aggregate_reduction(ilp_feasibility)] +impl crate::rules::AggregateReductionResult for ReductionHamiltonianCircuitToILP {} + +#[crate::reduction(transform = upper_bound { + num_vars = "3 * num_edges", + num_constraints = "2 * num_vertices + 2 * num_edges + 1", + num_nonzeros = "10 * num_edges", + max_constraint_magnitude_bits = "num_vertices + 2", +})] +impl ReduceTo> for HamiltonianCircuit { + type Result = ReductionHamiltonianCircuitToILP; + fn reduce_to(&self) -> Result { + let n = self.num_vertices(); + let mut edges: Vec<_> = self + .graph() + .edges() + .into_iter() + .filter(|(u, v)| u != v) + .map(|(u, v)| (u.min(v), u.max(v))) + .collect(); + edges.sort_unstable(); + edges.dedup(); + let m = edges.len(); + let count = + m.checked_mul(3).ok_or( + crate::rules::ReductionError::integer_overflow::( + "counting circuit flow variables", + ), + )?; + let capacity = >::exact_i64( + n.saturating_sub(1), + "bounding spanning-cycle flow", + )?; + let mut variables = vec![IntegerVariable::binary(); m]; + variables.resize( + count, + IntegerVariable::new(Some(0), Some(capacity)) + .map_err(>::target_construction)?, + ); + let mut degree = vec![Vec::new(); n]; + let mut balance = vec![Vec::new(); n]; + let mut constraints = Vec::new(); + for (edge, &(u, v)) in edges.iter().enumerate() { + degree[u].push((edge, 1)); + degree[v].push((edge, 1)); + for (direction, (from, to)) in [(u, v), (v, u)].into_iter().enumerate() { + let flow = m + 2 * edge + direction; + balance[from].push((flow, 1)); + balance[to].push((flow, -1)); + constraints.push(LinearConstraint::le(vec![(flow, 1), (edge, -capacity)], 0)); + } + } + for (v, (degree, balance)) in degree.into_iter().zip(balance).enumerate() { + constraints.push(LinearConstraint::eq(degree, 2)); + constraints.push(LinearConstraint::eq( + balance, + if v == 0 { capacity } else { -1 }, + )); + } + if n < 3 { + constraints.push(LinearConstraint::eq(vec![], 1)); + } + let target = + Target::with_variables(variables, constraints, vec![], ObjectiveSense::Minimize) + .map_err(>::target_construction)?; + Ok(ReductionHamiltonianCircuitToILP { target, edges, n }) + } +} + +#[cfg(feature = "example-db")] +pub(crate) fn canonical_rule_example_specs() -> Vec { + vec![crate::example_db::specs::RuleExampleSpec { + id: "hamiltoniancircuit_to_ilp", + build: || { + crate::example_db::specs::rule_example_via_bounded_ilp(HamiltonianCircuit::new( + SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3), (0, 3)]), + )) + }, + }] +} + +#[cfg(test)] +#[path = "../unit_tests/rules/hamiltoniancircuit_ilp.rs"] +mod tests; diff --git a/src/rules/ksatisfiability_acyclicpartition.rs b/src/rules/ksatisfiability_acyclicpartition.rs index f1b404d0b..18b7dbde9 100644 --- a/src/rules/ksatisfiability_acyclicpartition.rs +++ b/src/rules/ksatisfiability_acyclicpartition.rs @@ -1,7 +1,7 @@ //! 3-SAT to the bounded-weight quotient-DAG partition problem. //! //! Compose the formal SAT-to-clique reduction with an incidence construction. -//! Unit vertex/edge items encode a clique under a cardinality bound. Two heavy +//! Unit-weight vertices and zero-weight edge items encode a clique. Two heavy //! anchors and polynomial arc costs force exactly two blocks and encode the //! incidence closure and clique-size inequalities, without digit-encoded weights. @@ -80,10 +80,7 @@ impl ReduceTo> for KSatisfiability { } let mut arcs = Vec::with_capacity(arc_count); let mut arc_costs = Vec::with_capacity(arc_count); - let profits = degree - .iter() - .map(|&d| d + 1) - .chain(std::iter::repeat_n(0, e)); + let profits = degree.iter().copied().chain(std::iter::repeat_n(0, e)); for (item, profit) in profits.enumerate() { arcs.push((source_vertex, item)); arc_costs.push(magnitude); @@ -96,13 +93,14 @@ impl ReduceTo> for KSatisfiability { arcs.push((v, n + j)); arc_costs.push(1); } - let mut weights = vec![1; items]; + let mut weights = vec![1; n]; + weights.resize(items, 0); weights.push(weight_bound - capacity); weights.push( weight_bound - >>::exact_i64( - items, - "representing incidence item count", + n - clique.k(), + "representing unselected clique vertex count", )?, ); let target = AcyclicPartition::new( @@ -141,13 +139,7 @@ fn incidence_parameters( .ok_or_else(overflow)?; let l = i64::try_from(items).map_err(|_| overflow())?; let k = i64::try_from(k).map_err(|_| overflow())?; - let next = k.checked_add(1).ok_or_else(overflow)?; - let capacity = if k % 2 == 0 { - (k / 2).checked_mul(next) - } else { - k.checked_mul(next / 2) - } - .ok_or_else(overflow)?; + let capacity = k; let magnitude = l .checked_mul(2) .and_then(|x| x.checked_add(1)) @@ -157,10 +149,10 @@ fn incidence_parameters( .and_then(|x| x.checked_mul(2)) .and_then(|x| x.checked_add(1)) .ok_or_else(overflow)?; - let square = k.checked_mul(k).ok_or_else(overflow)?; + let twice_edges = k.checked_mul(k.saturating_sub(1)).ok_or_else(overflow)?; let cost_bound = magnitude .checked_mul(l) - .and_then(|x| x.checked_sub(square)) + .and_then(|x| x.checked_sub(twice_edges)) .ok_or_else(overflow)?; Ok((target_n, arcs, capacity, magnitude, bound, cost_bound)) } diff --git a/src/rules/mod.rs b/src/rules/mod.rs index e2762b988..3baa44186 100644 --- a/src/rules/mod.rs +++ b/src/rules/mod.rs @@ -42,6 +42,7 @@ pub(crate) mod graphpartitioning_qubo; pub(crate) mod hamiltoniancircuit_biconnectivityaugmentation; pub(crate) mod hamiltoniancircuit_bottlenecktravelingsalesman; pub(crate) mod hamiltoniancircuit_hamiltonianpath; +pub(crate) mod hamiltoniancircuit_ilp; pub(crate) mod hamiltoniancircuit_longestcircuit; pub(crate) mod hamiltoniancircuit_quadraticassignment; pub(crate) mod hamiltoniancircuit_ruralpostman; @@ -339,6 +340,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec Option> { - if problem.subsets().is_empty() { - return Some(vec![0; 2 * problem.budget()]); - } - - let singletons = (0..problem.universe_size()) - .map(|element| vec![element]) - .collect::>(); - let mut queue = VecDeque::from([(Vec::>::new(), Vec::::new())]); - let mut seen = HashSet::from([Vec::>::new()]); - - while let Some((computed, program)) = queue.pop_front() { - if computed.len() == problem.budget() { - continue; +fn subsets(set: &[usize]) -> Result>, SolveError> { + let mut result = vec![vec![]]; + for &element in set { + let previous = result.len(); + result.try_reserve_exact(previous)?; + for index in 0..previous { + let mut next = result[index].clone(); + next.push(element); + result.push(next); } - let mut available = singletons.iter().collect::>(); - available.extend(computed.iter()); - - for left in 0..available.len() { - for right in (left + 1)..available.len() { - if !disjoint(available[left], available[right]) { - continue; - } - let result = union(available[left], available[right]); - if computed.contains(&result) - || !problem.subsets().iter().any(|required| { - result - .iter() - .all(|element| required.binary_search(element).is_ok()) - }) - { - continue; - } - - let mut next_computed = computed.clone(); - next_computed.push(result); - let mut next_program = program.clone(); - next_program.extend([left, right]); - if problem - .subsets() - .iter() - .all(|required| next_computed.contains(required)) - { - next_program.resize(2 * problem.budget(), 0); - return Some(next_program); - } + } + Ok(result) +} - let mut key = next_computed.clone(); - key.sort(); - if seen.insert(key) { - queue.push_back((next_computed, next_program)); +pub(crate) fn solve(problem: &EnsembleComputation) -> Result>, SolveError> { + // Computed sets are nonempty disjoint unions of two nonempty operands. + if problem.subsets().iter().any(|set| set.len() < 2) { + return Ok(None); + } + // ponytail: enumerate subsets of required sets; use implicit subset search + // if correctness checks must handle large individual required sets. + let mut useful = BTreeSet::new(); + for required in problem.subsets() { + useful.extend(subsets(required)?.into_iter().filter(|set| set.len() >= 2)); + } + let mut useful: Vec<_> = useful.into_iter().collect(); + useful.sort_by_key(Vec::len); + let indices: BTreeMap<_, _> = useful + .iter() + .cloned() + .enumerate() + .map(|(index, set)| (set, index)) + .collect(); + let mut partitions = Vec::new(); + let mut constraints = Vec::new(); + for (index, set) in useful.iter().enumerate() { + let mut choices = vec![(index, -1)]; + for left in subsets(set)? + .into_iter() + .filter(|left| !left.is_empty() && left.len() < set.len() && left[0] == set[0]) + { + let right: Vec<_> = set + .iter() + .copied() + .filter(|element| left.binary_search(element).is_err()) + .collect(); + let choice = + useful + .len() + .checked_add(partitions.len()) + .ok_or(SolveError::IntegerOverflow( + "indexing ensemble union choices".into(), + ))?; + choices.push((choice, 1)); + for child in [&left, &right] { + if child.len() > 1 { + constraints.push(LinearConstraint::le( + vec![(choice, 1), (indices[child], -1)], + 0, + )); } } + partitions.push((index, left, right)); } + // A computed set has exactly one disjoint-union definition. + constraints.push(LinearConstraint::eq(choices, 0)); } - None -} - -fn disjoint(left: &[usize], right: &[usize]) -> bool { - !left + for required in problem.subsets() { + constraints.push(LinearConstraint::eq(vec![(indices[required], 1)], 1)); + } + let failure = |source| SolveError::IlpSolve { + problem: EnsembleComputation::NAME.into(), + source, + }; + let budget = >>::exact_i64( + problem.budget(), + "representing the ensemble operation budget", + ) + .map_err(ILPSolveError::from) + .map_err(failure)?; + let objective: Vec<_> = (0..useful.len()).map(|index| (index, 1)).collect(); + constraints.push(LinearConstraint::le(objective.clone(), budget)); + let target = ILP::::new( + useful.len() + partitions.len(), + constraints, + objective, + ObjectiveSense::Minimize, + ) + .map_err(>>::target_construction) + .map_err(ILPSolveError::from) + .map_err(failure)?; + let solution = match ILPSolver::new().solve(&target) { + Ok(solution) => solution, + Err(ILPSolveError::Infeasible) => return Ok(None), + Err(error) => return Err(failure(error)), + }; + let mut operands: BTreeMap, usize> = problem + .subsets() .iter() - .any(|element| right.binary_search(element).is_ok()) -} - -fn union(left: &[usize], right: &[usize]) -> Vec { - let mut result = Vec::with_capacity(left.len() + right.len()); - result.extend_from_slice(left); - result.extend_from_slice(right); - result.sort_unstable(); - result + .flatten() + .map(|&element| (vec![element], element)) + .collect(); + let mut program = Vec::new(); + // Partitions were generated in increasing parent cardinality. Every + // selected non-singleton operand therefore already has a program index. + for (choice, (index, left, right)) in partitions.iter().enumerate() { + if solution[useful.len() + choice] == 0 { + continue; + } + for child in [left, right] { + program.push( + *operands + .get(child) + .ok_or(failure(ILPSolveError::InvalidSolution( + "union operand was not computed".into(), + )))?, + ); + } + operands.insert( + useful[*index].clone(), + problem.universe_size() + program.len() / 2 - 1, + ); + } + program.resize(2 * problem.budget(), 0); + Ok(Some(program)) } #[cfg(test)] diff --git a/src/solvers/customized/mod.rs b/src/solvers/customized/mod.rs index fb939b952..40ff0c469 100644 --- a/src/solvers/customized/mod.rs +++ b/src/solvers/customized/mod.rs @@ -4,7 +4,6 @@ //! performed by the solver capability registry rather than a downcast chain. pub(crate) mod closest_vector_problem; -#[cfg(test)] pub(crate) mod ensemble_computation; pub(crate) mod fd_subset_search; pub(crate) mod grouping_by_swapping; @@ -12,6 +11,7 @@ pub(crate) mod minimum_cost_circulation; pub(crate) mod minimum_decision_tree; pub(crate) mod minimum_intersection_graph_basis; pub(crate) mod partial_feedback_edge_set; +pub(crate) mod quadratic_congruences; pub(crate) mod rooted_tree_arrangement; pub(crate) mod shortest_common_superstring; mod solver; diff --git a/src/solvers/customized/quadratic_congruences.rs b/src/solvers/customized/quadratic_congruences.rs new file mode 100644 index 000000000..50f88f835 --- /dev/null +++ b/src/solvers/customized/quadratic_congruences.rs @@ -0,0 +1,202 @@ +//! Exact bounded square roots: prime-power lifting, CRT, and meet in the middle. +//! CRT proof: https://kconrad.math.uconn.edu/blurbs/ugradnumthy/crt.pdf +//! Hensel lifting: https://www.math-cs.gordon.edu/~kcrisman/mat338/section-74.html + +use crate::models::algebraic::{QuadraticCongruences, QuadraticDiophantineEquations}; +use crate::solvers::SolveError; +use num_bigint::BigUint; +use num_traits::{One, Pow, Zero}; + +fn prime_powers(modulus: &BigUint) -> Vec<(BigUint, usize)> { + let mut remaining = modulus.clone(); + let mut prime = BigUint::from(2u8); + let mut factors = Vec::new(); + // ponytail: exact trial division; use certified faster factoring if large + // prime factors, rather than the number of CRT choices, become the bottleneck. + while &prime * &prime <= remaining { + let mut exponent = 0; + while (&remaining % &prime).is_zero() { + remaining /= ′ + exponent += 1; + } + if exponent > 0 { + factors.push((prime.clone(), exponent)); + } + prime += if prime == BigUint::from(2u8) { + 1u8 + } else { + 2u8 + }; + } + if remaining > BigUint::one() { + factors.push((remaining, 1)); + } + factors +} + +/// Return the complete root classes, allowing a smaller period for nonunits. +fn local_roots(a: &BigUint, prime: &BigUint, exponent: usize) -> Option<(BigUint, Vec)> { + let full = Pow::pow(prime.clone(), exponent); + let mut unit = a % &full; + if unit.is_zero() { + return Some(( + Pow::pow(prime.clone(), exponent.div_ceil(2)), + vec![BigUint::zero()], + )); + } + let mut valuation = 0; + while (&unit % prime).is_zero() { + unit /= prime; + valuation += 1; + } + if valuation % 2 != 0 { + return None; + } + let mut roots = Vec::new(); + let mut candidate = BigUint::zero(); + while &candidate < prime { + if (&candidate * &candidate) % prime == &unit % prime { + roots.push(candidate.clone()); + } + candidate += 1u8; + } + if roots.is_empty() { + return None; + } + let mut period = prime.clone(); + for _ in 1..exponent - valuation { + let next = &period * prime; + let mut lifted = Vec::new(); + for root in roots { + if prime == &BigUint::from(2u8) { + for digit in 0u8..2 { + let value = &root + &period * digit; + if (&value * &value) % &next == &unit % &next { + lifted.push(value); + } + } + } else { + let difference = (&unit % &next + &next - (&root * &root) % &next) % &next; + let inverse = (&root * 2u8) + .modinv(prime) + .expect("odd-prime unit derivative is invertible"); + let digit = (difference / &period * inverse) % prime; + lifted.push(root + &period * digit); + } + } + if lifted.is_empty() { + return None; + } + roots = lifted; + period = next; + } + let scale: BigUint = Pow::pow(prime.clone(), valuation / 2); + Some(( + &period * &scale, + roots.into_iter().map(|root| root * &scale).collect(), + )) +} + +fn half_sums(choices: &[Vec], modulus: &BigUint) -> Result, SolveError> { + let mut sums = vec![BigUint::zero()]; + for choices in choices { + let mut next = Vec::new(); + for choice in choices { + next.try_reserve(sums.len())?; + next.extend(sums.iter().map(|sum| (sum + choice) % modulus)); + } + next.sort_unstable(); + next.dedup(); + sums = next; + } + Ok(sums) +} + +fn bounded_root(a: &BigUint, b: &BigUint, c: &BigUint) -> Result, SolveError> { + if c <= &BigUint::one() { + return Ok(None); + } + let Some(classes) = prime_powers(b) + .into_iter() + .map(|(prime, exponent)| local_roots(a, &prime, exponent)) + .collect::>>() + else { + return Ok(None); + }; + let modulus: BigUint = classes.iter().map(|(period, _)| period).product(); + let choices: Vec> = classes + .into_iter() + .map(|(period, roots)| { + let other = &modulus / . + let weight = &other + * other + .modinv(&period) + .expect("prime-power periods are coprime"); + roots + .into_iter() + .map(|root| root * &weight % &modulus) + .collect() + }) + .collect(); + if &modulus < c { + // Every root class has a positive representative in 1..=M. + let residue = choices.iter().map(|roots| &roots[0]).sum::() % &modulus; + return Ok(Some(if residue.is_zero() { modulus } else { residue })); + } + let split = choices.len() / 2; + let left = half_sums(&choices[..split], &modulus)?; + let right = half_sums(&choices[split..], &modulus)?; + // Since c <= M, zero residues are excluded, including after wraparound. + let wrapped_start = &modulus + BigUint::one(); + for a in left { + for start in [&BigUint::one(), &wrapped_start] { + let lower = if start > &a { + start - &a + } else { + BigUint::zero() + }; + let index = right.partition_point(|b| b < &lower); + if let Some(b) = right.get(index) { + let residue = (&a + b) % &modulus; + let x = if residue.is_zero() { + modulus.clone() + } else { + residue + }; + if &x < c { + return Ok(Some(x)); + } + } + } + } + Ok(None) +} + +pub(crate) fn solve(problem: &QuadraticCongruences) -> Result, SolveError> { + bounded_root(problem.a(), problem.b(), problem.c()) +} + +pub(crate) fn solve_diophantine( + problem: &QuadraticDiophantineEquations, +) -> Result, SolveError> { + if problem.c() < &(problem.a() + problem.b()) { + return Ok(None); + } + let (mut gcd, mut remainder) = (problem.a().clone(), problem.b().clone()); + while !remainder.is_zero() { + (gcd, remainder) = (remainder.clone(), gcd % remainder); + } + if !(problem.c() % &gcd).is_zero() { + return Ok(None); + } + let a = problem.a() / &gcd; + let b = problem.b() / &gcd; + let residue = if b.is_one() { + BigUint::zero() + } else { + problem.c() / &gcd * a.modinv(&b).expect("dividing by gcd makes a invertible") % &b + }; + // y >= 1 is equivalent to ax² <= c-b, including equality. + let bound = ((problem.c() - problem.b()) / problem.a()).sqrt() + BigUint::one(); + bounded_root(&residue, &b, &bound) +} diff --git a/src/solvers/customized/solver.rs b/src/solvers/customized/solver.rs index 2325120e7..58bfb5194 100644 --- a/src/solvers/customized/solver.rs +++ b/src/solvers/customized/solver.rs @@ -114,6 +114,28 @@ register_customized_solver!( |problem| super::closest_vector_problem::solve(problem).map(Some) ); +register_customized_solver!( + crate::models::algebraic::QuadraticCongruences, + "prime-power-crt", + super::quadratic_congruences::solve +); +register_customized_solver!( + crate::models::algebraic::QuadraticDiophantineEquations, + "prime-power-crt", + super::quadratic_congruences::solve_diophantine +); + +register_customized_solver!( + crate::models::misc::RegisterSufficiency, + "register-topological-search", + |problem: &crate::models::misc::RegisterSufficiency| Ok(problem.solve_exact()) +); +register_customized_solver!( + crate::models::misc::EnsembleComputation, + "useful-union-ilp", + super::ensemble_computation::solve +); + register_customized_solver!( crate::models::decision::Decision, "cvp-sphere-enumeration", diff --git a/src/solvers/pipelines.rs b/src/solvers/pipelines.rs index 17e52907e..bee2f1666 100644 --- a/src/solvers/pipelines.rs +++ b/src/solvers/pipelines.rs @@ -220,8 +220,7 @@ register_ilp_pipeline! { register_ilp_pipeline! { ("HamiltonianCircuit", [("graph", "SimpleGraph")]), - ("DecisionLongestCircuit", [("graph", "SimpleGraph"), ("weight", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "i64"), ("bounds", "general")]), + ("ILP", [("variable", "i64"), ("coefficient", "i64"), ("bounds", "bounded")]), } register_ilp_pipeline! { diff --git a/src/unit_tests/rules/acyclicpartition_ilp.rs b/src/unit_tests/rules/acyclicpartition_ilp.rs index 9bc534825..235cc0c33 100644 --- a/src/unit_tests/rules/acyclicpartition_ilp.rs +++ b/src/unit_tests/rules/acyclicpartition_ilp.rs @@ -17,6 +17,90 @@ fn small_instance() -> AcyclicPartition { ) } +#[test] +fn crossing_budget_bounds_the_number_of_occupied_parts() { + let graph = DirectedGraph::new(4, vec![(0, 1), (1, 3), (0, 2), (2, 3), (1, 2)]); + let source = AcyclicPartition::new( + graph.clone(), + vec![8, 1, 1, 8], + vec![10, 8, 10, 8, 1], + 10, + 17, + ); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + // Anchors cannot share a part. The two paths already cost at least 16; + // putting any interior vertex in a third part adds at least 10. + assert_eq!(reduction.target_problem().num_vars(), 4); + for bound in [9, 10] { + for budget in [15, 16, 17, 19, 25, 26] { + let source = AcyclicPartition::new( + graph.clone(), + vec![8, 1, 1, 8], + vec![10, 8, 10, 8, 1], + bound, + budget, + ); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + let expected = BruteForce::new().solve(&source).unwrap(); + match ILPSolver::new().solve(reduction.target_problem()) { + Ok(solution) => { + assert!(expected.is_some()); + assert!( + source + .evaluate(&reduction.extract_solution(&solution).unwrap()) + .unwrap() + .0 + ); + } + Err(crate::solvers::ILPSolveError::Infeasible) => assert!(expected.is_none()), + Err(error) => panic!("{error}"), + } + } + } +} + +#[test] +fn fixed_vertex_cardinality_preserves_every_two_part_witness() { + let mut arcs = Vec::new(); + let mut costs = Vec::new(); + for (v, profit) in [(1, 2), (2, 3), (3, 1), (4, 0), (5, 0), (6, 0)] { + arcs.extend([(0, v), (v, 7)]); + costs.extend([20, 20 - profit]); + } + // Two distinct neighbor pairs, with two separate items for pair (1,2). + // Both duplicate items can belong to the selected part simultaneously. + arcs.extend([(1, 4), (2, 4), (2, 5), (3, 5), (1, 6), (2, 6)]); + costs.extend([1; 6]); + let source = AcyclicPartition::new( + DirectedGraph::new(8, arcs), + vec![8, 1, 1, 1, 0, 0, 0, 9], + costs, + 10, + 118, + ); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + for mask in 0..64 { + let labels = std::iter::once(0) + .chain((0..6).map(|v| (mask >> v) & 1)) + .chain([1]) + .collect::>(); + let assignment = labels + .iter() + .map(|&label| i64::try_from(label).unwrap()) + .collect(); + assert_eq!( + reduction + .target_problem() + .evaluate(&assignment) + .unwrap() + .value + .is_some(), + source.evaluate(&labels).unwrap().0, + "partition {labels:?}" + ); + } +} + #[test] fn test_acyclicpartition_to_ilp_closed_loop() { let source = small_instance(); diff --git a/src/unit_tests/rules/bmf_ilp.rs b/src/unit_tests/rules/bmf_ilp.rs index 22720c913..5aa996351 100644 --- a/src/unit_tests/rules/bmf_ilp.rs +++ b/src/unit_tests/rules/bmf_ilp.rs @@ -3,6 +3,26 @@ use crate::models::algebraic::{ObjectiveSense, ILP}; use crate::rules::test_helpers::assert_bf_vs_ilp; use crate::rules::{ReduceTo, ReductionResult}; +#[test] +fn mutually_incompatible_edges_reduce_factor_search_symmetry() { + use crate::solvers::ILPSolver; + use crate::traits::Problem; + let source = BMF::new( + vec![ + vec![true, false, false], + vec![false, true, false], + vec![false, false, true], + ], + 3, + ); + let reduced = ReduceTo::>::reduce_to(&source).unwrap(); + // No two diagonal edges can share a factor. After naming their factors, + // each diagonal needs at most one additional coverage bit. + assert!(reduced.target_problem().num_vars() <= 21); + let solution = ILPSolver::new().solve(&source).unwrap(); + assert_eq!(source.evaluate(&solution).unwrap().0, Some(6)); +} + #[test] fn test_bmf_to_ilp_structure() { // 2x2 identity matrix, rank 1 @@ -10,10 +30,10 @@ fn test_bmf_to_ilp_structure() { let reduction: ReductionBMFToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); - // Four factor bits and one coverage bit per true diagonal entry. - assert_eq!(ilp.num_vars(), 6); - assert_eq!(ilp.num_constraints(), 8); - assert_eq!(ilp.num_nonzeros(), 14); + // The first anchored edge fixes its factor; the other diagonal is impossible. + assert_eq!(ilp.num_vars(), 4); + assert_eq!(ilp.num_constraints(), 5); + assert_eq!(ilp.num_nonzeros(), 4); assert_eq!(ilp.sense(), ObjectiveSense::Minimize); } @@ -43,8 +63,8 @@ fn test_bmf_to_ilp_trivial() { let reduction: ReductionBMFToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); - // Two factor bits and one coverage bit. - assert_eq!(ilp.num_vars(), 3); + // Both factor bits are fixed by the only edge, so coverage needs no extra bit. + assert_eq!(ilp.num_vars(), 2); } #[test] @@ -82,10 +102,10 @@ fn bmf_sparse_coverage_preserves_values_and_zero_dimensions() { } let source = BMF::new(vec![vec![true]], 2); let reduced = ReduceTo::>::reduce_to(&source).unwrap(); - // Both rank products are true; selecting just one coverage indicator is valid. - let witness = reduced.extract_solution(&vec![1, 1, 1, 1, 1, 0]).unwrap(); + // The anchored first factor covers the edge; extra memberships remain valid. + let witness = reduced.extract_solution(&vec![1, 1, 1, 1]).unwrap(); assert_eq!(source.evaluate(&witness).unwrap(), Min(Some(4))); - assert!(reduced.extract_solution(&vec![1, 1, 1, 1, 0, 0]).is_err()); + assert!(reduced.extract_solution(&vec![0, 0, 0, 0]).is_err()); assert!(reduced.extract_solution(&vec![]).is_err()); let huge = BMF::new(vec![vec![false]], usize::MAX); assert!(matches!( diff --git a/src/unit_tests/rules/closestvectorproblem_qubo.rs b/src/unit_tests/rules/closestvectorproblem_qubo.rs index c26561578..5556ce205 100644 --- a/src/unit_tests/rules/closestvectorproblem_qubo.rs +++ b/src/unit_tests/rules/closestvectorproblem_qubo.rs @@ -2,14 +2,43 @@ use super::*; use crate::solvers::BruteForce; use crate::traits::Problem; +#[test] +fn translated_target_does_not_expand_the_coefficient_search() { + let source = + ClosestVectorProblem::new(vec![vec![3, 0], vec![0, 3]], vec![3_000_001, -3_000_001]) + .unwrap(); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + // Rounding each coordinate to a multiple of three leaves residual (1, -1). + // Every other lattice point has strictly greater squared distance than two. + assert_eq!(reduction.target_problem().num_vars(), 0); + let witness = reduction.extract_solution(&vec![]).unwrap(); + assert_eq!(witness, vec![1_000_000, -1_000_000]); + assert_eq!(source.evaluate(&witness).unwrap().0, Some(2)); +} + +#[test] +fn scaled_orthogonal_basis_has_a_small_coefficient_encoding() { + let source = ClosestVectorProblem::new(vec![vec![100, 0], vec![0, 100]], vec![100, 0]).unwrap(); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + // Distance at the zero vector is 100. Thus |z_0| <= 2 and |z_1| <= 1; + // the symmetric coefficient box needs only three plus two binary bits. + assert!(reduction.target_problem().num_vars() <= 5); + let optimum = BruteForce::new() + .solve(reduction.target_problem()) + .unwrap() + .unwrap(); + let witness = reduction.extract_solution(&optimum).unwrap(); + assert_eq!(source.evaluate(&witness).unwrap().unwrap(), 0); + assert_eq!(witness, vec![1, 0]); +} + fn canonical_cvp() -> ClosestVectorProblem { - ClosestVectorProblem::new(vec![vec![2, 0], vec![1, 2]], vec![3_i64, 2]).unwrap() + ClosestVectorProblem::new(vec![vec![2, 0], vec![1, 2]], vec![3_i64, 1]).unwrap() } fn canonical_bits() -> Vec { - vec![ - false, false, false, true, true, false, false, true, false, false, true, - ] + // z_0 = 1 and z_1 = bit_0. + vec![true] } #[test] @@ -73,8 +102,8 @@ fn test_closestvectorproblem_to_qubo_closed_loop() { let source_solution = reduction.extract_solution(&target_solution).unwrap(); assert_eq!(source_solution, vec![1, 1]); - assert_eq!(source.evaluate(&source_solution).unwrap().0, Some(0)); - assert_eq!(reduction.target_problem().num_vars(), 11); + assert_eq!(source.evaluate(&source_solution).unwrap().0, Some(1)); + assert_eq!(reduction.target_problem().num_vars(), 1); } #[test] @@ -115,13 +144,15 @@ fn test_closestvectorproblem_to_qubo_preserves_squared_distance_up_to_constant() #[test] fn test_closestvectorproblem_to_qubo_coefficients() { - let reduction = ReduceTo::>::reduce_to(&canonical_cvp()).unwrap(); + let source = + ClosestVectorProblem::new(vec![vec![1, 0, 0], vec![1, 1, 0]], vec![0, 0, 2]).unwrap(); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); let qubo = reduction.target_problem(); - - assert_eq!(qubo.get(0, 0), Some(&-248)); - assert_eq!(qubo.get(0, 1), Some(&16)); - assert_eq!(qubo.get(0, 6), Some(&4)); - assert_eq!(qubo.get(6, 6), Some(&-241)); + // Expanding from lower coefficients (-2,-2): G = [[1,1],[1,2]], + // the linear projection G*lower is (-4,-6). + assert_eq!(qubo.get(0, 0), Some(&-7)); + assert_eq!(qubo.get(0, 3), Some(&2)); + assert_eq!(qubo.get(3, 3), Some(&-10)); } #[test] @@ -132,15 +163,15 @@ fn test_closestvectorproblem_to_qubo_exact_range_decoding() { vec![1, 1] ); - let duplicate = vec![ - true, false, false, true, false, true, true, true, true, true, false, - ]; - assert_eq!(reduction.extract_solution(&duplicate).unwrap(), vec![1, 1]); + let source = ClosestVectorProblem::new(vec![vec![1, 0]], vec![0, 2]).unwrap(); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + // The range [-2,2] uses weights 1,2,1; both offsets below equal two. + let first = vec![false, true, false]; + let duplicate = vec![true, false, true]; + assert_eq!(reduction.extract_solution(&first).unwrap(), vec![0]); + assert_eq!(reduction.extract_solution(&duplicate).unwrap(), vec![0]); assert_eq!( - reduction - .target_problem() - .evaluate(&canonical_bits()) - .unwrap(), + reduction.target_problem().evaluate(&first).unwrap(), reduction.target_problem().evaluate(&duplicate).unwrap() ); } @@ -162,8 +193,11 @@ fn test_closestvectorproblem_to_qubo_preserves_optimum_outside_old_box() { #[test] fn test_closestvectorproblem_to_qubo_reports_numeric_boundaries() { let absolute_value = ClosestVectorProblem::new(vec![vec![1]], vec![i64::MIN]).unwrap(); + let reduction = ReduceTo::>::reduce_to(&absolute_value).unwrap(); + assert_eq!(reduction.extract_solution(&vec![]).unwrap(), vec![i64::MIN]); + let gram_overflow = ClosestVectorProblem::new(vec![vec![i64::MAX]], vec![0]).unwrap(); assert!(matches!( - ReduceTo::>::reduce_to(&absolute_value), + ReduceTo::>::reduce_to(&gram_overflow), Err(crate::rules::ReductionError::IntegerOverflow { .. }) )); @@ -182,7 +216,7 @@ fn test_closestvectorproblem_to_qubo_canonical_example_spec() { assert_eq!(example.source.problem, "ClosestVectorProblem"); assert_eq!(example.target.problem, "QUBO"); - assert_eq!(example.target.instance["num_vars"], 11); + assert_eq!(example.target.instance["num_vars"], 1); assert_eq!( example.solutions[0].source_config, serde_json::json!([1, 1]) diff --git a/src/unit_tests/rules/hamiltoniancircuit_ilp.rs b/src/unit_tests/rules/hamiltoniancircuit_ilp.rs new file mode 100644 index 000000000..296c1c428 --- /dev/null +++ b/src/unit_tests/rules/hamiltoniancircuit_ilp.rs @@ -0,0 +1,71 @@ +use super::*; +use crate::solvers::{BruteForce, ILPSolveError, ILPSolver}; +use crate::traits::Problem; + +#[test] +fn test_hamiltoniancircuit_to_ilp_closed_loop() { + let edges = [(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]; + for mask in 0..64 { + let graph = SimpleGraph::new( + 4, + edges + .iter() + .enumerate() + .filter_map(|(index, &edge)| (mask & (1 << index) != 0).then_some(edge)) + .collect(), + ); + let source = HamiltonianCircuit::new(graph); + let expected = BruteForce::new().solve(&source).unwrap(); + let reduction = ReduceTo::::reduce_to(&source).unwrap(); + match ILPSolver::new().solve(reduction.target_problem()) { + Ok(solution) => { + assert!(expected.is_some(), "graph {mask}"); + assert!( + source + .evaluate(&reduction.extract_solution(&solution).unwrap()) + .unwrap() + .0 + ); + } + Err(ILPSolveError::Infeasible) => assert!(expected.is_none(), "graph {mask}"), + Err(error) => panic!("{error}"), + } + } +} + +#[test] +fn circuit_flow_requires_one_cycle_and_three_distinct_vertices() { + for (n, edges, expected) in [ + (0, vec![], false), + (1, vec![(0, 0)], false), + (2, vec![(0, 1), (0, 1)], false), + ( + 6, + vec![(0, 1), (1, 2), (0, 2), (3, 4), (4, 5), (3, 5)], + false, + ), + (3, vec![(0, 0), (0, 1), (0, 1), (1, 2), (0, 2)], true), + ] { + let source = HamiltonianCircuit::new(SimpleGraph::new(n, edges)); + let reduction = ReduceTo::::reduce_to(&source).unwrap(); + match ILPSolver::new().solve(reduction.target_problem()) { + Ok(solution) => { + assert!(expected); + assert!( + source + .evaluate(&reduction.extract_solution(&solution).unwrap()) + .unwrap() + .0 + ); + assert!(reduction + .extract_solution(&vec![0; solution.len()]) + .is_err()); + assert!(reduction + .extract_solution(&vec![0; solution.len() + 1]) + .is_err()); + } + Err(ILPSolveError::Infeasible) => assert!(!expected), + Err(error) => panic!("{error}"), + } + } +} diff --git a/src/unit_tests/rules/ksatisfiability_acyclicpartition.rs b/src/unit_tests/rules/ksatisfiability_acyclicpartition.rs index d12fc33e7..7b68f443c 100644 --- a/src/unit_tests/rules/ksatisfiability_acyclicpartition.rs +++ b/src/unit_tests/rules/ksatisfiability_acyclicpartition.rs @@ -7,6 +7,34 @@ use crate::topology::Graph; use crate::traits::Problem; use crate::variant::K3; +#[test] +fn incidence_capacity_counts_selected_vertices_independently_of_edges() { + let source = KSatisfiability::::new( + 2, + vec![ + CNFClause::new(vec![1, 2, 1]), + CNFClause::new(vec![-1, 2, 2]), + ], + ); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + let clique = reduction.sat_to_clique.target_problem(); + let target = reduction.target_problem(); + let n = clique.num_vertices(); + let items = target.num_vertices() - 2; + // Vertex count is the clique budget. Edge items certify adjacency and + // must not weaken that independent cardinality constraint. + assert_eq!(&target.vertex_weights()[..n], &vec![1; n]); + assert_eq!(&target.vertex_weights()[n..items], &vec![0; items - n]); + assert_eq!( + *target.weight_bound() - target.vertex_weights()[items], + clique.k() as i64 + ); + assert_eq!( + *target.weight_bound() - target.vertex_weights()[items + 1], + (n - clique.k()) as i64 + ); +} + #[test] fn test_ksatisfiability_to_acyclicpartition_closed_loop() { let source = KSatisfiability::::new(1, vec![CNFClause::new(vec![1, 1, 1])]); @@ -143,10 +171,10 @@ fn test_ksatisfiability_to_acyclicpartition_multi_variable_closed_loop() { #[test] fn test_incidence_parameters_checked_arithmetic() { - assert_eq!(incidence_parameters(1, 0, 1).unwrap(), (3, 2, 1, 3, 5, 2)); + assert_eq!(incidence_parameters(1, 0, 1).unwrap(), (3, 2, 1, 3, 5, 3)); assert_eq!( incidence_parameters(4, 3, 2).unwrap(), - (9, 20, 3, 15, 21, 101) + (9, 20, 2, 15, 19, 103) ); for (n, e, k) in [ (usize::MAX, 1, 1), diff --git a/src/unit_tests/solvers/customized/ensemble_computation.rs b/src/unit_tests/solvers/customized/ensemble_computation.rs index 44146eb12..fcc35fb5c 100644 --- a/src/unit_tests/solvers/customized/ensemble_computation.rs +++ b/src/unit_tests/solvers/customized/ensemble_computation.rs @@ -3,7 +3,20 @@ use crate::solvers::BruteForce; use crate::traits::Problem; #[test] -fn test_breadth_first_search_ensemble_computation_matches_brute_force() { +fn disjoint_required_sets_need_independent_union_operations() { + // Each triple needs two unions, and disjoint triples cannot share a union. + for budget in [3, 4] { + let problem = EnsembleComputation::new(6, vec![vec![0, 1, 2], vec![3, 4, 5]], budget); + let actual = solve(&problem).unwrap(); + assert_eq!(actual.is_some(), budget == 4); + if let Some(solution) = actual { + assert_eq!(problem.evaluate(&solution).unwrap().0, Some(4)); + } + } +} + +#[test] +fn test_useful_union_ensemble_computation_matches_brute_force() { let subsets = [ vec![], vec![0], @@ -18,7 +31,7 @@ fn test_breadth_first_search_ensemble_computation_matches_brute_force() { for second in &subsets { let problem = EnsembleComputation::new(3, vec![first.clone(), second.clone()], 2); let expected = BruteForce::new().solve(&problem).unwrap(); - let actual = solve(&problem); + let actual = solve(&problem).unwrap(); assert_eq!( actual .as_ref() @@ -32,12 +45,12 @@ fn test_breadth_first_search_ensemble_computation_matches_brute_force() { } #[test] -fn test_breadth_first_search_ensemble_computation_reuses_intermediate_sets() { +fn test_useful_union_ensemble_computation_reuses_intermediate_sets() { let problem = EnsembleComputation::new( 6, vec![vec![0, 1], vec![0, 1, 2, 3], vec![0, 1, 2, 3, 4, 5]], 5, ); - let solution = solve(&problem).unwrap(); + let solution = solve(&problem).unwrap().unwrap(); assert_eq!(problem.evaluate(&solution).unwrap().0, Some(5)); } diff --git a/src/unit_tests/solvers/registry.rs b/src/unit_tests/solvers/registry.rs index 535fab3cc..56e9b0eca 100644 --- a/src/unit_tests/solvers/registry.rs +++ b/src/unit_tests/solvers/registry.rs @@ -156,11 +156,24 @@ fn ilp_negative_intermediate_does_not_require_remaining_value_mappings() { Err(ILPSolveError::Infeasible) )); // Exercise completed-value recovery through the explicit optimization route. - let mut path = original.path.clone(); - path.insert( - 2, - ExactProblemKey::new("LongestCircuit", path[1].variant.clone()), - ); + let graph = BTreeMap::from([("graph".into(), "SimpleGraph".into())]); + let weighted_graph = BTreeMap::from([ + ("graph".into(), "SimpleGraph".into()), + ("weight".into(), "i64".into()), + ]); + let path = vec![ + ExactProblemKey::new("HamiltonianCircuit", graph), + ExactProblemKey::new("DecisionLongestCircuit", weighted_graph.clone()), + ExactProblemKey::new("LongestCircuit", weighted_graph), + ExactProblemKey::new( + "ILP", + BTreeMap::from([ + ("variable".into(), "bool".into()), + ("coefficient".into(), "i64".into()), + ("bounds".into(), "general".into()), + ]), + ), + ]; let reducers = path .windows(2) .map(|pair| { diff --git a/src/unit_tests/solvers/resolver.rs b/src/unit_tests/solvers/resolver.rs index cf5159707..a72fbefb5 100644 --- a/src/unit_tests/solvers/resolver.rs +++ b/src/unit_tests/solvers/resolver.rs @@ -4,6 +4,151 @@ use crate::solvers::{solve, SolveOutcome, SolverExecution, SolverRequest}; use crate::traits::Problem; use std::collections::BTreeMap; +#[test] +fn arithmetic_solvers_find_large_bounded_roots() { + use num_bigint::BigUint; + + let x = (BigUint::from(1u8) << 60usize) + BigUint::from(1u8); + let modulus = BigUint::from(3u8).pow(100); + let cases = [ + ( + "QuadraticCongruences", + serde_json::json!({"a": (&x * &x).to_string(), "b": modulus.to_string(), "c": (&x + BigUint::from(1u8)).to_string()}), + ), + ( + "QuadraticDiophantineEquations", + serde_json::json!({"a": "6", "b": (&modulus * 6u8).to_string(), "c": ((&x * &x + &modulus) * 6u8).to_string()}), + ), + ]; + for (name, data) in cases { + let problem = load_dyn(name, &BTreeMap::new(), data).unwrap(); + let result = solve(&problem, SolverRequest::Customized).unwrap(); + let SolveOutcome::Optimal { solution, .. } = result.outcome else { + panic!("expected the independently constructed root for {name}"); + }; + assert_eq!(solution, serde_json::to_value(&x).unwrap()); + assert_eq!(problem.evaluate_dyn(&solution).unwrap(), "Or(true)"); + } +} + +#[test] +fn arithmetic_solvers_match_integer_enumeration() { + for b in 1u64..=20 { + for a in 0..b { + for c in [1u64, 2, 4, 9] { + let expected = (1..c).any(|x| x * x % b == a); + let problem = load_dyn( + "QuadraticCongruences", + &BTreeMap::new(), + serde_json::json!({"a": a.to_string(), "b": b.to_string(), "c": c.to_string()}), + ) + .unwrap(); + let actual = solve(&problem, SolverRequest::Customized).unwrap(); + assert_eq!( + matches!(actual.outcome, SolveOutcome::Optimal { .. }), + expected, + "x² = {a} mod {b}, x < {c}" + ); + if let SolveOutcome::Optimal { solution, .. } = actual.outcome { + assert_eq!(problem.evaluate_dyn(&solution).unwrap(), "Or(true)"); + } + } + } + } + for a in 1u64..=4 { + for b in 1u64..=12 { + for c in 1u64..=30 { + let expected = (1..c).any(|x| a * x * x < c && (c - a * x * x) % b == 0); + let problem = load_dyn( + "QuadraticDiophantineEquations", + &BTreeMap::new(), + serde_json::json!({"a": a.to_string(), "b": b.to_string(), "c": c.to_string()}), + ) + .unwrap(); + let actual = solve(&problem, SolverRequest::Customized).unwrap(); + assert_eq!( + matches!(actual.outcome, SolveOutcome::Optimal { .. }), + expected, + "{a}x² + {b}y = {c}" + ); + if let SolveOutcome::Optimal { solution, .. } = actual.outcome { + assert_eq!(problem.evaluate_dyn(&solution).unwrap(), "Or(true)"); + } + } + } + } +} + +#[test] +fn register_solver_matches_exhaustive_ordering_search() { + use crate::models::misc::RegisterSufficiency; + for mask in 0..64 { + let arcs: Vec<_> = [(1, 0), (2, 0), (2, 1), (3, 0), (3, 1), (3, 2)] + .into_iter() + .enumerate() + .filter_map(|(i, edge)| (mask & (1 << i) != 0).then_some(edge)) + .collect(); + for bound in 0..=4 { + let model = RegisterSufficiency::new(4, arcs.clone(), bound); + let problem = load_dyn( + RegisterSufficiency::NAME, + &BTreeMap::new(), + serde_json::to_value(model).unwrap(), + ) + .unwrap(); + let expected = solve(&problem, SolverRequest::BruteForce).unwrap(); + let actual = solve(&problem, SolverRequest::Customized).unwrap(); + assert_eq!( + matches!(actual.outcome, SolveOutcome::Optimal { .. }), + matches!(expected.outcome, SolveOutcome::Optimal { .. }), + "DAG {mask}, bound {bound}" + ); + if let SolveOutcome::Optimal { solution, .. } = actual.outcome { + assert_eq!(problem.evaluate_dyn(&solution).unwrap(), "Or(true)"); + } + } + } +} + +#[test] +fn register_solver_checks_identical_dependency_groups() { + use crate::models::misc::RegisterSufficiency; + // Each consumer needs twelve inputs simultaneously. The final consumer + // requires both results, so thirteen registers suffice and eleven cannot. + let arcs = (0..12) + .map(|v| (24, v)) + .chain((12..24).map(|v| (25, v))) + .chain([(26, 24), (26, 25)]) + .collect::>(); + for (bound, expected) in [(11, false), (13, true)] { + let model = RegisterSufficiency::new(27, arcs.clone(), bound); + let actual = model.solve_exact(); + assert_eq!(actual.is_some(), expected); + if let Some(solution) = actual { + assert!(model.evaluate(&solution).unwrap().0); + } + } +} + +#[test] +fn ensemble_solver_returns_a_minimum_shared_union_program() { + use crate::models::misc::EnsembleComputation; + let model = EnsembleComputation::new(5, vec![vec![0, 1, 2], vec![0, 1, 3], vec![0, 1, 4]], 8); + let problem = load_dyn( + EnsembleComputation::NAME, + &BTreeMap::new(), + serde_json::to_value(model).unwrap(), + ) + .unwrap(); + let actual = solve(&problem, SolverRequest::Customized).unwrap(); + let SolveOutcome::Optimal { solution, .. } = actual.outcome else { + panic!("shared union program exists"); + }; + // Three distinct triples each need a gate and all triples can share {0,1}. + // No triple is a disjoint union of two available singletons: optimum = 4. + assert_eq!(problem.evaluate_dyn(&solution).unwrap(), "Min(4)"); +} + #[test] fn tree_and_weighted_sequencing_default_to_ilp() { let tree_variant = BTreeMap::from([ From 44537006fb7d869d4b0edbefa80c54297203e41c Mon Sep 17 00:00:00 2001 From: Xiwei Pan <90967972+isPANN@users.noreply.github.com> Date: Thu, 1 Oct 2026 01:23:54 -0700 Subject: [PATCH 19/22] Simplify reduction results and exact solver bookkeeping --- src/rules/acyclicpartition_ilp.rs | 17 +++++++------- src/rules/hamiltoniancircuit_ilp.rs | 15 ++++++------- .../customized/ensemble_computation.rs | 22 +++++++------------ .../customized/quadratic_congruences.rs | 13 ++++------- 4 files changed, 27 insertions(+), 40 deletions(-) diff --git a/src/rules/acyclicpartition_ilp.rs b/src/rules/acyclicpartition_ilp.rs index 6cbe9fd59..3f902d19c 100644 --- a/src/rules/acyclicpartition_ilp.rs +++ b/src/rules/acyclicpartition_ilp.rs @@ -9,7 +9,7 @@ use crate::rules::traits::{ReduceTo, ReductionResult}; pub struct ReductionAcyclicPartitionToILP { target: ILP, n: usize, - parts: Option, + one_hot: bool, } impl ReductionResult for ReductionAcyclicPartitionToILP { @@ -20,7 +20,7 @@ impl ReductionResult for ReductionAcyclicPartitionToILP { &self.target } - /// One-hot decode: for each vertex v, output the unique c with x_{v,c} = 1. + /// Decode one-hot memberships or return the direct binary part labels. fn extract_solution( &self, target_solution: &::Solution, @@ -32,11 +32,10 @@ impl ReductionResult for ReductionAcyclicPartitionToILP { "target ILP assignment is infeasible", )?; - match self.parts { - Some(parts) => { - crate::rules::ilp_helpers::one_hot_decode_rows(target_solution, self.n, parts, 0) - } - None => crate::rules::ilp_helpers::decode_usize_values(&target_solution[..self.n]), + if self.one_hot { + crate::rules::ilp_helpers::one_hot_decode_rows(target_solution, self.n, self.n, 0) + } else { + crate::rules::ilp_helpers::decode_usize_values(&target_solution[..self.n]) } } } @@ -220,7 +219,7 @@ fn two_part_reduction( Ok(ReductionAcyclicPartitionToILP { target, n, - parts: None, + one_hot: false, }) } @@ -332,7 +331,7 @@ impl ReduceTo> for AcyclicPartition { Ok(ReductionAcyclicPartitionToILP { target, n, - parts: Some(parts), + one_hot: true, }) } } diff --git a/src/rules/hamiltoniancircuit_ilp.rs b/src/rules/hamiltoniancircuit_ilp.rs index 7cd90aa66..c7b46ff5c 100644 --- a/src/rules/hamiltoniancircuit_ilp.rs +++ b/src/rules/hamiltoniancircuit_ilp.rs @@ -10,8 +10,7 @@ type Target = ILP; #[derive(Debug, Clone)] pub struct ReductionHamiltonianCircuitToILP { target: Target, - edges: Vec<(usize, usize)>, - n: usize, + graph: SimpleGraph, } impl ReductionResult for ReductionHamiltonianCircuitToILP { @@ -29,14 +28,11 @@ impl ReductionResult for ReductionHamiltonianCircuitToILP { |value| value.value.is_some(), "target ILP assignment is infeasible", )?; - let selected: Vec<_> = solution[..self.edges.len()] + let selected: Vec<_> = solution[..self.graph.num_edges()] .iter() .map(|&value| value == 1) .collect(); - crate::rules::graph_helpers::edges_to_cycle_order( - &SimpleGraph::new(self.n, self.edges.clone()), - &selected, - ) + crate::rules::graph_helpers::edges_to_cycle_order(&self.graph, &selected) } } @@ -105,7 +101,10 @@ impl ReduceTo> for HamiltonianCircuit { let target = Target::with_variables(variables, constraints, vec![], ObjectiveSense::Minimize) .map_err(>::target_construction)?; - Ok(ReductionHamiltonianCircuitToILP { target, edges, n }) + Ok(ReductionHamiltonianCircuitToILP { + target, + graph: SimpleGraph::new(n, edges), + }) } } diff --git a/src/solvers/customized/ensemble_computation.rs b/src/solvers/customized/ensemble_computation.rs index e3030921e..45878e05c 100644 --- a/src/solvers/customized/ensemble_computation.rs +++ b/src/solvers/customized/ensemble_computation.rs @@ -52,13 +52,9 @@ pub(crate) fn solve(problem: &EnsembleComputation) -> Result>, .copied() .filter(|element| left.binary_search(element).is_err()) .collect(); - let choice = - useful - .len() - .checked_add(partitions.len()) - .ok_or(SolveError::IntegerOverflow( - "indexing ensemble union choices".into(), - ))?; + let choice = useful.len().checked_add(partitions.len()).ok_or_else(|| { + SolveError::IntegerOverflow("indexing ensemble union choices".into()) + })?; choices.push((choice, 1)); for child in [&left, &right] { if child.len() > 1 { @@ -116,13 +112,11 @@ pub(crate) fn solve(problem: &EnsembleComputation) -> Result>, continue; } for child in [left, right] { - program.push( - *operands - .get(child) - .ok_or(failure(ILPSolveError::InvalidSolution( - "union operand was not computed".into(), - )))?, - ); + program.push(*operands.get(child).ok_or_else(|| { + failure(ILPSolveError::InvalidSolution( + "union operand was not computed".into(), + )) + })?); } operands.insert( useful[*index].clone(), diff --git a/src/solvers/customized/quadratic_congruences.rs b/src/solvers/customized/quadratic_congruences.rs index 50f88f835..36f3b4b6f 100644 --- a/src/solvers/customized/quadratic_congruences.rs +++ b/src/solvers/customized/quadratic_congruences.rs @@ -105,10 +105,10 @@ fn half_sums(choices: &[Vec], modulus: &BigUint) -> Result next.try_reserve(sums.len())?; next.extend(sums.iter().map(|sum| (sum + choice) % modulus)); } - next.sort_unstable(); - next.dedup(); sums = next; } + sums.sort_unstable(); + sums.dedup(); Ok(sums) } @@ -158,13 +158,8 @@ fn bounded_root(a: &BigUint, b: &BigUint, c: &BigUint) -> Result let index = right.partition_point(|b| b < &lower); if let Some(b) = right.get(index) { let residue = (&a + b) % &modulus; - let x = if residue.is_zero() { - modulus.clone() - } else { - residue - }; - if &x < c { - return Ok(Some(x)); + if !residue.is_zero() && &residue < c { + return Ok(Some(residue)); } } } From b466840f12f4e05bb5053de4aa64380e049b8db6 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Sun, 4 Oct 2026 04:10:20 +0800 Subject: [PATCH 20/22] fix: bound solver precomputation by small witness budgets --- .../customized/ensemble_computation.rs | 9 ++- .../customized/quadratic_congruences.rs | 14 ++++ .../customized/ensemble_computation.rs | 21 +++++ src/unit_tests/solvers/resolver.rs | 80 +++++++++++++++++++ 4 files changed, 122 insertions(+), 2 deletions(-) diff --git a/src/solvers/customized/ensemble_computation.rs b/src/solvers/customized/ensemble_computation.rs index 45878e05c..edf961d4d 100644 --- a/src/solvers/customized/ensemble_computation.rs +++ b/src/solvers/customized/ensemble_computation.rs @@ -21,8 +21,13 @@ fn subsets(set: &[usize]) -> Result>, SolveError> { } pub(crate) fn solve(problem: &EnsembleComputation) -> Result>, SolveError> { - // Computed sets are nonempty disjoint unions of two nonempty operands. - if problem.subsets().iter().any(|set| set.len() < 2) { + // A k-element set needs k-1 disjoint unions of singleton leaves, even + // when intermediate results are shared with other required sets. + if problem + .subsets() + .iter() + .any(|set| set.len() < 2 || set.len() - 1 > problem.budget()) + { return Ok(None); } // ponytail: enumerate subsets of required sets; use implicit subset search diff --git a/src/solvers/customized/quadratic_congruences.rs b/src/solvers/customized/quadratic_congruences.rs index 36f3b4b6f..d3719f240 100644 --- a/src/solvers/customized/quadratic_congruences.rs +++ b/src/solvers/customized/quadratic_congruences.rs @@ -116,6 +116,20 @@ fn bounded_root(a: &BigUint, b: &BigUint, c: &BigUint) -> Result if c <= &BigUint::one() { return Ok(None); } + // Check small witness spaces before factoring or enumerating prime roots. + // One complete positive residue period suffices when c exceeds b. + let limit = (c - BigUint::one()).min(b.clone()); + if limit <= BigUint::from(4096u32) { + let residue = a % b; + let mut candidate = BigUint::one(); + while candidate <= limit { + if (&candidate * &candidate) % b == residue { + return Ok(Some(candidate)); + } + candidate += 1u8; + } + return Ok(None); + } let Some(classes) = prime_powers(b) .into_iter() .map(|(prime, exponent)| local_roots(a, &prime, exponent)) diff --git a/src/unit_tests/solvers/customized/ensemble_computation.rs b/src/unit_tests/solvers/customized/ensemble_computation.rs index fcc35fb5c..ed556ec85 100644 --- a/src/unit_tests/solvers/customized/ensemble_computation.rs +++ b/src/unit_tests/solvers/customized/ensemble_computation.rs @@ -2,6 +2,27 @@ use super::*; use crate::solvers::BruteForce; use crate::traits::Problem; +#[test] +fn large_required_sets_reject_insufficient_union_budgets() { + let required: Vec<_> = (0..64).collect(); + for budget in [1, 62] { + let problem = EnsembleComputation::new(64, vec![required.clone()], budget); + assert_eq!(solve(&problem).unwrap(), None); + } +} + +#[test] +fn required_set_union_budget_includes_the_boundary() { + for budget in [2, 3, 4] { + let problem = EnsembleComputation::new(4, vec![vec![0, 1, 2, 3]], budget); + let solution = solve(&problem).unwrap(); + assert_eq!(solution.is_some(), budget >= 3); + if let Some(solution) = solution { + assert_eq!(problem.evaluate(&solution).unwrap().0, Some(3)); + } + } +} + #[test] fn disjoint_required_sets_need_independent_union_operations() { // Each triple needs two unions, and disjoint triples cannot share a union. diff --git a/src/unit_tests/solvers/resolver.rs b/src/unit_tests/solvers/resolver.rs index a72fbefb5..c2d01b44f 100644 --- a/src/unit_tests/solvers/resolver.rs +++ b/src/unit_tests/solvers/resolver.rs @@ -4,6 +4,86 @@ use crate::solvers::{solve, SolveOutcome, SolverExecution, SolverRequest}; use crate::traits::Problem; use std::collections::BTreeMap; +#[test] +fn arithmetic_solvers_check_small_witness_ranges_before_large_moduli() { + let cases = [ + ( + "QuadraticCongruences", + 1u64, + 1_000_000_007u64, + 2u64, + Some(1u64), + ), + ("QuadraticCongruences", 4, 1_000_000_007, 2, None), + ("QuadraticCongruences", 4, 1_000_000_007, 3, Some(2)), + ("QuadraticCongruences", 0, 1, 2, Some(1)), + ("QuadraticCongruences", 0, 7, 7, None), + ("QuadraticCongruences", 0, 7, 8, Some(7)), + ( + "QuadraticDiophantineEquations", + 1, + 1_000_000_007, + 1_000_000_008, + Some(1), + ), + ( + "QuadraticDiophantineEquations", + 1, + 1_000_000_007, + 1_000_000_009, + None, + ), + ]; + for (name, a, b, c, expected) in cases { + let problem = load_dyn( + name, + &BTreeMap::new(), + serde_json::json!({"a": a.to_string(), "b": b.to_string(), "c": c.to_string()}), + ) + .unwrap(); + let result = solve(&problem, SolverRequest::Default).unwrap(); + match (result.outcome, expected) { + (SolveOutcome::Optimal { solution, .. }, Some(witness)) => { + assert_eq!( + solution, + serde_json::to_value(num_bigint::BigUint::from(witness)).unwrap() + ); + assert_eq!(problem.evaluate_dyn(&solution).unwrap(), "Or(true)"); + } + (SolveOutcome::Infeasible, None) => {} + (actual, expected) => { + panic!("{name}({a}, {b}, {c}): {actual:?}, expected {expected:?}") + } + } + } +} + +#[test] +fn arithmetic_solver_search_boundary_matches_integer_enumeration() { + for b in [5005u64, 6561, 8192] { + for a in [0u64, 1, 2, 9, 16, 49] { + for c in [4097u64, 4098, 10_000] { + let expected = (1..c).any(|x| x * x % b == a); + let problem = load_dyn( + "QuadraticCongruences", + &BTreeMap::new(), + serde_json::json!({"a": a.to_string(), "b": b.to_string(), "c": c.to_string()}), + ) + .unwrap(); + let result = solve(&problem, SolverRequest::Customized).unwrap(); + assert_eq!( + matches!(result.outcome, SolveOutcome::Optimal { .. }), + expected, + "x² = {a} mod {b}, x < {c}" + ); + if let SolveOutcome::Optimal { solution, .. } = result.outcome { + assert_eq!(problem.evaluate_dyn(&solution).unwrap(), "Or(true)"); + } + } + } + } +} + #[test] fn arithmetic_solvers_find_large_bounded_roots() { use num_bigint::BigUint; From 4e35c584430b1da3518da4863b99ef85a0ee15b2 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Sun, 4 Oct 2026 04:16:00 +0800 Subject: [PATCH 21/22] fix: remove needless borrow flagged by current CI clippy --- src/rules/maximumindependentset_triangular.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/rules/maximumindependentset_triangular.rs b/src/rules/maximumindependentset_triangular.rs index 644c75bae..46d72ccbb 100644 --- a/src/rules/maximumindependentset_triangular.rs +++ b/src/rules/maximumindependentset_triangular.rs @@ -56,7 +56,7 @@ impl ReduceTo> let mapping_error = |error: crate::rules::ReductionError| { error.for_reduction::>() }; - let result = triangular::map_weighted(n, &edges).map_err(&mapping_error)?; + let result = triangular::map_weighted(n, &edges).map_err(mapping_error)?; let weights = triangular::map_unit_weights(&result).map_err(mapping_error)?; let grid = result.to_triangular_subgraph(); let target = MaximumIndependentSet::new(grid, weights); From 8377fe5a5e1b4263a370a935861d5aca77c6c244 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Sun, 4 Oct 2026 04:32:56 +0800 Subject: [PATCH 22/22] fix: cap solver preprocessing and handle large union chains --- .../customized/ensemble_computation.rs | 82 +++++++++++-- .../customized/quadratic_congruences.rs | 114 +++++++++++++----- .../customized/ensemble_computation.rs | 51 ++++++++ src/unit_tests/solvers/resolver.rs | 40 ++++++ 4 files changed, 244 insertions(+), 43 deletions(-) diff --git a/src/solvers/customized/ensemble_computation.rs b/src/solvers/customized/ensemble_computation.rs index edf961d4d..0668decef 100644 --- a/src/solvers/customized/ensemble_computation.rs +++ b/src/solvers/customized/ensemble_computation.rs @@ -1,4 +1,4 @@ -//! Exact verification solver: choose useful unions, rather than ordering circuit slots. +//! Direct union chains and bounded subset enumeration with a compact ILP fallback. use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::EnsembleComputation; @@ -21,6 +21,29 @@ fn subsets(set: &[usize]) -> Result>, SolveError> { } pub(crate) fn solve(problem: &EnsembleComputation) -> Result>, SolveError> { + solve_with_union_limit(problem, 65_536) +} + +fn pad_program( + problem: &EnsembleComputation, + mut program: Vec, +) -> Result, SolveError> { + let length = problem + .budget() + .checked_mul(2) + .ok_or_else(|| SolveError::IntegerOverflow("sizing the ensemble program".into()))?; + let padding = length.checked_sub(program.len()).ok_or_else(|| { + SolveError::IntegerOverflow("ensemble program exceeds its operation budget".into()) + })?; + program.try_reserve_exact(padding)?; + program.resize(length, 0); + Ok(program) +} + +fn solve_with_union_limit( + problem: &EnsembleComputation, + union_limit: usize, +) -> Result>, SolveError> { // A k-element set needs k-1 disjoint unions of singleton leaves, even // when intermediate results are shared with other required sets. if problem @@ -30,11 +53,49 @@ pub(crate) fn solve(problem: &EnsembleComputation) -> Result>, { return Ok(None); } - // ponytail: enumerate subsets of required sets; use implicit subset search - // if correctness checks must handle large individual required sets. + let required: BTreeSet<_> = problem.subsets().iter().collect(); + if required.is_empty() { + return pad_program(problem, Vec::new()).map(Some); + } + if required.len() == 1 { + // A chain attains the k-1 lower bound for one distinct required set. + let set = required.first().expect("one required set"); + let mut program = vec![set[0], set[1]]; + for &element in &set[2..] { + let previous = problem + .universe_size() + .checked_add(program.len() / 2 - 1) + .ok_or_else(|| SolveError::IntegerOverflow("indexing an ensemble union".into()))?; + program.extend([previous, element]); + } + return pad_program(problem, program).map(Some); + } + let failure = |source| SolveError::IlpSolve { + problem: EnsembleComputation::NAME.into(), + source, + }; + // All subsets and unordered disjoint partitions of one k-element set + // require (3^k-1)/2-k variables. Sum this upper bound before allocating; + // repeated intermediate sets only decrease the actual construction size. + let fits = required + .iter() + .try_fold(union_limit, |remaining, set| { + let power = 3usize.checked_pow(u32::try_from(set.len()).ok()?)?; + remaining.checked_sub((power - 1) / 2 - set.len()) + }) + .is_some(); + if !fits { + // Explicit ILP dispatch uses the compact slot encoding, not this + // customized solver, so this fallback cannot recurse. + return match ILPSolver::new().solve(problem) { + Ok(solution) => Ok(Some(solution)), + Err(ILPSolveError::Infeasible) => Ok(None), + Err(error) => Err(failure(error)), + }; + } let mut useful = BTreeSet::new(); - for required in problem.subsets() { - useful.extend(subsets(required)?.into_iter().filter(|set| set.len() >= 2)); + for set in &required { + useful.extend(subsets(set)?.into_iter().filter(|set| set.len() >= 2)); } let mut useful: Vec<_> = useful.into_iter().collect(); useful.sort_by_key(Vec::len); @@ -74,13 +135,9 @@ pub(crate) fn solve(problem: &EnsembleComputation) -> Result>, // A computed set has exactly one disjoint-union definition. constraints.push(LinearConstraint::eq(choices, 0)); } - for required in problem.subsets() { - constraints.push(LinearConstraint::eq(vec![(indices[required], 1)], 1)); + for set in &required { + constraints.push(LinearConstraint::eq(vec![(indices[*set], 1)], 1)); } - let failure = |source| SolveError::IlpSolve { - problem: EnsembleComputation::NAME.into(), - source, - }; let budget = >>::exact_i64( problem.budget(), "representing the ensemble operation budget", @@ -128,8 +185,7 @@ pub(crate) fn solve(problem: &EnsembleComputation) -> Result>, problem.universe_size() + program.len() / 2 - 1, ); } - program.resize(2 * problem.budget(), 0); - Ok(Some(program)) + pad_program(problem, program).map(Some) } #[cfg(test)] diff --git a/src/solvers/customized/quadratic_congruences.rs b/src/solvers/customized/quadratic_congruences.rs index d3719f240..ce07e2a9c 100644 --- a/src/solvers/customized/quadratic_congruences.rs +++ b/src/solvers/customized/quadratic_congruences.rs @@ -1,4 +1,4 @@ -//! Exact bounded square roots: prime-power lifting, CRT, and meet in the middle. +//! Exact bounded square roots with budgeted CRT preprocessing. //! CRT proof: https://kconrad.math.uconn.edu/blurbs/ugradnumthy/crt.pdf //! Hensel lifting: https://www.math-cs.gordon.edu/~kcrisman/mat338/section-74.html @@ -7,15 +7,32 @@ use crate::solvers::SolveError; use num_bigint::BigUint; use num_traits::{One, Pow, Zero}; -fn prime_powers(modulus: &BigUint) -> Vec<(BigUint, usize)> { +struct WorkBudget(BigUint); + +struct BudgetExceeded; + +impl WorkBudget { + fn take(&mut self, amount: BigUint) -> Result<(), BudgetExceeded> { + if amount > self.0 { + return Err(BudgetExceeded); + } + self.0 -= amount; + Ok(()) + } +} + +fn prime_powers( + modulus: &BigUint, + budget: &mut WorkBudget, +) -> Result, BudgetExceeded> { let mut remaining = modulus.clone(); let mut prime = BigUint::from(2u8); let mut factors = Vec::new(); - // ponytail: exact trial division; use certified faster factoring if large - // prime factors, rather than the number of CRT choices, become the bottleneck. while &prime * &prime <= remaining { + budget.take(BigUint::one())?; let mut exponent = 0; while (&remaining % &prime).is_zero() { + budget.take(BigUint::one())?; remaining /= ′ exponent += 1; } @@ -31,18 +48,23 @@ fn prime_powers(modulus: &BigUint) -> Vec<(BigUint, usize)> { if remaining > BigUint::one() { factors.push((remaining, 1)); } - factors + Ok(factors) } /// Return the complete root classes, allowing a smaller period for nonunits. -fn local_roots(a: &BigUint, prime: &BigUint, exponent: usize) -> Option<(BigUint, Vec)> { +fn local_roots( + a: &BigUint, + prime: &BigUint, + exponent: usize, + budget: &mut WorkBudget, +) -> Result)>, BudgetExceeded> { let full = Pow::pow(prime.clone(), exponent); let mut unit = a % &full; if unit.is_zero() { - return Some(( + return Ok(Some(( Pow::pow(prime.clone(), exponent.div_ceil(2)), vec![BigUint::zero()], - )); + ))); } let mut valuation = 0; while (&unit % prime).is_zero() { @@ -50,8 +72,9 @@ fn local_roots(a: &BigUint, prime: &BigUint, exponent: usize) -> Option<(BigUint valuation += 1; } if valuation % 2 != 0 { - return None; + return Ok(None); } + budget.take(prime.clone())?; let mut roots = Vec::new(); let mut candidate = BigUint::zero(); while &candidate < prime { @@ -61,7 +84,7 @@ fn local_roots(a: &BigUint, prime: &BigUint, exponent: usize) -> Option<(BigUint candidate += 1u8; } if roots.is_empty() { - return None; + return Ok(None); } let mut period = prime.clone(); for _ in 1..exponent - valuation { @@ -85,16 +108,16 @@ fn local_roots(a: &BigUint, prime: &BigUint, exponent: usize) -> Option<(BigUint } } if lifted.is_empty() { - return None; + return Ok(None); } roots = lifted; period = next; } let scale: BigUint = Pow::pow(prime.clone(), valuation / 2); - Some(( + Ok(Some(( &period * &scale, roots.into_iter().map(|root| root * &scale).collect(), - )) + ))) } fn half_sums(choices: &[Vec], modulus: &BigUint) -> Result, SolveError> { @@ -112,30 +135,50 @@ fn half_sums(choices: &[Vec], modulus: &BigUint) -> Result Ok(sums) } +fn enumerate_root( + a: &BigUint, + b: &BigUint, + mut candidate: BigUint, + limit: &BigUint, +) -> Option { + let residue = a % b; + while &candidate <= limit { + if (&candidate * &candidate) % b == residue { + return Some(candidate); + } + candidate += 1u8; + } + None +} + fn bounded_root(a: &BigUint, b: &BigUint, c: &BigUint) -> Result, SolveError> { if c <= &BigUint::one() { return Ok(None); } - // Check small witness spaces before factoring or enumerating prime roots. - // One complete positive residue period suffices when c exceeds b. + // A prefix finds cheap witnesses regardless of the size of c. One complete + // positive residue period suffices when c exceeds b. let limit = (c - BigUint::one()).min(b.clone()); - if limit <= BigUint::from(4096u32) { - let residue = a % b; - let mut candidate = BigUint::one(); - while candidate <= limit { - if (&candidate * &candidate) % b == residue { - return Ok(Some(candidate)); - } - candidate += 1u8; - } - return Ok(None); + let prefix = limit.clone().min(BigUint::from(4096u32)); + if let Some(root) = enumerate_root(a, b, BigUint::one(), &prefix) { + return Ok(Some(root)); } - let Some(classes) = prime_powers(b) - .into_iter() - .map(|(prime, exponent)| local_roots(a, &prime, exponent)) - .collect::>>() - else { + if prefix == limit { return Ok(None); + } + let fallback = || enumerate_root(a, b, &prefix + BigUint::one(), &limit); + // Never spend more trials on factorization and prime-root scans than + // there are remaining witnesses. Exhaustion requests enumeration, not NO. + let mut budget = WorkBudget(&limit - &prefix); + let classes = (|| { + prime_powers(b, &mut budget)? + .into_iter() + .map(|(prime, exponent)| local_roots(a, &prime, exponent, &mut budget)) + .collect::>, BudgetExceeded>>() + })(); + let classes = match classes { + Ok(Some(classes)) => classes, + Ok(None) => return Ok(None), + Err(BudgetExceeded) => return Ok(fallback()), }; let modulus: BigUint = classes.iter().map(|(period, _)| period).product(); let choices: Vec> = classes @@ -158,6 +201,17 @@ fn bounded_root(a: &BigUint, b: &BigUint, c: &BigUint) -> Result return Ok(Some(if residue.is_zero() { modulus } else { residue })); } let split = choices.len() / 2; + let combinations: BigUint = [&choices[..split], &choices[split..]] + .into_iter() + .map(|half| { + half.iter() + .map(|roots| BigUint::from(roots.len())) + .product::() + }) + .sum(); + if budget.take(combinations).is_err() { + return Ok(fallback()); + } let left = half_sums(&choices[..split], &modulus)?; let right = half_sums(&choices[split..], &modulus)?; // Since c <= M, zero residues are excluded, including after wraparound. diff --git a/src/unit_tests/solvers/customized/ensemble_computation.rs b/src/unit_tests/solvers/customized/ensemble_computation.rs index ed556ec85..89288cc1a 100644 --- a/src/unit_tests/solvers/customized/ensemble_computation.rs +++ b/src/unit_tests/solvers/customized/ensemble_computation.rs @@ -2,6 +2,57 @@ use super::*; use crate::solvers::BruteForce; use crate::traits::Problem; +#[test] +fn large_single_required_set_uses_an_optimal_union_chain() { + let required: Vec<_> = (0..64).collect(); + for budget in [63, 70] { + // Duplicate requirements and input ordering must not defeat the shortcut. + let reversed: Vec<_> = required.iter().rev().copied().collect(); + let problem = EnsembleComputation::new(64, vec![required.clone(), reversed], budget); + let solution = solve(&problem).unwrap().unwrap(); + assert_eq!(solution.len(), 2 * budget); + assert_eq!(problem.evaluate(&solution).unwrap().0, Some(63)); + } +} + +#[test] +fn compact_fallback_matches_brute_force_on_feasible_and_infeasible_programs() { + for sets in [ + vec![vec![0, 1], vec![1, 2]], + vec![vec![0, 1], vec![0, 1, 2]], + ] { + for budget in 1..=3 { + let problem = EnsembleComputation::new(3, sets.clone(), budget); + let expected = BruteForce::new().solve(&problem).unwrap(); + // Zero construction budget exercises the production fallback on + // an instance small enough for independent exhaustive verification. + let actual = solve_with_union_limit(&problem, 0).unwrap(); + assert_eq!( + actual.as_ref().map(|s| problem.evaluate(s).unwrap()), + expected.as_ref().map(|s| problem.evaluate(s).unwrap()) + ); + } + } +} + +#[test] +fn empty_requirements_and_unrepresentable_program_lengths() { + let empty = EnsembleComputation::new(0, vec![], 2); + let solution = solve(&empty).unwrap().unwrap(); + assert_eq!(solution, vec![0; 4]); + assert_eq!(empty.evaluate(&solution).unwrap().0, Some(0)); + let too_long = EnsembleComputation::new(2, vec![vec![0, 1]], usize::MAX); + assert!(matches!( + solve(&too_long), + Err(SolveError::IntegerOverflow(_)) + )); + let fallback_error = EnsembleComputation::new(3, vec![vec![0, 1], vec![1, 2]], usize::MAX); + assert!(matches!( + solve_with_union_limit(&fallback_error, 0), + Err(SolveError::IlpSolve { .. }) + )); +} + #[test] fn large_required_sets_reject_insufficient_union_budgets() { let required: Vec<_> = (0..64).collect(); diff --git a/src/unit_tests/solvers/resolver.rs b/src/unit_tests/solvers/resolver.rs index c2d01b44f..eea8cdbec 100644 --- a/src/unit_tests/solvers/resolver.rs +++ b/src/unit_tests/solvers/resolver.rs @@ -16,6 +16,46 @@ fn arithmetic_solvers_check_small_witness_ranges_before_large_moduli() { ), ("QuadraticCongruences", 4, 1_000_000_007, 2, None), ("QuadraticCongruences", 4, 1_000_000_007, 3, Some(2)), + ("QuadraticCongruences", 1, 1_000_000_007, 4098, Some(1)), + // Exhausting factorization's work budget resumes the witness search. + ( + "QuadraticCongruences", + 4100 * 4100, + 1_000_000_007, + 4101, + Some(4100), + ), + ( + "QuadraticCongruences", + 4101 * 4101, + 1_000_000_007, + 4101, + None, + ), + // Factoring finishes, but scanning all prime roots would cost more. + ( + "QuadraticCongruences", + 4500 * 4500 % 10_007, + 10_007, + 4501, + Some(4500), + ), + ( + "QuadraticCongruences", + 4501 * 4501 % 10_007, + 10_007, + 4501, + None, + ), + // Zero and nonunit root classes stay compact above the prefix. + ("QuadraticCongruences", 0, 1 << 28, 20_000, Some(16_384)), + ( + "QuadraticCongruences", + 12_288 * 12_288, + 1 << 30, + 13_000, + Some(12_288), + ), ("QuadraticCongruences", 0, 1, 2, Some(1)), ("QuadraticCongruences", 0, 7, 7, None), ("QuadraticCongruences", 0, 7, 8, Some(7)),