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/lib.rs b/src/lib.rs index 93dfbd4b7..afeac7aa1 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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/parameter_formula_validation.rs b/src/unit_tests/parameter_formula_validation.rs new file mode 100644 index 000000000..8709c0c8f --- /dev/null +++ b/src/unit_tests/parameter_formula_validation.rs @@ -0,0 +1,186 @@ +use crate::parameters::ParameterRelation; +use crate::registry::DynProblem; +use crate::rules::{registry::ReductionEntry, ReductionGraph}; +use serde_json::{json, Value}; +use std::collections::BTreeMap; + +type SourceKey = (String, BTreeMap); + +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(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)) + .or_default() + .push(source.instance); + } + sources +} + +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 = 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(); + 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; + }; + expected += transform.expressions().count(); + let label = format!( + "{} {:?} -> {} {:?}", + entry.source_name, + entry.source_variant(), + entry.target_name, + entry.target_variant() + ); + let result = (|| { + let source = source_for(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() { + 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 {}", + transform.relation(), + 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 formula fields were not checked"); +} 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!( diff --git a/src/unit_tests/symbolic_parameter_contracts.rs b/src/unit_tests/symbolic_parameter_contracts.rs index 1ba9b0d78..19665fe1c 100644 --- a/src/unit_tests/symbolic_parameter_contracts.rs +++ b/src/unit_tests/symbolic_parameter_contracts.rs @@ -152,3 +152,286 @@ 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(); + 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), + 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")); +}