From f7ccf2dfa198cbfe2300a050f2a03d2b2280df7e Mon Sep 17 00:00:00 2001 From: Xiwei Pan Date: Mon, 14 Sep 2026 12:59:24 +0800 Subject: [PATCH 01/42] fix: normalize equal-size pairings in 3-partition witness extraction Normalize filler triples before reconstructing the source matching. Include the reverse-construction proof and noncanonical-witness regressions. --- docs/paper/reductions.typ | 4 +- ...threedimensionalmatching_threepartition.rs | 168 +++++------------- ...threedimensionalmatching_threepartition.rs | 65 +++++++ 3 files changed, 113 insertions(+), 124 deletions(-) diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index ee531965e..ad22273a8 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -19075,9 +19075,9 @@ The following table shows concrete target-variable counts for example instances, ($arrow.r.double$) Given a perfect matching $M'$, form one ABCD group for every source triple. If $m_l in M'$, combine $u_l$ with the unique first-occurrence $B$, $C$, and $D$ items of coordinates $(a_l, b_l, c_l)$; otherwise combine $u_l$ with the corresponding later-occurrence dummy items. Because $r = 32 q$ prevents carries between the $r$, $r^2$, $r^3$, and $r^4$ digits, every such group sums to $T_1$, so the tagged instance has a 4-partition. For each tagged 4-set choose any two members $a_i, a_j$ and let the other two be $a_k, a_l$. Then ${w_i, w_j, u_(i j)}$ and ${w_k, w_l, u'_(i j)}$ both sum to $B$. Every pairing gadget not used this way joins one filler in a triple ${u_(i j), u'_(i j), 20 T_2}$. Hence the produced 3-Partition instance is feasible. - ($arrow.l.double$) In any feasible target solution every number lies strictly between $B / 4$ and $B / 2$, so the partition really is into triples. Modulo 4, regular numbers are congruent to 1, pairing numbers to 2, and fillers to 0. Therefore every triple is either of type $(1, 1, 2)$ or $(0, 2, 2)$. The $(0, 2, 2)$ triples identify the unused pairing gadgets, leaving a family of $(1, 1, 2)$ triples that reconstructs a 4-partition of the tagged numbers. Since $1 + 2 + 4 + 8 equiv 15 mod 16$, every recovered tagged 4-set contains exactly one former $A$-, $B$-, $C$-, and $D$-item. The carry-free base-$r$ encoding then forces each ABCD group to be either a real group (all first occurrences) or a dummy group (all later occurrences). The real groups pick exactly $q$ source triples, one for each coordinate of $W$, $X$, and $Y$, so they form a perfect 3-dimensional matching. + ($arrow.l.double$) In any feasible target solution every number lies strictly between $B / 4$ and $B / 2$, so the partition really is into triples. Modulo 4, regular numbers are congruent to 1, pairing numbers to 2, and fillers to 0. Therefore every triple is either of type $(1, 1, 2)$ or $(0, 2, 2)$. First normalize the $(0, 2, 2)$ triples as in @garey1979: if a filler shares a triple with pairing elements $p, q$, exchange $q$ with the original mate of $p$. Both have the same size, since every original pair sums to $44 T_2 + 4 = B - 20 T_2$, so both affected triples remain valid. Each exchange fixes a filler triple without disturbing a previously fixed one. After normalization, every remaining original pair occurs in two $(1, 1, 2)$ triples. Their four actual regular elements sum to $2 B - (44 T_2 + 4) = 84 T_2 + 4$, so the corresponding tagged numbers sum to $T_2$. These disjoint four-sets reconstruct a 4-partition. Since $1 + 2 + 4 + 8 equiv 15 mod 16$, every recovered tagged 4-set contains exactly one former $A$-, $B$-, $C$-, and $D$-item. The carry-free base-$r$ encoding then forces each ABCD group to be either a real group (all first occurrences) or a dummy group (all later occurrences). The real groups pick exactly $q$ source triples, one for each coordinate of $W$, $X$, and $Y$, so they form a perfect 3-dimensional matching. - _Solution extraction._ Reverse the 4-Partition $arrow.r$ 3-Partition gadget by pairing each triple containing some $u_(i j)$ with the unique triple containing the matching $u'_(i j)$. This recovers the tagged 4-set. Undo the mod-16 tags to obtain one ABCD group, discard every dummy group whose $B$, $C$, and $D$ items are not first occurrences, and read the selected source triple from the surviving $A$-item. + _Solution extraction._ Normalize filler triples by the equal-size exchanges above, maintaining each element's current group and position. Then pair the remaining triples containing original mates $u_(i j), u'_(i j)$ and collect their four actual regular elements; their indices need not equal the indices used to construct the pairing gadget. The normalization and pairing take linear time in the target element count. Undo the mod-16 tags to obtain one ABCD group, discard every dummy group whose $B$, $C$, and $D$ items are not first occurrences, and read the selected source triple from the surviving $A$-item. ] #let tdm_ilp = load-example("ThreeDimensionalMatching", "ILP") diff --git a/src/rules/threedimensionalmatching_threepartition.rs b/src/rules/threedimensionalmatching_threepartition.rs index 03a6ce030..02201b248 100644 --- a/src/rules/threedimensionalmatching_threepartition.rs +++ b/src/rules/threedimensionalmatching_threepartition.rs @@ -9,7 +9,6 @@ use crate::models::misc::ThreePartition; use crate::models::set::ThreeDimensionalMatching; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; -use std::collections::HashMap; #[derive(Debug, Clone, Copy)] enum Step2Item { @@ -33,18 +32,6 @@ enum Step2Item { }, } -#[derive(Debug, Clone, Copy)] -enum PairingKind { - U, - UPrime, -} - -#[derive(Debug, Default, Clone, Copy)] -struct PairUsage { - saw_u: bool, - uprime_regulars: Option<[usize; 2]>, -} - /// Result of reducing ThreeDimensionalMatching to ThreePartition. #[derive(Debug, Clone)] pub struct ReductionThreeDimensionalMatchingToThreePartition { @@ -67,27 +54,6 @@ impl ReductionThreeDimensionalMatchingToThreePartition { self.pairing_start() + 2 * self.pair_keys.len() } - fn classify_target_element(&self, element_index: usize) -> TargetElement { - if element_index < self.num_regulars() { - return TargetElement::Regular { - step2_index: element_index, - }; - } - - if element_index < self.filler_start() { - let pairing_offset = element_index - self.pairing_start(); - let pair_index = pairing_offset / 2; - let kind = if pairing_offset.is_multiple_of(2) { - PairingKind::U - } else { - PairingKind::UPrime - }; - return TargetElement::Pairing { pair_index, kind }; - } - - TargetElement::Filler - } - fn decode_real_group(&self, step2_group: [usize; 4]) -> Option { let mut a_item = None; let mut b_item = None; @@ -143,6 +109,8 @@ impl ReductionThreeDimensionalMatchingToThreePartition { #[cfg(test)] fn build_target_witness(&self, source_solution: &[usize]) -> Vec { + use std::collections::HashMap; + let mut a_indices = vec![0usize; self.num_source_triples]; let mut first_b_by_w = HashMap::new(); let mut first_c_by_x = HashMap::new(); @@ -298,98 +266,54 @@ impl ReductionResult for ReductionThreeDimensionalMatchingToThreePartition { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - - Ok({ - let mut groups = vec![Vec::new(); self.target.num_groups()]; - for (element_index, &group_index) in target_solution.iter().enumerate() { - groups[group_index].push(element_index); - } - - let mut pair_usage: HashMap<(usize, usize), PairUsage> = HashMap::new(); - - for members in groups.into_iter().filter(|members| !members.is_empty()) { - let mut regulars = Vec::new(); - let mut pairing = None; - let mut has_filler = false; - - for element_index in members { - match self.classify_target_element(element_index) { - TargetElement::Regular { step2_index } => regulars.push(step2_index), - TargetElement::Pairing { pair_index, kind } => { - pairing = Some((pair_index, kind)) - } - TargetElement::Filler => has_filler = true, - } - } - - if has_filler || regulars.len() != 2 { - continue; - } - - let Some((pair_index, kind)) = pairing else { - continue; - }; + let mut groups = vec![Vec::with_capacity(3); self.target.num_groups()]; + let mut positions = Vec::with_capacity(target_solution.len()); + for (element, &group) in target_solution.iter().enumerate() { + positions.push((group, groups[group].len())); + groups[group].push(element); + } - let pair_key = self.pair_keys[pair_index]; - let regular_pair = sorted_pair(regulars[0], regulars[1]); - let usage = pair_usage.entry(pair_key).or_default(); + // Garey--Johnson's reverse construction first normalizes filler triples. + // Each has two pairing elements whose sum equals that of an original + // U/UPrime pair. Exchange the second element with the first's mate: + // they have equal sizes, so both affected triples remain valid. A mate + // cannot belong to an already normalized filler triple unless it is + // already here, so each iteration permanently normalizes one triple. + // Initial index order puts regulars first and fillers last; exchanges + // only move pairing elements, preserving those positions. + let pairing_start = self.pairing_start(); + for filler in self.filler_start()..target_solution.len() { + let (group, _) = positions[filler]; + let first = groups[group][0]; + let second = groups[group][1]; + let mate = pairing_start + ((first - pairing_start) ^ 1); + let (mate_group, mate_slot) = positions[mate]; + groups[group][1] = mate; + groups[mate_group][mate_slot] = second; + positions[mate] = (group, 1); + positions[second] = (mate_group, mate_slot); + } - match kind { - PairingKind::U => { - if regular_pair == [pair_key.0, pair_key.1] { - usage.saw_u = true; - } - } - PairingKind::UPrime => { - usage.uprime_regulars = Some(regular_pair); - } - } + let mut source_solution = vec![false; self.num_source_triples]; + for first in (pairing_start..self.filler_start()).step_by(2) { + let (left, _) = positions[first]; + if groups[left][0] >= pairing_start { + continue; // This complete pair is used by a filler triple. } - - let mut source_solution = vec![false; self.num_source_triples]; - - for ((left, right), usage) in pair_usage { - let Some(other_two) = usage.uprime_regulars else { - continue; - }; - if !usage.saw_u { - continue; - } - - let mut group = [left, right, other_two[0], other_two[1]]; - group.sort_unstable(); - if group.windows(2).any(|window| window[0] == window[1]) { - continue; - } - - if let Some(source_triple) = self.decode_real_group(group) { - source_solution[source_triple] = true; - } + let (right, _) = positions[first + 1]; + // Use the actual regular elements, not the pair's construction + // indices: equal-valued pairing elements are interchangeable. + let regulars = [ + groups[left][0], + groups[left][1], + groups[right][0], + groups[right][1], + ]; + if let Some(source_triple) = self.decode_real_group(regulars) { + source_solution[source_triple] = true; } - - source_solution - }) - } -} - -#[derive(Debug, Clone, Copy)] -enum TargetElement { - Regular { - step2_index: usize, - }, - Pairing { - pair_index: usize, - kind: PairingKind, - }, - Filler, -} - -fn sorted_pair(a: usize, b: usize) -> [usize; 2] { - if a <= b { - [a, b] - } else { - [b, a] + } + Ok(source_solution) } } diff --git a/src/unit_tests/rules/threedimensionalmatching_threepartition.rs b/src/unit_tests/rules/threedimensionalmatching_threepartition.rs index f9cb2913b..f4379897f 100644 --- a/src/unit_tests/rules/threedimensionalmatching_threepartition.rs +++ b/src/unit_tests/rules/threedimensionalmatching_threepartition.rs @@ -123,3 +123,68 @@ fn test_threedimensionalmatching_to_threepartition_uncovered_coordinate_maps_to_ "target instance should be infeasible" ); } + +#[test] +fn test_threedimensionalmatching_to_threepartition_extracts_noncanonical_partition() { + let (source, reduction) = reduce(1, &[(0, 0, 0)]); + // A mathematically valid witness found independently by HiGHS. Both regular + // triples initially contain UPrime elements; filler triples mix pair IDs. + // Keep the witness fixed so this regression does not depend on the backend. + let witness = vec![ + 4, 0, 0, 4, 2, 1, 3, 3, 6, 0, 6, 4, 5, 5, 2, 1, 3, 1, 6, 2, 5, + ]; + assert!(reduction.target_problem().evaluate(&witness).unwrap().0); + let extracted = reduction.extract_solution(&witness).unwrap(); + assert_eq!(extracted, vec![true]); + assert!(source.evaluate(&extracted).unwrap().0); + // Group labels have no mathematical significance. + let relabeled = witness.iter().map(|group| 6 - group).collect(); + assert_eq!(reduction.extract_solution(&relabeled).unwrap(), extracted); +} + +#[test] +fn test_threedimensionalmatching_to_threepartition_equal_size_permutations() { + let (source, reduction) = reduce(2, &[(0, 0, 0), (0, 1, 1), (1, 0, 0), (1, 1, 1)]); + let target = reduction.target_problem(); + for matching in [[1, 0, 0, 1], [0, 1, 1, 0]] { + let mut witness = reduction.build_target_witness(&matching); + let mut exchanges = 0; + // Cumulative equal-size exchanges preserve a valid partition while + // exercising regular-item identities, mixed fillers, and dummy groups. + for left in 0..target.num_elements() { + for right in left + 1..target.num_elements() { + if target.sizes()[left] == target.sizes()[right] && witness[left] != witness[right] + { + witness.swap(left, right); + assert!(target.evaluate(&witness).unwrap().0); + let extracted = reduction.extract_solution(&witness).unwrap(); + assert!(source.evaluate(&extracted).unwrap().0); + exchanges += 1; + } + } + } + assert!(exchanges > 0); + } +} + +#[test] +fn test_threedimensionalmatching_to_threepartition_rejects_invalid_partitions() { + let (_, reduction) = reduce(1, &[(0, 0, 0)]); + let valid = reduction.build_target_witness(&[1]); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![]), Ok(value) if { value.is_valid() }) + ); + let mut invalid = valid.clone(); + invalid[0] = reduction.target_problem().num_groups(); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &invalid), Ok(value) if { value.is_valid() }) + ); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![0; valid.len()]), Ok(value) if { value.is_valid() }) + ); + let mut wrong_sum = valid; + wrong_sum.swap(0, 2); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &wrong_sum), Ok(value) if { value.is_valid() }) + ); +} From 0bc59e46b0ce94edab794e2405d9f1944bd22909 Mon Sep 17 00:00:00 2001 From: Xiwei Pan Date: Mon, 14 Sep 2026 12:59:24 +0800 Subject: [PATCH 02/42] refactor: separate reduction semantics, solver execution, and QUBO storage Keep witness and value mappings on executed reduction results, separate finite brute-force enumeration from model semantics, and make numeric failures explicit. Use native HiGHS execution and sprs-backed QUBO matrices; update registered construction, callers, documentation, and regression tests together. --- .claude/CLAUDE.md | 53 +- .claude/skills/add-model/SKILL.md | 28 +- .claude/skills/add-rule/SKILL.md | 120 ++- .claude/skills/check-issue/SKILL.md | 10 +- .claude/skills/dev-setup/SKILL.md | 2 +- .claude/skills/review-quality/SKILL.md | 32 +- .claude/skills/review-structural/SKILL.md | 23 +- .claude/skills/verify-reduction/SKILL.md | 423 ++++------ .config/nextest.toml | 12 +- .github/workflows/ci.yml | 2 +- Cargo.toml | 7 +- Makefile | 7 +- codecov.yml | 4 +- docs/paper/reductions.typ | 241 ++---- docs/src/design.md | 303 +++++-- docs/src/static/trait-hierarchy-dark.svg | 766 +----------------- docs/src/static/trait-hierarchy.svg | 766 +----------------- docs/src/static/trait-hierarchy.typ | 18 +- .../src/commands/evaluate.rs | 2 +- problemreductions-cli/src/commands/extract.rs | 4 +- problemreductions-cli/src/dispatch.rs | 159 +++- problemreductions-cli/src/mcp/tools.rs | 2 +- problemreductions-cli/src/test_support.rs | 59 +- problemreductions-cli/tests/cli_tests.rs | 17 +- problemreductions-macros/src/lib.rs | 36 +- src/example_db/specs.rs | 2 +- src/lib.rs | 4 +- .../algebraic/algebraic_equations_over_gf2.rs | 8 +- src/models/algebraic/bmf.rs | 17 +- .../algebraic/closest_vector_problem.rs | 149 ++-- .../consecutive_block_minimization.rs | 8 +- .../consecutive_ones_matrix_augmentation.rs | 8 +- .../algebraic/consecutive_ones_submatrix.rs | 8 +- src/models/algebraic/equilibrium_point.rs | 8 +- .../algebraic/feasible_basis_extension.rs | 8 +- src/models/algebraic/ilp.rs | 17 +- src/models/algebraic/minimum_matrix_cover.rs | 65 +- .../algebraic/minimum_matrix_domination.rs | 8 +- .../algebraic/minimum_weight_decoding.rs | 8 +- ...mum_weight_solution_to_linear_equations.rs | 8 +- src/models/algebraic/quadratic_assignment.rs | 8 +- src/models/algebraic/quadratic_congruences.rs | 13 +- .../quadratic_diophantine_equations.rs | 13 +- src/models/algebraic/qubo.rs | 130 ++- .../algebraic/simultaneous_incongruences.rs | 28 +- .../algebraic/sparse_matrix_compression.rs | 8 +- src/models/decision.rs | 58 +- src/models/formula/circuit.rs | 8 +- src/models/formula/ksat.rs | 8 +- .../formula/maximum_2_satisfiability.rs | 8 +- src/models/formula/nae_satisfiability.rs | 8 +- src/models/formula/non_tautology.rs | 8 +- .../formula/one_in_three_satisfiability.rs | 8 +- src/models/formula/planar_3_satisfiability.rs | 8 +- src/models/formula/qbf.rs | 8 +- src/models/formula/sat.rs | 8 +- src/models/graph/acyclic_partition.rs | 8 +- .../balanced_complete_bipartite_subgraph.rs | 8 +- src/models/graph/biclique_cover.rs | 11 +- .../graph/biconnectivity_augmentation.rs | 8 +- .../graph/bottleneck_traveling_salesman.rs | 8 +- .../bounded_component_spanning_forest.rs | 8 +- .../graph/bounded_diameter_spanning_tree.rs | 8 +- .../graph/degree_constrained_spanning_tree.rs | 8 +- src/models/graph/directed_hamiltonian_path.rs | 13 +- .../directed_two_commodity_integral_flow.rs | 14 +- src/models/graph/disjoint_connecting_paths.rs | 8 +- src/models/graph/eulerian_path.rs | 11 +- src/models/graph/generalized_hex.rs | 8 +- src/models/graph/graph_partitioning.rs | 8 +- src/models/graph/hamiltonian_circuit.rs | 9 +- src/models/graph/hamiltonian_path.rs | 11 +- .../hamiltonian_path_between_two_vertices.rs | 11 +- src/models/graph/highly_connected_deletion.rs | 8 +- src/models/graph/integral_flow_bundles.rs | 33 +- .../graph/integral_flow_homologous_arcs.rs | 41 +- .../graph/integral_flow_with_multipliers.rs | 41 +- src/models/graph/isomorphic_spanning_tree.rs | 8 +- src/models/graph/kclique.rs | 8 +- src/models/graph/kcoloring.rs | 8 +- src/models/graph/kernel.rs | 8 +- src/models/graph/kth_best_spanning_tree.rs | 10 +- .../graph/length_bounded_disjoint_paths.rs | 12 +- src/models/graph/longest_circuit.rs | 8 +- src/models/graph/longest_path.rs | 8 +- src/models/graph/max_cut.rs | 8 +- src/models/graph/maximal_is.rs | 8 +- src/models/graph/maximum_achromatic_number.rs | 8 +- src/models/graph/maximum_clique.rs | 8 +- src/models/graph/maximum_co_k_plex.rs | 8 +- .../graph/maximum_common_edge_subgraph.rs | 16 +- .../graph/maximum_contact_map_overlap.rs | 12 +- src/models/graph/maximum_domatic_number.rs | 9 +- .../graph/maximum_edge_weighted_k_clique.rs | 8 +- src/models/graph/maximum_independent_set.rs | 8 +- .../graph/maximum_leaf_spanning_tree.rs | 8 +- src/models/graph/maximum_matching.rs | 8 +- src/models/graph/min_max_multicenter.rs | 8 +- .../minimum_capacitated_spanning_tree.rs | 8 +- src/models/graph/minimum_cost_circulation.rs | 8 +- src/models/graph/minimum_cost_maximum_flow.rs | 8 +- .../graph/minimum_covering_by_cliques.rs | 8 +- .../graph/minimum_cut_into_bounded_sets.rs | 8 +- src/models/graph/minimum_dominating_set.rs | 8 +- .../graph/minimum_dummy_activities_pert.rs | 8 +- src/models/graph/minimum_edge_cost_flow.rs | 8 +- src/models/graph/minimum_feedback_arc_set.rs | 8 +- .../graph/minimum_feedback_vertex_set.rs | 8 +- ...imum_geometric_connected_dominating_set.rs | 8 +- src/models/graph/minimum_graph_bandwidth.rs | 9 +- .../graph/minimum_intersection_graph_basis.rs | 20 +- src/models/graph/minimum_maximal_matching.rs | 8 +- src/models/graph/minimum_metric_dimension.rs | 8 +- src/models/graph/minimum_multiway_cut.rs | 8 +- src/models/graph/minimum_sum_multicenter.rs | 8 +- src/models/graph/minimum_vertex_cover.rs | 123 +-- src/models/graph/mixed_chinese_postman.rs | 8 +- src/models/graph/mod.rs | 4 - src/models/graph/monochromatic_triangle.rs | 8 +- src/models/graph/multiple_choice_branching.rs | 8 +- .../graph/multiple_copy_file_allocation.rs | 8 +- .../graph/optimal_linear_arrangement.rs | 9 +- src/models/graph/partial_feedback_edge_set.rs | 8 +- src/models/graph/partition_into_cliques.rs | 8 +- src/models/graph/partition_into_forests.rs | 8 +- .../graph/partition_into_paths_of_length_2.rs | 9 +- .../graph/partition_into_perfect_matchings.rs | 8 +- src/models/graph/partition_into_triangles.rs | 9 +- .../graph/path_constrained_network_flow.rs | 13 +- .../graph/prize_collecting_steiner_forest.rs | 27 +- src/models/graph/rooted_tree_arrangement.rs | 13 +- src/models/graph/rural_postman.rs | 8 +- .../graph/shortest_weight_constrained_path.rs | 8 +- src/models/graph/spin_glass.rs | 8 +- src/models/graph/steiner_tree.rs | 34 +- src/models/graph/steiner_tree_in_graphs.rs | 396 --------- .../graph/strong_connectivity_augmentation.rs | 8 +- src/models/graph/subgraph_isomorphism.rs | 20 +- src/models/graph/traveling_salesman.rs | 8 +- .../graph/undirected_flow_lower_bounds.rs | 8 +- .../undirected_two_commodity_integral_flow.rs | 36 +- src/models/misc/additional_key.rs | 8 +- src/models/misc/betweenness.rs | 8 +- src/models/misc/bin_packing.rs | 9 +- .../misc/boyce_codd_normal_form_violation.rs | 8 +- src/models/misc/capacity_assignment.rs | 8 +- src/models/misc/closest_string.rs | 8 +- src/models/misc/closest_substring.rs | 34 +- src/models/misc/clustering.rs | 8 +- src/models/misc/conjunctive_boolean_query.rs | 8 +- .../misc/conjunctive_query_foldability.rs | 19 +- ...onsistency_of_database_frequency_tables.rs | 28 +- src/models/misc/cosine_product_integration.rs | 8 +- src/models/misc/cyclic_ordering.rs | 8 +- src/models/misc/dynamic_storage_allocation.rs | 17 +- src/models/misc/ensemble_computation.rs | 16 +- src/models/misc/expected_retrieval_cost.rs | 33 +- src/models/misc/factoring.rs | 10 +- .../misc/feasible_register_assignment.rs | 8 +- src/models/misc/flow_shop_scheduling.rs | 10 +- src/models/misc/grouping_by_swapping.rs | 8 +- .../misc/integer_expression_membership.rs | 8 +- src/models/misc/job_shop_scheduling.rs | 25 +- src/models/misc/knapsack.rs | 8 +- src/models/misc/kth_largest_m_tuple.rs | 23 +- src/models/misc/longest_common_subsequence.rs | 10 +- src/models/misc/maximum_likelihood_ranking.rs | 9 +- src/models/misc/minimum_axiom_set.rs | 8 +- .../minimum_code_generation_one_register.rs | 9 +- ...um_code_generation_parallel_assignments.rs | 9 +- ...mum_code_generation_unlimited_registers.rs | 9 +- src/models/misc/minimum_decision_tree.rs | 28 +- ...imum_discrete_planar_inverse_kinematics.rs | 28 +- .../misc/minimum_disjunctive_normal_form.rs | 8 +- ...minimum_external_macro_data_compression.rs | 34 +- .../misc/minimum_fault_detection_test_set.rs | 12 +- ...minimum_internal_macro_data_compression.rs | 20 +- .../minimum_register_sufficiency_for_loops.rs | 9 +- .../misc/minimum_tardiness_sequencing.rs | 16 +- .../misc/minimum_weight_and_or_graph.rs | 8 +- src/models/misc/multiprocessor_scheduling.rs | 8 +- .../misc/non_liveness_free_petri_net.rs | 8 +- .../misc/numerical_3_dimensional_matching.rs | 8 +- .../numerical_matching_with_target_sums.rs | 9 +- src/models/misc/open_shop_scheduling.rs | 34 +- .../optimum_communication_spanning_tree.rs | 8 +- src/models/misc/paintshop.rs | 8 +- src/models/misc/partially_ordered_knapsack.rs | 8 +- src/models/misc/partition.rs | 8 +- .../misc/precedence_constrained_scheduling.rs | 38 +- src/models/misc/preemptive_scheduling.rs | 11 +- src/models/misc/production_planning.rs | 40 +- .../misc/rectilinear_picture_compression.rs | 8 +- src/models/misc/register_sufficiency.rs | 8 +- .../misc/resource_constrained_scheduling.rs | 8 +- ...ng_to_minimize_weighted_completion_time.rs | 10 +- .../scheduling_with_individual_deadlines.rs | 32 +- ...ing_to_minimize_maximum_cumulative_cost.rs | 8 +- ...equencing_to_minimize_tardy_task_weight.rs | 11 +- ...ng_to_minimize_weighted_completion_time.rs | 10 +- ...quencing_to_minimize_weighted_tardiness.rs | 8 +- ...uencing_with_deadlines_and_set_up_times.rs | 11 +- ...encing_with_release_times_and_deadlines.rs | 10 +- .../misc/sequencing_within_intervals.rs | 11 +- .../misc/shortest_common_supersequence.rs | 10 +- .../misc/shortest_common_superstring.rs | 10 +- src/models/misc/square_tiling.rs | 10 +- src/models/misc/stacker_crane.rs | 8 +- src/models/misc/staff_scheduling.rs | 35 +- .../misc/string_to_string_correction.rs | 8 +- src/models/misc/subset_product.rs | 8 +- src/models/misc/subset_sum.rs | 8 +- src/models/misc/sum_of_squares_partition.rs | 8 +- src/models/misc/three_partition.rs | 8 +- src/models/misc/timetable_design.rs | 8 +- src/models/mod.rs | 5 +- src/models/set/comparative_containment.rs | 8 +- src/models/set/consecutive_sets.rs | 11 +- src/models/set/exact_cover_by_3_sets.rs | 8 +- src/models/set/integer_knapsack.rs | 37 +- src/models/set/maximum_set_packing.rs | 8 +- src/models/set/minimum_cardinality_key.rs | 8 +- src/models/set/minimum_hitting_set.rs | 8 +- src/models/set/minimum_set_covering.rs | 8 +- src/models/set/prime_attribute_name.rs | 8 +- .../set/rooted_tree_storage_assignment.rs | 8 +- src/models/set/set_basis.rs | 10 +- src/models/set/set_splitting.rs | 8 +- src/models/set/three_dimensional_matching.rs | 8 +- .../set/two_dimensional_consecutive_sets.rs | 8 +- src/registry/dyn_problem.rs | 126 +-- src/rules/acyclicpartition_ilp.rs | 9 +- .../balancedcompletebipartitesubgraph_ilp.rs | 2 - src/rules/bicliquecover_bmf.rs | 2 - src/rules/biconnectivityaugmentation_ilp.rs | 9 - src/rules/binpacking_ilp.rs | 4 +- src/rules/bmf_bicliquecover.rs | 2 - src/rules/bmf_ilp.rs | 2 - src/rules/bottlenecktravelingsalesman_ilp.rs | 7 - .../boundedcomponentspanningforest_ilp.rs | 4 +- src/rules/capacityassignment_ilp.rs | 6 +- src/rules/circuit_ilp.rs | 9 - src/rules/circuit_sat.rs | 2 - src/rules/circuit_spinglass.rs | 8 - src/rules/closeststring_ilp.rs | 26 +- src/rules/closestsubstring_ilp.rs | 49 +- src/rules/closestvectorproblem_qubo.rs | 52 +- src/rules/clustering_ilp.rs | 6 +- src/rules/coloring_ilp.rs | 9 +- src/rules/coloring_qubo.rs | 44 +- src/rules/consecutiveblockminimization_ilp.rs | 9 +- .../consecutiveonesmatrixaugmentation_ilp.rs | 9 +- src/rules/consecutiveonessubmatrix_ilp.rs | 2 - ...onsistencyofdatabasefrequencytables_ilp.rs | 25 +- ...ximumindependentset_integralflowbundles.rs | 7 - ...imumdominatingset_minimumsummulticenter.rs | 7 - ...nminimumdominatingset_minmaxmulticenter.rs | 7 - ...onminimumvertexcover_hamiltoniancircuit.rs | 88 +- src/rules/directedhamiltonianpath_ilp.rs | 4 +- .../directedtwocommodityintegralflow_ilp.rs | 2 - src/rules/disjointconnectingpaths_ilp.rs | 9 +- src/rules/ensemblecomputation_ilp.rs | 26 +- src/rules/eulerianpath_ilp.rs | 46 +- ...tcoverby3sets_algebraicequationsovergf2.rs | 2 - ...overby3sets_boundeddiameterspanningtree.rs | 8 - src/rules/exactcoverby3sets_ilp.rs | 2 - .../exactcoverby3sets_maximumsetpacking.rs | 2 - .../exactcoverby3sets_minimumaxiomset.rs | 2 - ...verby3sets_minimumfaultdetectiontestset.rs | 2 - .../exactcoverby3sets_staffscheduling.rs | 2 - src/rules/exactcoverby3sets_subsetproduct.rs | 2 - src/rules/expectedretrievalcost_ilp.rs | 23 +- src/rules/factoring_circuit.rs | 23 +- src/rules/factoring_ilp.rs | 2 - src/rules/feasibleregisterassignment_ilp.rs | 2 - src/rules/flowshopscheduling_ilp.rs | 2 - src/rules/graph.rs | 105 ++- src/rules/graph_helpers.rs | 90 +- src/rules/graphpartitioning_ilp.rs | 2 - src/rules/graphpartitioning_maxcut.rs | 2 - src/rules/graphpartitioning_qubo.rs | 15 +- ...oniancircuit_biconnectivityaugmentation.rs | 54 +- ...niancircuit_bottlenecktravelingsalesman.rs | 7 +- .../hamiltoniancircuit_hamiltonianpath.rs | 39 +- .../hamiltoniancircuit_longestcircuit.rs | 13 +- .../hamiltoniancircuit_quadraticassignment.rs | 8 - src/rules/hamiltoniancircuit_ruralpostman.rs | 21 +- src/rules/hamiltoniancircuit_stackercrane.rs | 7 - ...ncircuit_strongconnectivityaugmentation.rs | 18 - .../hamiltoniancircuit_travelingsalesman.rs | 7 +- ...onianpath_degreeconstrainedspanningtree.rs | 48 +- src/rules/hamiltonianpath_ilp.rs | 9 +- .../hamiltonianpath_isomorphicspanningtree.rs | 2 - ...onianpathbetweentwovertices_longestpath.rs | 8 - src/rules/highlyconnecteddeletion_ilp.rs | 38 +- src/rules/ilp_bool_ilp_i64.rs | 2 - src/rules/ilp_casts.rs | 110 --- src/rules/ilp_helpers.rs | 52 +- src/rules/ilp_i64_ilp_bool.rs | 31 +- src/rules/ilp_i64_ilp_f64.rs | 86 ++ src/rules/ilp_qubo.rs | 40 +- src/rules/integerknapsack_ilp.rs | 2 - src/rules/integralflowbundles_ilp.rs | 2 - src/rules/integralflowhomologousarcs_ilp.rs | 2 - src/rules/integralflowwithmultipliers_ilp.rs | 2 - src/rules/isomorphicspanningtree_ilp.rs | 9 +- ...lique_balancedcompletebipartitesubgraph.rs | 2 - src/rules/kclique_conjunctivebooleanquery.rs | 2 - src/rules/kclique_ilp.rs | 2 - src/rules/kclique_subgraphisomorphism.rs | 2 - src/rules/kcoloring_bicliquecover.rs | 32 +- src/rules/kcoloring_clustering.rs | 2 - src/rules/kcoloring_partitionintocliques.rs | 2 - ...kcoloring_twodimensionalconsecutivesets.rs | 8 - src/rules/knapsack_ilp.rs | 2 - src/rules/knapsack_qubo.rs | 24 +- src/rules/ksatisfiability_acyclicpartition.rs | 7 - src/rules/ksatisfiability_bicliquecover.rs | 27 +- src/rules/ksatisfiability_cyclicordering.rs | 7 - ...bility_directedtwocommodityintegralflow.rs | 2 - ...tisfiability_feasibleregisterassignment.rs | 7 - src/rules/ksatisfiability_kclique.rs | 7 - src/rules/ksatisfiability_kernel.rs | 7 - .../ksatisfiability_minimumvertexcover.rs | 32 +- .../ksatisfiability_monochromatictriangle.rs | 1 - ...satisfiability_oneinthreesatisfiability.rs | 7 - .../ksatisfiability_preemptivescheduling.rs | 2 - .../ksatisfiability_quadraticcongruences.rs | 7 - ...fiability_quadraticdiophantineequations.rs | 2 - src/rules/ksatisfiability_qubo.rs | 40 +- .../ksatisfiability_registersufficiency.rs | 13 - ...atisfiability_simultaneousincongruences.rs | 8 +- src/rules/ksatisfiability_subsetsum.rs | 2 - src/rules/ksatisfiability_timetabledesign.rs | 2 - src/rules/lengthboundeddisjointpaths_ilp.rs | 17 +- src/rules/longestcircuit_ilp.rs | 2 - src/rules/longestcommonsubsequence_ilp.rs | 4 +- ...commonsubsequence_maximumindependentset.rs | 2 - src/rules/longestpath_ilp.rs | 2 - src/rules/maxcut_minimumcutintoboundedsets.rs | 2 - src/rules/maxcut_minimummatrixcover.rs | 14 +- src/rules/maximalis_ilp.rs | 2 - src/rules/maximum2satisfiability_ilp.rs | 2 - src/rules/maximum2satisfiability_maxcut.rs | 2 - src/rules/maximumclique_ilp.rs | 2 - .../maximumclique_maximumindependentset.rs | 2 - src/rules/maximumcokplex_ilp.rs | 2 - src/rules/maximumcommonedgesubgraph_ilp.rs | 17 +- src/rules/maximumcontactmapoverlap_ilp.rs | 17 +- src/rules/maximumdomaticnumber_ilp.rs | 2 - src/rules/maximumedgeweightedkclique_ilp.rs | 2 - src/rules/maximumindependentset_gridgraph.rs | 2 - .../maximumindependentset_maximumclique.rs | 2 - ...maximumindependentset_maximumsetpacking.rs | 4 - src/rules/maximumindependentset_triangular.rs | 2 - src/rules/maximumleafspanningtree_ilp.rs | 2 - src/rules/maximumlikelihoodranking_ilp.rs | 2 - src/rules/maximummatching_ilp.rs | 2 - .../maximummatching_maximumsetpacking.rs | 2 - src/rules/maximumsetpacking_ilp.rs | 4 +- src/rules/maximumsetpacking_qubo.rs | 10 +- .../minimumcapacitatedspanningtree_ilp.rs | 2 - ...mcostmaximumflow_minimumcostcirculation.rs | 2 - src/rules/minimumcoveringbycliques_ilp.rs | 16 +- ...bycliques_minimumintersectiongraphbasis.rs | 55 +- src/rules/minimumcutintoboundedsets_ilp.rs | 2 - ...mumdiscreteplanarinversekinematics_qubo.rs | 80 +- src/rules/minimumdominatingset_ilp.rs | 2 - src/rules/minimumedgecostflow_ilp.rs | 2 - ...minimumexternalmacrodatacompression_ilp.rs | 38 +- src/rules/minimumfaultdetectiontestset_ilp.rs | 2 - src/rules/minimumfeedbackarcset_ilp.rs | 2 - src/rules/minimumfeedbackvertexset_ilp.rs | 2 - ...minimumcodegenerationunlimitedregisters.rs | 7 - src/rules/minimumgraphbandwidth_ilp.rs | 6 +- src/rules/minimumhittingset_ilp.rs | 2 - ...minimuminternalmacrodatacompression_ilp.rs | 2 - src/rules/minimummatrixcover_ilp.rs | 4 +- src/rules/minimummaximalmatching_ilp.rs | 2 - ...maximalmatching_maximumachromaticnumber.rs | 2 - ...maximalmatching_minimummatrixdomination.rs | 182 +---- src/rules/minimummetricdimension_ilp.rs | 2 - src/rules/minimummultiwaycut_ilp.rs | 2 - src/rules/minimummultiwaycut_qubo.rs | 76 +- src/rules/minimumsetcovering_ilp.rs | 2 - src/rules/minimumsummulticenter_ilp.rs | 2 - src/rules/minimumtardinesssequencing_ilp.rs | 8 +- ...nimumvertexcover_comparativecontainment.rs | 7 - .../minimumvertexcover_ensemblecomputation.rs | 16 +- ...mumvertexcover_longestcommonsubsequence.rs | 2 - ...inimumvertexcover_maximumindependentset.rs | 4 - ...inimumvertexcover_minimumfeedbackarcset.rs | 2 - ...mumvertexcover_minimumfeedbackvertexset.rs | 2 - .../minimumvertexcover_minimumhittingset.rs | 2 - .../minimumvertexcover_minimumsetcovering.rs | 2 - ...imumvertexcover_minimumweightandorgraph.rs | 2 - src/rules/minimumweightdecoding_ilp.rs | 2 - src/rules/minmaxmulticenter_ilp.rs | 2 - src/rules/mixedchinesepostman_ilp.rs | 2 - src/rules/mod.rs | 6 +- src/rules/monochromatictriangle_ilp.rs | 2 - src/rules/multiplechoicebranching_ilp.rs | 1 - src/rules/multiplecopyfileallocation_ilp.rs | 2 - src/rules/multiprocessorscheduling_ilp.rs | 6 +- src/rules/naesatisfiability_ilp.rs | 2 - src/rules/naesatisfiability_maxcut.rs | 8 - ...fiability_partitionintoperfectmatchings.rs | 2 - src/rules/naesatisfiability_setsplitting.rs | 2 - ...atching_numericalmatchingwithtargetsums.rs | 55 +- .../numericalmatchingwithtargetsums_ilp.rs | 2 - src/rules/openshopscheduling_ilp.rs | 1 - ...ement_consecutiveonesmatrixaugmentation.rs | 7 - src/rules/optimallineararrangement_ilp.rs | 6 +- ...uencingtominimizeweightedcompletiontime.rs | 2 - .../optimumcommunicationspanningtree_ilp.rs | 2 - src/rules/paintshop_ilp.rs | 2 - src/rules/paintshop_qubo.rs | 24 +- src/rules/partiallyorderedknapsack_ilp.rs | 2 - src/rules/partition_binpacking.rs | 2 - .../partition_cosineproductintegration.rs | 2 - .../partition_integralflowwithmultipliers.rs | 15 +- src/rules/partition_knapsack.rs | 2 - .../partition_multiprocessorscheduling.rs | 2 - src/rules/partition_openshopscheduling.rs | 38 +- src/rules/partition_productionplanning.rs | 2 - ...ion_sequencingtominimizetardytaskweight.rs | 16 +- src/rules/partition_subsetsum.rs | 15 - src/rules/partition_sumofsquarespartition.rs | 9 - src/rules/partitionintocliques_ilp.rs | 19 +- ...ionintocliques_minimumcoveringbycliques.rs | 74 +- ...flength2_boundedcomponentspanningforest.rs | 2 - src/rules/partitionintopathsoflength2_ilp.rs | 6 +- src/rules/partitionintotriangles_ilp.rs | 6 +- src/rules/pathconstrainednetworkflow_ilp.rs | 2 - .../precedenceconstrainedscheduling_ilp.rs | 6 +- src/rules/preemptivescheduling_ilp.rs | 2 - ...rizecollectingsteinerforest_steinertree.rs | 86 +- src/rules/quadraticassignment_ilp.rs | 6 +- src/rules/qubo_casts.rs | 24 +- src/rules/qubo_ilp.rs | 12 +- .../rectilinearpicturecompression_ilp.rs | 2 - src/rules/registersufficiency_ilp.rs | 2 - src/rules/registry.rs | 19 +- .../resourceconstrainedscheduling_ilp.rs | 6 +- ...arrangement_rootedtreestorageassignment.rs | 2 - src/rules/rootedtreestorageassignment_ilp.rs | 4 +- src/rules/ruralpostman_ilp.rs | 2 - src/rules/sat_circuitsat.rs | 5 +- src/rules/sat_coloring.rs | 18 - src/rules/sat_ksat.rs | 4 - src/rules/sat_maximumindependentset.rs | 9 - src/rules/sat_minimumdominatingset.rs | 9 - ...tisfiability_integralflowhomologousarcs.rs | 2 - .../satisfiability_maximum2satisfiability.rs | 9 - src/rules/satisfiability_naesatisfiability.rs | 9 - src/rules/satisfiability_nontautology.rs | 2 - ...ingtominimizeweightedcompletiontime_ilp.rs | 9 +- .../schedulingwithindividualdeadlines_ilp.rs | 9 +- ...cingtominimizemaximumcumulativecost_ilp.rs | 4 +- ...sequencingtominimizetardytaskweight_ilp.rs | 10 +- ...ingtominimizeweightedcompletiontime_ilp.rs | 2 - ...quencingtominimizeweightedtardiness_ilp.rs | 2 - ...equencingwithdeadlinesandsetuptimes_ilp.rs | 4 +- src/rules/sequencingwithinintervals_ilp.rs | 23 +- ...uencingwithreleasetimesanddeadlines_ilp.rs | 4 +- src/rules/setsplitting_betweenness.rs | 2 - src/rules/setsplitting_ilp.rs | 2 - src/rules/shortestcommonsupersequence_ilp.rs | 4 +- .../shortestweightconstrainedpath_ilp.rs | 2 - src/rules/sparsematrixcompression_ilp.rs | 6 +- src/rules/spinglass_maxcut.rs | 4 - src/rules/spinglass_qubo.rs | 41 +- src/rules/stackercrane_ilp.rs | 4 +- src/rules/steinertree_ilp.rs | 12 +- src/rules/steinertreeingraphs_ilp.rs | 167 ---- src/rules/stringtostringcorrection_ilp.rs | 16 +- .../strongconnectivityaugmentation_ilp.rs | 2 - src/rules/subgraphisomorphism_ilp.rs | 6 +- src/rules/subsetsum_closestvectorproblem.rs | 30 +- .../subsetsum_integerexpressionmembership.rs | 2 - src/rules/subsetsum_partition.rs | 2 - src/rules/sumofsquarespartition_ilp.rs | 6 +- src/rules/test_helpers.rs | 27 +- src/rules/threedimensionalmatching_ilp.rs | 2 - ...mensionalmatching_minimumweightdecoding.rs | 9 - ...partition_resourceconstrainedscheduling.rs | 2 - ..._sequencingwithreleasetimesanddeadlines.rs | 19 +- src/rules/timetabledesign_ilp.rs | 2 - src/rules/traits.rs | 39 +- src/rules/travelingsalesman_ilp.rs | 19 +- src/rules/travelingsalesman_qubo.rs | 215 +++-- src/rules/undirectedflowlowerbounds_ilp.rs | 2 - .../undirectedtwocommodityintegralflow_ilp.rs | 2 - src/rules/unitdiskmapping/ksg/mapping.rs | 61 +- .../unitdiskmapping/triangular/mapping.rs | 18 +- src/solvers/brute_force.rs | 106 ++- .../customized/closest_vector_problem.rs | 32 +- .../customized/minimum_decision_tree.rs | 25 +- .../customized/shortest_common_superstring.rs | 29 +- src/solvers/customized/solver.rs | 12 +- src/solvers/ilp/adapter.rs | 239 ++++++ src/solvers/ilp/mod.rs | 4 +- src/solvers/ilp/solver.rs | 214 +---- src/solvers/mod.rs | 18 +- src/solvers/pipelines.rs | 147 ---- src/solvers/registry.rs | 119 ++- src/solvers/resolver.rs | 72 +- src/truth_table.rs | 97 ++- src/types.rs | 55 +- src/unit_tests/example_db.rs | 30 +- src/unit_tests/graph_models.rs | 11 +- .../algebraic/algebraic_equations_over_gf2.rs | 11 +- src/unit_tests/models/algebraic/bmf.rs | 19 +- .../algebraic/closest_vector_problem.rs | 80 +- .../consecutive_block_minimization.rs | 7 +- .../consecutive_ones_matrix_augmentation.rs | 7 +- .../algebraic/consecutive_ones_submatrix.rs | 11 +- .../models/algebraic/equilibrium_point.rs | 7 +- .../algebraic/feasible_basis_extension.rs | 6 +- src/unit_tests/models/algebraic/ilp.rs | 5 +- .../models/algebraic/minimum_matrix_cover.rs | 37 +- .../algebraic/minimum_matrix_domination.rs | 11 +- .../algebraic/minimum_weight_decoding.rs | 6 +- ...mum_weight_solution_to_linear_equations.rs | 6 +- .../models/algebraic/quadratic_assignment.rs | 11 +- .../models/algebraic/quadratic_congruences.rs | 22 +- .../quadratic_diophantine_equations.rs | 17 +- src/unit_tests/models/algebraic/qubo.rs | 108 ++- .../algebraic/simultaneous_incongruences.rs | 28 +- .../algebraic/sparse_matrix_compression.rs | 6 +- src/unit_tests/models/decision.rs | 63 +- src/unit_tests/models/formula/circuit.rs | 11 +- src/unit_tests/models/formula/ksat.rs | 13 +- .../formula/maximum_2_satisfiability.rs | 6 +- .../models/formula/nae_satisfiability.rs | 2 +- .../models/formula/non_tautology.rs | 7 +- .../formula/one_in_three_satisfiability.rs | 7 +- .../models/formula/planar_3_satisfiability.rs | 7 +- src/unit_tests/models/formula/qbf.rs | 22 +- src/unit_tests/models/formula/sat.rs | 6 +- .../models/graph/acyclic_partition.rs | 7 +- .../balanced_complete_bipartite_subgraph.rs | 6 +- src/unit_tests/models/graph/biclique_cover.rs | 14 +- .../graph/biconnectivity_augmentation.rs | 7 +- .../graph/bottleneck_traveling_salesman.rs | 7 +- .../bounded_component_spanning_forest.rs | 6 +- .../graph/bounded_diameter_spanning_tree.rs | 6 +- .../graph/degree_constrained_spanning_tree.rs | 6 +- .../models/graph/directed_hamiltonian_path.rs | 11 +- .../directed_two_commodity_integral_flow.rs | 18 +- .../models/graph/disjoint_connecting_paths.rs | 6 +- src/unit_tests/models/graph/eulerian_path.rs | 14 +- .../models/graph/generalized_hex.rs | 6 +- .../models/graph/graph_partitioning.rs | 6 +- .../models/graph/hamiltonian_circuit.rs | 11 +- .../models/graph/hamiltonian_path.rs | 6 +- .../hamiltonian_path_between_two_vertices.rs | 6 +- .../models/graph/highly_connected_deletion.rs | 7 +- .../models/graph/integral_flow_bundles.rs | 6 +- .../graph/integral_flow_homologous_arcs.rs | 11 +- .../graph/integral_flow_with_multipliers.rs | 3 +- .../models/graph/isomorphic_spanning_tree.rs | 6 +- src/unit_tests/models/graph/kclique.rs | 6 +- src/unit_tests/models/graph/kcoloring.rs | 13 +- src/unit_tests/models/graph/kernel.rs | 6 +- .../models/graph/kth_best_spanning_tree.rs | 6 +- .../graph/length_bounded_disjoint_paths.rs | 7 +- .../models/graph/longest_circuit.rs | 6 +- src/unit_tests/models/graph/longest_path.rs | 6 +- src/unit_tests/models/graph/max_cut.rs | 8 +- src/unit_tests/models/graph/maximal_is.rs | 9 +- .../models/graph/maximum_achromatic_number.rs | 6 +- src/unit_tests/models/graph/maximum_clique.rs | 11 +- .../models/graph/maximum_co_k_plex.rs | 6 +- .../graph/maximum_common_edge_subgraph.rs | 7 +- .../graph/maximum_contact_map_overlap.rs | 7 +- .../models/graph/maximum_domatic_number.rs | 7 +- .../graph/maximum_edge_weighted_k_clique.rs | 7 +- .../models/graph/maximum_independent_set.rs | 10 +- .../graph/maximum_leaf_spanning_tree.rs | 18 +- .../models/graph/maximum_matching.rs | 4 +- .../models/graph/min_max_multicenter.rs | 6 +- .../minimum_capacitated_spanning_tree.rs | 8 +- .../models/graph/minimum_cost_circulation.rs | 6 +- .../models/graph/minimum_cost_maximum_flow.rs | 6 +- .../graph/minimum_covering_by_cliques.rs | 7 +- .../graph/minimum_cut_into_bounded_sets.rs | 14 +- .../models/graph/minimum_dominating_set.rs | 9 +- .../graph/minimum_dummy_activities_pert.rs | 6 +- .../models/graph/minimum_edge_cost_flow.rs | 6 +- .../models/graph/minimum_feedback_arc_set.rs | 13 +- .../graph/minimum_feedback_vertex_set.rs | 6 +- ...imum_geometric_connected_dominating_set.rs | 7 +- .../models/graph/minimum_graph_bandwidth.rs | 11 +- .../graph/minimum_intersection_graph_basis.rs | 12 +- .../models/graph/minimum_maximal_matching.rs | 2 +- .../models/graph/minimum_metric_dimension.rs | 7 +- .../models/graph/minimum_multiway_cut.rs | 8 +- .../models/graph/minimum_sum_multicenter.rs | 6 +- .../models/graph/minimum_vertex_cover.rs | 4 +- .../models/graph/mixed_chinese_postman.rs | 6 +- .../models/graph/monochromatic_triangle.rs | 6 +- .../models/graph/multiple_choice_branching.rs | 7 +- .../graph/multiple_copy_file_allocation.rs | 6 +- .../graph/optimal_linear_arrangement.rs | 11 +- .../models/graph/partial_feedback_edge_set.rs | 7 +- .../models/graph/partition_into_cliques.rs | 6 +- .../models/graph/partition_into_forests.rs | 6 +- .../graph/partition_into_paths_of_length_2.rs | 6 +- .../graph/partition_into_perfect_matchings.rs | 6 +- .../models/graph/partition_into_triangles.rs | 11 +- .../graph/path_constrained_network_flow.rs | 6 +- .../graph/prize_collecting_steiner_forest.rs | 29 +- .../models/graph/rooted_tree_arrangement.rs | 6 +- src/unit_tests/models/graph/rural_postman.rs | 13 +- .../graph/shortest_weight_constrained_path.rs | 6 +- src/unit_tests/models/graph/spin_glass.rs | 4 +- src/unit_tests/models/graph/steiner_tree.rs | 41 +- .../models/graph/steiner_tree_in_graphs.rs | 232 ------ .../graph/strong_connectivity_augmentation.rs | 11 +- .../models/graph/subgraph_isomorphism.rs | 6 +- .../models/graph/traveling_salesman.rs | 8 +- .../graph/undirected_flow_lower_bounds.rs | 6 +- .../undirected_two_commodity_integral_flow.rs | 3 +- src/unit_tests/models/misc/additional_key.rs | 6 +- src/unit_tests/models/misc/betweenness.rs | 7 +- src/unit_tests/models/misc/bin_packing.rs | 18 +- .../misc/boyce_codd_normal_form_violation.rs | 7 +- .../models/misc/capacity_assignment.rs | 6 +- src/unit_tests/models/misc/closest_string.rs | 17 +- .../models/misc/closest_substring.rs | 39 +- src/unit_tests/models/misc/clustering.rs | 6 +- .../models/misc/conjunctive_boolean_query.rs | 6 +- .../misc/conjunctive_query_foldability.rs | 22 +- ...onsistency_of_database_frequency_tables.rs | 5 +- .../models/misc/cosine_product_integration.rs | 6 +- src/unit_tests/models/misc/cyclic_ordering.rs | 7 +- .../models/misc/dynamic_storage_allocation.rs | 7 +- .../models/misc/ensemble_computation.rs | 7 +- .../models/misc/expected_retrieval_cost.rs | 7 +- src/unit_tests/models/misc/factoring.rs | 6 +- .../misc/feasible_register_assignment.rs | 11 +- .../models/misc/flow_shop_scheduling.rs | 18 +- .../models/misc/grouping_by_swapping.rs | 7 +- .../misc/integer_expression_membership.rs | 16 +- .../models/misc/job_shop_scheduling.rs | 3 +- src/unit_tests/models/misc/knapsack.rs | 11 +- .../models/misc/kth_largest_m_tuple.rs | 22 +- .../models/misc/longest_common_subsequence.rs | 13 +- .../models/misc/maximum_likelihood_ranking.rs | 11 +- .../models/misc/minimum_axiom_set.rs | 12 +- .../minimum_code_generation_one_register.rs | 6 +- ...um_code_generation_parallel_assignments.rs | 6 +- ...mum_code_generation_unlimited_registers.rs | 6 +- .../models/misc/minimum_decision_tree.rs | 22 +- ...imum_discrete_planar_inverse_kinematics.rs | 12 +- .../misc/minimum_disjunctive_normal_form.rs | 6 +- ...minimum_external_macro_data_compression.rs | 8 +- .../misc/minimum_fault_detection_test_set.rs | 7 +- ...minimum_internal_macro_data_compression.rs | 8 +- .../minimum_register_sufficiency_for_loops.rs | 6 +- .../misc/minimum_tardiness_sequencing.rs | 16 +- .../misc/minimum_weight_and_or_graph.rs | 7 +- .../models/misc/multiprocessor_scheduling.rs | 16 +- .../misc/non_liveness_free_petri_net.rs | 7 +- .../misc/numerical_3_dimensional_matching.rs | 7 +- .../numerical_matching_with_target_sums.rs | 7 +- .../models/misc/open_shop_scheduling.rs | 16 +- .../optimum_communication_spanning_tree.rs | 6 +- src/unit_tests/models/misc/paintshop.rs | 4 +- .../models/misc/partially_ordered_knapsack.rs | 11 +- src/unit_tests/models/misc/partition.rs | 6 +- .../misc/precedence_constrained_scheduling.rs | 11 +- .../models/misc/preemptive_scheduling.rs | 11 +- .../models/misc/production_planning.rs | 6 +- .../misc/rectilinear_picture_compression.rs | 16 +- .../models/misc/register_sufficiency.rs | 11 +- .../misc/resource_constrained_scheduling.rs | 18 +- ...ng_to_minimize_weighted_completion_time.rs | 11 +- .../scheduling_with_individual_deadlines.rs | 6 +- ...ing_to_minimize_maximum_cumulative_cost.rs | 11 +- ...equencing_to_minimize_tardy_task_weight.rs | 16 +- ...ng_to_minimize_weighted_completion_time.rs | 16 +- ...quencing_to_minimize_weighted_tardiness.rs | 6 +- ...uencing_with_deadlines_and_set_up_times.rs | 6 +- ...encing_with_release_times_and_deadlines.rs | 16 +- .../misc/sequencing_within_intervals.rs | 21 +- .../misc/shortest_common_supersequence.rs | 6 +- .../misc/shortest_common_superstring.rs | 6 +- src/unit_tests/models/misc/square_tiling.rs | 7 +- src/unit_tests/models/misc/stacker_crane.rs | 6 +- .../models/misc/staff_scheduling.rs | 6 +- .../misc/string_to_string_correction.rs | 11 +- src/unit_tests/models/misc/subset_product.rs | 11 +- src/unit_tests/models/misc/subset_sum.rs | 11 +- .../models/misc/sum_of_squares_partition.rs | 6 +- src/unit_tests/models/misc/three_partition.rs | 7 +- .../models/misc/timetable_design.rs | 6 +- .../models/set/comparative_containment.rs | 7 +- src/unit_tests/models/set/consecutive_sets.rs | 7 +- .../models/set/exact_cover_by_3_sets.rs | 7 +- src/unit_tests/models/set/integer_knapsack.rs | 35 +- .../models/set/maximum_set_packing.rs | 4 +- .../models/set/minimum_cardinality_key.rs | 7 +- .../models/set/minimum_hitting_set.rs | 7 +- .../models/set/minimum_set_covering.rs | 4 +- .../models/set/prime_attribute_name.rs | 7 +- .../set/rooted_tree_storage_assignment.rs | 6 +- src/unit_tests/models/set/set_basis.rs | 17 +- src/unit_tests/models/set/set_splitting.rs | 2 +- .../models/set/three_dimensional_matching.rs | 7 +- .../set/two_dimensional_consecutive_sets.rs | 7 +- src/unit_tests/reduction_graph.rs | 32 +- src/unit_tests/registry/dispatch.rs | 102 ++- src/unit_tests/registry/variant.rs | 14 +- src/unit_tests/rules/acyclicpartition_ilp.rs | 5 +- .../balancedcompletebipartitesubgraph_ilp.rs | 5 +- .../rules/biconnectivityaugmentation_ilp.rs | 16 +- .../rules/bottlenecktravelingsalesman_ilp.rs | 27 +- .../boundedcomponentspanningforest_ilp.rs | 5 +- .../rules/capacityassignment_ilp.rs | 10 +- src/unit_tests/rules/circuit_ilp.rs | 8 +- src/unit_tests/rules/circuit_spinglass.rs | 13 +- src/unit_tests/rules/closeststring_ilp.rs | 10 +- src/unit_tests/rules/closestsubstring_ilp.rs | 10 +- .../rules/closestvectorproblem_qubo.rs | 38 +- src/unit_tests/rules/clustering_ilp.rs | 5 +- src/unit_tests/rules/coloring_ilp.rs | 11 +- src/unit_tests/rules/coloring_qubo.rs | 17 +- ...onsistencyofdatabasefrequencytables_ilp.rs | 5 +- ...ximumindependentset_integralflowbundles.rs | 4 +- ...imumdominatingset_minimumsummulticenter.rs | 19 +- ...nminimumdominatingset_minmaxmulticenter.rs | 12 +- ...onminimumvertexcover_hamiltoniancircuit.rs | 49 +- .../rules/directedhamiltonianpath_ilp.rs | 5 +- .../directedtwocommodityintegralflow_ilp.rs | 10 +- .../rules/ensemblecomputation_ilp.rs | 10 +- src/unit_tests/rules/eulerianpath_ilp.rs | 5 +- ...tcoverby3sets_algebraicequationsovergf2.rs | 4 +- ...overby3sets_boundeddiameterspanningtree.rs | 22 +- .../exactcoverby3sets_staffscheduling.rs | 8 +- .../rules/exactcoverby3sets_subsetproduct.rs | 4 +- .../rules/expectedretrievalcost_ilp.rs | 33 +- src/unit_tests/rules/factoring_circuit.rs | 16 +- src/unit_tests/rules/factoring_ilp.rs | 14 +- .../rules/feasibleregisterassignment_ilp.rs | 5 +- .../rules/flowshopscheduling_ilp.rs | 5 +- src/unit_tests/rules/graph.rs | 246 +++++- .../rules/graphpartitioning_qubo.rs | 8 +- ...oniancircuit_biconnectivityaugmentation.rs | 29 +- .../hamiltoniancircuit_longestcircuit.rs | 19 +- .../hamiltoniancircuit_quadraticassignment.rs | 12 +- .../rules/hamiltoniancircuit_ruralpostman.rs | 28 + .../rules/hamiltoniancircuit_stackercrane.rs | 17 +- src/unit_tests/rules/hamiltonianpath_ilp.rs | 5 +- ...onianpathbetweentwovertices_longestpath.rs | 15 +- .../rules/highlyconnecteddeletion_ilp.rs | 23 +- src/unit_tests/rules/ilp_helpers.rs | 15 +- src/unit_tests/rules/ilp_i64_ilp_bool.rs | 24 +- .../{ilp_casts.rs => ilp_i64_ilp_f64.rs} | 25 +- src/unit_tests/rules/ilp_qubo.rs | 21 +- .../rules/integralflowbundles_ilp.rs | 5 +- .../rules/kcoloring_bicliquecover.rs | 13 +- ...kcoloring_twodimensionalconsecutivesets.rs | 13 +- src/unit_tests/rules/knapsack_qubo.rs | 2 +- .../rules/ksatisfiability_acyclicpartition.rs | 8 +- .../rules/ksatisfiability_bicliquecover.rs | 8 +- .../rules/ksatisfiability_cyclicordering.rs | 8 +- ...bility_directedtwocommodityintegralflow.rs | 12 +- ...tisfiability_feasibleregisterassignment.rs | 13 +- .../rules/ksatisfiability_kclique.rs | 8 +- .../rules/ksatisfiability_kernel.rs | 4 +- .../ksatisfiability_monochromatictriangle.rs | 4 +- ...satisfiability_oneinthreesatisfiability.rs | 4 +- .../ksatisfiability_preemptivescheduling.rs | 6 +- .../ksatisfiability_quadraticcongruences.rs | 6 +- src/unit_tests/rules/ksatisfiability_qubo.rs | 42 +- .../ksatisfiability_registersufficiency.rs | 12 +- ...atisfiability_simultaneousincongruences.rs | 2 +- .../rules/ksatisfiability_timetabledesign.rs | 7 +- .../rules/lengthboundeddisjointpaths_ilp.rs | 4 +- ...maximumindependentset_maximumsetpacking.rs | 2 +- .../rules/maximumindependentset_qubo.rs | 4 +- .../rules/maximumleafspanningtree_ilp.rs | 7 + .../maximummatching_maximumsetpacking.rs | 2 +- src/unit_tests/rules/maximumsetpacking_ilp.rs | 60 ++ .../rules/maximumsetpacking_qubo.rs | 4 +- .../minimumcapacitatedspanningtree_ilp.rs | 12 +- ...mcostmaximumflow_minimumcostcirculation.rs | 8 +- ...bycliques_minimumintersectiongraphbasis.rs | 8 - ...mumdiscreteplanarinversekinematics_qubo.rs | 98 ++- .../rules/minimumedgecostflow_ilp.rs | 5 +- .../rules/minimumfaultdetectiontestset_ilp.rs | 5 +- ...minimumcodegenerationunlimitedregisters.rs | 4 +- .../rules/minimummatrixcover_ilp.rs | 17 +- .../rules/minimummultiwaycut_qubo.rs | 41 +- ...nimumvertexcover_comparativecontainment.rs | 8 +- .../minimumvertexcover_ensemblecomputation.rs | 4 +- ...inimumvertexcover_maximumindependentset.rs | 2 +- .../minimumvertexcover_minimumsetcovering.rs | 2 +- .../rules/minimumvertexcover_qubo.rs | 4 +- .../rules/minimumweightdecoding_ilp.rs | 5 +- .../rules/monochromatictriangle_ilp.rs | 5 +- .../rules/multiplechoicebranching_ilp.rs | 10 +- src/unit_tests/rules/naesatisfiability_ilp.rs | 5 +- .../rules/naesatisfiability_maxcut.rs | 37 +- .../rules/naesatisfiability_setsplitting.rs | 4 +- .../numericalmatchingwithtargetsums_ilp.rs | 5 +- ...ement_consecutiveonesmatrixaugmentation.rs | 38 +- src/unit_tests/rules/paintshop_qubo.rs | 27 +- .../partition_integralflowwithmultipliers.rs | 4 - .../rules/partition_openshopscheduling.rs | 22 +- ...ion_sequencingtominimizetardytaskweight.rs | 24 +- src/unit_tests/rules/partition_subsetsum.rs | 12 +- .../rules/partition_sumofsquarespartition.rs | 4 +- .../rules/partitionintocliques_ilp.rs | 5 +- ...ionintocliques_minimumcoveringbycliques.rs | 26 +- .../precedenceconstrainedscheduling_ilp.rs | 5 +- ...rizecollectingsteinerforest_steinertree.rs | 102 ++- src/unit_tests/rules/qubo_casts.rs | 4 +- src/unit_tests/rules/reduction_path_parity.rs | 2 +- .../rules/registersufficiency_ilp.rs | 5 +- .../resourceconstrainedscheduling_ilp.rs | 5 +- .../rules/rootedtreestorageassignment_ilp.rs | 9 +- src/unit_tests/rules/sat_coloring.rs | 2 +- src/unit_tests/rules/sat_ksat.rs | 2 +- .../rules/sat_maximumindependentset.rs | 23 +- .../rules/sat_minimumdominatingset.rs | 63 +- .../satisfiability_maximum2satisfiability.rs | 14 +- .../rules/satisfiability_naesatisfiability.rs | 11 +- .../schedulingwithindividualdeadlines_ilp.rs | 5 +- ...sequencingtominimizetardytaskweight_ilp.rs | 26 +- ...ingtominimizeweightedcompletiontime_ilp.rs | 5 +- ...quencingtominimizeweightedtardiness_ilp.rs | 5 +- ...equencingwithdeadlinesandsetuptimes_ilp.rs | 5 +- .../rules/sequencingwithinintervals_ilp.rs | 5 +- ...uencingwithreleasetimesanddeadlines_ilp.rs | 10 +- src/unit_tests/rules/setsplitting_ilp.rs | 5 +- .../shortestweightconstrainedpath_ilp.rs | 3 +- src/unit_tests/rules/spinglass_maxcut.rs | 2 +- src/unit_tests/rules/spinglass_qubo.rs | 32 +- src/unit_tests/rules/steinertree_ilp.rs | 26 +- .../rules/steinertreeingraphs_ilp.rs | 30 - .../rules/stringtostringcorrection_ilp.rs | 5 +- .../strongconnectivityaugmentation_ilp.rs | 5 +- .../rules/subgraphisomorphism_ilp.rs | 6 +- .../rules/subsetsum_closestvectorproblem.rs | 38 +- .../subsetsum_integerexpressionmembership.rs | 8 +- .../rules/sumofsquarespartition_ilp.rs | 9 +- .../rules/threedimensionalmatching_ilp.rs | 12 +- ...mensionalmatching_minimumweightdecoding.rs | 6 +- ..._sequencingwithreleasetimesanddeadlines.rs | 3 +- src/unit_tests/rules/timetabledesign_ilp.rs | 5 +- src/unit_tests/rules/traits.rs | 78 +- src/unit_tests/rules/travelingsalesman_ilp.rs | 5 +- .../rules/travelingsalesman_qubo.rs | 85 +- .../rules/undirectedflowlowerbounds_ilp.rs | 5 +- .../undirectedtwocommodityintegralflow_ilp.rs | 10 +- src/unit_tests/solvers/brute_force.rs | 372 ++++++++- .../customized/closest_vector_problem.rs | 26 +- .../customized/minimum_decision_tree.rs | 49 +- .../customized/shortest_common_superstring.rs | 44 +- src/unit_tests/solvers/customized/solver.rs | 5 +- src/unit_tests/solvers/ilp/adapter.rs | 235 ++++++ src/unit_tests/solvers/ilp/solver.rs | 108 +-- src/unit_tests/solvers/registry.rs | 108 ++- src/unit_tests/solvers/resolver.rs | 92 +-- src/unit_tests/trait_consistency.rs | 2 +- src/unit_tests/traits.rs | 101 ++- src/unit_tests/truth_table.rs | 77 +- src/unit_tests/types.rs | 44 +- tests/suites/integration.rs | 49 ++ ...tisfiability_simultaneous_incongruences.rs | 2 +- tests/suites/reductions.rs | 12 +- tests/suites/simultaneous_incongruences.rs | 8 +- 875 files changed, 8581 insertions(+), 8905 deletions(-) delete mode 100644 src/models/graph/steiner_tree_in_graphs.rs delete mode 100644 src/rules/ilp_casts.rs create mode 100644 src/rules/ilp_i64_ilp_f64.rs delete mode 100644 src/rules/steinertreeingraphs_ilp.rs create mode 100644 src/solvers/ilp/adapter.rs delete mode 100644 src/unit_tests/models/graph/steiner_tree_in_graphs.rs rename src/unit_tests/rules/{ilp_casts.rs => ilp_i64_ilp_f64.rs} (71%) delete mode 100644 src/unit_tests/rules/steinertreeingraphs_ilp.rs create mode 100644 src/unit_tests/solvers/ilp/adapter.rs diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 438584ded..c8eb87e49 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -37,7 +37,7 @@ These repo-local skills live under `.claude/skills/*/SKILL.md`. - [propose](skills/propose/SKILL.md) -- Interactive brainstorming to help domain experts propose a new model or rule. Asks one question at a time, uses mathematical language (no programming jargon), and files a GitHub issue. - [final-review](skills/final-review/SKILL.md) -- Interactive maintainer review for PRs in "Final review" column. Merges main, walks through agentic review bullets with human, then merge or hold. - [dev-setup](skills/dev-setup/SKILL.md) -- Interactive wizard to install and configure all development tools for new maintainers. -- [verify-reduction](skills/verify-reduction/SKILL.md) -- Standalone mathematical verification of a reduction rule: Typst proof, constructor Python (≥5000 checks), adversary Python (≥5000 independent checks). Reports verdict, no artifacts saved. Also called as a subroutine by `/add-rule` (default behavior). +- [verify-reduction](skills/verify-reduction/SKILL.md) -- Standalone mathematical verification of a reduction rule: Typst proof, constructor Python, and independent adversary Python with coverage justified by the construction. Reports verdict, no artifacts saved. Also called as a subroutine by `/add-rule` (default behavior). - [update-papers](skills/update-papers/SKILL.md) -- Update research paper collection: download new papers from references.bib, retry failed downloads, sync to Google Drive, regenerate index.md. - [find-solver](skills/find-solver/SKILL.md) -- Interactive guide: match a real-world problem to a library model, explore reduction paths, recommend solvers (built-in + external), and generate a solution doc. - [find-problem](skills/find-problem/SKILL.md) -- Reverse of find-solver: given a solver for a model, discover what other problems it can handle via incoming reductions, ranked by effective complexity. @@ -107,7 +107,7 @@ make papers-pull # Pull PDFs from shared remote - Run `pred list` for the full catalog of problems, variants, and reductions; `pred show ` for details on a specific problem - `src/rules/` - Reduction rules + inventory registration - `src/models/decision.rs` - Generic `Decision

` wrapper converting optimization problems to decision problems -- `src/solvers/` - BruteForce reference solver returning problem solutions, ILP solver (feature-gated), decision search (binary search via Decision queries), and the exact-variant solver capability registry. Solver dispatch uses only registered customized implementations and fixed ILP pipelines; reduction-graph reachability does not imply solver availability. Run `pred inspect ` to see the registered capabilities for that instance. +- `src/solvers/` - BruteForce reference solver returning problem solutions, ILP solver, decision search (binary search via Decision queries), and the exact-variant solver capability registry. Solver dispatch uses only registered customized implementations and fixed ILP pipelines; reduction-graph reachability does not imply solver availability. Run `pred inspect ` to see the registered capabilities for that instance. - `src/traits.rs` - `Problem` trait - `src/rules/traits.rs` - `ReduceTo`, `ReduceToAggregate`, `ReductionResult`, `AggregateReductionResult` traits - `src/registry/` - Compile-time reduction metadata collection @@ -134,7 +134,7 @@ Problem (core trait — all problems must implement) ``` `BruteForceProblem` is a separate reference-solver capability. Its -`dimensions()` method describes only the finite Cartesian coordinate space used +fallible `num_variables()` and `dimension(variable)` methods describe only the finite Cartesian coordinate space used by the registered brute-force implementation. **Objective problems** (e.g., `MaximumIndependentSet`) typically use `Value = Max`, `Min`, or `Extremum`. @@ -161,13 +161,15 @@ Max, Min, Sum, Or, And, Extremum, ExtremumSense - `variant_params!` macro implements `Problem::variant()` — e.g., `crate::variant_params![G, W]` for two type params, `crate::variant_params![]` for none (see `src/variant.rs`) - `declare_variants!` proc macro registers concrete type instantiations with best-known complexity and registry-backed load/serialize/solution-solve metadata. One entry per problem may be marked `default`, and variable names in complexity strings are validated against the problem-owned parameter schema. Ordinary models are constructed directly from their construction schema. When user-facing construction differs from persisted JSON, define a model-local `#[derive(CreateSpec)]` DTO plus `TryFrom`, use its generated `FIELDS` in `ProblemSchemaEntry`, and register it with `create LocalSpec`; never add model-name branches in CLI or MCP code. - `decision_problem_meta!` macro registers `DecisionProblemMeta` for a concrete inner type, providing the `DECISION_NAME` constant. -- `register_decision_variant!` macro generates `declare_variants!`, `ProblemSchemaEntry`, and both `ReductionEntry` submissions (aggregate Decision→Opt + Turing Opt→Decision) for a `Decision

` variant. Callers must define inherent getters (`num_vertices()`, `num_edges()`, `k()`) on `Decision

` before invoking. Accepts an explicit structural `category` plus `dims`, `fields`, and `parameter_getters` parameters for problem-specific parameters. +- `register_decision_variant!` macro generates `declare_variants!`, `ProblemSchemaEntry`, and both `ReductionEntry` submissions (witness/aggregate Decision→Opt + Turing Opt→Decision) for a `Decision

` variant. Callers must define inherent getters (`num_vertices()`, `num_edges()`, `k()`) on `Decision

` before invoking. Accepts an explicit structural `category` plus `dims`, `fields`, and `parameter_getters` parameters for problem-specific parameters. - Problems parameterized by graph type `G` and optionally weight type `W` (problem-dependent) - `BruteForce::solve()` returns `Result, SolveError>`; `None` means exhaustive search proved infeasibility - `BruteForce::find_all_witnesses()` is a reference-testing helper for collecting every optimal or satisfying solution +- Each executed witness step constructs one result and shares its witness/value views through `Rc`. Document the rule's domain, witness premise, source guarantee, and infeasibility interpretation; all tied qualifying optima must map correctly. +- `SolutionAggregate` belongs to `solvers::BruteForce` witness selection. Models, pure reduction mappings, dynamic evaluation, and non-enumerative solving do not require it. See [executed lifecycle](../docs/src/design.md#executed-reduction-lifecycle). - `ReductionResult` provides `target_problem()` and `extract_solution()` for witness/config workflows; `AggregateReductionResult` provides `extract_value()` for aggregate/value workflows -- Every direct `extract_solution()` must call `validate_target_solution()` once before decoding; composed extractors delegate validation to the first direct decoder. -- Decode only the reduction's defined mathematical mapping. Reject malformed structure with `ExtractionError`; never panic, truncate, clamp, invent defaults, or add recovery branches. Explicit mathematical alternatives and sentinels are allowed. Test successful decoding and every rejected representation. +- Direct `extract_solution()` maps solutions under the reduction's mathematical premises. `pred extract` has the same witness precondition. Neither validates feasibility or optimality. Transport parses/types inputs; solver orchestration interprets aggregate outcomes before mapping. +- Decode only the reduction's defined mathematical mapping. Preserve reachable mathematical and representation errors; do not add fallback values or recovery branches for violations already excluded by the calling contract. Explicit mathematical alternatives and sentinels are allowed. - CLI-facing dynamic formatting uses aggregate wrapper names directly (for example `Max(2)`, `Min(None)`, `Or(true)`, or `Sum(56)`) - Graph types: SimpleGraph, PlanarGraph, BipartiteGraph, UnitDiskGraph, KingsSubgraph, TriangularSubgraph - Weight types: `One` (unit weight marker), `i64`, `f64` — all implement `WeightElement` trait @@ -210,7 +212,7 @@ Reduction graph nodes use variant key-value pairs from `Problem::variant()`: - 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 -- `Decision

→ P` is an aggregate-only edge (solve optimization, compare to bound); `P → Decision

` is a Turing edge (binary search over decision bound) +- `Decision

→ P` supplies witness and aggregate operations on one result (solve optimization, compare to bound, extract when the bound is met); `P → Decision

` is a Turing edge (binary search over decision bound) ### Extension Points - New models register dynamic load/serialize metadata through `declare_variants!` and, when finite enumeration exists, register it separately through `register_brute_force!`; neither belongs in CLI match arms @@ -228,22 +230,33 @@ Reduction graph nodes use variant key-value pairs from `Problem::variant()`: ### Numeric Contract -Follow the [numeric types and arithmetic standard](../docs/src/design.md#numeric-types-and-arithmetic) -for every model and reduction. `usize` is reserved for in-memory indices, -collection lengths, and brute-force dimensions; canonical problem parameters -are `u64`; signed mathematical integers use `i64`; and approximate -real values use finite `f64`. Before implementation, identify each numeric -input and domain, each computed total and result type, the largest supported -value, every range/sign-changing conversion, overflow behavior, and whether -arithmetic is exact or approximate. Use `TryFrom` at range boundaries and -checked arithmetic for derived values that may overflow. Rust construction, -serde, CLI, and MCP must enforce the same range. +Follow the [numeric contract](../docs/src/design.md#numeric-types-and-arithmetic) +for models and reductions. Identify numeric domains, stored result types, +conversion boundaries, and overflow behavior before implementation. Rust +construction, serde, CLI, and MCP must use the same model validation; backend +transport limits must not narrow that model domain. Issue contributors provide the mathematical definition, domains, and constraints; implementers derive the Rust representation. Do not require issue authors to choose implementation types or add implementation-specific numeric fields to issue templates. Changes to issue templates require user approval. +### Reduction and Solver Boundary + +Follow the canonical [responsibility boundaries](../docs/src/design.md#responsibility-boundaries), +[witness/aggregate contracts](../docs/src/design.md#witness-and-aggregate-reductions), +and [validation policy](../docs/src/design.md#validation-evidence). +Models own mathematical semantics; rules own construction and witness mappings; +adapters own numerical transport, termination interpretation, and returned-target +validation. Orchestration uses those results and maps solutions under the rules' premises. +External extraction parses and types submitted witnesses and assumes the rule's mathematical premises. Solver completion interprets the rule's value relationship before mapping; extraction does not validate feasibility or optimality. Fix shared paths and update all callers rather +than adding model-specific branches or independent backend optimality checks. + +In ILP tests, only `ILPSolveError::Infeasible` means infeasibility. Other errors +must fail with their details. Solver integration failures must be distinguished +from model or reduction errors; do not require backend precision stress tests +in every rule. See the design document for the shared search-representation contract. + ### File Naming - Reduction files: `src/rules/_.rs` (e.g., `maximumindependentset_qubo.rs`) - Model files: `src/models//.rs` — category is by input structure: `graph/` (graph input), `formula/` (boolean formula/circuit), `set/` (universe + subsets), `algebraic/` (matrix/linear system/lattice), `misc/` (other) @@ -270,7 +283,7 @@ fields to issue templates. Changes to issue templates require user approval. ### Coverage -New code must have >95% test coverage. Run `make coverage` to check. +New code must have >95% test coverage. Run `make coverage` to check. This is a hard gate: do not waive it, lower the threshold, or add exclusions to make a change pass. `make coverage` checks committed and uncommitted changed lines against `origin/main` using the workspace LCOV report; set `COVERAGE_BASE` when reviewing against another base. Whole-repository coverage is a separate metric. ### Naming @@ -286,7 +299,9 @@ See Key Patterns above for solver API signatures. Follow the reference files for Unit tests in `src/unit_tests/` linked via `#[path]` (see Core Modules above). Integration tests in `tests/suites/`, consolidated through `tests/main.rs`. Canonical example-db coverage lives in `src/unit_tests/example_db.rs`. -Model review automation checks for a dedicated test file under `src/unit_tests/models/...` with at least 3 test functions. The exact split of coverage is judged per model during review. +Model review checks for a dedicated test file under `src/unit_tests/models/...` +and evaluates its semantic coverage under the [validation policy](../docs/src/design.md#validation-evidence). +Do not impose minimum test-function, assertion, vertex, or generated-check counts. ## Documentation Locations - `README.md` — Project overview and quickstart diff --git a/.claude/skills/add-model/SKILL.md b/.claude/skills/add-model/SKILL.md index 6cf14b947..4df33fdb6 100644 --- a/.claude/skills/add-model/SKILL.md +++ b/.claude/skills/add-model/SKILL.md @@ -20,7 +20,7 @@ Before any implementation, collect all required information. If called from `iss | 3 | **Problem type** | Objective (`Max`/`Min`), witness (`bool`), or aggregate-only (`Sum`/`And`/custom `Aggregate`) | Objective (Maximize) | | 4 | **Type parameters** | Graph type `G`, weight type `W`, or other | `G: Graph`, `W: WeightElement` | | 5 | **Struct fields** | What the struct holds | `graph: G`, `weights: Vec` | -| 6 | **Configuration space** | What `dims()` returns | `vec![2; num_vertices]` for binary vertex selection | +| 6 | **Configuration space** | Mathematical solution representation and domain | One Boolean selection per vertex | | 7 | **Feasibility check** | How to validate a configuration | "All selected vertices must be pairwise adjacent" | | 8 | **Per-configuration value** | How `evaluate()` computes the aggregate contribution | "Return `Max(Some(total_weight))` for feasible configs" | | 9 | **Best known exact algorithm** | Complexity with variable definitions | "O(1.1996^n) by Xiao & Nagamochi (2017), where n = \|V\|" | @@ -74,15 +74,15 @@ Read these first to understand the patterns: ## Pre-review Checklist Before implementing, make sure the plan explicitly covers these items that structural review checks later: -- Follow `docs/src/design.md#numeric-types-and-arithmetic`: `usize` is for in-memory indices, collection lengths, and brute-force dimensions; registered problem size parameters use `u64`; signed mathematical integers use `i64`; Boolean data uses `bool`; and approximate real or rational data uses finite `f64`. Use another format only when required by the mathematical problem or schema, such as `BigUint` for arbitrary-precision problems or `One` for unit weights; implementation convenience is not sufficient, and there is no `i32` model or I/O format. Implementation-local values are outside this contract. +- Read the canonical [numeric contract](../../../docs/src/design.md#numeric-types-and-arithmetic), [responsibility boundaries](../../../docs/src/design.md#responsibility-boundaries), and [validation policy](../../../docs/src/design.md#validation-evidence). Derive fields and arithmetic from the model's mathematical domain. Evaluation must not depend on solver tolerances, statuses, or enumeration cardinality. Reuse existing representations and shared APIs. - Keep failure phases explicit: fallible constructors, create specs, serde-facing validation, and random generation return `ConstructionError`; `evaluate()` returns `EvaluationError`; no public model path returns `Result<_, String>`. Stored-field arithmetic and evaluation arithmetic are checked and reported in their own phase. -- Serde/CLI construction uses the same validation as `new`/`try_new`, and boundary tests cover the supported maximum without requiring impractical allocation. +- Serde/CLI construction uses the same validation as `new`/`try_new`, and focused tests cover actual representation risks without impractical allocation or backend precision stress cases. - `ProblemSchemaEntry` metadata is complete (`display_name`, `aliases`, `dimensions`, explicit `category`, and construction `fields`) - `Problem::Value` uses the correct aggregate wrapper and witness support is intentional - `declare_variants!` is present with exactly one `default` variant when multiple concrete variants exist - CLI discovery and `pred create ` support are included where applicable - A canonical model example is registered for example-db / `pred create --example` -- If the issue explicitly claims direct ILP solving, the plan also includes the direct ` -> ILP` rule with exact overhead metadata, feature-gated registration, strong regression tests, and ILP-enabled verification +- If the issue explicitly claims direct ILP solving, the plan also includes the direct ` -> ILP` rule with correct parameter metadata, registration, semantic regression tests, and representative solver integration - `docs/paper/reductions.typ` adds both the display-name dictionary entry and the `problem-def(...)` ## Step 1: Determine the category @@ -215,11 +215,11 @@ This example is now the canonical source for: If the issue explicitly says the model is solvable by reducing **directly** to ILP, implement `src/rules/_ilp.rs` in the **same PR** as the model. This is the one exception to the normal "one item per PR" policy: the direct ` -> ILP` rule is part of the model feature, not optional follow-up work. Completeness bar: -- Feature-gate the rule under `ilp-solver` and register it normally -- Add exact overhead expressions and any required size-field getters; metadata must match the constructed ILP exactly -- Add strong tests in `src/unit_tests/rules/_ilp.rs`: structure/metadata, closed-loop semantics vs the source problem or brute force, extraction, `solve_reduced()` or ILP path coverage when appropriate, and weighted/infeasible/pathological regressions whenever the model semantics admit them +- Register the native ILP rule normally; there is no ILP solver feature gate +- Declare parameter equalities or upper bounds using existing metadata; verify the relationship against the constructed ILP +- Add strong tests in `src/unit_tests/rules/_ilp.rs`: structure/metadata, closed-loop semantics vs the source problem or brute force, extraction, `solve_reduced()` or ILP path coverage when appropriate, and weighted/infeasible cases and arithmetic regressions justified by the construction - Update CLI/example-db/paper paths so the claimed ILP solver route is actually usable and documented -- Verify with ILP-enabled workspace commands, not just non-ILP unit tests +- Run the relevant solver integration tests as well as direct mathematical tests; HiGHS is a regular dependency, not an optional feature A direct ILP rule shipped with a model issue must match the completeness bar of a standalone production ILP reduction. Do not add a stub just to satisfy the issue text. @@ -227,12 +227,12 @@ A direct ILP rule shipped with a model issue must match the completeness bar of Create `src/unit_tests/models//.rs`: -Every model needs **at least 3 test functions** (the structural reviewer enforces this). Choose from the coverage areas below — pick whichever are relevant to the model: +Choose coverage from the model semantics and concrete implementation risks under the canonical validation policy. There is no required test-function count: -- **Creation/basic** — exercise constructor inputs, key accessors, `dims()` / `num_variables()`. +- **Creation/basic** — exercise constructor inputs, key accessors, and the mathematical witness domain. - **Evaluation** — valid and invalid configs so the feasibility boundary or aggregate contribution is explicit. - **Direction / sense** — verify runtime optimization sense only for models that use `Extremum<_>`. -- **Solver** — brute-force `solve()` returns the correct aggregate value; if witnesses are supported, verify `find_witness()` / `find_all_witnesses()` as well. +- **Solver** — where registered, brute-force `solve()` returns a correct solution; use `find_all_witnesses()` when the test needs all optimal/satisfying witnesses. Keep solver integration separate from direct model evaluation. - **Serialization** — round-trip serde (when the model is used in CLI/example-db flows). - **Paper example** — verify the worked example from the paper entry (see below). @@ -298,8 +298,8 @@ make test clippy # Must pass If Step 4.7 applied, run ILP-enabled workspace verification instead: ```bash -cargo clippy --all-targets --features ilp-highs -- -D warnings -cargo test --features "ilp-highs example-db" --workspace --verbose +cargo clippy --all-targets -- -D warnings +cargo test --features example-db --workspace --verbose ``` Structural and quality review is handled by the `review-pipeline` stage, not here. The run stage just needs to produce working code. @@ -333,4 +333,4 @@ Structural and quality review is handled by the `review-pipeline` stage, not her | Calling a panicking constructor from `TryFrom` | Share a fallible constructor and preserve its `ConstructionError`. | | Missing canonical model example | Add a builder in `src/example_db/model_builders.rs` and keep it aligned with paper/example workflows | | Paper example not tested | Must include `test__paper_example` that verifies the exact instance, solution, and solution count shown in the paper | -| Claiming direct ILP solving but leaving ` -> ILP` for later | If the issue promises a direct ILP path, implement that rule in the same PR with exact overhead metadata and production-level ILP tests | +| Claiming direct ILP solving but leaving ` -> ILP` for later | If the issue promises a direct ILP path, implement that rule in the same PR with correct parameter relationships and production-level ILP tests | diff --git a/.claude/skills/add-rule/SKILL.md b/.claude/skills/add-rule/SKILL.md index 3097be526..dd54ca36b 100644 --- a/.claude/skills/add-rule/SKILL.md +++ b/.claude/skills/add-rule/SKILL.md @@ -36,47 +36,31 @@ Before any implementation, collect all required information. If called from `iss If any item is missing, ask the user to provide it. Put a high standard on item 7 (concrete example): it must be in tutorial style with clear intuition and easy to understand. Do NOT proceed until the checklist is complete. -## Step 0.5: Type Compatibility Gate - -Check source/target `Value` types before any work: - -```bash -grep "type Value = " src/models/*/.rs src/models/*/.rs -``` - -**Compatible pairs for `ReduceTo` (witness-capable):** -- `Or`->`Or`, `Min`->`Min`, `Max`->`Max` (same type) -- `Or`->`Min`, `Or`->`Max` (feasibility embeds into optimization) - -**Incompatible — STOP if any of these:** -- `Min`->`Or` or `Max`->`Or` — optimization source has no threshold K; needs a decision-variant source model -- `Max`->`Min` or `Min`->`Max` — opposite optimization directions; needs `ReduceToAggregate` or a decision-variant wrapper -- `Or`->`Sum` or `Min`->`Sum` — Sum is aggregate-only; needs `ReduceToAggregate` -- Any pair involving `And` or `Sum` on the target side - -If incompatible, STOP and comment on the issue explaining the type mismatch and options. Do NOT proceed. - -## Numeric Safety Gate - -Read `docs/src/design.md#numeric-types-and-arithmetic`. Derive implementation -types, supported ranges, and checked conversions from the mathematical source, -target, and reduction algorithm. Use `usize` for in-memory indices, collection -lengths, and brute-force dimensions; `u64` for registered problem size -parameters; `i64` for signed mathematical integers; `bool` for Boolean data; -and finite `f64` for real or rational data. Another format needs mathematical -or target-schema justification; there is no `i32` boundary format. -Temporary reduction calculations are outside this format contract, but fields -written into the target must use the target model's format. - -Ask the contributor only when a mathematical domain or constraint is ambiguous; -do not ask them to choose Rust types. Do not use `as` for range/sign changes. -Check target-size arithmetic and auxiliary identifiers before constructing the -target, verify serde/CLI uses the same ranges, and add focused boundary tests. -The public reduction returns `ReductionError`: preserve a target constructor's -`ConstructionError` as `ReductionError::Construction`, and report reduction -arithmetic directly as the corresponding `ReductionError`; do not stringify or -silently handle either error. Convert model-derived `i64` values to `f64` only -through `i64_to_exact_f64`. +## Step 0.5: Mathematical and API Contract + +Read [the canonical witness/aggregate contract](../../../docs/src/design.md#witness-and-aggregate-reductions). +Resolve the source and target's concrete `Solution` and `Value` types from their +implementations and check the construction, extraction preconditions, and +objective relationship. Different optimization directions or numeric value +types do not by themselves invalidate a witness reduction. Use the existing +witness, aggregate, or Turing capability required by the actual operation. +Report a concrete mathematical or Rust implementation mismatch if one exists; +do not apply a wrapper-pair whitelist. + +## Arithmetic and Validation + +Follow [the canonical arithmetic and boundary policy](../../../docs/src/design.md#arithmetic) +and [validation evidence](../../../docs/src/design.md#validation-evidence). +Derive representation requirements from the source, target, and construction. +Ask for clarification only when the mathematical domain is ambiguous, not to +make the contributor choose Rust types. + +Check the construction's actual size arithmetic, coefficients, and auxiliary +identifiers. Preserve target `ConstructionError` as `ReductionError::Construction` +and report reduction arithmetic through `ReductionError`; do not stringify or +silently handle failures. Reuse shared conversion and extraction APIs according +to their contracts. Backend transport limits and precision checks belong to the +adapter, not this rule's applicability domain or mandatory test template. ## Reference Implementations @@ -90,11 +74,11 @@ Read these first to understand the patterns: **If `--no-verify` was passed, skip to Step 2.** -Invoke the `/verify-reduction` skill to mathematically verify the reduction before writing Rust code. This runs the full verification pipeline: Typst proof, constructor Python script (>=5000 checks), adversary subagent (>=5000 independent checks), and cross-comparison. +Invoke the `/verify-reduction` skill to mathematically verify the reduction before writing Rust code. This runs the full verification pipeline: Typst proof, constructor Python script, independent adversary checks, and cross-comparison with coverage justified by the construction. All verification artifacts are ephemeral — they exist only in conversation context and temp files. Nothing is committed to the repository. -**If verification FAILS: STOP. Report to user. Do NOT proceed to implementation.** +**Proceed to implementation only when verification reports VERIFIED. For FAILED or INCOMPLETE, report the concrete defect or missing evidence and resolve it before implementing.** If verification passes, the verified Python `reduce()` and `extract_solution()` functions, along with the YES/NO instances, carry forward in conversation context to inform Steps 2-5. Use them as the canonical spec for the Rust implementation. @@ -106,7 +90,7 @@ Create `src/rules/_.rs` (all lowercase, no underscores between w // Required structure: // 1. ReductionResult struct (holds the target problem + mapping state) // 2. ReductionResult trait impl (target_problem + extract_solution) -// 3. #[reduction(overhead = { ... })] on ReduceTo impl +// 3. #[reduction(transform = exact { ... })] on ReduceTo impl // 4. ReduceTo trait impl (reduce_to method) // 5. #[cfg(test)] #[path = "..."] mod tests; ``` @@ -130,31 +114,30 @@ impl ReductionResult for ReductionXToY { fn target_problem(&self) -> &Self::Target { &self.target } fn extract_solution( &self, - target_solution: &[usize], - ) -> crate::rules::ExtractionResult> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; + target_solution: &::Solution, + ) -> crate::rules::ExtractionResult<::Solution> { let source_solution = /* translate the verified mathematical mapping exactly */; Ok(source_solution) } } ``` -Every direct extractor must call `validate_target_solution()` once before decoding. It checks only length and value domains, not feasibility, optimality, or rule-specific structure; reject malformed structure with `ExtractionError`. +Follow the canonical [extraction contract](../../../docs/src/design.md#witness-and-aggregate-reductions). Document the mathematical premises and implement the mapping directly. The adapter accepts solver output; external callers supply witnesses under the same mathematical contract. Extraction does not validate feasibility or optimality. Do not recheck constraints or add errors for states excluded by construction. Solver orchestration uses aggregate mappings to handle required thresholds before witness extraction; do not independently certify optimality or compensate for a rule bug with source revalidation. -**ReduceTo with `#[reduction]` macro** (overhead is **required**): +**ReduceTo with `#[reduction]` macro** (a parameter relation is **required**): ```rust -#[reduction(overhead = { +#[reduction(transform = exact { field_name = "source_field", })] impl ReduceTo for SourceType { type Result = ReductionXToY; - fn reduce_to(&self) -> Self::Result { + fn reduce_to(&self) -> Result { // If Step 1 ran: translate the verified Python reduce() logic } } ``` -Each primitive reduction is determined by the exact source/target variant pair. Keep one primitive registration per endpoint pair and use only the `overhead` form of `#[reduction]`. +Each primitive reduction is determined by the exact source/target variant pair. Keep one primitive registration per endpoint pair and declare `transform = exact`, `upper_bound`, or `unavailable` according to the actual parameter relationship; follow `.claude/CLAUDE.md` for metadata requirements. **Aggregate-only reductions:** when the rule preserves aggregate values but cannot recover a source witness from a target witness, implement `AggregateReductionResult` + `ReduceToAggregate` instead of `ReductionResult` + `ReduceTo`. Those edges are not auto-registered by `#[reduction]` yet; register them manually with `ReductionEntry { reduce_aggregate_fn: ..., capabilities: EdgeCapabilities::aggregate_only(), ... }`. See `src/unit_tests/rules/traits.rs` and `src/unit_tests/rules/graph.rs` for the reference pattern. @@ -162,7 +145,7 @@ Each primitive reduction is determined by the exact source/target variant pair. Add to `src/rules/mod.rs`: - `mod _;` -- If feature-gated (e.g., ILP): wrap with `#[cfg(feature = "ilp-solver")]` +- Register native ILP rules normally; there is no ILP solver feature gate. ## Step 4: Write unit tests @@ -171,20 +154,20 @@ Create `src/unit_tests/rules/_.rs`: **Required: closed-loop test** (`test__to__closed_loop`): ```rust // 1. Create source problem instance -// 2. Reduce: let reduction = ReduceTo::::reduce_to(&source); +// 2. Reduce: let reduction = ReduceTo::::reduce_to(&source).unwrap(); // 3. Solve target: solver.find_all_witnesses(reduction.target_problem()) // 4. Extract: reduction.extract_solution(&target_sol) // 5. Verify: extracted solution is valid and optimal for source ``` -If Step 1 ran, use the verified YES/NO instances from conversation context to construct test cases. Include both a feasible (closed-loop) and infeasible (no witnesses) test. +If Step 1 ran, use the verified YES/NO instances from conversation context to construct test cases. Include feasible and infeasible cases when both exist; for always-feasible optimization models, check the objective relationship instead. Additional recommended tests: - Verify target problem structure (correct size, edges, constraints) - Edge cases (empty graph, single vertex, etc.) - Weight preservation (if applicable) -Test every malformed representation distinguished by the decoder (for example, zero or multiple one-hot selections, or duplicate permutation entries). The canonical example supplies shared wrong-length and out-of-domain tests. +Test the mathematical mapping for witnesses satisfying its premises, including all tied optima on suitable small instances. Malformed witnesses do not impose rejection requirements on extraction. Keep necessary parsing/type-conversion tests at the transport boundary. For aggregate-only reductions, replace the closed-loop witness test with value-chain tests: - Solve the target with `Solver::solve()` @@ -195,7 +178,7 @@ Link via `#[cfg(test)] #[path = "..."] mod tests;` at the bottom of the rule fil ## Step 5: Add canonical example -Define `canonical_rule_example_specs()` in the rule module and include it from `src/rules/mod.rs::canonical_rule_example_specs()`. This enrolls the rule in shared round-trip, wrong-length, and out-of-domain extraction tests. +Define `canonical_rule_example_specs()` in the rule module and include it from `src/rules/mod.rs::canonical_rule_example_specs()`. This enrolls the rule in shared example checks. Extraction correctness checks use witnesses satisfying the mapping contract; model evaluation retains its own domain checks. ## Step 6: Document in paper (MANDATORY — DO NOT SKIP) @@ -272,8 +255,8 @@ Structural and quality review is handled by the `review-pipeline` stage, not her ## Solver Rules - If the target problem already has a solver, use it directly. -- If the solving strategy requires ILP, implement the ILP reduction rule alongside (feature-gated under `ilp-solver`). -- A direct-to-ILP rule is a production reduction, not a stub. Match the completeness bar used by strong ILP reductions in this repo: exact overhead metadata, structure + closed-loop + extraction tests, weighted/infeasible/pathological regressions whenever the semantics require them, and ILP-enabled workspace verification. +- If the solving strategy requires ILP, implement and register the ILP reduction rule alongside. +- A direct-to-ILP rule is a production reduction, not a stub. Match the completeness bar used by strong ILP reductions in this repo: correct parameter relationships, structure + closed-loop + extraction tests, weighted/infeasible cases and arithmetic regressions justified by the construction, and representative solver integration. - When this rule is the companion to a `[Model]` issue that explicitly claims ILP solvability, it belongs in the same PR as the model. - If a custom solver is needed, implement in `src/solvers/` and document. @@ -304,10 +287,25 @@ Aggregate-only reductions currently have a narrower CLI surface: | Wrong overhead expression | Must accurately reflect the size relationship | | Adding extra reduction metadata or duplicate primitive endpoint registration | Keep one primitive registration per endpoint pair and use only the `overhead` form of `#[reduction]` | | Missing `extract_solution` mapping state | Store any index maps needed in the ReductionResult struct | -| Permissive extraction | Validate first, then map exactly or return `ExtractionError` | +| Permissive extraction | Map witnesses satisfying the documented premises directly; do not validate feasibility or optimality | | Not adding a canonical example | Add the rule-local spec and include it from `src/rules/mod.rs` | | Not regenerating reduction graph | Run `cargo run --example export_graph` after adding a rule | | Skipping Step 6 (paper documentation) | **Every rule MUST have a `reduction-rule` entry in the paper. This is mandatory, not optional. PRs without documentation will be rejected.** | | Source/target model not fully registered | Both problems must already have `ProblemSchemaEntry`, `declare_variants!`, registry aliases as needed, and a construction contract -- use `add-model` skill first | -| Treating a direct-to-ILP rule as a toy stub | Direct ILP reductions need exact overhead metadata and strong semantic regression tests, just like other production ILP rules | +| Treating a direct-to-ILP rule as a toy stub | Direct ILP reductions need correct parameter relationships and strong semantic regression tests, just like other production ILP rules | | Skipping verification for complex reductions | Verification is default for a reason — `--no-verify` is for trivial identity/complement reductions only | + +## Reduction lifecycle responsibilities + +Apply the canonical [executed lifecycle](../../../docs/src/design.md#executed-reduction-lifecycle). +State the rule's instance domain, qualifying-witness premise, source guarantee, +and infeasibility interpretation. Check every qualifying tied optimum in small +exhaustive cases where ties are relevant. A witness flag alone does not prove +complete solvability or that adjacent path premises compose. + +Construct each executed result once and share target, witness, value, and +completion state. Outcome interpretation uses the rule's mathematical relation; +ordinary extraction assumes its premises. Keep necessary dynamic/JSON conversion +and reachable representation failures, but no checked/unchecked extraction or +pure forwarding wrappers. Do not add `SolutionAggregate` bounds to models or +mathematical mappings; it belongs to brute-force witness selection. diff --git a/.claude/skills/check-issue/SKILL.md b/.claude/skills/check-issue/SKILL.md index e63957476..3f80845dd 100644 --- a/.claude/skills/check-issue/SKILL.md +++ b/.claude/skills/check-issue/SKILL.md @@ -192,10 +192,10 @@ If the algorithm is a high-level sketch rather than an implementable procedure ### 4d: Example Quality -- **Non-trivial**: Must have enough structure to exercise the reduction meaningfully (not just 2 vertices) +- **Meaningful structure**: Exercise the defining constraints or reduction gadgets; explain what an incorrect implementation would get wrong. - **Brute-force solvable**: Small enough to verify by hand or with `pred solve` - **Fully worked**: Shows the source instance, the reduction construction step by step, and the target instance — not just "apply the reduction to get..." -- **Round-trip testable**: The example must be complex enough to validate correctness via a closed-loop test: reduce the source instance → solve the target → extract the solution back → verify it is optimal for the source. A too-simple example (e.g., a single edge, a trivially satisfiable formula) can pass the round trip even with a buggy reduction. The example should have multiple feasible solutions with different objective values so that only a correct reduction maps to the true optimum. Rule of thumb: the source instance should have at least 2 suboptimal feasible solutions in addition to the optimal one. +- **Round-trip testable**: Choose examples that can expose a concrete construction, evaluation, objective-mapping, or extraction defect under the [canonical validation policy](../../../docs/src/design.md#validation-evidence). Explain the expected outcome independently of backend success. There is no fixed number of feasible alternatives that establishes correctness. --- @@ -242,7 +242,7 @@ Read the `size_fields` and any variant getters, then enumerate corner cases the | Set systems | empty universe, empty subsets, identical subsets, universe element appearing in no subset | | Algebraic | zero matrix, identity, singular matrix | -Then trace the **issue's** algorithm by hand against at least 2 corner cases that are not the worked example: +Then trace the **issue's** algorithm by hand on relevant corner cases beyond the worked example, chosen to test concrete assumptions: 1. Pick a corner case from the table above that the source model actually allows. 2. Simulate the issue's construction step by step. @@ -404,13 +404,13 @@ The formal definition must be **precise and implementable**: ### 4d: Example Quality -- **Non-trivial**: Enough vertices/variables to exercise constraints meaningfully (not just a triangle) +- **Meaningful structure**: Exercise the defining constraints or reduction gadgets; explain what an incorrect implementation would get wrong. - **Exercises core structure**: Examples must use the defining features of the problem. For instance, a "MultivariateQuadratic" example that only has linear terms does not exercise the quadratic structure → **Fail**. If the problem's name or definition highlights a specific structural feature (quadratic, k-colorable, bipartite, etc.), at least one example must exercise that feature. - **Expected outcome provided**: - Satisfaction problems must include a concrete valid / satisfying solution and say why it is valid - Optimization problems must include a concrete optimal solution and the optimal objective value - **Detailed enough for paper**: This example will appear in the paper — it needs to be illustrative -- **Round-trip testable**: The example must be complex enough that a round-trip test (construct instance → solve → verify) can catch implementation bugs. A too-simple instance (e.g., 2 vertices, a single clause) may have a trivially correct solution that passes even with a wrong implementation. The example should have multiple feasible configurations with different objective values (for optimization) or a mix of satisfying and non-satisfying configurations (for satisfaction problems), so that correctness is meaningfully tested. Rule of thumb: the instance should have at least 2 suboptimal feasible solutions in addition to the optimal one. +- **Round-trip testable**: Choose examples that can expose a concrete construction, evaluation, objective-mapping, or extraction defect under the [canonical validation policy](../../../docs/src/design.md#validation-evidence). Explain the expected outcome independently of backend success. There is no fixed number of feasible alternatives that establishes correctness. - **ILP-testable when claimed**: If the issue advertises a direct ILP path, the example should be rich enough to support strong ILP closed-loop tests rather than a degenerate "any formulation passes" case. ### 4e: Representation Feasibility diff --git a/.claude/skills/dev-setup/SKILL.md b/.claude/skills/dev-setup/SKILL.md index 333a693e0..f8039e9de 100644 --- a/.claude/skills/dev-setup/SKILL.md +++ b/.claude/skills/dev-setup/SKILL.md @@ -123,7 +123,7 @@ This runs `fmt-check + clippy + test`. Print a pass/fail summary for each stage. | Failure | Fix | |---------|-----| | `fmt-check` fails | Run `make fmt` to auto-fix | -| Linker errors in clippy/test | Missing C/C++ toolchain for `ilp-highs` feature. Install Xcode CLT (`xcode-select --install` on macOS) or `build-essential` (`sudo apt install build-essential` on Linux) | +| Linker errors in clippy/test | Missing C/C++ toolchain required by the HiGHS backend. Install Xcode CLT (`xcode-select --install` on macOS) or `build-essential` (`sudo apt install build-essential` on Linux) | | "HiGHS not found" or cmake errors | Install cmake: `brew install cmake` (macOS) or `sudo apt install cmake` (Linux) | | `cargo llvm-cov` fails with "missing llvm-profdata" | `rustup component add llvm-tools-preview` | diff --git a/.claude/skills/review-quality/SKILL.md b/.claude/skills/review-quality/SKILL.md index 7c43c8241..aeec3ad0d 100644 --- a/.claude/skills/review-quality/SKILL.md +++ b/.claude/skills/review-quality/SKILL.md @@ -67,13 +67,18 @@ Only check these if the diff touches `problemreductions-cli/`: ## Step 5: Evaluate Test Quality +Read the canonical [validation policy](../../../docs/src/design.md#validation-evidence). Flag tests that: -- **Only check types/shapes, not values**: e.g., `assert!(result.is_some())` without checking the solution is correct -- **Mirror the implementation**: Tests recomputing the same formula as the code prove nothing -- **Lack adversarial cases**: Only happy path. Tests must include infeasible configs and boundary cases -- **Use trivial instances only**: Single-edge or 2-node tests may pass with bugs. Need 5+ vertex instances -- **Closed-loop without verification**: Must verify extracted solution is **optimal** (compare brute-force on both source and target) -- **Assert count too low**: 1-2 asserts for non-trivial code is insufficient + +- Check only types/shapes when the behavior requires a semantic value or witness assertion. +- Mirror the implementation without an independent expected result. +- Miss a concrete construction branch, infeasible configuration, or representation risk relevant to the change. +- Fail to check the reduction's stated mapping or objective relationship. Use explicit witnesses or small exhaustive oracles as appropriate; arbitrary feasible witnesses need not be optimal. +- Treat backend failures as mathematical counterexamples, or relax tolerances to make integration tests pass. +- Duplicate shared backend decoding/precision checks across rules. + +Use the smallest instances that distinguish correct from incorrect behavior. +Judge assertions by what they establish, not vertex, assertion, or test counts. ## Output Format @@ -110,3 +115,18 @@ Flag tests that: ### Summary - [list of all ISSUE items as bullet points with severity] ``` + +## Reduction lifecycle responsibilities + +Apply the canonical [executed lifecycle](../../../docs/src/design.md#executed-reduction-lifecycle). +State the rule's instance domain, qualifying-witness premise, source guarantee, +and infeasibility interpretation. Check every qualifying tied optimum in small +exhaustive cases where ties are relevant. A witness flag alone does not prove +complete solvability or that adjacent path premises compose. + +Construct each executed result once and share target, witness, value, and +completion state. Outcome interpretation uses the rule's mathematical relation; +ordinary extraction assumes its premises. Keep necessary dynamic/JSON conversion +and reachable representation failures, but no checked/unchecked extraction or +pure forwarding wrappers. Do not add `SolutionAggregate` bounds to models or +mathematical mappings; it belongs to brute-force witness selection. diff --git a/.claude/skills/review-structural/SKILL.md b/.claude/skills/review-structural/SKILL.md index d291a7e02..bf6609374 100644 --- a/.claude/skills/review-structural/SKILL.md +++ b/.claude/skills/review-structural/SKILL.md @@ -57,7 +57,7 @@ Only run if review type includes "model". Given: problem name `P`, category `C`, | 5 | Aggregate value is present | `Grep("type Value =", file)` | | 6 | `#[cfg(test)]` + `#[path = "..."]` test link | `Grep("#\\[path =", file)` | | 7 | Test file exists | `Glob("src/unit_tests/models/{C}/{F}.rs")` | -| 8 | Test file has >= 3 test functions | `Grep("fn test_", test_file)` — count matches, FAIL if < 3 | +| 8 | Semantic test coverage | Inspect cases against the [validation policy](../../../docs/src/design.md#validation-evidence); require meaningful coverage of the changed behavior, not a test-function count. | | 9 | Registered in `{C}/mod.rs` | `Grep("mod {F}", "src/models/{C}/mod.rs")` | | 10 | Re-exported in `models/mod.rs` | `Grep("{P}", "src/models/mod.rs")` | | 11 | Variant registration exists | `Grep("declare_variants!|VariantEntry", file)` | @@ -66,7 +66,7 @@ Only run if review type includes "model". Given: problem name `P`, category `C`, | 14 | Canonical model example registered | `Grep("{P}", "src/example_db/model_builders.rs")` | | 15 | Paper `display-name` entry | `Grep('"{P}"', "docs/paper/reductions.typ")` | | 16 | Paper `problem-def` block | `Grep('problem-def.*"{P}"', "docs/paper/reductions.typ")` | -| 17 | Numeric and error contracts | Derive the expected boundary representation from the mathematical definition, then compare schema types, Rust fields, aggregate/total type, constructor and serde validation, conversions, overflow behavior, and boundary tests against `docs/src/design.md#numeric-types-and-arithmetic`. Verify construction paths return `ConstructionError`, `evaluate()` returns `EvaluationError`, and no public model path returns `Result<_, String>`. | +| 17 | Numeric and error contracts | Read the canonical [responsibility and arithmetic contract](../../../docs/src/design.md#responsibility-boundaries). Check model representation, constructor/serde consistency, and actual arithmetic risks; flag backend tolerances or enumeration limits used as model semantics. Verify construction paths return `ConstructionError`, `evaluate()` returns `EvaluationError`, and no public model path returns `Result<_, String>`. | ### Rule Checklist @@ -85,8 +85,8 @@ Only run if review type includes "rule". Given: source `S`, target `T`, rule fil | 9 | Canonical rule example registered | `Grep("canonical_rule_example_specs", rule file)` and verify it is included by `src/rules/mod.rs` | | 10 | Example-db lookup tests exist | `Grep("find_rule_example|build_rule_db", "src/unit_tests/example_db.rs")` | | 11 | Paper `reduction-rule` entry | `Grep('reduction-rule.*"{S}".*"{T}"', "docs/paper/reductions.typ")` | -| 12 | Extraction contract | Direct decoders call `validate_target_solution()`, enforce rule-specific structure, and test malformed cases; the helper does not establish feasibility or optimality. Composed extractors may delegate. | -| 13 | Numeric and error contracts | Compare source/target boundary types, size arithmetic, coefficients, bounds, auxiliary IDs, conversions, overflow behavior, and boundary tests against `docs/src/design.md#numeric-types-and-arithmetic`. Verify public reduction paths return `ReductionError`, preserve target `ConstructionError` as its construction cause, and never stringify or silently handle either failure. | +| 12 | Extraction contract | Follow the canonical responsibility boundaries. Both external extraction and internal rule mappings rely on documented premises; parsing and type conversion stay at the transport boundary. Reject repeated feasibility checks and error branches excluded by construction. Solver orchestration interprets aggregate thresholds to determine source answers. No independent optimality certification is required. | +| 13 | Numeric and error contracts | Check the [witness/aggregate contract](../../../docs/src/design.md#witness-and-aggregate-reductions) and actual construction arithmetic under the canonical policy. Do not reject different objective directions/value types or demand backend precision tests for every rule. Verify public reduction paths return `ReductionError`, preserve target `ConstructionError` as its construction cause, and never stringify or silently handle either failure. | ## Step 2b: Blacklisted File Check @@ -177,3 +177,18 @@ Flag any deviation as ISSUE. - X/Y issue compliance checks passed (if applicable) - [list of all FAIL/ISSUE items as bullet points] ``` + +## Reduction lifecycle responsibilities + +Apply the canonical [executed lifecycle](../../../docs/src/design.md#executed-reduction-lifecycle). +State the rule's instance domain, qualifying-witness premise, source guarantee, +and infeasibility interpretation. Check every qualifying tied optimum in small +exhaustive cases where ties are relevant. A witness flag alone does not prove +complete solvability or that adjacent path premises compose. + +Construct each executed result once and share target, witness, value, and +completion state. Outcome interpretation uses the rule's mathematical relation; +ordinary extraction assumes its premises. Keep necessary dynamic/JSON conversion +and reachable representation failures, but no checked/unchecked extraction or +pure forwarding wrappers. Do not add `SolutionAggregate` bounds to models or +mathematical mappings; it belongs to brute-force witness selection. diff --git a/.claude/skills/verify-reduction/SKILL.md b/.claude/skills/verify-reduction/SKILL.md index 91765ce3f..d0e42c3c9 100644 --- a/.claude/skills/verify-reduction/SKILL.md +++ b/.claude/skills/verify-reduction/SKILL.md @@ -1,13 +1,14 @@ --- name: verify-reduction -description: Standalone mathematical verification of a reduction rule — generates a Typst proof plus constructor and independent adversary scripts with at least 5000 checks each. Reports a verdict without saving artifacts. +description: Verify a reduction mathematically using a Typst proof and independent constructor/adversary scripts, with coverage chosen from the construction's risks. Report findings without committing artifacts. --- # Verify Reduction -Mathematical verification of a reduction rule. Produces a Typst proof + dual Python verification scripts, iterating until all checks pass. Reports a VERIFIED/FAILED verdict. All artifacts are ephemeral — nothing is committed to the repository. - -Use standalone to check correctness before implementation, or as a subroutine of `/add-rule` (which calls this by default). +Verify a reduction before implementation, standalone or as the default mathematical +verification step of `/add-rule`. Produce a proof and independent executable +checks in a temporary directory. Report what was established and any limitations; +finite checks support the proof but do not replace it. ## Invocation @@ -16,280 +17,148 @@ Use standalone to check correctness before implementation, or as a subroutine of /verify-reduction SubsetSum Partition ``` -## Step 0: Parse Input - -```bash -REPO=$(gh repo view --json nameWithOwner --jq .nameWithOwner) -ISSUE= -ISSUE_JSON=$(gh issue view "$ISSUE" --json title,body,number) -``` - -If invoked with problem names instead of an issue number, use the names directly. - -## Step 1: Read Issue, Study Models, Type Check - -```bash -gh issue view "$ISSUE" --json title,body -pred show --json -pred show --json -``` - -### Type compatibility gate — MANDATORY - -Check source/target `Value` types before any work. The `grep` only locates the definitions; it does -not resolve generic parameters or associated types: - -```bash -grep "type Value = " src/models/*/.rs src/models/*/.rs -``` - -Resolve both concrete types completely before declaring compatibility: - -1. Substitute every concrete generic argument from the proposed rule. -2. Follow every type alias and associated type to its defining `impl`. -3. Record the substitution chain and the source file evidence in the verification report. -4. If any generic or associated type remains unresolved, run a compile-backed temporary Rust probe - using `std::any::type_name::<::Value>()`. Build the probe from `/tmp` - with a path dependency on this repository; do not modify the repository. - -Never infer a Rust value type from the mathematical problem name, from unit-weight terminology, or -from the Python verifier's integer representation. In particular, arbitrary-precision Python -integers do not establish that a Rust objective type is `usize` or that it is closed under all -legal source instances. - -Required report format: +## Step 1: Read the Definition and Resolve the API + +For an issue, read it with `gh issue view --json title,body`. Inspect both +concrete models with `pred show --json` and read their implementations. +Extract the construction, mathematical domain, correctness argument, witness +mapping, parameter formulas, worked example, and references. Consult the cited +literature when needed to resolve a mathematical claim. + +Read the canonical [witness/aggregate contract](../../../docs/src/design.md#witness-and-aggregate-reductions), +[arithmetic policy](../../../docs/src/design.md#arithmetic), and +[validation policy](../../../docs/src/design.md#validation-evidence). + +Locate `Solution` and `Value` definitions with `rg`, substitute concrete generic +arguments, and follow associated types to their implementations. Record the +resolved types and source evidence. If resolution remains unclear, use a temporary +compile-backed Rust probe with a path dependency on the repository. Do not infer +Rust types from problem names, unit-weight terminology, or Python integers. + +Check the actual operation: + +- A witness reduction maps solutions and justifies feasibility and, where claimed, + optimality preservation. Different objective directions or Rust value types are + not automatic failures. For example, complementing an independent set of size + `k` gives a vertex cover of size `n-k` and reverses optimization direction. +- An aggregate reduction must justify its actual value conversion. Check the + domain of arithmetic the construction or mapping performs, not a hypothetical + conversion between all source and target objective values. +- A multi-query algorithm needs the existing Turing capability. An arbitrary + feasibility witness does not establish a source optimum without an argument. + +Report a concrete mathematical/API mismatch before implementation if one exists. +Do not replace that analysis with a wrapper-pair whitelist or backend range gate. + +## Step 2: Write the Proof + +Write a standalone Typst proof in the temporary directory containing: + +- Source/target definitions and the precise applicability domain. +- Construction steps with symbols defined before use. +- Independent forward and reverse correctness arguments. For optimization, + state the objective relationship and why target optima yield source optima. +- Witness extraction, including its mathematical preconditions. +- Target parameter formulas, distinguishing equalities from upper bounds. +- Small worked examples that exercise the construction. Include YES and NO + examples where both exist; for always-feasible optimization problems, show + the relevant objective relationship instead of inventing an infeasible case. + +Use enough detail to make the argument checkable. Do not substitute phrases such +as “obviously” or “the converse is similar” for a missing proof. Example size is +chosen for clarity and coverage, not a minimum vertex count. + +## Step 3: Implement Constructor Checks + +Write a temporary Python script with independent source/target feasibility and +objective oracles. Cover the claims relevant to this construction: + +| Claim | Evidence | +|-------|----------| +| Forward/reverse correctness | Small exhaustive instances or justified sampling; compare feasibility and the stated optimum relationship | +| Witness extraction | Target witnesses satisfying the mapping's preconditions produce valid source witnesses; check optimal mappings where claimed | +| Parameter formulas | Measure constructed targets and compare with equalities or upper bounds; use symbolic checking when it adds evidence | +| Target structure | Check the actual target invariants and gadget interactions | +| Worked examples | Reproduce the proof's values and witnesses | +| Arithmetic/case splits | Exercise concrete branches and representation risks in the construction | + +Choose exhaustive bounds and sampling from the construction's risks and cost. +Record bounds, seeds, counts, and omissions so the evidence is reproducible. +There is no universal minimum generated-check count. Do not duplicate solver +precision tests or require a backend to establish mathematical equivalence. +Python's arbitrary-precision arithmetic is not evidence that Rust construction +arithmetic cannot overflow; inspect the actual stored representation separately. + +## Step 4: Run Checks and Analyze Gaps + +Run the script and investigate failures. Correct the proof, construction, or +checker according to the evidence, then rerun affected checks. Map each proof +claim to its executable evidence or explain why it is established by proof alone. +Report untested areas rather than increasing check counts without new coverage. + +If a backend integration run is included, identify it separately. Record whether +failure occurs in construction, solving, extraction, or source validation. A +backend timeout, numerical rejection, or non-optimal termination is not itself a +counterexample to the reduction theorem and must not be reported as a pass. + +## Step 5: Independent Adversary Verification + +Dispatch an independent subagent with the problem definitions and Typst proof, +without the constructor script. Ask it to implement its own construction, +extraction, feasibility, and objective checks. It must not import the constructor +implementation. Have it challenge the proof's actual risks: + +- Complement/identity mappings: objective direction and witness correspondence. +- Algebraic mappings: case boundaries, coefficients, and extraction per case. +- Gadget mappings: unintended paths, gadget interactions, and target invariants. + +Use exhaustive checks or property-based strategies where they provide useful +independent coverage, not to satisfy a count. Reproduce applicable worked examples. +Compare both implementations on shared instances. Investigate disagreements; +structurally different but equivalent encodings may be valid. One checker passing +does not establish that the other checker is at fault. + +## Step 6: Review and Report + +Before reporting, confirm: + +- The concrete Rust types and actual witness/aggregate contract were checked. +- The proof covers construction, both directions, extraction, and parameters. +- Independent checks exercise relevant branches and mappings with reproducible + bounds/seeds; remaining gaps are stated. +- Disagreements and failures are resolved or explicitly reported. +- Mathematical evidence and backend integration results are distinguished. + +Report: ```text -TYPE RESOLUTION: - Source syntax: Min - Substitutions: W = One; ::Sum = i64 - Source resolved: Min - Target syntax: Min - Target resolved: Min - Full-domain compatibility: FAILED -``` - -**Compatible pairs for `ReduceTo` (witness-capable):** -- `Or`->`Or` -- `Min`->`Min`, `Max`->`Max` (identical resolved inner type) -- `Or`->`Min`, `Or`->`Max` (feasibility embeds into optimization) - -`Min`->`Min` or `Max`->`Max` with `S != T` is not automatically compatible. Proceed -only if the rule or source model declares a bound covering every legal source instance and the -verification proves a total, order-preserving conversion over that full declared domain. Otherwise -STOP and report a value-domain mismatch. - -**Incompatible — STOP if any of these:** -- `Min`->`Or` or `Max`->`Or` — optimization source has no threshold K; needs a decision-variant source model -- `Max`->`Min` or `Min`->`Max` — opposite optimization directions; needs `ReduceToAggregate` or a decision-variant wrapper -- `Or`->`Sum` or `Min`->`Sum` — Sum is aggregate-only; needs `ReduceToAggregate` -- Any pair involving `And` or `Sum` on the target side - -**Regression case:** `MinimumDominatingSet` resolves to `Min` because -`::Sum = i64`; `MinimumHittingSet` resolves to `Min`. Report -`Min -> Min`, not `Min -> Min`. Without an explicit source-size bound, -the full-domain type gate fails even though the classical cardinality reduction is mathematically -correct and exhaustive small-instance checks pass. - -If incompatible, STOP and report the type mismatch and options. Do NOT proceed. - -### If compatible - -Extract: construction algorithm, correctness argument, overhead formulas, worked example, reference. Use WebSearch if the issue is incomplete. - -## Step 2: Write Typst Proof - -Write a standalone Typst proof (in a temp file, not committed). - -**Mandatory structure:** - -```typst -== Source $arrow.r$ Target -#theorem[...] -#proof[ - _Construction._ (numbered steps, every symbol defined before first use) - _Correctness._ - ($arrow.r.double$) ... (genuinely independent, NOT "the converse is similar") - ($arrow.l.double$) ... - _Solution extraction._ ... -] -*Overhead.* (table with target metric -> formula) -*Feasible example.* (YES instance, >=3 variables, fully worked with numbers) -*Infeasible example.* (NO instance, fully worked — show WHY no solution exists) -``` - -**Hard rules:** -- Zero instances of "clearly", "obviously", "it is easy to see", "straightforward" -- Zero scratch work ("Wait", "Hmm", "Actually", "Let me try") -- Two examples minimum, both with >=3 variables/vertices -- Every symbol defined before first use - -## Step 3: Write Constructor Python Script - -Write a Python verification script (temp file) with ALL 7 mandatory sections: - -| Section | What to verify | Notes | -|---------|---------------|-------| -| 1. Symbolic (sympy) | Overhead formulas symbolically for general n | "The overhead is trivial" is NOT an excuse to skip | -| 2. Exhaustive forward+backward | Source feasible <=> target feasible | n <= 5 minimum. ALL instances or >=300 sampled per (n,m) | -| 3. Solution extraction | Extract source solution from every feasible target witness | Most commonly skipped section. DO NOT SKIP | -| 4. Overhead formula | Build target, measure actual size, compare against formula | Catches off-by-one in construction | -| 5. Structural properties | Target well-formed, no degenerate cases | Gadget reductions: girth, connectivity, widget structure | -| 6. YES example | Reproduce exact Typst feasible example numbers | Every value must match | -| 7. NO example | Reproduce exact Typst infeasible example, verify both sides infeasible | Must verify WHY infeasible | - -### Minimum check counts — STRICTLY ENFORCED - -| Type | Minimum checks | Minimum n | -|------|---------------|-----------| -| Identity (same graph, different objective) | 10,000 | n <= 6 | -| Algebraic (padding, complement, case split) | 10,000 | n <= 5 | -| Gadget (widget, cycle construction) | 5,000 | n <= 5 | - -Every reduction gets at least 5,000 checks regardless of perceived simplicity. - -## Step 4: Run and Iterate - -```bash -python3 /tmp/verify__.py -``` - -### Iteration 1: Fix failures - -Run the script. Fix any failures. Re-run until 0 failures. - -### Iteration 2: Check count audit - -Print and fill this table honestly: - -``` -CHECK COUNT AUDIT: - Total checks: ___ (minimum: 5,000) - Forward direction: ___ instances (minimum: all n <= 5) - Backward direction: ___ instances (minimum: all n <= 5) - Solution extraction: ___ feasible instances tested - Overhead formula: ___ instances compared - Symbolic (sympy): ___ identities verified - YES example: verified? [yes/no] - NO example: verified? [yes/no] - Structural properties: ___ checks -``` - -If ANY line is below minimum, enhance the script and re-run. Do NOT proceed. - -### Iteration 3: Gap analysis - -List EVERY claim in the Typst proof and whether it's tested: - -``` -CLAIM TESTED BY -"Universe has 2n elements" Section 4: overhead -"Complementarity forces consistency" Section 3: extraction -"Forward: NAE-sat -> valid splitting" Section 2: exhaustive -... -``` - -If any claim has no test, add one. If untestable, document WHY. - -## Step 5: Adversary Verification - -Dispatch a subagent that reads ONLY the Typst proof (not the constructor script) and independently implements + tests the reduction. - -**Adversary requirements:** -- Own `reduce()` function from scratch -- Own `extract_solution()` function -- Own `is_feasible_source()` and `is_feasible_target()` validators -- Exhaustive forward + backward for n <= 5 -- `hypothesis` property-based testing (>=2 strategies) -- Reproduce both Typst examples (YES and NO) -- >=5,000 total checks -- Must NOT import from the constructor script - -**Typed adversary focus** (include in prompt): -- **Identity reductions:** exhaustive enumeration n <= 6, edge-case configs (all-zero, all-one, alternating) -- **Algebraic reductions:** case boundary conditions (e.g., S = 2T exactly, S = 2T +/- 1), per-case extraction -- **Gadget reductions:** widget structure invariants, traversal patterns, interior vertex isolation - -### Cross-comparison - -After both scripts pass, compare `reduce()` outputs on shared instances. Both must produce structurally identical targets and agree on feasibility for all tested instances. - -### Verdict table - -| Constructor | Adversary | Cross-compare | Verdict | Action | -|-------------|-----------|---------------|---------|--------| -| Pass | Pass | Agree | **VERIFIED** | Done (or proceed to add-rule Step 2) | -| Pass | Pass | Disagree | **Suspect** | Investigate — may be isomorphic or latent bug | -| Pass | Fail | -- | **Adversary bug** | Fix adversary or clarify Typst spec | -| Fail | Pass | -- | **Constructor bug** | Fix constructor, re-run from Step 4 | -| Fail | Fail | -- | **Proof bug** | Re-examine Typst proof, return to Step 2 | - -## Step 6: Self-Review Checklist - -Every item must be YES. If any is NO, go back and fix. - -### Typst proof -- [ ] Construction with numbered steps, symbols defined before use -- [ ] Correctness with independent => and <= paragraphs -- [ ] Solution extraction section present -- [ ] Overhead table with formulas -- [ ] YES example (>=3 variables, fully worked) -- [ ] NO example (fully worked, explains WHY infeasible) -- [ ] Zero hand-waving language -- [ ] Zero scratch work - -### Type gate -- [ ] Concrete Rust `Value` types fully resolved with substitution evidence -- [ ] Different numeric domains either rejected or covered by an explicit full-domain range proof - -### Constructor Python -- [ ] 0 failures, >=5,000 total checks -- [ ] All 7 sections present and non-empty -- [ ] Exhaustive n <= 5 -- [ ] Extraction tested for every feasible instance -- [ ] Gap analysis: every Typst claim has a test - -### Adversary Python -- [ ] 0 failures, >=5,000 total checks -- [ ] Independent implementation (no imports from constructor) -- [ ] `hypothesis` PBT with >=2 strategies -- [ ] Reproduces both Typst examples - -### Cross-consistency -- [ ] Cross-comparison: 0 disagreements, 0 feasibility mismatches - -## Step 7: Report Verdict - -Report the final verdict to the user: - -``` -VERIFICATION RESULT: VERIFIED / FAILED - Source: - Target: - Constructor checks: - Adversary checks: - Cross-comparison: instances, 0 disagreements - Issue: # +VERIFICATION RESULT: VERIFIED / FAILED / INCOMPLETE + Source and target: + Mathematical claim and applicability domain:

+ Constructor coverage: + Independent coverage: + Cross-comparison: + Remaining gaps or counterexamples:
+ Backend integration, if run: ``` -If called as a subroutine of `/add-rule`, the verified Python `reduce()`, `extract_solution()`, and YES/NO instances remain in conversation context for use in the Rust implementation steps. No files are saved. - -If called standalone, the verdict is the final output. The user can inspect the proof and scripts interactively during the session. - -## Common Mistakes - -| Mistake | Consequence | -|---------|-------------| -| Proceeding past type gate with incompatible types | Wasted work — math may be correct but `ReduceTo` impl is impossible | -| Adversary imports from constructor script | Rejected — must be independent | -| No `hypothesis` PBT in adversary | Rejected | -| Section 1 (symbolic) empty | Rejected — "overhead is trivial" is not an excuse | -| Only YES example, no NO example | Rejected | -| n <= 3 or n <= 4 "because it's simple" | Rejected — minimum n <= 5 | -| No gap analysis | Rejected — perform before proceeding | -| Example has < 3 variables | Rejected — too degenerate | -| Either script has < 5,000 checks | Rejected — enhance testing | -| Extraction (Section 3) not tested | Rejected — most commonly skipped | -| Cross-comparison skipped | Rejected | -| Disagreements dismissed without investigation | Rejected | -| Saving artifacts to the repository | All files are ephemeral — use temp directory, nothing committed | +Use VERIFIED only when the proof and independent checks support the stated claim; +use FAILED for an established defect and INCOMPLETE for unresolved evidence. +When called by `/add-rule`, provide the checked construction, extraction, and +examples for the Rust implementation. Keep proof/scripts/results temporary; do +not commit generated verification artifacts. + +## Reduction lifecycle responsibilities + +Apply the canonical [executed lifecycle](../../../docs/src/design.md#executed-reduction-lifecycle). +State the rule's instance domain, qualifying-witness premise, source guarantee, +and infeasibility interpretation. Check every qualifying tied optimum in small +exhaustive cases where ties are relevant. A witness flag alone does not prove +complete solvability or that adjacent path premises compose. + +Construct each executed result once and share target, witness, value, and +completion state. Outcome interpretation uses the rule's mathematical relation; +ordinary extraction assumes its premises. Keep necessary dynamic/JSON conversion +and reachable representation failures, but no checked/unchecked extraction or +pure forwarding wrappers. Do not add `SolutionAggregate` bounds to models or +mathematical mappings; it belongs to brute-force witness selection. diff --git a/.config/nextest.toml b/.config/nextest.toml index 402f0ff76..a4e5c4b83 100644 --- a/.config/nextest.toml +++ b/.config/nextest.toml @@ -7,15 +7,7 @@ # whole job was SIGTERM-killed at the runner level) — nextest reports it as a # clean per-test timeout failure instead of stalling the job. # -# The large headroom (300s, not a tight ~10s) is deliberate. The subprocess -# example tests in tests/suites/examples.rs shell out to `cargo run --example -# … --features ilp-highs`: -# - In the Test job, CI pre-builds those examples (see ci.yml) so the -# subprocess reuses artifacts and each test runs in well under a second. -# - In the Code Coverage job, the subprocess inherits llvm-cov's -# `-C instrument-coverage` RUSTFLAGS, so it recompiles the examples -# *instrumented* (a non-instrumented pre-build would not match its -# fingerprint, so pre-building there is pointless). That instrumented -# recompile legitimately takes >120s, hence the 300s bound. +# The large headroom (300s, not a tight ~10s) is deliberate: this is a final +# safety bound for genuinely hung tests, not the expected runtime budget. [profile.default] slow-timeout = { period = "60s", terminate-after = 5 } diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3ef1a9fd8..aa3f64c0e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -191,5 +191,5 @@ jobs: uses: codecov/codecov-action@v5 with: files: lcov.info - fail_ci_if_error: false # Don't fail CI if upload fails + fail_ci_if_error: true token: ${{ secrets.CODECOV_TOKEN }} diff --git a/Cargo.toml b/Cargo.toml index 1fa1906fa..6f5d69279 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,10 +27,11 @@ serde = { version = "1.0", features = ["derive"] } # Persisted problem and reduction data must preserve every finite f64 on replay. serde_json = { version = "1.0", features = ["float_roundtrip"] } thiserror = "2.0" -num-bigint = "0.4" -num-rational = "0.4" +num-bigint = { version = "0.4", features = ["serde"] } +num-rational = { version = "0.4", features = ["serde"] } num-traits = "0.2" -good_lp = { version = "=1.14.2", default-features = false, features = ["highs"] } +sprs = { version = "0.11.5", default-features = false, features = ["serde"] } +highs = "=2.4.0" inventory = "0.3" ordered-float = "5.0" rand = "0.10" diff --git a/Makefile b/Makefile index 1cb637f32..1a3634480 100644 --- a/Makefile +++ b/Makefile @@ -163,10 +163,13 @@ paper: cargo run --features "$(TEST_FEATURES)" --example export_schemas typst compile --root . docs/paper/reductions.typ docs/paper/reductions.pdf -# Generate coverage report (requires: cargo install cargo-llvm-cov) +# Check changed-line coverage against the PR base, including uncommitted changes. +COVERAGE_BASE ?= origin/main +# Requires cargo-llvm-cov and uv. coverage: @command -v cargo-llvm-cov >/dev/null 2>&1 || { echo "Installing cargo-llvm-cov..."; cargo install cargo-llvm-cov; } - cargo llvm-cov --workspace --html --open + cargo llvm-cov --workspace --lcov --output-path target/coverage.lcov + uvx diff-cover target/coverage.lcov --compare-branch $(COVERAGE_BASE) --fail-under 95 --total-percent-float --format html:target/coverage-diff.html # Clean build artifacts clean: diff --git a/codecov.yml b/codecov.yml index 27c6f0cd2..7091d9e6f 100644 --- a/codecov.yml +++ b/codecov.yml @@ -10,11 +10,11 @@ coverage: project: default: target: 95% - threshold: 2% + threshold: 0% patch: default: target: 95% - threshold: 2% + threshold: 0% # Exclude proc-macro crate from coverage since it runs at compile time # and traditional runtime coverage tools cannot measure it. diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index ad22273a8..ad8527b32 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -369,7 +369,6 @@ "ShortestCommonSuperstring": [Shortest Common Superstring], "StaffScheduling": [Staff Scheduling], "SteinerTree": [Steiner Tree], - "SteinerTreeInGraphs": [Steiner Tree in Graphs], "MinimumAxiomSet": [Minimum Axiom Set], "MinimumExternalMacroDataCompression": [Minimum External Macro Data Compression], "MinimumInternalMacroDataCompression": [Minimum Internal Macro Data Compression], @@ -1342,7 +1341,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| } #{ - let x = load-model-example("DecisionMinimumVertexCover") + let x = load-model-example("DecisionMinimumVertexCover", variant: (graph: "SimpleGraph", weight: "i64")) let inner = x.instance.inner let nv = graph-num-vertices(x.instance) let ne = graph-num-edges(x.instance) @@ -3219,11 +3218,13 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| let steiner-verts = tree-verts.filter(v => not terminals.contains(v)) [ #problem-def("SteinerTree")[ - Given an undirected graph $G = (V, E)$ with edge weights $w: E -> RR_(>= 0)$ and a set of terminal vertices $T subset.eq V$ with $|T| >= 2$, find a tree $S = (V_S, E_S)$ in $G$ such that $T subset.eq V_S$, minimizing $sum_(e in E_S) w(e)$. Vertices in $V_S backslash T$ are called _Steiner vertices_. + Given an undirected graph $G = (V, E)$ with edge weights $w: E -> ZZ$ and a set of terminal vertices $T subset.eq V$ with $|T| >= 1$, find a tree $S = (V_S, E_S)$ in $G$ such that $T subset.eq V_S$, minimizing $sum_(e in E_S) w(e)$. Vertices in $V_S backslash T$ are called _Steiner vertices_. ][ One of Karp's 21 NP-complete problems @karp1972, foundational in network design with applications in telecommunications backbone routing, VLSI chip interconnect, pipeline planning, and phylogenetic tree construction. When $T = V$, the problem reduces to the minimum spanning tree (polynomial). The NP-hardness arises from choosing which Steiner vertices to include. - The best known exact algorithm runs in $O^*(3^(|T|) dot n + 2^(|T|) dot n^2)$ time via Dreyfus--Wagner dynamic programming over terminal subsets @dreyfuswagner1971. Byrka _et al._ achieved a $ln(4) + epsilon approx 1.39$-approximation @byrka2013; the classic 2-approximation uses the minimum spanning tree of the terminal distance graph. + For nonnegative weights, Dreyfus--Wagner runs in $O^*(3^(|T|) dot n + 2^(|T|) dot n^2)$ time using dynamic programming over terminal subsets @dreyfuswagner1971. Byrka _et al._ achieved a $ln(4) + epsilon approx 1.39$-approximation @byrka2013; the classic 2-approximation uses the minimum spanning tree of the terminal distance graph. + + For signed weights, enumerating the $2^(n-|T|)$ nonterminal subsets and computing a minimum spanning tree on each induced graph gives an $O(2^(n-|T|) n^2)$ exact algorithm. Selected edges must still form a single acyclic tree; disconnected negative edges and cycles are invalid. With one terminal, the zero-edge tree at that terminal is feasible, but a tree containing negative edges can have lower cost. // Find the unique direct terminal-terminal edge (both endpoints in T, not in the optimal tree) #let terminal-set = terminals @@ -3809,74 +3810,6 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| ] } -#{ - let x = load-model-example("SteinerTreeInGraphs") - let nv = graph-num-vertices(x.instance) - let edges = x.instance.graph.edges - let ne = edges.len() - let terminals = x.instance.terminals - let weights = x.instance.edge_weights - let sol = (config: x.optimal_config, metric: x.optimal_value) - let opt-weight = metric-value(sol.metric) - // Derive tree edges from optimal config - let tree-edge-indices = sol.config.enumerate().filter(((i, v)) => v).map(((i, _)) => i) - let tree-edges = tree-edge-indices.map(i => edges.at(i)) - // Steiner vertices: non-terminal vertices that appear in tree edges - let steiner-verts = range(nv).filter(v => not terminals.contains(v) and tree-edges.any(e => e.at(0) == v or e.at(1) == v)) - [ - #problem-def("SteinerTreeInGraphs")[ - Given an undirected graph $G = (V, E)$ with edge weights $w: E -> RR_(>= 0)$ and a set of terminal vertices $R subset.eq V$, find a subtree $T$ of $G$ that spans all terminals in $R$ and minimizes the total edge weight $sum_(e in T) w(e)$. - ][ - A classical NP-complete problem from Karp's list (as "Steiner Tree in Graphs," Garey & Johnson ND12) @karp1972. Central to network design, VLSI layout, and phylogenetic reconstruction. The problem generalizes minimum spanning tree (where $R = V$) and shortest path (where $|R| = 2$). The Dreyfus--Wagner dynamic programming algorithm @dreyfuswagner1971 solves it in $O(3^k dot n + 2^k dot n^2 + n^3)$ time, where $k = |R|$ and $n = |V|$. Bjorklund et al. @bjorklund2007 achieved $O^*(2^k)$ using subset convolution over the Mobius algebra, and Nederlof @nederlof2009 gave an $O^*(2^k)$ polynomial-space algorithm. - - *Example.* Consider a graph $G$ with $n = #nv$ vertices and $|E| = #ne$ edges. The terminals are $R = {#terminals.map(i => $v_#i$).join(", ")}$ (blue). The optimal Steiner tree uses Steiner vertex #steiner-verts.map(i => $v_#i$).join(", ") (gray, dashed border) and edges #tree-edges.map(e => [$\{v_#(e.at(0)), v_#(e.at(1))\}$]).join(", ") with total weight #tree-edge-indices.map(i => str(weights.at(i))).join(" + ") $= #opt-weight$. - - #pred-commands( - "pred create --example SteinerTreeInGraphs -o steiner-tree-in-graphs.json", - "pred solve steiner-tree-in-graphs.json", - "pred evaluate steiner-tree-in-graphs.json --config " + cli-config(x.optimal_config), - ) - - #figure({ - // Graph: 6 vertices arranged in two rows (layout positions) - let verts = ((0, 1), (1.5, 1), (3, 1), (1.5, -0.5), (3, -0.5), (4.5, 0.25)) - canvas(length: 1cm, { - import draw: * - // Edge (0,2) idx=1 would otherwise pass straight through the collinear - // vertex $v_1$ at $(1.5, 1)$, so route it as a quadratic Bezier arc above. - let arc-ctrl = ("1": (1.5, 1.85)) - for (idx, (u, v)) in edges.enumerate() { - let on-tree = tree-edges.any(t => (t.at(0) == u and t.at(1) == v) or (t.at(0) == v and t.at(1) == u)) - let stk = if on-tree { 2pt + graph-colors.at(0) } else { 1pt + luma(200) } - let key = str(idx) - if key in arc-ctrl { - let c = arc-ctrl.at(key) - bezier(verts.at(u), verts.at(v), c, stroke: stk) - let mx = 0.25 * verts.at(u).at(0) + 0.5 * c.at(0) + 0.25 * verts.at(v).at(0) - let my = 0.25 * verts.at(u).at(1) + 0.5 * c.at(1) + 0.25 * verts.at(v).at(1) - draw.content((mx, my + 0.18), text(7pt, fill: luma(80))[#weights.at(idx)]) - } else { - g-edge(verts.at(u), verts.at(v), stroke: stk) - let mx = (verts.at(u).at(0) + verts.at(v).at(0)) / 2 - let my = (verts.at(u).at(1) + verts.at(v).at(1)) / 2 - draw.content((mx, my), text(7pt, fill: luma(80))[#weights.at(idx)]) - } - } - for (k, pos) in verts.enumerate() { - let is-terminal = terminals.contains(k) - let is-steiner = steiner-verts.contains(k) - g-node(pos, name: "v" + str(k), - fill: if is-terminal { graph-colors.at(0) } else if is-steiner { luma(220) } else { white }, - stroke: if is-steiner { (dash: "dashed", paint: graph-colors.at(0)) } else { 1pt + black }, - label: if is-terminal { text(fill: white)[$v_#k$] } else { [$v_#k$] }) - } - }) - }, - caption: [Steiner Tree: terminals $R = {#terminals.map(i => $v_#i$).join(", ")}$ (blue), Steiner vertex #steiner-verts.map(i => $v_#i$).join(", ") (dashed). Optimal tree (blue edges) has weight #opt-weight.], - ) - ] - ] -} #{ let x = load-model-example("MinimumSumMulticenter") @@ -4945,10 +4878,21 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| ] } +// Expand small sparse QUBO examples only for typesetting their matrices. +#let qubo-matrix(instance) = { + let m = instance.matrix + range(m.nrows).map(i => { + let row = range(m.ncols).map(_ => 0) + for k in range(m.indptr.at(i), m.indptr.at(i + 1)) { + row.at(m.indices.at(k)) = m.data.at(k) + } + row + }) +} #{ let x = load-model-example("QUBO") - let n = x.instance.num_vars - let Q = x.instance.matrix + let n = x.instance.matrix.nrows + let Q = qubo-matrix(x.instance) let sol = (config: x.optimal_config, metric: x.optimal_value) let xstar = sol.config let fstar = metric-value(sol.metric) @@ -5436,21 +5380,20 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| let basis = x.instance.basis let target = x.instance.target let sol = (config: x.optimal_config, metric: x.optimal_value) - let dist = metric-value(sol.metric) let coords = sol.config // Compute B*x: sum over j of coords[j] * basis[j] let dim = basis.at(0).len() let bx = range(dim).map(d => coords.enumerate().fold(0.0, (acc, (j, c)) => acc + c * basis.at(j).at(d))) // Format basis vectors let fmt-vec(v) = $paren.l #v.map(e => str(e)).join(", ") paren.r^top$ - let dist-rounded = calc.round(dist, digits: 3) + let distance-squared = range(dim).fold(0, (total, d) => total + calc.pow(bx.at(d) - target.at(d), 2)) [ #problem-def("ClosestVectorProblem")[ - Given a full-column-rank integer lattice basis $bold(B) in ZZ^(m times n)$, whose columns span $cal(L)(bold(B)) = {bold(B) bold(x) : bold(x) in ZZ^n}$, and target $bold(t) in RR^m$, find $bold(x) in ZZ^n$ minimizing $norm(bold(B) bold(x) - bold(t))_2$. + Given a full-column-rank integer lattice basis $bold(B) in ZZ^(m times n)$, whose columns span $cal(L)(bold(B)) = {bold(B) bold(x) : bold(x) in ZZ^n}$, and target $bold(t) in RR^m$, find $bold(x) in ZZ^n$ minimizing $norm(bold(B) bold(x) - bold(t))_2^2$. ][ - The Closest Vector Problem is a fundamental lattice problem @micciancio2002 and is NP-hard @vanemde1981. The implementation provides an integer-target variant for exact reduction data and a finite-`f64` target variant for real input; both keep the lattice basis integral and place no bounds on $bold(x)$. Its reference solver uses exact rational Gram--Schmidt projections and sphere-enumeration bounds following the recursive enumeration structure of Fincke and Pohst @fincke1985. Finite `f64` targets are interpreted as their exact binary rational values. The solver is intended for small instances. Kannan's enumeration algorithm @kannan1987 solves CVP in $n^(O(n))$ time; Micciancio and Voulgaris @micciancio2010 improved this to deterministic $O^*(4^n)$, and Aggarwal, Dadush, and Stephens-Davidowitz @aggarwal2015 achieved randomized $O^*(2^n)$. + The Closest Vector Problem is a fundamental lattice problem @micciancio2002 and is NP-hard @vanemde1981. The implementation provides an integer-target variant for exact reduction data and a finite-`f64` target variant for real input; both keep the lattice basis integral and place no bounds on $bold(x)$. Its reference solver uses exact rational Gram--Schmidt projections and sphere-enumeration bounds following the recursive enumeration structure of Fincke and Pohst @fincke1985. Model evaluation returns the squared distance as an exact rational, preserving the minimizers of Euclidean distance. Finite `f64` targets are interpreted as their exact binary rational values. The solver is intended for small instances. Kannan's enumeration algorithm @kannan1987 solves CVP in $n^(O(n))$ time; Micciancio and Voulgaris @micciancio2010 improved this to deterministic $O^*(4^n)$, and Aggarwal, Dadush, and Stephens-Davidowitz @aggarwal2015 achieved randomized $O^*(2^n)$. - *Example.* Consider the 2D lattice with basis #range(basis.len()).map(j => $bold(b)_#(j + 1) = #fmt-vec(basis.at(j))$).join(", ") and target $bold(t) = #fmt-vec(target)$. The point $bold(B)(#coords.map(c => str(c)).join(","))^top = (#bx.map(v => str(int(v))).join(", "))^top$ equals the target, so it is a closest lattice point with distance #dist-rounded. + *Example.* Consider the 2D lattice with basis #range(basis.len()).map(j => $bold(b)_#(j + 1) = #fmt-vec(basis.at(j))$).join(", ") and target $bold(t) = #fmt-vec(target)$. The point $bold(B)(#coords.map(c => str(c)).join(","))^top = (#bx.map(v => str(int(v))).join(", "))^top$ equals the target, so it is a closest lattice point with squared distance #distance-squared. #pred-commands( "pred create --example ClosestVectorProblem -o closest-vector-problem.json", @@ -5485,7 +5428,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| content((rel: (-0.3, 0), to: "b2.mid"), text(7pt)[$bold(b)_2$]) content((rel: (0.45, 0.3), to: "p" + str(coords.at(0)) + str(coords.at(1))), text(7pt)[$bold(B)(#coords.map(c => str(c)).join(","))^top$]) }), - caption: [2D lattice with basis #range(basis.len()).map(j => $bold(b)_#(j + 1) = #fmt-vec(basis.at(j))$).join(", "). Target $bold(t) = #fmt-vec(target)$ (red) and closest lattice point $bold(B)(#coords.map(c => str(c)).join(","))^top = (#bx.map(v => str(int(v))).join(", "))^top$ (blue). Distance $approx #dist-rounded$.], + caption: [2D lattice with basis #range(basis.len()).map(j => $bold(b)_#(j + 1) = #fmt-vec(basis.at(j))$).join(", "). Target $bold(t) = #fmt-vec(target)$ (red) and closest lattice point $bold(B)(#coords.map(c => str(c)).join(","))^top = (#bx.map(v => str(int(v))).join(", "))^top$ (blue). Squared distance $#distance-squared$.], ) ] ] @@ -7604,7 +7547,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| $ min_(c, p_1, dots, p_n) max_(1 lt.eq i lt.eq n) d_H (c, s_i [p_i .. p_i + ell)), $ where $d_H$ is the Hamming distance and $s_i [p_i .. p_i + ell)$ is the length-$ell$ substring of $s_i$ starting at position $p_i$. ][ - Introduced by #cite(, form: "prose"), who showed that the decision version is NP-complete (even over the binary alphabet) and gave the first polynomial-time approximation scheme. Closest Substring strictly generalizes Closest String: the special case $ell = |s_i|$ for all $i$ forces a unique window in each string and recovers Closest String. The registered exact baseline enumerates every center in $Sigma^ell$ together with every tuple of window starts, giving $O(q^ell dot product_i (|s_i| - ell + 1))$ configurations. + Introduced by #cite(, form: "prose"), who showed that the decision version is NP-complete (even over the binary alphabet) and gave the first polynomial-time approximation scheme. Closest Substring strictly generalizes Closest String: the special case $ell = |s_i|$ for all $i$ forces a unique window in each string and recovers Closest String. The registered exact baseline enumerates every center in $Sigma^ell$ together with every tuple of window starts, giving $O(q^ell dot product_i (|s_i| - ell + 1))$ configurations. Writing $W = sum_i (|s_i| - ell + 1)$, AM–GM bounds this count by $q^ell (W/n)^n$; the registered complexity uses this bound without storing the product as an instance parameter. *Example.* Let $Sigma = {0, 1}$ ($q = #alphabet-size$), $ell = #ell$, and consider the $n = #n$ binary strings $s_1 = #fmt-str(strings.at(0))$, $s_2 = #fmt-str(strings.at(1))$, $s_3 = #fmt-str(strings.at(2))$. @@ -8097,7 +8040,7 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| #problem-def("KthLargestMTuple")[ Given $m$ finite sets $X_1, dots, X_m$ of positive integers, a bound $B in ZZ^+$, and a threshold $K in ZZ^+$, count the number of distinct $m$-tuples $(x_1, dots, x_m) in X_1 times dots.c times X_m$ satisfying $sum_(i=1)^m x_i >= B$. The answer is _yes_ iff this count is at least $K$. ][ - The $K$th Largest $m$-Tuple problem is MP10 in Garey and Johnson's appendix @garey1979. It is _not known to be in NP_, because a "yes" certificate may need to exhibit $K$ qualifying tuples and $K$ can be exponentially large. The problem is PP-complete under polynomial-time Turing reductions @haase2016, though the special case $m = 2$, $K = 1$ is NP-complete via reduction from Subset Sum. In the general case, the only known exact approach is brute-force enumeration of all $product_(i=1)^m |X_i|$ tuples, so the registered catalog complexity is `total_tuples * num_sets`#footnote[No algorithm improving on brute-force is known for the general $K$th Largest $m$-Tuple problem.]. + The $K$th Largest $m$-Tuple problem is MP10 in Garey and Johnson's appendix @garey1979. It is _not known to be in NP_, because a "yes" certificate may need to exhibit $K$ qualifying tuples and $K$ can be exponentially large. The problem is PP-complete under polynomial-time Turing reductions @haase2016, though the special case $m = 2$, $K = 1$ is NP-complete via reduction from Subset Sum. In the general case, the only known exact approach is brute-force enumeration of all $product_(i=1)^m |X_i|$ tuples, so AM–GM gives the registered catalog bound `(num_elements / num_sets)^num_sets * num_sets`#footnote[No algorithm improving on brute-force is known for the general $K$th Largest $m$-Tuple problem.]. *Example.* Let $m = #m$, $B = #bound$, and $K = #k$ with sets #sets.enumerate().map(((i, s)) => [$X_#(i+1) = {#fmt-values(s)}$]).join([, ]). The Cartesian product has $#total$ tuples. Exactly #k tuples have sum at least #bound, so the answer is _yes_ (count $= K$). The evaluator enumerates the Cartesian product internally and stops once it has found $K$ qualifying tuples. @@ -11978,7 +11921,7 @@ the displayed rule, extracted from the corresponding `pred path` entry. let basis = cvp_qubo.source.instance.basis let target = cvp_qubo.source.instance.target let coords = cvp_qubo_sol.source_config - let matrix = cvp_qubo.target.instance.matrix + let matrix = qubo-matrix(cvp_qubo.target.instance) let bits = cvp_qubo_sol.target_config let lower = (-23, -14) let anchor = range(target.len()).map(d => lower.enumerate().fold(0.0, (acc, (i, x)) => acc + x * basis.at(i).at(d))) @@ -12005,7 +11948,7 @@ the displayed rule, extracted from the corresponding `pred path` entry. *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 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.* 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.matrix.nrows 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 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. @@ -12252,7 +12195,7 @@ where $P$ is a penalty weight large enough that any constraint violation costs m together with target $ bold(t) = (#fmt-values(ss-cvp-target-vec))^top $ in the standard CVP model, with no coefficient bounds. - *Step 3 -- Verify the canonical witness.* The fixture stores coefficients $(#fmt-values(ss-cvp-x))$. Its first four entries select sizes $3$ and $8$, and the final three are carry coefficients. The first coordinate block has residual $(1,0,0,1)$, the second has $(0,-1,-1,0)$, and all bit-equation residuals are zero. Thus the Euclidean distance is $sqrt(4) = 2$. + *Step 3 -- Verify the canonical witness.* The fixture stores coefficients $(#fmt-values(ss-cvp-x))$. Its first four entries select sizes $3$ and $8$, and the final three are carry coefficients. The first coordinate block has residual $(1,0,0,1)$, the second has $(0,-1,-1,0)$, and all bit-equation residuals are zero. Thus the squared-distance objective is $4$. *Witness semantics.* The example DB stores one canonical minimizer. This source instance also has another satisfying subset, $(1, 1, 1, 0)$, so the reduction has multiple optimal CVP witnesses even though only one is serialized. ], @@ -12261,15 +12204,15 @@ where $P$ is a penalty weight large enough that any constraint violation costs m ][ _Construction._ Let $n$ be the number of items, and let $b >= 1$ be the maximum bit length of their nonnegative sizes and target $T$. Write $s_(i,j), t_j in {0,1}$ for bit $j$ of size $s_i$ and target $T$. Introduce integer coefficients $x_0, dots, x_(n-1)$ and carries $c_1, dots, c_(b-1)$, with fixed boundary values $c_0=c_b=0$. The displacement vector consists of $x_i$ for all items, then $x_i-1$ for all items, then residuals $ r_j = sum_(i=0)^(n-1) s_(i,j) x_i + c_j - 2 c_(j+1) - t_j $ - in descending bit order. These linear expressions define the integer basis columns and a target containing only zeros and ones. Carry columns are also ordered by descending bit index. The first $n$ coordinate rows form an identity on item columns; the remaining carry block has unit pivots in this order. Consequently the full-column-rank check has no exponentially growing pivots. + in descending bit order. These linear expressions define the integer basis columns and a target containing only zeros and ones. Carry columns are also ordered by descending bit index. The first $n$ coordinate rows form an identity on item columns; the remaining carry block has unit pivots in this order. This triangular block together with the item identity proves full column rank. _Correctness._ Every integer vector satisfies $ norm(bold(B) bold(z)-bold(t))_2^2 = sum_i (x_i^2 + (x_i-1)^2) + sum_j r_j^2 >= n. $ - ($arrow.r.double$) For a binary subset summing to $T$, ordinary integer addition gives carries $0 <= c_j <= n$ satisfying all bit equations and both boundaries. Its squared distance equals $n$. ($arrow.l.double$) Squared distance at most $n$ forces each $x_i in {0,1}$ and each $r_j=0$. Multiplying the bit equations by $2^j$ and summing cancels the internal carries, yielding $sum_i s_i x_i=T$. Thus the optimum is $sqrt(n)$ exactly for YES instances. Empty item lists and target zero use the same construction. + ($arrow.r.double$) For a binary subset summing to $T$, ordinary integer addition gives carries $0 <= c_j <= n$ satisfying all bit equations and both boundaries. Its squared distance equals $n$. ($arrow.l.double$) Squared distance at most $n$ forces each $x_i in {0,1}$ and each $r_j=0$. Multiplying the bit equations by $2^j$ and summing cancels the internal carries, yielding $sum_i s_i x_i=T$. Thus the squared-distance optimum is $n$ exactly for YES instances. Empty item lists and target zero use the same construction. - _Solution extraction._ Validate the target configuration once and require a finite distance exactly $sqrt(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 distance proves NO and provides no source witness. + _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 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. On a 64-bit platform this bounds $n < 2^30$; the threshold and the unit squared-distance gap remain distinguishable in the target's floating-point evaluation. The paired coordinates and boundary carry equations also ensure every threshold witness has exactly evaluated small integer residuals. 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. 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 model evaluates squared distances in exact rational arithmetic and the solver uses exact rational sphere-enumeration bounds; runtime limitations are separate from the mathematical equivalence. ] ] } @@ -12441,7 +12384,7 @@ where $P$ is a penalty weight large enough that any constraint violation costs m #let ks_qubo = load-example("Knapsack", "QUBO") #let ks_qubo_sol = ks_qubo.solutions.at(0) #let ks_qubo_num_items = ks_qubo.source.instance.weights.len() -#let ks_qubo_num_slack = ks_qubo.target.instance.num_vars - ks_qubo_num_items +#let ks_qubo_num_slack = ks_qubo.target.instance.matrix.nrows - ks_qubo_num_items #let ks_qubo_penalty = 1 + ks_qubo.source.instance.values.fold(0, (a, b) => a + b) #let ks_qubo_selected = ks_qubo_sol.source_config.enumerate().filter(((i, x)) => x).map(((i, x)) => i) #let ks_qubo_sel_weight = ks_qubo_selected.fold(0, (a, i) => a + ks_qubo.source.instance.weights.at(i)) @@ -12460,7 +12403,7 @@ where $P$ is a penalty weight large enough that any constraint violation costs m *Step 2 -- Introduce slack variables.* The inequality $sum_i w_i x_i lt.eq C$ becomes an equality by adding $B = #ks_qubo_num_slack$ binary slack bits that encode unused capacity: $ #ks_qubo.source.instance.weights.enumerate().map(((i, w)) => $#w x_#i$).join($+$) + #range(ks_qubo_num_slack).map(j => $#calc.pow(2, j) s_#j$).join($+$) = #ks_qubo.source.instance.capacity $ - This gives $n + B = #ks_qubo_num_items + #ks_qubo_num_slack = #ks_qubo.target.instance.num_vars$ QUBO variables. + This gives $n + B = #ks_qubo_num_items + #ks_qubo_num_slack = #ks_qubo.target.instance.matrix.nrows$ QUBO variables. *Step 3 -- Add the penalty objective.* With penalty $P = 1 + sum_i v_i = #ks_qubo_penalty$, the QUBO minimizes $ H = -(#ks_qubo.source.instance.values.enumerate().map(((i, v)) => $#v x_#i$).join($+$)) + #ks_qubo_penalty (#ks_qubo.source.instance.weights.enumerate().map(((i, w)) => $#w x_#i$).join($+$) + #range(ks_qubo_num_slack).map(j => $#calc.pow(2, j) s_#j$).join($+$) - #ks_qubo.source.instance.capacity)^2 $ @@ -12501,7 +12444,7 @@ where $P$ is a penalty weight large enough that any constraint violation costs m *Step 2 -- One-hot variables.* Introduce one binary selector per sampled orientation: $ underbrace(y_(1,0) y_(1,1), "link 1") #h(6pt) underbrace(y_(2,0) y_(2,1), "link 2") $ - The QUBO therefore has $2 + 2 = #mdpik_qubo.target.instance.num_vars$ variables. + The QUBO therefore has $2 + 2 = #mdpik_qubo.target.instance.matrix.nrows$ variables. *Step 3 -- Quadratic energy.* The geometric coefficients are $c = (2, 0, 1, 0)$ for the $x$-coordinate and $s = (0, 2, 0, 1)$ for the $y$-coordinate, so the position term is $ (2 y_(1,0) + y_(2,0) - 2)^2 + (2 y_(1,1) + y_(2,1) - 1)^2. $ @@ -12515,18 +12458,18 @@ where $P$ is a penalty weight large enough that any constraint violation costs m _Construction._ For each link $j in {1, dots, n}$ and sample index $a in {0, dots, m_j - 1}$, introduce a binary variable $y_(j,a) in {0,1}$ with the intended meaning "$y_(j,a) = 1$ iff link $j$ chooses orientation $phi_(j,a)$." Define $ c_(j,a) = l_j cos phi_(j,a), quad s_(j,a) = l_j sin phi_(j,a). $ Let - $ P = 1 + (sum_(j,a) |c_(j,a)| + |g_x|)^2 + (sum_(j,a) |s_(j,a)| + |g_y|)^2. $ + $ D = (sum_(j,a) |c_(j,a)| + |g_x|)^2 + (sum_(j,a) |s_(j,a)| + |g_y|)^2, quad P = 2(1 + D). $ The QUBO objective is the sum of three terms: $ H = underbrace((sum_(j,a) c_(j,a) y_(j,a) - g_x)^2 + (sum_(j,a) s_(j,a) y_(j,a) - g_y)^2)_"position error" + underbrace(P sum_(j=1)^n (sum_(a=0)^(m_j - 1) y_(j,a) - 1)^2)_"one-hot" + underbrace(P sum_(j=2)^n sum_((a,b) in.not A_j) y_(j-1,a) y_(j,b))_"forbidden pairs". $ - Expanding with $y_(j,a)^2 = y_(j,a)$ gives the upper-triangular QUBO matrix. As usual, the additive constant $g_x^2 + g_y^2$ is dropped. + Expanding with $y_(j,a)^2 = y_(j,a)$ gives the upper-triangular QUBO matrix. The implementation drops the full additive constant $C = g_x^2 + g_y^2 + n P$, including the one-hot constants. - _Correctness._ ($arrow.r.double$) Any feasible inverse-kinematics configuration $a_1, dots, a_n$ maps to the one-hot assignment with $y_(j,a_j) = 1$ and all other selectors $0$. Every one-hot penalty vanishes, every consecutive pair lies in the relevant admissible set, and the remaining QUBO objective equals the squared end-effector distance up to the dropped additive constant. ($arrow.l.double$) If some link is not one-hot, then $(sum_a y_(j,a) - 1)^2 >= 1$, so the assignment pays at least $P$. If every link is one-hot but some consecutive pair is forbidden, then exactly one forbidden-pair monomial is active at that junction, again contributing at least $P$. By definition of $P$, every decoded source configuration has squared distance at most $P - 1$, while the dropped-constant geometric term is bounded below by $-(g_x^2 + g_y^2)$. Therefore every violating assignment has strictly larger energy than every feasible source assignment. Among the penalty-zero assignments, minimizing $H$ is exactly minimizing the source squared distance. + _Correctness._ ($arrow.r.double$) Any feasible inverse-kinematics configuration $a_1, dots, a_n$ maps to the one-hot assignment with $y_(j,a_j) = 1$ and all other selectors $0$. Every one-hot penalty vanishes, every consecutive pair lies in the relevant admissible set, and the remaining QUBO objective equals the squared end-effector distance up to the dropped additive constant. ($arrow.l.double$) If some link is not one-hot, then $(sum_a y_(j,a) - 1)^2 >= 1$, so the assignment pays at least $P$. If every link is one-hot but some consecutive pair is forbidden, then exactly one forbidden-pair monomial is active at that junction, again contributing at least $P$. Every feasible assignment has $H <= D$, whereas every violating assignment has $H >= P$. Thus a feasible source has only qualifying optima; if the source is infeasible, every target assignment has $H >= P$. Among the penalty-zero assignments, minimizing $H$ is exactly minimizing the source squared distance. - _Solution extraction._ For each link block $j$, read the unique active selector $y_(j,a) = 1$ and output its sample index $a$. If the decoded index vector violates an admissible-pair constraint, the source evaluator rejects it with `Min(None)`. + _Value recovery and extraction._ For target optimum $E$, compare $E$ to $3(1+D)/2 - C$, which lies strictly between the feasible and infeasible energy ranges. An optimum above this separator yields `Min(None)` for the source. Otherwise the source optimum is $E+C$, and each block has a unique active selector whose sample index is the source witness. Value recovery precedes extraction; extraction does not recheck one-hot or pair feasibility. Floating-point results follow the numerical contract; the scaled penalty leaves a gap proportional to the coefficient scale, and nonfinite construction arithmetic is an error. ] #let mwc_qubo = load-example("MinimumMultiwayCut", "QUBO") @@ -12564,23 +12507,22 @@ where $P$ is a penalty weight large enough that any constraint violation costs m *Step 6 -- Verify a solution.* The QUBO ground state $bold(x) = (#fmt-values(mwc_qubo_sol.target_config))$ decodes to the partition: vertex 0 in component 0, vertices 1--3 in component 1, vertex 4 in component 2. Cut edges: $\{#mwc_qubo_cut_indices.map(i => "(" + str(mwc_qubo_edges.at(i).at(0)) + "," + str(mwc_qubo_edges.at(i).at(1)) + ")").join(", ")\}$ with total weight #mwc_qubo_cut_indices.map(i => str(mwc_qubo_weights.at(i))).join(" + ") $= #mwc_qubo_cut_cost$ #sym.checkmark. ], )[ - The multiway cut problem requires a partition of vertices into $k$ components — one per terminal — minimizing the total weight of edges crossing components. The penalty method (@sec:penalty-method) encodes two constraints as QUBO penalties: (1) each vertex belongs to exactly one component (one-hot), and (2) each terminal is pinned to its own component. The cut-cost Hamiltonian counts edge weight across distinct components. Reference: @Heidari2022. + A multiway cut deletes edges to separate every terminal pair. For signed weights, every negative edge is deleted first; the remaining nonnegative problem is represented by a terminal-labelled partition. The penalty method (@sec:penalty-method) enforces one label per vertex and pins each terminal to its own label. Reference for the partition encoding: @Heidari2022. ][ - _Construction._ Given $G = (V, E)$ with $n = |V|$, edge weights $w: E -> RR_(>0)$, and $k$ terminals $T = {t_0, ..., t_(k-1)}$. Introduce $n k$ binary variables $x_(u,t) in {0,1}$ (indexed by $u dot k + t$), where $x_(u,t) = 1$ means vertex $u$ is in terminal $t$'s component. Let $alpha = 1 + sum_(e in E) w(e)$. + _Construction._ Given $G = (V, E)$ with integer weights $w: E -> ZZ$ and $k >= 2$ distinct terminals, let $w^+(e) = max(w(e), 0)$ and $C_- = sum_(e: w(e) < 0) w(e)$. Introduce $n k$ binary variables $x_(u,t)$, where label $t$ indicates the terminal group of vertex $u$. Let $alpha = 1 + sum_(e in E) w^+(e)$. - The QUBO Hamiltonian is $H = H_A + H_B$ where: + The Hamiltonian is $H = H_A + H_B$, with $ H_A = alpha (sum_(u in V) (1 - sum_(t=0)^(k-1) x_(u,t))^2 + sum_(i=0)^(k-1) sum_(s != i) x_(t_i, s)) $ - The first term is a _one-hot constraint_ ensuring each vertex is assigned to exactly one component. The second term _pins_ each terminal $t_i$ to position $i$ by penalizing any other assignment. Expanding the one-hot term using $x^2 = x$: - $ Q_(u k+t, u k+t) = -alpha, quad Q_(u k+s, u k+t) = 2 alpha quad (s < t) $ - Terminal pinning adds $alpha$ to the diagonal $Q_(t_i k+s, t_i k+s)$ for $s != i$, canceling the one-hot incentive. + and + $ H_B = sum_((u,v) in E) sum_(s != t) w^+(u,v) x_(u,s) x_(v,t). $ + The implemented QUBO omits the constant $n alpha$ from $H_A$. + + _Correctness._ Deleting a negative edge strictly reduces cost and cannot reconnect terminals, so every source optimum deletes all negative edges. Both Hamiltonian terms are nonnegative for every binary assignment. A pinned one-hot assignment exists and has energy at most $sum_e w^+(e) < alpha$; any constraint violation costs at least $alpha$. Thus every target optimum is pinned and one-hot. - The cut-cost Hamiltonian: - $ H_B = sum_((u,v) in E) sum_(s != t) w(u,v) dot x_(u,s) dot x_(v,t) $ - counts the total weight of edges whose endpoints lie in different components. + Given any feasible deletion set, label each remaining connected component by its terminal, choosing any label for components without a terminal. Every nonnegative edge crossing labels was already deleted. Conversely, deleting all negative edges and every edge crossing labels separates the terminals. These two directions show that the minimum partition cost plus $C_-$ is exactly the minimum source cost. Consequently every QUBO optimum recovers a source optimum, whose value is the QUBO optimum plus $n alpha + C_-$. All source instances are feasible because deleting all edges separates distinct terminals. - _Correctness._ ($arrow.r.double$) A valid multiway cut with cost $C$ maps to a QUBO solution with $H_A = 0$ (valid partition with correct terminal pinning) and $H_B = C$. ($arrow.l.double$) If $H_A > 0$, the penalty $alpha > sum_e w(e)$ exceeds the entire cut-cost range, so any QUBO minimizer has $H_A = 0$, encoding a valid partition. Among valid partitions, $H_B$ equals the cut cost, and the minimizer achieves the minimum multiway cut. + _Solution extraction._ Find the selected label of each vertex. Delete edge $(u,v)$ exactly when $w(u,v) < 0$ or its endpoint labels differ. Extraction assumes an optimal target witness; it does not check the one-hot constraints again. Integer coefficient overflow is reported by construction. - _Solution extraction._ For each vertex $u$, find terminal position $t$ with $x_(u,t) = 1$. For each edge $(u,v)$, output 1 (cut) if $u$ and $v$ are in different components, 0 otherwise. ] #reduction-rule("GraphPartitioning", "QUBO")[ @@ -12616,8 +12558,8 @@ where $P$ is a penalty weight large enough that any constraint violation costs m "pred solve bundle.json", "pred evaluate qubo.json --config " + cli-config(qubo_ilp_sol.source_config), ) - Source: $n = #qubo_ilp.source.instance.num_vars$ binary variables, 3 off-diagonal terms \ - Target: #qubo_ilp.target.instance.variables.len() ILP variables ($#qubo_ilp.source.instance.num_vars$ original $+ #(qubo_ilp.target.instance.variables.len() - qubo_ilp.source.instance.num_vars)$ auxiliary), #qubo_ilp.target.instance.constraints.len() McCormick constraints \ + Source: $n = #qubo_ilp.source.instance.matrix.nrows$ binary variables, 3 off-diagonal terms \ + Target: #qubo_ilp.target.instance.variables.len() ILP variables ($#qubo_ilp.source.instance.matrix.nrows$ original $+ #(qubo_ilp.target.instance.variables.len() - qubo_ilp.source.instance.matrix.nrows)$ auxiliary), #qubo_ilp.target.instance.constraints.len() McCormick constraints \ Canonical optimal witness: $bold(x) = (#fmt-values(qubo_ilp_sol.source_config))$ #sym.checkmark ], )[ @@ -14005,7 +13947,7 @@ The following reductions to Integer Linear Programming are straightforward formu "pred solve bundle.json", "pred evaluate tsp.json --config " + cli-config(tsp_qubo_sol.source_config), ) - *Step 1 -- Encode each tour position as a binary variable.* A tour is a permutation of $n$ vertices. Introduce $n^2 = #tsp_qubo.target.instance.num_vars$ binary variables $x_(v,p)$: vertex $v$ is at position $p$. + *Step 1 -- Encode each tour position as a binary variable.* A tour is a permutation of $n$ vertices. Introduce $n^2 = #tsp_qubo.target.instance.matrix.nrows$ binary variables $x_(v,p)$: vertex $v$ is at position $p$. $ underbrace(x_(0,0) x_(0,1) x_(0,2), "vertex 0") #h(4pt) underbrace(x_(1,0) x_(1,1) x_(1,2), "vertex 1") #h(4pt) underbrace(x_(2,0) x_(2,1) x_(2,2), "vertex 2") $ *Step 2 -- Penalize invalid permutations.* The penalty $A = 1 + |w_(01)| + |w_(02)| + |w_(12)| = 1 + 1 + 2 + 3 = 7$ ensures any row/column constraint violation outweighs any tour cost. Row constraints (each vertex at exactly one position) and column constraints (each position has one vertex) contribute diagonal $-7$ and off-diagonal $+14$ within each group.\ @@ -14019,15 +13961,18 @@ The following reductions to Integer Linear Programming are straightforward formu )[ Position-based QUBO encoding @lucas2014 maps a Hamiltonian tour to $n^2$ binary variables $x_(v,p)$, where $x_(v,p) = 1$ iff city $v$ is visited at position $p$. The QUBO Hamiltonian $H = H_A + H_B + H_C$ combines permutation constraints with the distance objective ($n^2$ variables indexed by $v dot n + p$). ][ - _Construction._ For graph $G = (V, E)$ with $n = |V|$ and edge weights $w_(u v)$. Let $A = 1 + sum_((u,v) in E) |w_(u v)|$ be the penalty coefficient. + _Construction._ For $n >= 3$, discard self-loops and retain one cheapest edge per endpoint pair; a Hamiltonian cycle on at least three vertices uses neither a loop nor two parallel edges. Let $b = min(0, min_e w(e))$, using $b=0$ if no edges remain. Define $c(e)=w(e)-b >= 0$ and $A=1+sum_e c(e)$. + + _Variables:_ Binary $x_(v,p)$ indicates that vertex $v$ occupies position $p$, with index $v n+p$. The Hamiltonian is + $ H = A sum_v (1-sum_p x_(v,p))^2 + A sum_p (1-sum_v x_(v,p))^2 + sum_(u= A-2n A$, return source infeasibility. Otherwise the source optimum is $E+2n A+n b$. Read the unique vertex at each position and select the stored cheapest edge between consecutive vertices. This maps every qualifying optimum; extraction does not certify permutation or edge feasibility again. Checked construction arithmetic reports coefficients that cannot be represented in the integer QUBO. - _Correctness._ ($arrow.r.double$) A valid tour defines a permutation matrix satisfying $H_A = H_B = 0$; the $H_C$ terms sum to the tour cost. ($arrow.l.double$) The minimum-energy state has $H_A = H_B = 0$ (penalty $A$ exceeds any tour cost), so it encodes a valid permutation; $H_C$ equals the tour cost, selecting the shortest tour. + _Small instances._ The source witness is a connected edge set with degree two at every vertex. For $n=1$ its optimum is the cheapest self-loop, if one exists. For $n=2$ it is the two cheapest parallel edges joining the vertices, if two exist. For $n=0$ the source has no cycle. These cases are solved during construction by selecting the one or two smallest relevant edge weights in linear time. The target is a zero objective on $n^2$ variables; every target optimum maps to the stored source optimum or infeasibility. This preserves the source model's accepted graph domain. - _Solution extraction._ From QUBO solution $x^*$, for each position $p$ find the unique vertex $v$ with $x^*_(v n + p) = 1$. Map consecutive position pairs to edge indices. ] #let lcs_mis = load-example("LongestCommonSubsequence", "MaximumIndependentSet") @@ -15233,24 +15178,6 @@ The following reductions to Integer Linear Programming are straightforward formu _Solution extraction._ For each position $p$, return the unique $i$ with $x_(i,p)=1$, using the existing one-hot decoder. There are $m^2+m^3$ binary variables and $2m+3m^3+m r$ constraints, where $r$ is the number of unreachable ordered required-arc pairs; hence at most $2m+4m^3$ constraints. ] -#reduction-rule("SteinerTreeInGraphs", "ILP")[ - Select edges and certify terminal connectivity by sending one unit of flow from a root terminal to every other terminal through the selected subgraph. -][ - _Construction._ Fix a root terminal $r in R$. Variables: binary $y_(u,v)$ for each undirected edge $\{u,v\}$ and nonnegative flow variables $f^t_(u,v)$ on each directed edge orientation for every terminal $t in R backslash {r}$. The ILP is: - $ - min quad & sum_({u,v} in E) w_(u,v) y_(u,v) \ - "subject to" quad & sum_(u) f^t_(u,v) - sum_(w) f^t_(v,w) = b_(t,v) quad forall t in R backslash {r}, v in V \ - & f^t_(u,v) <= y_(u,v) quad forall {u, v} in E, t in R backslash {r} \ - & f^t_(v,u) <= y_(u,v) quad forall {u, v} in E, t in R backslash {r} \ - & y_(u,v) in {0, 1}, f^t_(u,v) in ZZ_(>=0), - $ - where $b_(t,v) = -1$ if $v = r$, $b_(t,v) = 1$ if $v = t$, and $b_(t,v) = 0$ otherwise. - - _Correctness._ ($arrow.r.double$) A Steiner tree supports a unit flow from the root to every other terminal using exactly its selected edges, with the same total weight. ($arrow.l.double$) Any feasible ILP solution selects a connected subgraph spanning all terminals, and with nonnegative edge weights an optimum solution is a minimum-weight Steiner tree. - - _Solution extraction._ Output the binary edge-selection vector $(y_e)_(e in E)$. -] - // Scheduling #reduction-rule("FlowShopScheduling", "ILP")[ @@ -16609,12 +16536,14 @@ Problems parameterized by graph type, weight type, target type, or clause width _Solution extraction._ Return the target configuration unchanged. ] +The numerical variant embeddings below preserve individual stored coefficients or coordinates. Their algebraic identities describe the formal objectives. Floating-point model evaluation still follows finite `f64` arithmetic and can round intermediate expressions; a lossless scalar embedding does not certify backend optimality. CVP instead evaluates its stored coordinates with exact rational squared distances. + #reduction-rule("SpinGlass", "SpinGlass")[ An Ising spin-glass instance with integer couplings and fields ($J_(i j), h_i in ZZ$) converts to the floating-point variant ($J_(i j), h_i in RR$) through exact `i64_to_exact_f64` embeddings. The graph topology is preserved. ][ _Construction._ Given $"SpinGlass"(G, bold(J), bold(h))$ with $J_(i j) in ZZ$ and $h_i in ZZ$, construct $"SpinGlass"(G, bold(J)', bold(h)')$ with $J'_(i j) = J_(i j) in RR$ and $h'_i = h_i in RR$. - _Correctness._ The spin-glass Hamiltonian $H(bold(s)) = sum_((i,j) in E) J_(i j) s_i s_j + sum_i h_i s_i$ is preserved exactly under the integer-to-float embedding (no rounding). Spin configurations and the objective value are unchanged. + _Correctness._ The spin-glass Hamiltonian $H(bold(s)) = sum_((i,j) in E) J_(i j) s_i s_j + sum_i h_i s_i$ is the same formal Hamiltonian under the coefficient embedding. Spin configurations are unchanged. _Solution extraction._ Return the target configuration unchanged. ] @@ -16634,9 +16563,9 @@ Problems parameterized by graph type, weight type, target type, or clause width #reduction-rule("ClosestVectorProblem", "ClosestVectorProblem")[ An integer-target CVP instance converts to the floating-target variant by embedding every target coordinate with `i64_to_exact_f64`. The integer lattice basis is copied unchanged. ][ - _Construction._ Given $(B, bold(t))$ with $B in ZZ^(m times n)$ and $bold(t) in ZZ^m$, construct $(B, bold(t)')$ with $t'_i = "f64"(t_i)$ for every exactly representable coordinate $|t_i| lt.eq 2^53 - 1$. + _Construction._ Given $(B, bold(t))$ with $B in ZZ^(m times n)$ and $bold(t) in ZZ^m$, construct $(B, bold(t)')$ with $t'_i = "f64"(t_i)$ when every target coordinate satisfies $abs(t_i) <= 2^53 - 1$, the supported conversion range. - _Correctness._ Exact coordinate conversion gives $bold(t)' = bold(t)$ in $RR^m$. Therefore $norm(B bold(x) - bold(t)')_2 = norm(B bold(x) - bold(t))_2$ for every $bold(x) in ZZ^n$, so the minimizers coincide. + _Correctness._ Exact coordinate conversion gives $bold(t)' = bold(t)$ in $RR^m$. Therefore $norm(B bold(x) - bold(t)')_2^2 = norm(B bold(x) - bold(t))_2^2$ for every $bold(x) in ZZ^n$, so the minimizers coincide. _Solution extraction._ Return the integer coefficient vector unchanged. ] @@ -16644,7 +16573,7 @@ Problems parameterized by graph type, weight type, target type, or clause width #reduction-rule("QUBO", "QUBO")[ An integer QUBO converts to the floating-coefficient variant by embedding every matrix coefficient with `i64_to_exact_f64`. ][ - _Construction._ Given $Q in ZZ^(n times n)$, construct $Q' in RR^(n times n)$ with $Q'_(i j) = "f64"(Q_(i j))$ for every exactly representable coefficient $|Q_(i j)| lt.eq 2^53 - 1$. + _Construction._ Given $Q in ZZ^(n times n)$, construct $Q' in RR^(n times n)$ with $Q'_(i j) = "f64"(Q_(i j))$ when every matrix coefficient satisfies $abs(Q_(i j)) <= 2^53 - 1$, the supported conversion range. _Correctness._ For every binary vector $bold(x)$, exact coefficient conversion gives $bold(x)^top Q' bold(x) = bold(x)^top Q bold(x)$. The objective ordering and minimizers are preserved. @@ -16907,11 +16836,11 @@ The following table shows concrete target-variable counts for example instances, #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. ][ - _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-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 converted integer is outside the supported range $[-(2^53 - 1), 2^53 - 1]$. _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. - _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-integer and coefficient conversions preserve the assignment after the standard target-solution validation. Numerical solver accuracy is independent of the mathematical coefficient conversion. Binary encoding returns $x_i = L_i + sum_j w_(i j)y_(i j)$. ] #let hc_hp = load-example("HamiltonianCircuit", "HamiltonianPath") @@ -17263,7 +17192,7 @@ The following table shows concrete target-variable counts for example instances, *Multiplicity:* The fixture stores one canonical Hamiltonian circuit. Rotating or reversing that same cycle yields equivalent target witnesses with the same extracted cover. ], )[ - Garey and Johnson's Theorem 3.4 replaces each source edge by a 12-vertex cover-testing gadget and uses $k$ selector vertices to choose $k$ source vertices whose incident gadget-paths together cover every gadget @garey1979. In the unit-weight decision setting, the constructed graph is Hamiltonian iff the source graph has a vertex cover of size at most $k$. + Garey and Johnson's Theorem 3.4 replaces each source edge by a 12-vertex cover-testing gadget and uses $k$ selector vertices to choose $k$ source vertices whose incident gadget-paths together cover every gadget @garey1979. The registered source uses the `One` weight variant of Decision Minimum Vertex Cover. The constructed graph is Hamiltonian iff the source graph has a vertex cover of size at most $k$. ][ _Construction._ Let the source be a unit-weight Decision Minimum Vertex Cover instance $(G = (V, E), k)$ with $G$ simple. For each edge $e = {u, v} in E$, create a gadget with vertices $(u, e, i)$ and $(v, e, i)$ for $1 <= i <= 6$. Add the two 6-chains on the $u$-side and $v$-side together with the four cross edges ${(u, e, 3), (v, e, 1)}$, ${(v, e, 3), (u, e, 1)}$, ${(u, e, 6), (v, e, 4)}$, and ${(v, e, 6), (u, e, 4)}$. For every source vertex $v$, order its incident edges as $e_(v[1]), dots, e_(v[deg(v)])$ and connect ${(v, e_(v[i]), 6), (v, e_(v[i+1]), 1)}$ for $1 <= i < deg(v)$, forming one path that contains exactly the gadget copies labeled by $v$. Finally add selector vertices $a_1, dots, a_k$ and join each selector to both endpoints of every non-isolated vertex-path. Thus the theorem branch has $k + 12|E|$ vertices and $14|E| + sum_(v in V^+) (deg(v)-1) + 2k|V^+|$ edges, where $V^+ = {v in V : deg(v) > 0}$. @@ -19556,36 +19485,22 @@ The following table shows concrete target-variable counts for example instances, )[ Bienstock, Goemans, Simchi-Levi, Williamson @BienstockGoemansSimchiLeviWilliamson1993 introduced the prize/penalty framework for prize-collecting network design; Tuncbag and coauthors @TuncbagEtAl2013PCSF @TuncbagEtAl2012RECOMB used the same artificial-root idea to translate PCSF into a rooted prize-collecting Steiner tree on biological networks. The combined construction recorded here adds a per-vertex auxiliary-terminal gadget that compiles the remaining omitted-prize term `beta * p(v)` into ordinary Steiner-tree edge costs, so the target is a plain (unweighted-prize) Steiner Tree instance. ][ - _Construction._ Given a PCSF instance with graph $G = (V, E)$, edge costs $c$, vertex prizes $p$, and parameters $beta >= 0$, $omega >= 0$, let $V_p = {v in V : p(v) > 0}$ and $k = |V_p|$. Build the target graph $H = (V_H, E_H)$ with weights $c_H$ and terminal set $T_H$ as follows. - - 1. Add a fresh artificial root $r$: $V_H = V union {r} union {t_v : v in V_p}$. - 2. Keep every original edge $e in E$ with $c_H(e) = c(e)$. - 3. For every $v in V$, add a root-attachment edge $(r, v)$ with $c_H((r, v)) = omega$. - 4. For every prized vertex $v in V_p$, add an include-edge $(v, t_v)$ with cost $0$ and an omit-edge $(r, t_v)$ with cost $beta dot p(v)$. - 5. Set $T_H = {r} union {t_v : v in V_p}$. Original vertices $V$ and the new gadget terminals coexist; only $r$ and the $t_v$ are terminals. - - Solve $"SteinerTree"(H, c_H, T_H)$ to obtain a minimum-weight tree $T^*$ spanning $T_H$. - - _Witness extraction._ From $T^*$ recover the PCSF witness $(V_F, E_F)$ by - - $ E_F = T^* inter E(G), quad V_F = { v in V : (v, t_v) in T^* } union { "endpoints of edges in" E_F }. $ + _Construction._ Given a PCSF instance with graph $G=(V,E)$, nonnegative edge costs $c$, nonnegative prizes $p$, and $beta, omega >= 0$, let $V_p={v in V:p(v)>0}$, $k=|V_p|$, and $M=omega+1$. Add an artificial root $r$ and one auxiliary terminal $t_v$ for each $v in V_p$. Keep every original edge with its cost, add $(r,v)$ of cost $omega$ for every original vertex, and add $(v,t_v)$ of cost $M$ and $(r,t_v)$ of cost $M+beta p(v)$. The terminal set is ${r} union {t_v:v in V_p}$. - Equivalently, deleting $r$ and the gadget vertices ${t_v}$ from $T^*$ leaves a disjoint union of trees on $V$; $V_F$ is the set of original vertices touched by this restricted forest, and $E_F$ is exactly $T^* inter E(G)$. Both directions are consistent because: + _Forward bound._ Given any source forest $F$, attach each component once to $r$. For each prized vertex, choose its include edge if selected and its omit edge otherwise. The auxiliary terminals are leaves; the result is a tree spanning all terminals, with cost $f(F)+k M$. Thus $"OPT"_T <= "OPT"_F+k M$. - - any prized vertex $v$ in $V_F$ pays the cost-$0$ include-edge $(v, t_v)$ to reach $t_v$ inside $T^*$; - - any prized vertex $v$ omitted from $V_F$ has $t_v$ joined to the tree exclusively through $(r, t_v)$, paying $beta dot p(v)$. + _Reverse bound._ In an optimal target tree, every auxiliary terminal is a leaf. If both edges at $t_v$ were selected, delete $(r,t_v)$ and add $(r,v)$. The latter edge cannot already be selected, since those three edges would form a cycle. The replacement reconnects the two components created by deletion and decreases cost by $M+beta p(v)-omega >= 1$, a contradiction. - _Correctness._ ($arrow.r.double$) Given any feasible source forest $F$, attach each connected component of $F$ to $r$ via exactly one root-attachment edge (cost $omega$ per component) and resolve each gadget locally: take $(v, t_v)$ if $v in V_F$, else $(r, t_v)$. The resulting subgraph of $H$ is connected, spans $T_H$, and is a tree because every gadget is paid by exactly one of its two edges and the only chord that could close a cycle is removed by the choice of a single root-attachment edge per component. Its cost equals + Extract original selected edges, their endpoints, and each prized vertex whose include edge is selected. This is a feasible source forest. After deleting the auxiliary leaves, each original component has exactly one root attachment, by connectivity and acyclicity. Extraction may discard isolated zero-prize vertices, which cannot increase cost because $omega>=0$. Every omitted positive prize has its omit edge selected; omit edges at vertices retained by original edges only add nonnegative target cost. Therefore $f(F) <= "cost"(T)-k M$. Together with the forward bound, this proves $"OPT"_T="OPT"_F+k M$ and optimality of every extracted optimal target witness. - $ sum_(e in E_F) c(e) + omega dot kappa(F) + beta dot sum_(v in.not V_F) p(v) + 0 = f'(F). $ + _Witness extraction._ Return + $ E_F=T inter E(G), quad V_F={v:(v,t_v) in T} union {"endpoints of edges in" E_F}. $ + No source optimization is performed during extraction. - ($arrow.l.double$) Conversely, given an optimal Steiner tree $T^*$, the restriction $E_F = T^* inter E(G)$ is acyclic (subset of a tree) and respects the PCSF feasibility constraint that selected edges only touch selected vertices, because every endpoint $v$ of an edge in $E_F$ is forced into $V_F$ by the extraction rule. Each connected component of $F$ corresponds to a maximal subtree of $T^*$ confined to $V$, and any optimal $T^*$ uses exactly one root-attachment edge per component (a second incident root edge could be replaced by a cheaper internal path, contradicting optimality). Each prized vertex $v in V_F$ is reached by $T^*$ via original edges, so the include-edge $(v, t_v)$ is selected for free; each omitted prized vertex contributes the omit-edge $(r, t_v)$ of cost $beta dot p(v)$. Summing the contributions reproduces $f'(F)$, so $"cost"_H(T^*) = f'(F^*)$ at optima and the extracted forest is optimal for PCSF. + _Overhead._ The exact counts remain $|V_H|=n+k+1$, $|E_H|=m+n+2k$, and $|T_H|=k+1$. Coefficients are computed with checked integer arithmetic in the native representation. - _Overhead._ With $n = |V|$, $m = |E|$, and $k = |V_p|$: - $ |V_H| = n + k + 1, quad |E_H| = m + n + 2 k, quad |T_H| = k + 1. $ - Every quantity is linear in the source instance size, so the reduction is a polynomial-time transformation. + _Boundary cases._ With no positive prizes, only $r$ is a terminal and its zero-edge tree maps to the empty forest. The proof also covers $beta=0$ and $omega=0$, including ties. For two adjacent vertices with edge cost zero, prizes $(1,2)$, $beta=1$, and $omega=5$, the source optimum is the empty forest of cost $3$; the target optimum selects both omit edges and costs $3+2 dot 6=15$. - _Remark._ The artificial-root edges all share cost $omega$. Tuncbag et al. originally used this construction with $omega = c$ for any positive scalar $c$ acting as a per-component penalty; we follow that convention. When $omega = 0$, root-attachment edges become free and the construction degenerates: any rooted spanning tree of the prized-vertex closure achieves the same cost, but the witness-extraction recipe still recovers a feasible (cost-equivalent) PCSF forest, possibly with a different component count. ] #pagebreak() diff --git a/docs/src/design.md b/docs/src/design.md index 54ed4f47f..4cabef52c 100644 --- a/docs/src/design.md +++ b/docs/src/design.md @@ -21,7 +21,7 @@ choosing numeric fields or implementing arithmetic in a model or reduction. ## Problem Model -Every problem implements `Problem`. The associated `Value` type is the per-configuration aggregate returned by `evaluate()`. Solvers fold these values across the configuration space, and witness-capable aggregates can also recover representative configurations. +Every problem implements `Problem`. The associated `Value` type is the per-configuration aggregate returned by `evaluate()`. The brute-force solver folds these values across the configuration space and uses its `SolutionAggregate` capability to select corresponding witnesses. Specialized solvers and ILP backends return their solutions directly; model evaluation does not require that selection capability. ```rust,ignore trait Problem: Clone { @@ -37,7 +37,7 @@ trait Problem: Clone { ``` - **`Problem`** — the base trait. Every problem declares a mathematical `Solution` type, evaluates that type directly, and reports its canonical instance parameters. For example, a 4-vertex MIS uses `Vec`; `evaluate(&[true, false, true, false])` returns `Ok(Max(Some(2)))` if vertices 0 and 2 form an independent set, or `Ok(Max(None))` if they share an edge. Inherent getters such as `num_vertices()` and `num_edges()` supply the named parameters used by reduction expressions. -- **`BruteForceProblem`** — the reference-solver capability for registered variants with a finite Cartesian coordinate space. Its `dimensions()` method and the Cartesian iterator belong to the brute-force solver, not to the mathematical `Problem` contract. +- **`BruteForceProblem`** — the reference-solver capability for registered variants with a finite Cartesian coordinate space. Its fallible `num_variables()` and `dimension(variable)` methods describe coordinates without allocating their vector. These methods and the Cartesian iterator belong to the brute-force solver, not to the mathematical `Problem` contract. - **Objective problems** — typically use `Max`, `Min`, or `Extremum` as `Value`. - **Feasibility problems** — typically use `Or`. - **Solve contract** — a successful solve always returns the problem's `Solution`; a global count or statistic without a representative solution is not a `Problem` solve. @@ -93,39 +93,183 @@ SpinGlass couplings and its objective result use `i64`, while the temporary temporary calculations are also outside the contract, but numeric fields written into its target model must follow the target model's numeric format. -Weight variants are `One`, `i64`, and `f64`, with `One ⊂ i64 ⊂ f64`. -`i64 → f64` is a fallible reduction using a checked conversion in -`±(2^53-1)`, not `as f64`. +Supported weight variants are `One`, `i64`, and `f64`. + +### Responsibility boundaries + +| Layer | Contract | +|-------|----------| +| Model (`Problem`) | Defines instances, witnesses, feasibility, and objectives in its declared mathematical representation. Evaluation is independent of backend tolerances, statuses, and enumeration capacity. | +| Reduction (`ReduceTo`, `ReductionResult`) | Constructs the target within the rule's mathematical domain and maps target witnesses satisfying the stated preconditions to source witnesses. It owns coefficient arithmetic, parameter relationships, and mapping correctness. | +| Backend adapter | Encodes the target, executes the backend, interprets statuses, decodes numerical results, and validates the returned witness against the original target model. | +| Solver orchestration | Executes registered capabilities and reduction chains, interprets aggregate results, and extracts source witnesses under the reduction contracts. | +| CLI / MCP | Uses public construction, evaluation, and solving APIs and presents their results. | + +Models and rules do not repair backend results, change constraints to make a +solver succeed, or independently prove a backend's global optimality. Invalid +returned witnesses and operational failures must be explicit errors. A backend's +numerical limitations do not justify a package-wide certificate system or +downgrading every successful result. + +Search-space cardinalities belong to the solver capability, not the mathematical +model. Actual model storage and witness representation constraints still apply. + +### Witness and aggregate reductions + +`ReductionResult::extract_solution()` maps `Target::Solution` to +`Source::Solution`; it does not require equal `Problem::Value` types. Resolve +concrete associated types from the implementation, then check the mathematical +mapping and its Rust implementation rather than applying a wrapper-pair whitelist. + +For an optimization reduction, explain why target optima map to source optima. +Opposite directions are valid when the objective relationship reverses order: +independent-set size `k` corresponds to vertex-cover size `n-k` by complementing +the witness. Different numeric value types do not require conversion of an +objective that the extractor never converts. Check the domain and arithmetic of +conversions the construction or mapping actually performs. + +Value-only operations use `ReduceToAggregate` / `AggregateReductionResult` and +must justify their actual `extract_value()` relationship. Multi-query algorithms +use the existing Turing reduction capability. A feasibility witness alone does +not establish an optimization result without the required mathematical argument. + +`Problem::evaluate()` defines feasibility as well as objective values. A successful +call can return an infeasible value such as `Or(false)` or `Max(None)`; absence +of an `EvaluationError` does not imply a valid witness. The adapter validates +backend output before returning it. Both typed extraction and `pred extract` +assume witnesses satisfying the reduction's documented premises; neither checks +feasibility or optimality. JSON parsing and type conversion remain at the transport +boundary. Evaluation may supply requested display values without acting as an +acceptance gate. Solver orchestration interprets aggregate mappings to determine +source outcomes before invoking witness mappings. + +### Executed reduction lifecycle + +A witness reduction is one algorithm with construction and reverse mapping. +`reduce_to()` returns the target and all mapping state in one result. Each +executed chain step constructs that result once. Its witness and optional +aggregate `Rc` views share one allocation; obtaining another view does not +reconstruct or copy the target. `Decision

-> P` stores the bound with that +same result. + +For every rule, document its instance domain, required target witness quality +and conditions, source guarantee, and treatment of source infeasibility. +The guarantee applies to every qualifying witness, including tied optima. +A witness-capable edge alone does not establish a complete-solving procedure: +composition must establish the preceding edge's witness premise. + +| Example | Required recovery | +|---|---| +| MVC -> MIS | Complement a maximum independent set to obtain a minimum cover | +| SAT -> MIS | With `m` clauses, optimum size `m` permits witness extraction; an optimum below `m` means UNSAT | +| Binary ILP -> QUBO | Use the constructed energy relationship to obtain a source optimum or source infeasibility; a QUBO optimum alone does not establish ILP feasibility | +| MVC -> MIS -> SetPacking -> ILP | Apply the stored ILP-to-packing and packing-to-MIS mappings, then the complement mapping | +| TSP -> QUBO | Shift signed edge costs uniformly; the energy threshold distinguishes source infeasibility, and the stored offset recovers tour cost | +| Discrete inverse kinematics -> QUBO | Restore omitted constants and compare against the gap between feasible distance and constraint penalties before decoding orientations | +| MultiwayCut -> QUBO | Always delete negative edges; optimize nonnegative cut cost and decode an optimal terminal partition | +| Aggregate-only operation | Map the final value without selecting any witness, including `Sum` | + +The mathematical thresholds and objective relationships belong to the rule. +Solver completion invokes the executed step's concrete `interpret_optimum` +operation before its witness mapping. This operation shares the constructed +result and does not query the model registry. Ordinary extraction uses only the +witness mapping. Typed chain, executed path, and JSON extraction share the same +reverse traversal; dynamic/JSON methods perform necessary representation +conversion rather than introducing another extraction contract. + +`SolutionAggregate` is defined in `solvers/brute_force.rs` and exported through +`solvers` for enumeration clients. It compares candidate and aggregate values; +it is not a model-feasibility interface. Concrete variant declarations generate +`DynProblem` transport implementations using the value's own `is_valid` +semantics, without aggregation or solver-registration requirements. A concrete +hand-registered dynamic type can use `impl_dyn_problem!` directly. + +Witness and aggregate describe what can be recovered. Turing describes a +potentially adaptive query procedure. Exact witness recovery does not establish +approximation or counting preservation; those require their own proofs. ### Arithmetic -- Keep arithmetic in the declared type. Exact values use checked `i64` - operations; approximate values use finite `f64` operations. -- Constructors and reductions reject an arithmetic step that would overflow - `i64` when producing a stored field. They do not cap every magnitude at - `2^53-1`. `evaluate()` never widens, wraps, saturates, or silently - approximates. -- Do not promote an `i64` calculation to `i128`, `BigInt`, or `BigUint` to - accept a larger instance. +- Integer models and reductions preserve integer values in their declared + representation. Report actual arithmetic overflow explicitly; do not wrap, + saturate, or silently approximate. Reuse an existing exact representation + when the mathematical model requires it. +- Floating-point models and rules use ordinary finite `f64` arithmetic and its + rounding. Check non-finite results and do not deliberately discard nonzero + coefficients. Backend feasibility tolerances must not expand the model's + feasible set. A declared input convention, such as checking probability sums, + is distinct from accepting a solver's returned assignment. +- CVP evaluates squared distance as `Min` through its + `squared_distance()` method. Integer coordinates enter exact integer arithmetic; + finite `f64` targets retain their stored binary rational values. For example, + the zero lattice point and target `(3, 4)` have objective `25`. The customized + solver uses the same coordinate conversion. SubsetSum compares squared distance + with its integer item count. JSON evaluation uses the dependency's rational + serialization; CLI display uses fractions such as `Min(9/16)`. +- `i64_to_exact_f64()` accepts integers in `[-(2^53-1), 2^53-1]` and rejects + everything outside that supported conversion range. This is a conservative + interface limit, not the set of all exactly representable f64 integers. Ordinary conversion + into a floating-point model and backend transport are separate + responsibilities. Neither a lossless scalar conversion nor `transform = exact` + proves error-free floating-point evaluation or backend optimality; the latter + describes parameter relationships only. +- Preserve real construction and witness-structure checks, including bounds + derived by the reduction and adjacency preservation in geometric mappings. + Do not add exact arithmetic solely to audit a floating-point backend or reject + a mathematical reduction because that backend may struggle to solve it. ### Boundaries -- Use `From` only for value-preserving conversions and `TryFrom` when range, - sign, or domain can change. Do not use `as` for model-derived values. +- Use value-preserving conversions where possible and checked conversions for + range/sign changes. A floating-point model's declared rounding is not a + lossless-conversion requirement. Reuse `i64_to_exact_f64` where lossless scalar + conversion is actually required; backend input acceptance belongs to the + adapter and must not narrow integer model domains. - Converting a registered parameter getter from `usize` to `u64` is an internal - invariant of `Problem::parameters()`, not a recoverable construction error. A valid - instance's registered parameters must already fit `u64`; the - implementation checks this conversion to prevent silent truncation. -- Symbolic parameter evaluation may use arbitrary-precision integers for local - intermediate arithmetic, but a materialized `ProblemParameters` must fit `u64`. -- An `i64` to `f64` conversion is explicit and fallible: it succeeds only - for `|value| ≤ 2^53-1`. Use one shared helper at weight casts, solver - adapters, and other exact-to-float hubs. -- A lattice-to-`UnitDiskGraph` reduction converts coordinates fallibly and - rejects a stored `f64` geometry that would change source adjacency. -- Rust constructors keep `i64` fields as `i64`. CLI and MCP JSON encoding - of an `i64` with `|value| > 2^53-1` errors; there is no string encoding - and no clamping. + invariant of `Problem::parameters()`, not a recoverable construction error. + Check it to prevent silent truncation. +- Symbolic parameter evaluation may use arbitrary-precision intermediates, but + materialized `ProblemParameters` must fit `u64`. +- A lattice-to-`UnitDiskGraph` reduction must reject a stored geometry that + changes source adjacency. This is a mathematical reduction requirement. +- Rust constructors retain their declared integer fields. Existing serde JSON + serialization can emit i64 integer values beyond the consecutive-integer range + of f64. Describe the actual codec and consumer representation; do not impose + a universal f64 gate on Rust models or claim one exists in CLI/MCP. + +### Validation evidence + +Model tests check definitions and direct evaluation. Reduction tests check +construction, witness mappings, objective relationships, and parameter formulas +using explicit witnesses or small exhaustive enumeration. Choose cases that can +expose a concrete defect; there is no minimum vertex, assertion, test-function, +or generated-check count that establishes correctness. + +Keep representative solver integration tests and report whether failures occur +in construction, solving, extraction, or source validation. Backend timeout or +numerical failure is not evidence that a reduction theorem is false. Test backend +decoding and transport boundaries once in their shared implementation, not in +every rule. Retain arithmetic regressions that detect actual coefficient loss or +incorrect mappings. Do not enlarge tolerances to make a failing test pass. + +### Search representation + +`BruteForceProblem::num_variables()` and `dimension(variable)` return +`Result`. The caller supplies an index below the coordinate +count. Derived counts and cardinalities use checked arithmetic. Shared +`cartesian_dimensions()` materializes these values with fallible allocation for +registered solving and inspection; models do not call it during evaluation. + +The Cartesian iterator advances coordinates until mixed-radix exhaustion. Its +complete search count need not fit `usize`, and it does not implement +`ExactSizeIterator`. An empty product has one empty candidate; any zero-sized +coordinate makes the product empty. Native masks and dense tables retain their +actual representation limits and report errors before overflowing or allocating +an unrepresentable table. These are implementation limits, not difficulty budgets. + +`TruthTable` construction and deserialization share checked row-count and shape +validation. Variable-arity constructors return `ConstructionError` for unsupported +row counts or allocation failures. Valid tables retain the same JSON format. ## Variant System @@ -252,7 +396,6 @@ impl ReductionResult for ReductionISToVC { &self, target_sol: &Vec, ) -> crate::rules::ExtractionResult> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_sol)?; Ok(target_sol.iter().map(|&x| !x).collect()) } } @@ -260,33 +403,29 @@ impl ReductionResult for ReductionISToVC { ### Solution extraction contract -`ReductionResult::extract_solution` accepts one complete target configuration -and returns the source configuration defined by the reduction. Extraction is a -fallible boundary, not a recovery mechanism: +`ReductionResult::extract_solution` maps a complete target solution satisfying +the rule's mathematical premises into a source solution. The adapter establishes +target validity for internal solves. External callers supply witnesses under the +same contract. Rules requiring optimal target solutions document that requirement. +Source YES/NO and optimization outcomes are interpreted by solver orchestration, +not by the extraction chain. Invalid external witnesses have no mapping-correctness +guarantee. -1. In every direct extractor, call `validate_target_solution()` once before - indexing or decoding. Composed extractors delegate this check. -2. Validate any structure required by the inverse mapping, such as exactly-one - blocks, permutations, paths, flows, or schedules. -3. Apply the reduction's mathematical inverse once and return a source - configuration with the required length and domains. -4. Return `ExtractionError` when a precondition is not satisfied. - -Do not truncate or pad input, substitute zero for missing data, select the -first of several invalid candidates, retry with another mapping, or panic on -caller-provided configuration data. Empty and singleton instances should flow -through the same mathematical mapping unless the reduction itself has a -genuine mathematical case distinction. +Do not repeat checks implied by target constraints or successful construction. +Do not truncate or pad input, substitute values for missing data, retry another +mapping, or add runtime acceptance checks to compensate for a rule defect. +Keep actual mathematical case distinctions and representation errors that can +occur for inputs satisfying the mapping's premises. Zero and sentinel values remain valid when the source model explicitly gives them meaning. For example, `MaximumCommonEdgeSubgraph` includes an "unmapped" sentinel in its source dimensions. Missing target data must never be interpreted as that sentinel. -Each conditional in an extractor should therefore either reject a named -invariant violation or implement a case in the reduction's mathematics. A -normal extractor has one validation phase followed by one decoding phase; it -does not accumulate compatibility or fallback branches. +Each conditional in an extractor should implement a case in the reduction's +mathematics or report an error that remains reachable under its premises. +The external boundary handles parsing and type conversion; extraction does not +accumulate feasibility checks, compatibility branches, or fallbacks. The `#[reduction]` attribute on the `ReduceTo` impl registers the reduction in the global registry (via `inventory`): @@ -409,20 +548,43 @@ 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. Each pipeline terminates at `ILP` or `ILP`, which is solved by HiGHS via `good_lp`. | - -ILP results are optimal or infeasible according to HiGHS numerical tolerances; -zero MIP gaps do not imply mathematical exactness. Integer extraction rounds -variable assignments, validates the original constraints, and recomputes the -source objective with checked integer arithmetic. Floating-point objective -comparisons in numerical regression tests use an explicit acceptance policy -in source units (absolute and relative tolerances of `1e-7` for the QUBO solver -regression), separate from the `1e-6` variable-rounding tolerance. This test -policy is not a universal bound on backend objective error. - -When an ILP target witness misses a source decision threshold, the solver -returns `ILPSolveError::UnresolvedDecision`, not infeasibility: the witness -alone cannot prove that no qualifying source solution exists. +| **ILPSolver** | Executes a problem's registered ILP pipeline. Each pipeline terminates at a native `ILP` with bool/i64 variables and i64/f64 coefficients, solved by the shared HiGHS adapter through its native Rust bindings. | + +### ILP execution boundary + +`ILPSolver::solve

() -> Result` is the typed entry +point. Adapter failures retain their classified errors. Registry lookup, +concrete-terminal dispatch, aggregate interpretation, +and reduction-chain extraction belong to orchestration. Integer pipelines end +at native integer ILPs; they do not need a float-coefficient cast edge to execute. +Explicit coefficient-conversion rules retain their own mathematical contracts. + +The shared internal `HighsAdapter` borrows an `ILP` and returns its existing +`Vec` witness representation. It encodes the backend model, executes it, +interprets termination, checks returned integer values, and validates constraints +and objective arithmetic against the original ILP. It does not inspect source +model names, query reduction registrations, or extract source witnesses. +Unsupported transport produces `InexactTransport`; a rejected returned witness +produces `InvalidSolution`. A validation failure must not relax model constraints. + +Optimality and infeasibility are backend conclusions under HiGHS's numerical +contract, not independent mathematical certificates. An accepted optimum requires +both an optimal backend termination and successful witness validation. Timeouts, +non-optimal termination, and invalid results are errors, not infeasibility. +Variable decoding tolerances belong to the adapter; they do not define source or +target feasibility, nor a universal objective-error allowance for tests. + +After accepting a target optimum, orchestration must apply the reduction's +aggregate mapping to interpret a source decision threshold. If that optimum +cannot meet the threshold, the source answer is NO. A merely feasible witness +or failed solve is insufficient for that conclusion. Typed solving, dynamic +solving, and explicit CLI bundles must share the same interpretation and witness +mapping. + +Fixed pipelines and explicit CLI bundles reuse the executed `ReductionChain` +and the solver completion path. Aggregate mappings interpret an accepted target +optimum before witness extraction. Source evaluation computes requested output +values and propagates evaluation errors; it is not another feasibility gate. ## JSON Serialization @@ -438,3 +600,18 @@ let restored: MaximumIndependentSet = from_json(&json)?; ## Contributing See [Call for Contributions](./open-problems.md) for the recommended issue-based workflow (no coding required). + +### QUBO coefficient storage + +QUBO stores coefficients in `sprs::CsMat` using CSR order. Construction from +linear/quadratic terms preserves last-assignment semantics; reductions accumulate +coefficients with their existing checked arithmetic before compression. Exact +zeros need no stored entry. Evaluation visits the upper triangle in row/column +order, retaining checked integer addition and floating-point summation order. + +`QUBO::from_sparse` accepts a square CSR or CSC matrix; `matrix()` returns the +CSR matrix and `get(i, j)` returns an owned coefficient, including zero for an +unstored in-bounds entry. `from_matrix` and CLI `--matrix` accept dense input. +Persisted QUBO JSON stores the `sprs` matrix object (`storage`, `nrows`, `ncols`, +`indptr`, `indices`, `data`); variable count comes from the matrix dimensions. +Rules, numeric casts, and solver reductions consume sparse coefficients directly. diff --git a/docs/src/static/trait-hierarchy-dark.svg b/docs/src/static/trait-hierarchy-dark.svg index e932783a4..3e35702f4 100644 --- a/docs/src/static/trait-hierarchy-dark.svg +++ b/docs/src/static/trait-hierarchy-dark.svg @@ -1,765 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/docs/src/static/trait-hierarchy.svg b/docs/src/static/trait-hierarchy.svg index 571ca9c90..a1b82bfb0 100644 --- a/docs/src/static/trait-hierarchy.svg +++ b/docs/src/static/trait-hierarchy.svg @@ -1,765 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/docs/src/static/trait-hierarchy.typ b/docs/src/static/trait-hierarchy.typ index b6c167ffd..a9ce6343d 100644 --- a/docs/src/static/trait-hierarchy.typ +++ b/docs/src/static/trait-hierarchy.typ @@ -24,14 +24,15 @@ spacing: (8mm, 12mm), // Problem trait (top center) - node((0.6, 0), box(width: 55mm, align(left)[ + node((1, 0), box(width: 55mm, align(left)[ #strong[trait Problem]\ #text(size: 8pt, fill: secondary)[ `const NAME: &str`\ `type Solution`\ `type Value: Clone`\ - `fn size() -> ProblemParameters`\ - `fn evaluate(&solution) -> Value`\ + `fn parameters() -> ProblemParameters`\ + `fn evaluate(&solution)`\ + ` -> Result`\ `fn variant() -> Vec<(&str, &str)>` ] ]), fill: trait-fill, corner-radius: 6pt, inset: 10pt, name: ), @@ -41,7 +42,8 @@ #strong[trait Aggregate]\ #text(size: 8pt, fill: secondary)[ `fn identity() -> Self`\ - `fn combine(self, other) -> Self`\ + `fn combine(self, other)`\ + ` -> Result`\ `fn is_absorbing(&self) -> bool`\ #strong[trait SolutionAggregate: Aggregate]\ `fn contributes_to_solution(...)` @@ -49,17 +51,19 @@ ]), fill: trait-fill, corner-radius: 6pt, inset: 10pt, name: ), // Brute-force capability (bottom center) - node((0.7, 1), box(width: 48mm, align(left)[ + node((1, 1), box(width: 48mm, align(left)[ #strong[trait BruteForceProblem]\ #text(size: 8pt, fill: secondary)[ `extends Problem`\ - `fn dimensions() -> Vec`\ + `num_variables()`\ + `dimension(i: usize)`\ + `→ Result`\ #text(style: "italic")[reference solver only] ] ]), fill: trait-fill, corner-radius: 6pt, inset: 10pt, name: ), // Common value types (bottom right) - node((1.4, 1), box(width: 48mm, align(left)[ + node((0, 2), box(width: 48mm, align(left)[ #strong[Common Value Types]\ #text(size: 8pt, fill: secondary)[ `Max | Min | Extremum`\ diff --git a/problemreductions-cli/src/commands/evaluate.rs b/problemreductions-cli/src/commands/evaluate.rs index 7ce07cadf..32d379780 100644 --- a/problemreductions-cli/src/commands/evaluate.rs +++ b/problemreductions-cli/src/commands/evaluate.rs @@ -28,7 +28,7 @@ pub fn evaluate(input: &Path, config_str: &str, out: &OutputConfig) -> Result<() let config: serde_json::Value = serde_json::from_str(config_str).context("Config is not valid JSON")?; - let result = problem.evaluate_dyn(&config)?; + let (result, _) = problem.evaluate_dyn(&config)?; out.emit( || result.to_string(), diff --git a/problemreductions-cli/src/commands/extract.rs b/problemreductions-cli/src/commands/extract.rs index 79736a984..4b6a60db2 100644 --- a/problemreductions-cli/src/commands/extract.rs +++ b/problemreductions-cli/src/commands/extract.rs @@ -30,9 +30,7 @@ pub fn extract(input: &Path, config_str: &str, out: &OutputConfig) -> Result<()> let replay = BundleReplay::prepare(&bundle)?; - let target_eval = replay.target.evaluate_dyn(&target_config)?; - - let (source_config, source_eval) = replay.extract(&target_config)?; + let (source_config, source_eval, target_eval) = replay.extract(&target_config)?; out.emit( || { diff --git a/problemreductions-cli/src/dispatch.rs b/problemreductions-cli/src/dispatch.rs index 341125bb4..7ec0719a2 100644 --- a/problemreductions-cli/src/dispatch.rs +++ b/problemreductions-cli/src/dispatch.rs @@ -42,7 +42,7 @@ impl LoadedProblem { pub fn brute_force_num_variables(&self) -> Result> { brute_force_dimensions(&self.inner) .map(|dimensions| dimensions.map(|dimensions| dimensions.len())) - .map_err(|error| anyhow::anyhow!("solver capability registry is invalid: {error}")) + .map_err(|error| anyhow::anyhow!("cannot inspect brute-force coordinates: {error}")) } pub fn solve(&self, request: SolverRequest) -> Result { @@ -77,7 +77,7 @@ pub struct SolverCapabilitiesView { pub fn solver_capabilities_view(problem: &LoadedProblem) -> Result { let key = ExactProblemKey::new(problem.problem_name(), problem.variant_map()); let registered = solver_capabilities(&key) - .map_err(|error| anyhow::anyhow!("solver capability registry is invalid: {error}"))?; + .map_err(|error| anyhow::anyhow!("cannot inspect brute-force coordinates: {error}"))?; let customized = registered .customized .map(|entry| CustomizedSolverCapabilityView { @@ -292,19 +292,15 @@ impl BundleReplay { }) } - /// Map a target-space configuration back to the source space and evaluate it. + /// Map a target witness under the reduction contract and evaluate for display. pub fn extract( &self, target_config: &serde_json::Value, - ) -> Result<(serde_json::Value, String)> { + ) -> Result<(serde_json::Value, String, String)> { + let (target_eval, _) = self.target.evaluate_dyn(target_config)?; let source_config = self.chain.extract_solution_json(target_config.clone())?; - let source_eval = self.source.evaluate_witness_dyn(&source_config)?.ok_or_else(|| { - problemreductions::rules::ExtractionError::invalid(format!( - "extracted solution is infeasible for {}; the reduction did not establish a source solution", - self.source_name - )) - })?; - Ok((source_config, source_eval)) + let (source_eval, _) = self.source.evaluate_dyn(&source_config)?; + Ok((source_config, source_eval, target_eval)) } /// Solve the target and map the result back to the source problem. @@ -312,25 +308,12 @@ impl BundleReplay { pub(crate) fn solve(&self, request: SolverRequest) -> Result { let target_result = self.target.solve(request)?; let solver = target_result.solver; - let (source_outcome, target_outcome) = match target_result.outcome { - SolveOutcome::Optimal { - solution: target_solution, - evaluation: target_evaluation, - } => { - let (source_solution, source_evaluation) = self.extract(&target_solution)?; - ( - SolveOutcome::Optimal { - solution: source_solution, - evaluation: source_evaluation, - }, - SolveOutcome::Optimal { - solution: target_solution, - evaluation: target_evaluation, - }, - ) - } - SolveOutcome::Infeasible => (SolveOutcome::Infeasible, SolveOutcome::Infeasible), - }; + let target_outcome = target_result.outcome; + let source_outcome = problemreductions::solvers::complete_reduction( + &*self.source, + &self.chain, + &target_outcome, + )?; Ok(BundleSolveResult { source_name: self.source_name.clone(), @@ -428,7 +411,59 @@ mod tests { use serde_json::json; #[test] - fn bundle_rejects_infeasible_extracted_witness() { + fn ilp_qubo_bundle_maps_optima_and_infeasibility() { + use problemreductions::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; + use problemreductions::Problem; + + for rhs in [1, -1] { + let ilp = ILP::::new( + 2, + vec![LinearConstraint::le(vec![(0, 1), (1, 1)], rhs)], + vec![(0, 3), (1, 2)], + ObjectiveSense::Maximize, + ) + .unwrap(); + let source = ProblemJson { + problem_type: "ILP".into(), + variant: ILP::::variant() + .into_iter() + .map(|(key, value)| (key.to_string(), value.to_string())) + .collect(), + data: serde_json::to_value(&ilp).unwrap(), + }; + let route = crate::commands::reduce::parse_path_json( + r#"{"path":[{"from":{"name":"ILP","variant":{"variable":"bool","coefficient":"i64"}},"to":{"name":"QUBO","variant":{"weight":"i64"}}}]}"#, + ).unwrap(); + let bundle = crate::commands::reduce::execute_route(source, route).unwrap(); + let replay = BundleReplay::prepare(&bundle).unwrap(); + for backend in [SolverRequest::BruteForce, SolverRequest::Ilp] { + let result = replay.solve(backend).unwrap(); + if rhs == 1 { + let SolveOutcome::Optimal { solution, .. } = result.source_outcome else { + panic!("the ILP has an optimum"); + }; + assert_eq!(solution, json!([1, 0])); + let SolveOutcome::Optimal { + solution: target, .. + } = result.target_outcome + else { + panic!("the QUBO has an optimum"); + }; + assert_eq!(target, json!([true, false, false])); + assert_eq!(replay.extract(&target).unwrap().0, solution); + } else { + assert_eq!(result.source_outcome, SolveOutcome::Infeasible); + assert!(matches!( + result.target_outcome, + SolveOutcome::Optimal { .. } + )); + } + } + } + } + + #[test] + fn bundle_maps_satisfiability_outcomes_through_the_value_relation() { for (clauses, feasible) in [ (vec![vec![1, 1, 1], vec![-1, -1, -1]], false), (vec![vec![1, 1, 1], vec![1, 1, 1]], true), @@ -460,13 +495,61 @@ mod tests { assert!(matches!(result.unwrap().source_outcome, SolveOutcome::Optimal { evaluation, .. } if evaluation == "Or(true)")); } else { - let error = result.err().unwrap(); - assert!(error - .downcast_ref::() - .is_some()); - assert!(error - .to_string() - .contains("extracted solution is infeasible")); + assert!(matches!( + result.unwrap().source_outcome, + SolveOutcome::Infeasible + )); + } + } + } + + #[test] + fn bundle_and_registered_pipeline_agree_on_decision_thresholds() { + for bound in [0, 1] { + let source = ProblemJson { + problem_type: "DecisionMinimumVertexCover".into(), + variant: BTreeMap::from([ + ("graph".into(), "SimpleGraph".into()), + ("weight".into(), "i64".into()), + ]), + data: json!({ + "inner": {"graph": {"num_vertices": 2, "edges": [[0,1]]}, "weights": [1,1]}, + "bound": bound, + }), + }; + let route = crate::commands::reduce::parse_path_json( + r#"{"path":[{ + "from":{"name":"DecisionMinimumVertexCover","variant":{"graph":"SimpleGraph","weight":"i64"}}, + "to":{"name":"MinimumVertexCover","variant":{"graph":"SimpleGraph","weight":"i64"}} + }]}"#, + ).unwrap(); + let bundle = crate::commands::reduce::execute_route(source, route).unwrap(); + let replay = BundleReplay::prepare(&bundle).unwrap(); + if bound == 1 { + assert_eq!( + replay.extract(&json!([true, false])).unwrap().0, + json!([true, false]) + ); + } + for backend in [ + SolverRequest::BruteForce, + SolverRequest::Ilp, + SolverRequest::Default, + ] { + let result = replay.solve(backend).unwrap(); + assert!(matches!( + result.target_outcome, + SolveOutcome::Optimal { .. } + )); + assert_eq!( + matches!(result.source_outcome, SolveOutcome::Infeasible), + bound == 0 + ); + let direct = replay.source.solve(backend).unwrap(); + assert_eq!( + matches!(direct.outcome, SolveOutcome::Infeasible), + bound == 0 + ); } } } diff --git a/problemreductions-cli/src/mcp/tools.rs b/problemreductions-cli/src/mcp/tools.rs index 09cddd892..149d7398c 100644 --- a/problemreductions-cli/src/mcp/tools.rs +++ b/problemreductions-cli/src/mcp/tools.rs @@ -414,7 +414,7 @@ impl McpServer { let pj: ProblemJson = serde_json::from_str(problem_json)?; let problem = load_problem(&pj.problem_type, &pj.variant, pj.data)?; - let result = problem.evaluate_dyn(config)?; + let (result, _) = problem.evaluate_dyn(config)?; let json = serde_json::json!({ "problem": problem.problem_name(), "config": config, diff --git a/problemreductions-cli/src/test_support.rs b/problemreductions-cli/src/test_support.rs index ba3d7e15d..a63f94154 100644 --- a/problemreductions-cli/src/test_support.rs +++ b/problemreductions-cli/src/test_support.rs @@ -5,8 +5,9 @@ use problemreductions::registry::{ }; use problemreductions::rules::registry::{ReductionEntry, ReductionParameterDeclarations}; use problemreductions::rules::{AggregateReductionResult, VariantReductionResult}; +use problemreductions::solvers::SolutionAggregate; use problemreductions::traits::Problem; -use problemreductions::types::{Aggregate, Extremum, Max, SolutionAggregate}; +use problemreductions::types::{Aggregate, Extremum, Max}; use serde::{Deserialize, Serialize}; use std::any::Any; use std::collections::BTreeMap; @@ -67,8 +68,12 @@ impl Problem for AggregateValueSource { } impl problemreductions::solvers::BruteForceProblem for AggregateValueSource { - fn dimensions(&self) -> Vec { - vec![2; self.values.len()] + fn num_variables(&self) -> Result { + Ok(self.values.len()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } @@ -109,8 +114,12 @@ impl Problem for AggregateValueTarget { } impl problemreductions::solvers::BruteForceProblem for AggregateValueTarget { - fn dimensions(&self) -> Vec { - vec![2] + fn num_variables(&self) -> Result { + Ok(1usize) + } + + fn dimension(&self, variable: usize) -> Result { + Ok([2][variable]) } } @@ -139,24 +148,23 @@ fn decode_bits(indices: Vec) -> Vec { fn cartesian_indices( dimensions: Vec, ) -> Result>, problemreductions::solvers::SolveError> { - let total = if dimensions.is_empty() { - 1 - } else if dimensions.contains(&0) { - 0 + let mut current = if dimensions.contains(&0) { + None } else { - dimensions.iter().try_fold(1usize, |total, &dimension| { - total.checked_mul(dimension).ok_or_else(|| { - problemreductions::solvers::SolveError::SearchSpaceOverflow(dimensions.clone()) - }) - })? + Some(vec![0; dimensions.len()]) }; - Ok((0..total).map(move |mut index| { - let mut coordinates = vec![0; dimensions.len()]; + Ok(std::iter::from_fn(move || { + let result = current.take()?; + let mut next = result.clone(); for position in (0..dimensions.len()).rev() { - coordinates[position] = index % dimensions[position]; - index /= dimensions[position]; + next[position] += 1; + if next[position] < dimensions[position] { + current = Some(next); + break; + } + next[position] = 0; } - coordinates + Some(result) })) } @@ -166,7 +174,7 @@ where P::Value: Aggregate, { let mut total = P::Value::identity(); - for indices in cartesian_indices(problem.dimensions())? { + for indices in cartesian_indices(problemreductions::solvers::cartesian_dimensions(problem)?)? { total = total.combine(problem.evaluate(&decode_bits(indices))?)?; } Ok(total) @@ -180,7 +188,7 @@ where P::Value: SolutionAggregate, { let total = solve_cartesian(problem)?; - for indices in cartesian_indices(problem.dimensions())? { + for indices in cartesian_indices(problemreductions::solvers::cartesian_dimensions(problem)?)? { let solution = decode_bits(indices); let value = problem.evaluate(&solution)?; if P::Value::contributes_to_solution(&value, &total) { @@ -199,7 +207,7 @@ where { let total = solve_cartesian(problem)?; let mut witnesses = Vec::new(); - for indices in cartesian_indices(problem.dimensions())? { + for indices in cartesian_indices(problemreductions::solvers::cartesian_dimensions(problem)?)? { let solution = decode_bits(indices); let value = problem.evaluate(&solution)?; if P::Value::contributes_to_solution(&value, &total) { @@ -331,7 +339,7 @@ problemreductions::inventory::submit! { let problem = any .downcast_ref::() .expect("AggregateValueSource brute-force dimensions type mismatch"); - problemreductions::solvers::BruteForceProblem::dimensions(problem) + problemreductions::solvers::cartesian_dimensions(problem) }, solve_fn: solve_dynamic::, solve_typed_fn: solve_typed::, @@ -379,7 +387,7 @@ problemreductions::inventory::submit! { let problem = any .downcast_ref::() .expect("AggregateValueTarget brute-force dimensions type mismatch"); - problemreductions::solvers::BruteForceProblem::dimensions(problem) + problemreductions::solvers::cartesian_dimensions(problem) }, solve_fn: solve_dynamic::, solve_typed_fn: solve_typed::, @@ -490,3 +498,6 @@ pub(crate) fn aggregate_bundle() -> ReductionBundle { ], } } + +problemreductions::impl_dyn_problem!(AggregateValueSource); +problemreductions::impl_dyn_problem!(AggregateValueTarget); diff --git a/problemreductions-cli/tests/cli_tests.rs b/problemreductions-cli/tests/cli_tests.rs index 90224cc29..28a9432ed 100644 --- a/problemreductions-cli/tests/cli_tests.rs +++ b/problemreductions-cli/tests/cli_tests.rs @@ -9648,7 +9648,7 @@ fn test_extract_roundtrip_mis_to_qubo() { } #[test] -fn test_extract_rejects_structurally_invalid_one_hot_config() { +fn test_extract_decodes_a_qualifying_tour() { let problem_file = std::env::temp_dir().join("pred_test_extract_tsp_in.json"); let bundle_file = std::env::temp_dir().join("pred_test_extract_tsp_bundle.json"); @@ -9686,19 +9686,22 @@ fn test_extract_rejects_structurally_invalid_one_hot_config() { let extract_out = pred() .args([ + "--json", "extract", bundle_file.to_str().unwrap(), "--config", - "[false,false,false,false,false,false,false,false,false]", + "[true,false,false,false,true,false,false,false,true]", ]) .output() .unwrap(); - assert!(!extract_out.status.success()); - let stderr = String::from_utf8(extract_out.stderr).unwrap(); assert!( - stderr.contains("tour position 0 does not select exactly one vertex"), - "unexpected stderr: {stderr}" + extract_out.status.success(), + "{}", + String::from_utf8_lossy(&extract_out.stderr) ); + let json: serde_json::Value = serde_json::from_slice(&extract_out.stdout).unwrap(); + assert_eq!(json["solution"], serde_json::json!([true, true, true])); + assert_eq!(json["evaluation"], "Min(3)"); std::fs::remove_file(&problem_file).ok(); std::fs::remove_file(&bundle_file).ok(); @@ -9950,7 +9953,7 @@ fn test_extract_rejects_tampered_target_data() { // what the reduction chain actually produces. let bundle_text = std::fs::read_to_string(&bundle_file).unwrap(); let mut bundle: serde_json::Value = serde_json::from_str(&bundle_text).unwrap(); - bundle["target"]["data"]["matrix"][0][0] = serde_json::json!(999.0); + bundle["target"]["data"]["matrix"]["data"][0] = serde_json::json!(999.0); let mut f = std::fs::File::create(&tampered_file).unwrap(); f.write_all(bundle.to_string().as_bytes()).unwrap(); diff --git a/problemreductions-macros/src/lib.rs b/problemreductions-macros/src/lib.rs index 9ee54082e..a50843782 100644 --- a/problemreductions-macros/src/lib.rs +++ b/problemreductions-macros/src/lib.rs @@ -531,6 +531,30 @@ fn generate_reduction_entry( quote! { None } }; + let aggregate_view = if attrs.aggregate { + quote! { Some(result.clone()) } + } else { + quote! { None } + }; + + let interpret_optimum = if attrs.aggregate { + quote! { + Some({ + let result = result.clone(); + std::rc::Rc::new(move |solution: &dyn std::any::Any| { + let solution = solution.downcast_ref::<<#target_type as crate::traits::Problem>::Solution>() + .ok_or_else(|| crate::rules::ExtractionError::invalid("target solution type mismatch"))?; + let target = crate::rules::ReductionResult::target_problem(result.as_ref()); + let value = crate::traits::Problem::evaluate(target, solution)?; + let value = crate::rules::AggregateReductionResult::extract_value(result.as_ref(), value); + Ok(value.is_valid()) + }) + }) + } + } else { + quote! { None } + }; + // Collect generic parameter info from the impl block let type_generics = collect_type_generic_names(&impl_block.generics); @@ -572,12 +596,17 @@ fn generate_reduction_entry( unavailable: vec![#(#unavailable_tokens),*], }, module_path: module_path!(), - reduce_fn: Some(|src: &dyn std::any::Any| -> Result, crate::rules::ReductionError> { + reduce_fn: Some(|src: &dyn std::any::Any| -> Result { let src = src.downcast_ref::<#source_type>().ok_or_else( crate::rules::ReductionError::source_type_mismatch::<#source_type, #target_type>, )?; let result = <#source_type as crate::rules::ReduceTo<#target_type>>::reduce_to(src)?; - Ok(Box::new(result)) + let result = std::rc::Rc::new(result); + Ok(crate::rules::registry::ExecutedStep { + aggregate: #aggregate_view, + interpret_optimum: #interpret_optimum, + witness: result, + }) }), reduce_aggregate_fn: #reduce_aggregate_fn, turing: false, @@ -787,7 +816,7 @@ pub fn register_brute_force(input: TokenStream) -> TokenStream { let problem = any .downcast_ref::<#ty>() .expect("brute-force registration received the wrong problem type"); - <#ty as crate::solvers::BruteForceProblem>::dimensions(problem) + crate::solvers::cartesian_dimensions(problem) }, solve_fn: |any| { let problem = any @@ -944,6 +973,7 @@ fn generate_declare_variants(input: &DeclareVariantsInput) -> syn::Result(source: S) -> RuleExample where S: Problem + Serialize + ReduceTo>, V: crate::models::algebraic::VariableDomain, - C: crate::models::algebraic::ILPCoefficient + Serialize, + C: crate::models::algebraic::ILPCoefficient + Serialize + serde::de::DeserializeOwned, >>::Result: ReductionResult>, S::Solution: Serialize, diff --git a/src/lib.rs b/src/lib.rs index 69381f493..ae4edab0c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -75,8 +75,8 @@ pub mod prelude { MultipleChoiceBranching, MultipleCopyFileAllocation, OptimalLinearArrangement, PartialFeedbackEdgeSet, PartitionIntoCliques, PartitionIntoPathsOfLength2, PartitionIntoTriangles, PathConstrainedNetworkFlow, RootedTreeArrangement, RuralPostman, - ShortestWeightConstrainedPath, SteinerTreeInGraphs, TravelingSalesman, - UndirectedFlowLowerBounds, UndirectedTwoCommodityIntegralFlow, + ShortestWeightConstrainedPath, TravelingSalesman, UndirectedFlowLowerBounds, + UndirectedTwoCommodityIntegralFlow, }; pub use crate::models::misc::{ AdditionalKey, BinPacking, BoyceCoddNormalFormViolation, CapacityAssignment, CbqRelation, diff --git a/src/models/algebraic/algebraic_equations_over_gf2.rs b/src/models/algebraic/algebraic_equations_over_gf2.rs index 32649b5f2..57d09ef2d 100644 --- a/src/models/algebraic/algebraic_equations_over_gf2.rs +++ b/src/models/algebraic/algebraic_equations_over_gf2.rs @@ -204,8 +204,12 @@ impl Problem for AlgebraicEquationsOverGF2 { } impl crate::solvers::BruteForceProblem for AlgebraicEquationsOverGF2 { - fn dimensions(&self) -> Vec { - vec![2; self.num_variables] + fn num_variables(&self) -> Result { + Ok(self.num_variables) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } diff --git a/src/models/algebraic/bmf.rs b/src/models/algebraic/bmf.rs index 52aeb23e8..a5971dcac 100644 --- a/src/models/algebraic/bmf.rs +++ b/src/models/algebraic/bmf.rs @@ -247,9 +247,20 @@ impl Problem for BMF { } impl crate::solvers::BruteForceProblem for BMF { - fn dimensions(&self) -> Vec { - // B: m*k + C: k*n binary variables - vec![2; self.m * self.k + self.k * self.n] + fn num_variables(&self) -> Result { + ((self.m).checked_mul(self.k).ok_or_else(|| { + crate::solvers::SolveError::IntegerOverflow("computing the coordinate count".into()) + })?) + .checked_add((self.k).checked_mul(self.n).ok_or_else(|| { + crate::solvers::SolveError::IntegerOverflow("computing the coordinate count".into()) + })?) + .ok_or_else(|| { + crate::solvers::SolveError::IntegerOverflow("computing the coordinate count".into()) + }) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } diff --git a/src/models/algebraic/closest_vector_problem.rs b/src/models/algebraic/closest_vector_problem.rs index d166b32fb..d2ab02c83 100644 --- a/src/models/algebraic/closest_vector_problem.rs +++ b/src/models/algebraic/closest_vector_problem.rs @@ -1,11 +1,14 @@ //! Closest Vector Problem (CVP). //! //! Given an integer lattice basis `B` and a target vector `t`, find integer -//! coefficients `x` minimizing `||Bx - t||_2`. +//! coefficients `x` minimizing the squared distance `||Bx - t||_2^2`. use crate::registry::{ConstructionError, CreateSpec, ProblemSchemaEntry, VariantDimension}; use crate::traits::{EvaluationError, Problem}; use crate::types::Min; +use num_bigint::BigInt; +use num_rational::BigRational; +use num_traits::Zero; use serde::{Deserialize, Serialize}; /// Target coordinate domains supported by [`ClosestVectorProblem`]. @@ -16,8 +19,8 @@ pub trait ClosestVectorTarget: Clone + std::fmt::Debug + 'static { /// Validate one stored target coordinate. fn validate(&self, index: usize) -> Result<(), ConstructionError>; - /// Convert one coordinate for numerical evaluation and solving. - fn to_f64(&self) -> Result; + /// Represent a stored coordinate exactly for distance evaluation and solving. + fn to_rational(&self) -> BigRational; } impl ClosestVectorTarget for i64 { @@ -27,9 +30,8 @@ impl ClosestVectorTarget for i64 { Ok(()) } - fn to_f64(&self) -> Result { - crate::types::i64_to_exact_f64(*self) - .map_err(|error| EvaluationError::InexactFloatConversion(error.to_string())) + fn to_rational(&self) -> BigRational { + BigRational::from_integer((*self).into()) } } @@ -46,8 +48,8 @@ impl ClosestVectorTarget for f64 { } } - fn to_f64(&self) -> Result { - Ok(*self) + fn to_rational(&self) -> BigRational { + BigRational::from_float(*self).expect("CVP target coordinate must be finite") } } @@ -119,7 +121,7 @@ impl ClosestVectorProblem { basis.len() ))); } - if independent_rows(&basis, ambient_dimension)?.is_none() { + if independent_rows(&basis, ambient_dimension).is_none() { return Err(ConstructionError::Conversion( "closest-vector basis columns must be linearly independent".into(), )); @@ -147,62 +149,72 @@ impl ClosestVectorProblem { &self.target } - pub(crate) fn independent_rows(&self) -> Result, ConstructionError> { - independent_rows(&self.basis, self.ambient_dimension())?.ok_or_else(|| { - ConstructionError::Conversion( - "closest-vector basis columns must be linearly independent".into(), - ) - }) + pub(crate) fn independent_rows(&self) -> Vec { + independent_rows(&self.basis, self.ambient_dimension()) + .expect("CVP basis columns must be independent") + } + + /// Exact squared distance from the lattice point to the stored target. + pub fn squared_distance(&self, solution: &[i64]) -> Result { + if solution.len() != self.num_basis_vectors() { + return Err(EvaluationError::InvalidConfiguration(format!( + "expected {} closest-vector coefficients, got {}", + self.num_basis_vectors(), + solution.len() + ))); + } + Ok(self + .target + .iter() + .enumerate() + .map(|(row, target)| { + let coordinate: BigInt = solution + .iter() + .zip(&self.basis) + .map(|(&coefficient, column)| BigInt::from(coefficient) * column[row]) + .sum(); + let difference = BigRational::from_integer(coordinate) - target.to_rational(); + &difference * &difference + }) + .sum()) } } -fn independent_rows( - basis: &[Vec], - ambient_dimension: usize, -) -> Result>, ConstructionError> { +fn independent_rows(basis: &[Vec], ambient_dimension: usize) -> Option> { let num_columns = basis.len(); if num_columns == 0 { - return Ok(Some(Vec::new())); + return Some(Vec::new()); } let mut matrix = (0..ambient_dimension) - .map(|row| basis.iter().map(|column| column[row]).collect::>()) + .map(|row| { + basis + .iter() + .map(|column| BigInt::from(column[row])) + .collect::>() + }) .collect::>(); - let mut previous_pivot = 1_i64; + let mut previous_pivot = BigInt::from(1); let mut row_indices = (0..ambient_dimension).collect::>(); for column in 0..num_columns { - let Some(pivot_row) = (column..ambient_dimension).find(|&row| matrix[row][column] != 0) - else { - return Ok(None); - }; + let pivot_row = (column..ambient_dimension).find(|&row| !matrix[row][column].is_zero())?; matrix.swap(column, pivot_row); row_indices.swap(column, pivot_row); - let pivot = matrix[column][column]; + let pivot = matrix[column][column].clone(); for row in (column + 1)..ambient_dimension { for next_column in (column + 1)..num_columns { - let left = matrix[row][next_column] - .checked_mul(pivot) - .ok_or_else(rank_overflow)?; - let right = matrix[row][column] - .checked_mul(matrix[column][next_column]) - .ok_or_else(rank_overflow)?; - let numerator = left.checked_sub(right).ok_or_else(rank_overflow)?; - matrix[row][next_column] = numerator - .checked_div(previous_pivot) - .ok_or_else(rank_overflow)?; + matrix[row][next_column] = (&matrix[row][next_column] * &pivot + - &matrix[row][column] * &matrix[column][next_column]) + / &previous_pivot; } - matrix[row][column] = 0; + matrix[row][column] = BigInt::zero(); } previous_pivot = pivot; } row_indices.truncate(num_columns); - Ok(Some(row_indices)) -} - -fn rank_overflow() -> ConstructionError { - ConstructionError::IntegerOverflow("checking closest-vector basis rank".into()) + Some(row_indices) } impl<'de, T> Deserialize<'de> for ClosestVectorProblem @@ -230,58 +242,15 @@ where { const NAME: &'static str = "ClosestVectorProblem"; type Solution = Vec; - type Value = Min; + type Value = Min; crate::problem_parameters![ ("ambient_dimension", ambient_dimension), ("num_basis_vectors", num_basis_vectors), ]; - fn evaluate(&self, solution: &Self::Solution) -> Result, EvaluationError> { - if solution.len() != self.num_basis_vectors() { - return Err(EvaluationError::InvalidConfiguration(format!( - "expected {} closest-vector coefficients, got {}", - self.num_basis_vectors(), - solution.len() - ))); - } - - let mut displacement = self - .target - .iter() - .map(ClosestVectorTarget::to_f64) - .collect::, _>>()?; - for value in &mut displacement { - *value = -*value; - } - - for (&coefficient, column) in solution.iter().zip(&self.basis) { - let coefficient = crate::types::i64_to_exact_f64(coefficient) - .map_err(|error| EvaluationError::InexactFloatConversion(error.to_string()))?; - for (value, &basis_entry) in displacement.iter_mut().zip(column) { - let basis_entry = crate::types::i64_to_exact_f64(basis_entry) - .map_err(|error| EvaluationError::InexactFloatConversion(error.to_string()))?; - let next = *value + coefficient * basis_entry; - if !next.is_finite() { - return Err(EvaluationError::NonFiniteResult( - "computing closest-vector displacement".into(), - )); - } - *value = next; - } - } - - let squared_norm = displacement.into_iter().try_fold(0.0, |total, value| { - let next = total + value * value; - if next.is_finite() { - Ok(next) - } else { - Err(EvaluationError::NonFiniteResult( - "computing closest-vector norm".into(), - )) - } - })?; - Ok(Min(Some(squared_norm.sqrt()))) + fn evaluate(&self, solution: &Self::Solution) -> Result { + Ok(Min(Some(self.squared_distance(solution)?))) } fn variant() -> Vec<(&'static str, &'static str)> { @@ -303,7 +272,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec Vec { - vec![self.num_cols; self.num_cols] + fn num_variables(&self) -> Result { + Ok(self.num_cols) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(self.num_cols) } } diff --git a/src/models/algebraic/consecutive_ones_matrix_augmentation.rs b/src/models/algebraic/consecutive_ones_matrix_augmentation.rs index 0ddbe58a6..46031689b 100644 --- a/src/models/algebraic/consecutive_ones_matrix_augmentation.rs +++ b/src/models/algebraic/consecutive_ones_matrix_augmentation.rs @@ -173,8 +173,12 @@ impl Problem for ConsecutiveOnesMatrixAugmentation { } impl crate::solvers::BruteForceProblem for ConsecutiveOnesMatrixAugmentation { - fn dimensions(&self) -> Vec { - vec![self.num_cols(); self.num_cols()] + fn num_variables(&self) -> Result { + Ok(self.num_cols()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(self.num_cols()) } } diff --git a/src/models/algebraic/consecutive_ones_submatrix.rs b/src/models/algebraic/consecutive_ones_submatrix.rs index 8c8514e14..94cf051cc 100644 --- a/src/models/algebraic/consecutive_ones_submatrix.rs +++ b/src/models/algebraic/consecutive_ones_submatrix.rs @@ -215,8 +215,12 @@ impl Problem for ConsecutiveOnesSubmatrix { } impl crate::solvers::BruteForceProblem for ConsecutiveOnesSubmatrix { - fn dimensions(&self) -> Vec { - vec![2; self.num_cols()] + fn num_variables(&self) -> Result { + Ok(self.num_cols()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } diff --git a/src/models/algebraic/equilibrium_point.rs b/src/models/algebraic/equilibrium_point.rs index 6d3392c40..e5fdfd024 100644 --- a/src/models/algebraic/equilibrium_point.rs +++ b/src/models/algebraic/equilibrium_point.rs @@ -247,8 +247,12 @@ impl Problem for EquilibriumPoint { } impl crate::solvers::BruteForceProblem for EquilibriumPoint { - fn dimensions(&self) -> Vec { - self.range_sets.iter().map(|m| m.len()).collect() + fn num_variables(&self) -> Result { + Ok(self.range_sets.len()) + } + + fn dimension(&self, variable: usize) -> Result { + Ok(self.range_sets[variable].len()) } } diff --git a/src/models/algebraic/feasible_basis_extension.rs b/src/models/algebraic/feasible_basis_extension.rs index 88396d820..5f55d335a 100644 --- a/src/models/algebraic/feasible_basis_extension.rs +++ b/src/models/algebraic/feasible_basis_extension.rs @@ -463,8 +463,12 @@ impl Problem for FeasibleBasisExtension { } impl crate::solvers::BruteForceProblem for FeasibleBasisExtension { - fn dimensions(&self) -> Vec { - vec![2; self.num_columns() - self.num_required()] + fn num_variables(&self) -> Result { + Ok(self.num_columns() - self.num_required()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } diff --git a/src/models/algebraic/ilp.rs b/src/models/algebraic/ilp.rs index 7f4111e35..709e2299f 100644 --- a/src/models/algebraic/ilp.rs +++ b/src/models/algebraic/ilp.rs @@ -6,9 +6,7 @@ use crate::registry::{ConstructionError, FieldInfo, ProblemSchemaEntry, VariantDimension}; use crate::traits::{EvaluationError, Problem}; -use crate::types::{ - i64_to_exact_f64, Extremum, NumericArithmeticError, NumericSize, WeightElement, -}; +use crate::types::{Extremum, NumericArithmeticError, NumericSize, WeightElement}; use serde::{Deserialize, Deserializer, Serialize}; use std::fmt::Debug; use std::marker::PhantomData; @@ -80,19 +78,14 @@ impl ILPCoefficient for f64 { const NAME: &'static str = "f64"; fn from_integer(value: i64) -> Result { - i64_to_exact_f64(value).map_err(|_| { - EvaluationError::InexactFloatConversion( - "transporting an integer variable into an f64 ILP expression".into(), - ) - }) + Ok(value as f64) } fn satisfies(lhs: Self, comparison: Comparison, rhs: Self) -> bool { - let tolerance = 1e-9 * lhs.abs().max(rhs.abs()).max(1.0); match comparison { - Comparison::Le => lhs <= rhs + tolerance, - Comparison::Ge => lhs >= rhs - tolerance, - Comparison::Eq => (lhs - rhs).abs() <= tolerance, + Comparison::Le => lhs <= rhs, + Comparison::Ge => lhs >= rhs, + Comparison::Eq => lhs == rhs, } } } diff --git a/src/models/algebraic/minimum_matrix_cover.rs b/src/models/algebraic/minimum_matrix_cover.rs index 8df124c06..c969d25d5 100644 --- a/src/models/algebraic/minimum_matrix_cover.rs +++ b/src/models/algebraic/minimum_matrix_cover.rs @@ -3,7 +3,7 @@ //! Given an n×n nonnegative integer matrix A, find a sign assignment //! f: {1,...,n} → {-1,+1} minimizing Σ a_ij · f(i) · f(j). -use crate::registry::{FieldInfo, ProblemSchemaEntry}; +use crate::registry::{ConstructionError, FieldInfo, ProblemSchemaEntry}; use crate::traits::Problem; use crate::types::Min; use serde::{Deserialize, Serialize}; @@ -44,13 +44,13 @@ inventory::submit! { /// vec![3, 0, 0, 2], /// vec![1, 0, 0, 4], /// vec![0, 2, 4, 0], -/// ]); +/// ]).unwrap(); /// /// let solver = BruteForce::new(); /// let witness = solver.solve(&problem).unwrap(); /// assert!(witness.is_some()); /// ``` -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize)] pub struct MinimumMatrixCover { /// The n×n nonnegative integer matrix. matrix: Vec>, @@ -59,20 +59,23 @@ pub struct MinimumMatrixCover { impl MinimumMatrixCover { /// Create a new MinimumMatrixCover instance. /// - /// # Panics - /// - /// Panics if the matrix is not square or has inconsistent row lengths. - pub fn new(matrix: Vec>) -> Self { + /// Returns an error for a nonsquare matrix or a negative entry. + pub fn new(matrix: Vec>) -> Result { let n = matrix.len(); for (i, row) in matrix.iter().enumerate() { - assert_eq!( - row.len(), - n, - "Matrix must be square: row {i} has {} columns, expected {n}", - row.len() - ); + if row.len() != n { + return Err(ConstructionError::InvalidInput(format!( + "matrix row {i} has {} columns, expected {n}", + row.len() + ))); + } + if row.iter().any(|&entry| entry < 0) { + return Err(ConstructionError::InvalidInput(format!( + "matrix row {i} contains a negative entry" + ))); + } } - Self { matrix } + Ok(Self { matrix }) } /// Returns the number of rows (= columns) of the matrix. @@ -86,6 +89,17 @@ impl MinimumMatrixCover { } } +impl<'de> Deserialize<'de> for MinimumMatrixCover { + fn deserialize>(deserializer: D) -> Result { + #[derive(Deserialize)] + struct Data { + matrix: Vec>, + } + let data = Data::deserialize(deserializer)?; + Self::new(data.matrix).map_err(serde::de::Error::custom) + } +} + impl Problem for MinimumMatrixCover { const NAME: &'static str = "MinimumMatrixCover"; type Solution = Vec; @@ -140,8 +154,12 @@ impl Problem for MinimumMatrixCover { } impl crate::solvers::BruteForceProblem for MinimumMatrixCover { - fn dimensions(&self) -> Vec { - vec![2; self.num_rows()] + fn num_variables(&self) -> Result { + Ok(self.num_rows()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } @@ -159,12 +177,15 @@ pub(crate) fn canonical_model_example_specs() -> Vec Vec { - vec![2; self.num_ones()] + fn num_variables(&self) -> Result { + Ok(self.num_ones()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } diff --git a/src/models/algebraic/minimum_weight_decoding.rs b/src/models/algebraic/minimum_weight_decoding.rs index 6b17433ce..6f92fae39 100644 --- a/src/models/algebraic/minimum_weight_decoding.rs +++ b/src/models/algebraic/minimum_weight_decoding.rs @@ -181,8 +181,12 @@ impl Problem for MinimumWeightDecoding { } impl crate::solvers::BruteForceProblem for MinimumWeightDecoding { - fn dimensions(&self) -> Vec { - vec![2; self.num_cols()] + fn num_variables(&self) -> Result { + Ok(self.num_cols()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } diff --git a/src/models/algebraic/minimum_weight_solution_to_linear_equations.rs b/src/models/algebraic/minimum_weight_solution_to_linear_equations.rs index 478f0b21e..988f7eb62 100644 --- a/src/models/algebraic/minimum_weight_solution_to_linear_equations.rs +++ b/src/models/algebraic/minimum_weight_solution_to_linear_equations.rs @@ -259,8 +259,12 @@ impl Problem for MinimumWeightSolutionToLinearEquations { } impl crate::solvers::BruteForceProblem for MinimumWeightSolutionToLinearEquations { - fn dimensions(&self) -> Vec { - vec![2; self.num_variables()] + fn num_variables(&self) -> Result { + Ok(self.num_variables()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } diff --git a/src/models/algebraic/quadratic_assignment.rs b/src/models/algebraic/quadratic_assignment.rs index 72bafd830..e1974fb92 100644 --- a/src/models/algebraic/quadratic_assignment.rs +++ b/src/models/algebraic/quadratic_assignment.rs @@ -186,8 +186,12 @@ impl Problem for QuadraticAssignment { } impl crate::solvers::BruteForceProblem for QuadraticAssignment { - fn dimensions(&self) -> Vec { - vec![self.num_locations(); self.num_facilities()] + fn num_variables(&self) -> Result { + Ok(self.num_facilities()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(self.num_locations()) } } diff --git a/src/models/algebraic/quadratic_congruences.rs b/src/models/algebraic/quadratic_congruences.rs index 98212273c..eb7a01d1d 100644 --- a/src/models/algebraic/quadratic_congruences.rs +++ b/src/models/algebraic/quadratic_congruences.rs @@ -247,13 +247,12 @@ impl Problem for QuadraticCongruences { } impl crate::solvers::BruteForceProblem for QuadraticCongruences { - fn dimensions(&self) -> Vec { - let num_bits = self.witness_bit_length(); - if num_bits == 0 { - Vec::new() - } else { - vec![2; num_bits] - } + fn num_variables(&self) -> Result { + Ok(self.witness_bit_length()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2) } } diff --git a/src/models/algebraic/quadratic_diophantine_equations.rs b/src/models/algebraic/quadratic_diophantine_equations.rs index 6cef6e290..2115b01b6 100644 --- a/src/models/algebraic/quadratic_diophantine_equations.rs +++ b/src/models/algebraic/quadratic_diophantine_equations.rs @@ -254,13 +254,12 @@ impl Problem for QuadraticDiophantineEquations { } impl crate::solvers::BruteForceProblem for QuadraticDiophantineEquations { - fn dimensions(&self) -> Vec { - let num_bits = self.witness_bit_length(); - if num_bits == 0 { - Vec::new() - } else { - vec![2; num_bits] - } + fn num_variables(&self) -> Result { + Ok(self.witness_bit_length()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2) } } diff --git a/src/models/algebraic/qubo.rs b/src/models/algebraic/qubo.rs index 1c145bd6e..86466e024 100644 --- a/src/models/algebraic/qubo.rs +++ b/src/models/algebraic/qubo.rs @@ -7,6 +7,8 @@ use crate::traits::Problem; use crate::types::{Min, WeightElement}; use num_traits::Zero; use serde::{Deserialize, Serialize}; +use sprs::CsMat; +use std::collections::BTreeMap; inventory::submit! { ProblemSchemaEntry { @@ -56,12 +58,24 @@ inventory::submit! { /// assert!(solutions.contains(&vec![false, true])); /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde( + try_from = "QuboData", + bound(deserialize = "W: WeightElement + Deserialize<'de>") +)] pub struct QUBO { - /// Number of variables. - num_vars: usize, - /// Q matrix stored as upper triangular (row-major). - /// `Q[i][j]` for i <= j represents the coefficient of x_i * x_j - matrix: Vec>, + matrix: CsMat, +} + +#[derive(Deserialize)] +struct QuboData { + matrix: CsMat, +} + +impl TryFrom> for QUBO { + type Error = ConstructionError; + fn try_from(data: QuboData) -> Result { + Self::from_sparse(data.matrix) + } } #[derive(Debug, Deserialize, crate::CreateSpec)] @@ -95,12 +109,61 @@ impl QUBO { "QUBO matrix row {row} has length {actual}, expected {num_vars}" ))); } - for (row, values) in matrix.iter().enumerate() { - for (column, value) in values.iter().enumerate() { - value.validate_element(&format!("QUBO coefficient at ({row}, {column})"))?; + Self::from_rows( + matrix + .into_iter() + .map(|row| row.into_iter().enumerate()) + .collect(), + ) + } + + /// Create a QUBO from a square sparse matrix. Only its upper triangle is evaluated. + pub fn from_sparse(matrix: CsMat) -> Result { + if matrix.rows() != matrix.cols() { + return Err(ConstructionError::Conversion( + "QUBO matrix must be square".into(), + )); + } + let matrix = matrix.into_csr(); + for (row, values) in matrix.outer_iterator().enumerate() { + for (column, value) in values.iter() { + value + .validate_element("QUBO coefficient") + .map_err(|error| match error { + ConstructionError::NonFiniteFloat(message) => { + ConstructionError::NonFiniteFloat(format!( + "{message} at ({row}, {column})" + )) + } + error => error, + })?; } } - Ok(Self { num_vars, matrix }) + Ok(Self { matrix }) + } + + // Rows collect assignments and checked additions before compression. No library + // duplicate summation may replace the rule's numeric operations. + pub(crate) fn from_rows( + rows: Vec>, + ) -> Result { + let n = rows.len(); + let mut offsets = Vec::with_capacity(n + 1); + let mut indices = Vec::new(); + let mut values = Vec::new(); + offsets.push(0); + for row in rows { + for (column, value) in row { + if !value.to_sum().is_zero() { + indices.push(column); + values.push(value); + } + } + offsets.push(values.len()); + } + let matrix = CsMat::try_new((n, n), offsets, indices, values) + .map_err(|(_, _, _, error)| ConstructionError::Conversion(error.to_string()))?; + Self::from_sparse(matrix) } /// Create a QUBO from linear and quadratic terms. @@ -113,11 +176,11 @@ impl QUBO { quadratic: Vec<((usize, usize), W)>, ) -> Result { let num_vars = linear.len(); - let mut matrix = vec![vec![W::default(); num_vars]; num_vars]; + let mut matrix = vec![BTreeMap::new(); num_vars]; // Set diagonal (linear terms) for (i, val) in linear.into_iter().enumerate() { - matrix[i][i] = val; + matrix[i].insert(i, val); } // Set off-diagonal (quadratic terms) @@ -128,30 +191,34 @@ impl QUBO { ))); } if i < j { - matrix[i][j] = val; + matrix[i].insert(j, val); } else { - matrix[j][i] = val; + matrix[j].insert(i, val); } } - Self::from_matrix(matrix) + Self::from_rows(matrix) } } impl QUBO { /// Get the number of variables. pub fn num_vars(&self) -> usize { - self.num_vars + self.matrix.rows() } /// Get the Q matrix. - pub fn matrix(&self) -> &[Vec] { + pub fn matrix(&self) -> &CsMat { &self.matrix } - /// Get a specific matrix element `Q[i][j]`. - pub fn get(&self, i: usize, j: usize) -> Option<&W> { - self.matrix.get(i).and_then(|row| row.get(j)) + /// Get a coefficient, returning zero for an unstored entry and None outside the matrix. + pub fn get(&self, i: usize, j: usize) -> Option + where + W: Clone + Zero, + { + (i < self.num_vars() && j < self.num_vars()) + .then(|| self.matrix.get(i, j).cloned().unwrap_or_else(W::zero)) } } @@ -169,31 +236,26 @@ where &self, solution: &Self::Solution, ) -> Result, crate::traits::EvaluationError> { - if solution.len() != self.num_vars { + if solution.len() != self.num_vars() { return Err(crate::traits::EvaluationError::InvalidConfiguration( format!( "solution has {} variables, expected {}", solution.len(), - self.num_vars + self.matrix.rows() ), )); } let mut value = W::Sum::zero(); - for i in 0..self.num_vars { + for (i, row) in self.matrix.outer_iterator().enumerate() { if !solution[i] { continue; } - - for (j, &selected) in solution.iter().enumerate().skip(i) { - if !selected { - continue; - } - - if let Some(q_ij) = self.matrix.get(i).and_then(|row| row.get(j)) { + for (j, coefficient) in row.iter() { + if j >= i && solution[j] { value = W::checked_add_to_sum( value, - q_ij.to_sum(), + coefficient.to_sum(), "summing selected QUBO coefficients", )?; } @@ -212,8 +274,12 @@ impl crate::solvers::BruteForceProblem for QUBO where W: WeightElement + crate::variant::VariantParam, { - fn dimensions(&self) -> Vec { - vec![2; self.num_vars] + fn num_variables(&self) -> Result { + Ok(self.num_vars()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } diff --git a/src/models/algebraic/simultaneous_incongruences.rs b/src/models/algebraic/simultaneous_incongruences.rs index 968d8deda..36565ace0 100644 --- a/src/models/algebraic/simultaneous_incongruences.rs +++ b/src/models/algebraic/simultaneous_incongruences.rs @@ -79,11 +79,6 @@ impl SimultaneousIncongruences { .into()); } } - pairs.iter().try_fold(1i64, |lcm, &(_, modulus)| { - (lcm / gcd(lcm, modulus)) - .checked_mul(modulus) - .ok_or_else(|| "Least common multiple of moduli exceeds i64 range".to_string()) - })?; Ok(()) } @@ -105,9 +100,15 @@ impl SimultaneousIncongruences { } /// Compute the LCM of all moduli. - pub fn lcm_moduli(&self) -> i64 { - self.pairs.iter().fold(1i64, |lcm, &(_, modulus)| { - (lcm / gcd(lcm, modulus)) * modulus + pub fn lcm_moduli(&self) -> Result { + self.pairs.iter().try_fold(1i64, |lcm, &(_, modulus)| { + (lcm / gcd(lcm, modulus)) + .checked_mul(modulus) + .ok_or_else(|| { + crate::traits::EvaluationError::IntegerOverflow( + "computing the incongruence period".into(), + ) + }) }) } } @@ -141,15 +142,18 @@ impl Problem for SimultaneousIncongruences { fn evaluate(&self, solution: &Self::Solution) -> Result { Ok({ // x is a solution iff x % bᵢ ≠ aᵢ % bᵢ for every pair. - Or(self.pairs.iter().all(|&(a, b)| solution % b != a % b)) + Or(*solution >= 0 && self.pairs.iter().all(|&(a, b)| solution % b != a % b)) }) } } impl crate::solvers::BruteForceProblem for SimultaneousIncongruences { - fn dimensions(&self) -> Vec { - let lcm = usize::try_from(self.lcm_moduli()).expect("validated positive LCM fits usize"); - vec![lcm] + fn num_variables(&self) -> Result { + Ok(1) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(usize::try_from(self.lcm_moduli()?)?) } } diff --git a/src/models/algebraic/sparse_matrix_compression.rs b/src/models/algebraic/sparse_matrix_compression.rs index 724a0aff7..fd4dcfcb6 100644 --- a/src/models/algebraic/sparse_matrix_compression.rs +++ b/src/models/algebraic/sparse_matrix_compression.rs @@ -173,8 +173,12 @@ impl Problem for SparseMatrixCompression { } impl crate::solvers::BruteForceProblem for SparseMatrixCompression { - fn dimensions(&self) -> Vec { - vec![self.bound_k; self.num_rows()] + fn num_variables(&self) -> Result { + Ok(self.num_rows()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(self.bound_k) } } diff --git a/src/models/decision.rs b/src/models/decision.rs index 1acb637ca..be8918fed 100644 --- a/src/models/decision.rs +++ b/src/models/decision.rs @@ -96,7 +96,21 @@ macro_rules! register_decision_variant { >)?; let result = <$crate::models::decision::Decision<$inner> as $crate::rules::ReduceTo<$inner>>::reduce_to(source)?; - Ok(Box::new(result)) + let result = std::rc::Rc::new(result); + Ok($crate::rules::registry::ExecutedStep { + aggregate: Some(result.clone()), + interpret_optimum: Some({ + let result = result.clone(); + std::rc::Rc::new(move |solution: &dyn std::any::Any| { + let solution = solution.downcast_ref::<<$inner as $crate::traits::Problem>::Solution>() + .ok_or_else(|| $crate::rules::ExtractionError::invalid("target solution type mismatch"))?; + let target = $crate::rules::ReductionResult::target_problem(result.as_ref()); + let value = $crate::traits::Problem::evaluate(target, solution)?; + Ok($crate::rules::AggregateReductionResult::extract_value(result.as_ref(), value).is_valid()) + }) + }), + witness: result, + }) }), reduce_aggregate_fn: Some(|any| { let source = any @@ -317,12 +331,21 @@ where P: DecisionProblemMeta + crate::solvers::BruteForceProblem, P::Value: OptimizationValue, { - fn dimensions(&self) -> Vec { - self.inner.dimensions() + fn num_variables(&self) -> Result { + self.inner.num_variables() + } + + fn dimension(&self, variable: usize) -> Result { + self.inner.dimension(variable) } } -/// Aggregate reduction result for `Decision

-> P`. +/// Executed reduction from `Decision

` to its optimization problem. +/// +/// The target and decision bound belong to the same execution. An optimum +/// meeting the bound supplies a decision witness; an optimum missing the bound +/// establishes NO through `extract_value`. Witness extraction copies a target +/// witness that meets the bound and does not repeat the comparison. #[derive(Debug, Clone)] pub struct DecisionToOptimizationResult

where @@ -368,25 +391,11 @@ where } } -/// Witness reduction result for `Decision

-> P`. -/// -/// The configuration spaces are identical — a config that is optimal for -/// `P` and meets the bound is a valid `Decision

` witness. The -/// `extract_solution` is the identity function. -#[derive(Debug, Clone)] -pub struct DecisionToOptimizationWitnessResult

-where - P: Problem, - P::Value: OptimizationValue, -{ - target: P, -} - -impl

ReductionResult for DecisionToOptimizationWitnessResult

+impl

ReductionResult for DecisionToOptimizationResult

where P: DecisionProblemMeta + 'static, P::Solution: Clone, - P::Value: OptimizationValue + Serialize + DeserializeOwned, + P::Value: OptimizationValue, { type Source = Decision

; type Target = P; @@ -399,8 +408,6 @@ where &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution.clone()) } } @@ -409,13 +416,14 @@ impl

ReduceTo

for Decision

where P: DecisionProblemMeta + Clone + 'static, P::Solution: Clone, - P::Value: OptimizationValue + Serialize + DeserializeOwned, + P::Value: OptimizationValue, { - type Result = DecisionToOptimizationWitnessResult

; + type Result = DecisionToOptimizationResult

; fn reduce_to(&self) -> Result { - Ok(DecisionToOptimizationWitnessResult { + Ok(DecisionToOptimizationResult { target: self.inner.clone(), + bound: self.bound.clone(), }) } } diff --git a/src/models/formula/circuit.rs b/src/models/formula/circuit.rs index 2e078c074..1d20f16e2 100644 --- a/src/models/formula/circuit.rs +++ b/src/models/formula/circuit.rs @@ -358,8 +358,12 @@ impl Problem for CircuitSAT { } impl crate::solvers::BruteForceProblem for CircuitSAT { - fn dimensions(&self) -> Vec { - vec![2; self.variables.len()] + fn num_variables(&self) -> Result { + Ok(self.variables.len()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } diff --git a/src/models/formula/ksat.rs b/src/models/formula/ksat.rs index dede5be57..2c7302f44 100644 --- a/src/models/formula/ksat.rs +++ b/src/models/formula/ksat.rs @@ -267,8 +267,12 @@ impl Problem for KSatisfiability { } impl crate::solvers::BruteForceProblem for KSatisfiability { - fn dimensions(&self) -> Vec { - vec![2; self.num_vars] + fn num_variables(&self) -> Result { + Ok(self.num_vars) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } diff --git a/src/models/formula/maximum_2_satisfiability.rs b/src/models/formula/maximum_2_satisfiability.rs index 5085b21ff..98d6bc3ba 100644 --- a/src/models/formula/maximum_2_satisfiability.rs +++ b/src/models/formula/maximum_2_satisfiability.rs @@ -141,8 +141,12 @@ impl Problem for Maximum2Satisfiability { } impl crate::solvers::BruteForceProblem for Maximum2Satisfiability { - fn dimensions(&self) -> Vec { - vec![2; self.num_vars] + fn num_variables(&self) -> Result { + Ok(self.num_vars) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } diff --git a/src/models/formula/nae_satisfiability.rs b/src/models/formula/nae_satisfiability.rs index f65fdb717..8e38248d6 100644 --- a/src/models/formula/nae_satisfiability.rs +++ b/src/models/formula/nae_satisfiability.rs @@ -186,8 +186,12 @@ impl Problem for NAESatisfiability { } impl crate::solvers::BruteForceProblem for NAESatisfiability { - fn dimensions(&self) -> Vec { - vec![2; self.num_vars] + fn num_variables(&self) -> Result { + Ok(self.num_vars) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } diff --git a/src/models/formula/non_tautology.rs b/src/models/formula/non_tautology.rs index a45f40aab..66d8af9ca 100644 --- a/src/models/formula/non_tautology.rs +++ b/src/models/formula/non_tautology.rs @@ -177,8 +177,12 @@ impl Problem for NonTautology { } impl crate::solvers::BruteForceProblem for NonTautology { - fn dimensions(&self) -> Vec { - vec![2; self.num_vars] + fn num_variables(&self) -> Result { + Ok(self.num_vars) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } diff --git a/src/models/formula/one_in_three_satisfiability.rs b/src/models/formula/one_in_three_satisfiability.rs index f78004e32..653ca9043 100644 --- a/src/models/formula/one_in_three_satisfiability.rs +++ b/src/models/formula/one_in_three_satisfiability.rs @@ -154,8 +154,12 @@ impl Problem for OneInThreeSatisfiability { } impl crate::solvers::BruteForceProblem for OneInThreeSatisfiability { - fn dimensions(&self) -> Vec { - vec![2; self.num_vars] + fn num_variables(&self) -> Result { + Ok(self.num_vars) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } diff --git a/src/models/formula/planar_3_satisfiability.rs b/src/models/formula/planar_3_satisfiability.rs index f32349663..f1c2cf77d 100644 --- a/src/models/formula/planar_3_satisfiability.rs +++ b/src/models/formula/planar_3_satisfiability.rs @@ -150,8 +150,12 @@ impl Problem for Planar3Satisfiability { } impl crate::solvers::BruteForceProblem for Planar3Satisfiability { - fn dimensions(&self) -> Vec { - vec![2; self.num_vars] + fn num_variables(&self) -> Result { + Ok(self.num_vars) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } diff --git a/src/models/formula/qbf.rs b/src/models/formula/qbf.rs index 4aec9a7d7..fa641eb83 100644 --- a/src/models/formula/qbf.rs +++ b/src/models/formula/qbf.rs @@ -187,8 +187,12 @@ impl Problem for QuantifiedBooleanFormulas { } impl crate::solvers::BruteForceProblem for QuantifiedBooleanFormulas { - fn dimensions(&self) -> Vec { - vec![] + fn num_variables(&self) -> Result { + Ok(0) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(0) } } diff --git a/src/models/formula/sat.rs b/src/models/formula/sat.rs index fb0b6ff5f..4fdf99701 100644 --- a/src/models/formula/sat.rs +++ b/src/models/formula/sat.rs @@ -233,8 +233,12 @@ impl Problem for Satisfiability { } impl crate::solvers::BruteForceProblem for Satisfiability { - fn dimensions(&self) -> Vec { - vec![2; self.num_vars] + fn num_variables(&self) -> Result { + Ok(self.num_vars) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } diff --git a/src/models/graph/acyclic_partition.rs b/src/models/graph/acyclic_partition.rs index 2ed924353..0c89fbdde 100644 --- a/src/models/graph/acyclic_partition.rs +++ b/src/models/graph/acyclic_partition.rs @@ -256,8 +256,12 @@ impl crate::solvers::BruteForceProblem for AcyclicPartition where W: WeightElement + crate::variant::VariantParam, { - fn dimensions(&self) -> Vec { - vec![self.graph.num_vertices(); self.graph.num_vertices()] + fn num_variables(&self) -> Result { + Ok(self.graph.num_vertices()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(self.graph.num_vertices()) } } diff --git a/src/models/graph/balanced_complete_bipartite_subgraph.rs b/src/models/graph/balanced_complete_bipartite_subgraph.rs index 538bdbb82..d32b2d71c 100644 --- a/src/models/graph/balanced_complete_bipartite_subgraph.rs +++ b/src/models/graph/balanced_complete_bipartite_subgraph.rs @@ -177,8 +177,12 @@ impl Problem for BalancedCompleteBipartiteSubgraph { } impl crate::solvers::BruteForceProblem for BalancedCompleteBipartiteSubgraph { - fn dimensions(&self) -> Vec { - vec![2; self.num_vertices()] + fn num_variables(&self) -> Result { + Ok(self.num_vertices()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } diff --git a/src/models/graph/biclique_cover.rs b/src/models/graph/biclique_cover.rs index 6702a998f..d57236306 100644 --- a/src/models/graph/biclique_cover.rs +++ b/src/models/graph/biclique_cover.rs @@ -365,9 +365,14 @@ impl Problem for BicliqueCover { } impl crate::solvers::BruteForceProblem for BicliqueCover { - fn dimensions(&self) -> Vec { - // Each vertex has k binary variables (one per biclique) - vec![2; self.num_vertices() * self.k] + fn num_variables(&self) -> Result { + (self.num_vertices()).checked_mul(self.k).ok_or_else(|| { + crate::solvers::SolveError::IntegerOverflow("computing the coordinate count".into()) + }) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } diff --git a/src/models/graph/biconnectivity_augmentation.rs b/src/models/graph/biconnectivity_augmentation.rs index 1e026104c..f3682cc8a 100644 --- a/src/models/graph/biconnectivity_augmentation.rs +++ b/src/models/graph/biconnectivity_augmentation.rs @@ -264,8 +264,12 @@ where G: Graph + crate::variant::VariantParam, W: WeightElement + crate::variant::VariantParam, { - fn dimensions(&self) -> Vec { - vec![2; self.num_potential_edges()] + fn num_variables(&self) -> Result { + Ok(self.num_potential_edges()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } diff --git a/src/models/graph/bottleneck_traveling_salesman.rs b/src/models/graph/bottleneck_traveling_salesman.rs index fd5aaac2c..f3561b53e 100644 --- a/src/models/graph/bottleneck_traveling_salesman.rs +++ b/src/models/graph/bottleneck_traveling_salesman.rs @@ -192,8 +192,12 @@ impl Problem for BottleneckTravelingSalesman { } impl crate::solvers::BruteForceProblem for BottleneckTravelingSalesman { - fn dimensions(&self) -> Vec { - vec![2; self.graph.num_edges()] + fn num_variables(&self) -> Result { + Ok(self.graph.num_edges()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } diff --git a/src/models/graph/bounded_component_spanning_forest.rs b/src/models/graph/bounded_component_spanning_forest.rs index 155532cbc..c1b60de21 100644 --- a/src/models/graph/bounded_component_spanning_forest.rs +++ b/src/models/graph/bounded_component_spanning_forest.rs @@ -258,8 +258,12 @@ where G: Graph + crate::variant::VariantParam, W: WeightElement + crate::variant::VariantParam, { - fn dimensions(&self) -> Vec { - vec![self.max_components; self.graph.num_vertices()] + fn num_variables(&self) -> Result { + Ok(self.graph.num_vertices()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(self.max_components) } } diff --git a/src/models/graph/bounded_diameter_spanning_tree.rs b/src/models/graph/bounded_diameter_spanning_tree.rs index e156f4525..3c645a783 100644 --- a/src/models/graph/bounded_diameter_spanning_tree.rs +++ b/src/models/graph/bounded_diameter_spanning_tree.rs @@ -365,8 +365,12 @@ where G: Graph + VariantParam, W: WeightElement + VariantParam, { - fn dimensions(&self) -> Vec { - vec![2; self.edge_list.len()] + fn num_variables(&self) -> Result { + Ok(self.edge_list.len()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } diff --git a/src/models/graph/degree_constrained_spanning_tree.rs b/src/models/graph/degree_constrained_spanning_tree.rs index 005289d25..52c03b053 100644 --- a/src/models/graph/degree_constrained_spanning_tree.rs +++ b/src/models/graph/degree_constrained_spanning_tree.rs @@ -191,8 +191,12 @@ impl crate::solvers::BruteForceProblem for DegreeConstrainedSpanningTree where G: Graph + VariantParam, { - fn dimensions(&self) -> Vec { - vec![2; self.edge_list.len()] + fn num_variables(&self) -> Result { + Ok(self.edge_list.len()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } diff --git a/src/models/graph/directed_hamiltonian_path.rs b/src/models/graph/directed_hamiltonian_path.rs index 261e04250..5bec03d32 100644 --- a/src/models/graph/directed_hamiltonian_path.rs +++ b/src/models/graph/directed_hamiltonian_path.rs @@ -32,7 +32,7 @@ inventory::submit! { /// # Representation /// /// A configuration encodes a permutation using the Lehmer code: -/// `dims() = [n, n-1, ..., 2, 1]`, yielding `n!` reachable configurations. +/// `coordinate cardinalities = [n, n-1, ..., 2, 1]`, yielding `n!` reachable configurations. /// Each configuration is decoded to a permutation of `0..n`, and a solution is /// valid when every consecutive pair `(path[i], path[i+1])` is an arc in the /// directed graph. @@ -118,14 +118,13 @@ impl Problem for DirectedHamiltonianPath { } impl crate::solvers::BruteForceProblem for DirectedHamiltonianPath { - fn dimensions(&self) -> Vec { - lehmer_dims(self.graph.num_vertices()) + fn num_variables(&self) -> Result { + Ok(self.graph.num_vertices()) } -} -/// Returns the Lehmer code dimension vector for `n` items: `[n, n-1, ..., 2, 1]`. -pub(crate) fn lehmer_dims(n: usize) -> Vec { - (1..=n).rev().collect() + fn dimension(&self, variable: usize) -> Result { + Ok(self.graph.num_vertices() - variable) + } } /// Decode a Lehmer code into a permutation. diff --git a/src/models/graph/directed_two_commodity_integral_flow.rs b/src/models/graph/directed_two_commodity_integral_flow.rs index 11d7e53ef..5974e2986 100644 --- a/src/models/graph/directed_two_commodity_integral_flow.rs +++ b/src/models/graph/directed_two_commodity_integral_flow.rs @@ -319,12 +319,14 @@ impl Problem for DirectedTwoCommodityIntegralFlow { } impl crate::solvers::BruteForceProblem for DirectedTwoCommodityIntegralFlow { - fn dimensions(&self) -> Vec { - self.capacities - .iter() - .chain(self.capacities.iter()) - .map(|&c| (c as usize) + 1) - .collect() + fn num_variables(&self) -> Result { + Ok(2 * self.capacities.len()) + } + + fn dimension(&self, variable: usize) -> Result { + Ok(usize::try_from( + i128::from(self.capacities[variable % self.capacities.len()]) + 1, + )?) } } diff --git a/src/models/graph/disjoint_connecting_paths.rs b/src/models/graph/disjoint_connecting_paths.rs index 551e97e0f..ba694765d 100644 --- a/src/models/graph/disjoint_connecting_paths.rs +++ b/src/models/graph/disjoint_connecting_paths.rs @@ -201,8 +201,12 @@ impl crate::solvers::BruteForceProblem for DisjointConnectingPaths where G: Graph + VariantParam, { - fn dimensions(&self) -> Vec { - vec![2; self.num_edges()] + fn num_variables(&self) -> Result { + Ok(self.num_edges()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } diff --git a/src/models/graph/eulerian_path.rs b/src/models/graph/eulerian_path.rs index 661ebd04a..46bda0c9d 100644 --- a/src/models/graph/eulerian_path.rs +++ b/src/models/graph/eulerian_path.rs @@ -44,7 +44,7 @@ inventory::submit! { /// A configuration is an arc-ordering `pi`: position `t` carries the index of /// the arc occurrence used as the `t`-th arc of the trail. /// -/// `dims() = vec![m; m]` where `m = num_arcs()`. A configuration is feasible +/// `coordinate cardinalities = vec![m; m]` where `m = num_arcs()`. A configuration is feasible /// when: /// 1. it is a permutation of `0..m` (all values distinct, each in range), and /// 2. for every consecutive pair `(pi[t], pi[t+1])`, the target vertex of arc @@ -135,9 +135,12 @@ impl Problem for EulerianPath { } impl crate::solvers::BruteForceProblem for EulerianPath { - fn dimensions(&self) -> Vec { - let m = self.graph.num_arcs(); - vec![m; m] + fn num_variables(&self) -> Result { + Ok(self.graph.num_arcs()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(self.graph.num_arcs()) } } diff --git a/src/models/graph/generalized_hex.rs b/src/models/graph/generalized_hex.rs index 7e91bc9b9..fe3cf54cc 100644 --- a/src/models/graph/generalized_hex.rs +++ b/src/models/graph/generalized_hex.rs @@ -305,8 +305,12 @@ impl crate::solvers::BruteForceProblem for GeneralizedHex where G: Graph + VariantParam, { - fn dimensions(&self) -> Vec { - vec![] + fn num_variables(&self) -> Result { + Ok(0) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(0) } } diff --git a/src/models/graph/graph_partitioning.rs b/src/models/graph/graph_partitioning.rs index 6a59a6e48..79af630ba 100644 --- a/src/models/graph/graph_partitioning.rs +++ b/src/models/graph/graph_partitioning.rs @@ -138,8 +138,12 @@ impl crate::solvers::BruteForceProblem for GraphPartitioning where G: Graph + crate::variant::VariantParam, { - fn dimensions(&self) -> Vec { - vec![2; self.graph.num_vertices()] + fn num_variables(&self) -> Result { + Ok(self.graph.num_vertices()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } diff --git a/src/models/graph/hamiltonian_circuit.rs b/src/models/graph/hamiltonian_circuit.rs index 35ea2621c..f26c3105d 100644 --- a/src/models/graph/hamiltonian_circuit.rs +++ b/src/models/graph/hamiltonian_circuit.rs @@ -128,9 +128,12 @@ impl crate::solvers::BruteForceProblem for HamiltonianCircuit where G: Graph + VariantParam, { - fn dimensions(&self) -> Vec { - let n = self.graph.num_vertices(); - vec![n; n] + fn num_variables(&self) -> Result { + Ok(self.graph.num_vertices()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(self.graph.num_vertices()) } } diff --git a/src/models/graph/hamiltonian_path.rs b/src/models/graph/hamiltonian_path.rs index b31d6d2ff..01afc8349 100644 --- a/src/models/graph/hamiltonian_path.rs +++ b/src/models/graph/hamiltonian_path.rs @@ -38,7 +38,7 @@ inventory::submit! { /// vertex visited at step `i`. A valid solution must be a permutation of /// `0..n` where consecutive entries are adjacent in the graph. /// -/// The search space has `dims() = [n; n]` (each position can hold any of `n` +/// The search space has `coordinate cardinalities = [n; n]` (each position can hold any of `n` /// vertices), so brute-force enumerates `n^n` configurations. Only `n!` /// permutations can satisfy the constraints, but the encoding avoids complex /// variable-domain schemes and matches the problem's natural formulation. @@ -135,9 +135,12 @@ impl crate::solvers::BruteForceProblem for HamiltonianPath where G: Graph + VariantParam, { - fn dimensions(&self) -> Vec { - let n = self.graph.num_vertices(); - vec![n; n] + fn num_variables(&self) -> Result { + Ok(self.graph.num_vertices()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(self.graph.num_vertices()) } } diff --git a/src/models/graph/hamiltonian_path_between_two_vertices.rs b/src/models/graph/hamiltonian_path_between_two_vertices.rs index 7ae5bbbcd..624c4a445 100644 --- a/src/models/graph/hamiltonian_path_between_two_vertices.rs +++ b/src/models/graph/hamiltonian_path_between_two_vertices.rs @@ -46,7 +46,7 @@ inventory::submit! { /// - The last element equals `target_vertex` /// - Consecutive entries are adjacent in the graph /// -/// The search space has `dims() = [n; n]` (each position can hold any of `n` +/// The search space has `coordinate cardinalities = [n; n]` (each position can hold any of `n` /// vertices), so brute-force enumerates `n^n` configurations. /// /// # Type Parameters @@ -192,9 +192,12 @@ impl crate::solvers::BruteForceProblem for HamiltonianPathBetweenTwoVertices< where G: Graph + VariantParam, { - fn dimensions(&self) -> Vec { - let n = self.graph.num_vertices(); - vec![n; n] + fn num_variables(&self) -> Result { + Ok(self.graph.num_vertices()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(self.graph.num_vertices()) } } diff --git a/src/models/graph/highly_connected_deletion.rs b/src/models/graph/highly_connected_deletion.rs index d1f7fca64..55c067132 100644 --- a/src/models/graph/highly_connected_deletion.rs +++ b/src/models/graph/highly_connected_deletion.rs @@ -148,8 +148,12 @@ impl crate::solvers::BruteForceProblem for HighlyConnectedDeletion where G: Graph + VariantParam, { - fn dimensions(&self) -> Vec { - vec![2; self.graph.num_edges()] + fn num_variables(&self) -> Result { + Ok(self.graph.num_edges()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } diff --git a/src/models/graph/integral_flow_bundles.rs b/src/models/graph/integral_flow_bundles.rs index a9f99f931..2de08530d 100644 --- a/src/models/graph/integral_flow_bundles.rs +++ b/src/models/graph/integral_flow_bundles.rs @@ -151,7 +151,6 @@ impl IntegralFlowBundles { assert!(requirement > 0, "requirement must be positive"); let mut arc_covered = vec![false; num_arcs]; - let mut arc_upper_bounds = vec![i64::MAX; num_arcs]; for (bundle_index, (bundle, &capacity)) in bundles.iter().zip(&bundle_capacities).enumerate() @@ -172,7 +171,6 @@ impl IntegralFlowBundles { "bundle {bundle_index} contains duplicate arc index {arc_index}" ); arc_covered[arc_index] = true; - arc_upper_bounds[arc_index] = arc_upper_bounds[arc_index].min(capacity); } } @@ -181,13 +179,6 @@ impl IntegralFlowBundles { covered, "arc {arc_index} must belong to at least one bundle" ); - let domain = usize::try_from(arc_upper_bounds[arc_index]) - .ok() - .and_then(|bound| bound.checked_add(1)); - assert!( - domain.is_some(), - "bundle-derived upper bound for arc {arc_index} must fit into usize for dims()" - ); } Self { @@ -369,16 +360,20 @@ impl Problem for IntegralFlowBundles { } impl crate::solvers::BruteForceProblem for IntegralFlowBundles { - fn dimensions(&self) -> Vec { - self.arc_upper_bounds() - .into_iter() - .map(|bound| { - usize::try_from(bound) - .ok() - .and_then(|bound| bound.checked_add(1)) - .expect("bundle-derived arc upper bounds are validated in the constructor") - }) - .collect() + fn num_variables(&self) -> Result { + Ok(self.num_arcs()) + } + + fn dimension(&self, variable: usize) -> Result { + let bound = self + .bundles + .iter() + .zip(&self.bundle_capacities) + .filter(|(bundle, _)| bundle.contains(&variable)) + .map(|(_, &capacity)| capacity) + .min() + .expect("each arc belongs to at least one bundle"); + Ok(usize::try_from(i128::from(bound) + 1)?) } } diff --git a/src/models/graph/integral_flow_homologous_arcs.rs b/src/models/graph/integral_flow_homologous_arcs.rs index 0ea45a9e2..c60a7fb89 100644 --- a/src/models/graph/integral_flow_homologous_arcs.rs +++ b/src/models/graph/integral_flow_homologous_arcs.rs @@ -84,14 +84,8 @@ impl TryFrom for IntegralFlowHomologousArc return Err("homologous pair arc index is out of range".into()); } } - for &c in &capacities { - if usize::try_from(c) - .ok() - .and_then(|v| v.checked_add(1)) - .is_none() - { - return Err("capacity is too large".into()); - } + if capacities.iter().any(|&capacity| capacity < 0) { + return Err("capacities must be nonnegative".into()); } Ok(Self { graph: DirectedGraph::new(count, spec.arcs), @@ -135,15 +129,10 @@ impl IntegralFlowHomologousArcs { assert!(b < num_arcs, "homologous arc index {b} out of range"); } - for &capacity in &capacities { - assert!( - usize::try_from(capacity) - .ok() - .and_then(|value| value.checked_add(1)) - .is_some(), - "capacities must fit into usize for dims()" - ); - } + assert!( + capacities.iter().all(|&capacity| capacity >= 0), + "capacities must be nonnegative" + ); Self { graph, @@ -248,13 +237,6 @@ impl IntegralFlowHomologousArcs { Ok(crate::types::Or(balances[self.sink] >= self.requirement)) } - - fn domain_size(capacity: i64) -> usize { - usize::try_from(capacity) - .ok() - .and_then(|value| value.checked_add(1)) - .expect("capacity already validated to fit into usize") - } } impl Problem for IntegralFlowHomologousArcs { @@ -281,11 +263,12 @@ impl Problem for IntegralFlowHomologousArcs { } impl crate::solvers::BruteForceProblem for IntegralFlowHomologousArcs { - fn dimensions(&self) -> Vec { - self.capacities - .iter() - .map(|&capacity| Self::domain_size(capacity)) - .collect() + fn num_variables(&self) -> Result { + Ok(self.capacities.len()) + } + + fn dimension(&self, variable: usize) -> Result { + Ok(usize::try_from(i128::from(self.capacities[variable]) + 1)?) } } diff --git a/src/models/graph/integral_flow_with_multipliers.rs b/src/models/graph/integral_flow_with_multipliers.rs index d5730c110..c3ea1215d 100644 --- a/src/models/graph/integral_flow_with_multipliers.rs +++ b/src/models/graph/integral_flow_with_multipliers.rs @@ -83,14 +83,8 @@ impl TryFrom for IntegralFlowWithMultipli return Err("non-terminal multipliers must be positive".into()); } } - for &c in &spec.capacities { - if usize::try_from(c) - .ok() - .and_then(|v| v.checked_add(1)) - .is_none() - { - return Err("capacity is too large".into()); - } + if spec.capacities.iter().any(|&capacity| capacity < 0) { + return Err("capacities must be nonnegative".into()); } Ok(Self { graph: DirectedGraph::new(count, spec.arcs), @@ -140,15 +134,10 @@ impl IntegralFlowWithMultipliers { } } - for &capacity in &capacities { - let domain = usize::try_from(capacity) - .ok() - .and_then(|value| value.checked_add(1)); - assert!( - domain.is_some(), - "arc capacities must fit into usize for dims()" - ); - } + assert!( + capacities.iter().all(|&capacity| capacity >= 0), + "capacities must be nonnegative" + ); Self { graph, @@ -196,13 +185,6 @@ impl IntegralFlowWithMultipliers { self.capacities.iter().copied().max().unwrap_or(0) } - fn domain_size(capacity: i64) -> usize { - usize::try_from(capacity) - .ok() - .and_then(|value| value.checked_add(1)) - .expect("capacity already validated to fit into usize") - } - pub fn is_feasible(&self, config: &[usize]) -> Result { if config.len() != self.num_arcs() { return Ok(false); @@ -297,11 +279,12 @@ impl Problem for IntegralFlowWithMultipliers { } impl crate::solvers::BruteForceProblem for IntegralFlowWithMultipliers { - fn dimensions(&self) -> Vec { - self.capacities - .iter() - .map(|&capacity| Self::domain_size(capacity)) - .collect() + fn num_variables(&self) -> Result { + Ok(self.capacities.len()) + } + + fn dimension(&self, variable: usize) -> Result { + Ok(usize::try_from(i128::from(self.capacities[variable]) + 1)?) } } diff --git a/src/models/graph/isomorphic_spanning_tree.rs b/src/models/graph/isomorphic_spanning_tree.rs index ad2877684..2e18c821a 100644 --- a/src/models/graph/isomorphic_spanning_tree.rs +++ b/src/models/graph/isomorphic_spanning_tree.rs @@ -172,8 +172,12 @@ impl crate::solvers::BruteForceProblem for IsomorphicSpanningTree where G: Graph + VariantParam, { - fn dimensions(&self) -> Vec { - vec![self.graph.num_vertices(); self.graph.num_vertices()] + fn num_variables(&self) -> Result { + Ok(self.graph.num_vertices()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(self.graph.num_vertices()) } } diff --git a/src/models/graph/kclique.rs b/src/models/graph/kclique.rs index 07bca75e7..3cd2b6f1a 100644 --- a/src/models/graph/kclique.rs +++ b/src/models/graph/kclique.rs @@ -163,8 +163,12 @@ impl crate::solvers::BruteForceProblem for KClique where G: Graph + crate::variant::VariantParam, { - fn dimensions(&self) -> Vec { - vec![2; self.graph.num_vertices()] + fn num_variables(&self) -> Result { + Ok(self.graph.num_vertices()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } diff --git a/src/models/graph/kcoloring.rs b/src/models/graph/kcoloring.rs index 9f1662123..fa3fe5561 100644 --- a/src/models/graph/kcoloring.rs +++ b/src/models/graph/kcoloring.rs @@ -273,8 +273,12 @@ impl crate::solvers::BruteForceProblem for KColoring where G: Graph + VariantParam, { - fn dimensions(&self) -> Vec { - vec![self.num_colors; self.graph.num_vertices()] + fn num_variables(&self) -> Result { + Ok(self.graph.num_vertices()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(self.num_colors) } } diff --git a/src/models/graph/kernel.rs b/src/models/graph/kernel.rs index 359c5ad42..b0610bd22 100644 --- a/src/models/graph/kernel.rs +++ b/src/models/graph/kernel.rs @@ -136,8 +136,12 @@ impl Problem for Kernel { } impl crate::solvers::BruteForceProblem for Kernel { - fn dimensions(&self) -> Vec { - vec![2; self.graph.num_vertices()] + fn num_variables(&self) -> Result { + Ok(self.graph.num_vertices()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } diff --git a/src/models/graph/kth_best_spanning_tree.rs b/src/models/graph/kth_best_spanning_tree.rs index e841da212..6ee5d8f88 100644 --- a/src/models/graph/kth_best_spanning_tree.rs +++ b/src/models/graph/kth_best_spanning_tree.rs @@ -303,8 +303,14 @@ impl crate::solvers::BruteForceProblem for KthBestSpanningTree where W: WeightElement + crate::variant::VariantParam, { - fn dimensions(&self) -> Vec { - vec![2; self.k * self.graph.num_edges()] + fn num_variables(&self) -> Result { + (self.k).checked_mul(self.graph.num_edges()).ok_or_else(|| { + crate::solvers::SolveError::IntegerOverflow("computing the coordinate count".into()) + }) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } diff --git a/src/models/graph/length_bounded_disjoint_paths.rs b/src/models/graph/length_bounded_disjoint_paths.rs index a12a08172..78be3c4a8 100644 --- a/src/models/graph/length_bounded_disjoint_paths.rs +++ b/src/models/graph/length_bounded_disjoint_paths.rs @@ -243,8 +243,16 @@ impl crate::solvers::BruteForceProblem for LengthBoundedDisjointPaths where G: Graph + VariantParam, { - fn dimensions(&self) -> Vec { - vec![2; self.max_paths * self.graph.num_edges()] + fn num_variables(&self) -> Result { + (self.max_paths) + .checked_mul(self.graph.num_edges()) + .ok_or_else(|| { + crate::solvers::SolveError::IntegerOverflow("computing the coordinate count".into()) + }) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } diff --git a/src/models/graph/longest_circuit.rs b/src/models/graph/longest_circuit.rs index f60e24bff..9c01712d7 100644 --- a/src/models/graph/longest_circuit.rs +++ b/src/models/graph/longest_circuit.rs @@ -241,8 +241,12 @@ where G: Graph + crate::variant::VariantParam, W: WeightElement + crate::variant::VariantParam, { - fn dimensions(&self) -> Vec { - vec![2; self.graph.num_edges()] + fn num_variables(&self) -> Result { + Ok(self.graph.num_edges()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } diff --git a/src/models/graph/longest_path.rs b/src/models/graph/longest_path.rs index 2423f24fb..f6d5d189d 100644 --- a/src/models/graph/longest_path.rs +++ b/src/models/graph/longest_path.rs @@ -254,8 +254,12 @@ where G: Graph + crate::variant::VariantParam, W: WeightElement + crate::variant::VariantParam, { - fn dimensions(&self) -> Vec { - vec![2; self.graph.num_edges()] + fn num_variables(&self) -> Result { + Ok(self.graph.num_edges()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } diff --git a/src/models/graph/max_cut.rs b/src/models/graph/max_cut.rs index 0d414cfac..a10666c97 100644 --- a/src/models/graph/max_cut.rs +++ b/src/models/graph/max_cut.rs @@ -262,8 +262,12 @@ where G: Graph + crate::variant::VariantParam, W: WeightElement + crate::variant::VariantParam, { - fn dimensions(&self) -> Vec { - vec![2; self.graph.num_vertices()] + fn num_variables(&self) -> Result { + Ok(self.graph.num_vertices()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } diff --git a/src/models/graph/maximal_is.rs b/src/models/graph/maximal_is.rs index 079e0752d..249ad2e80 100644 --- a/src/models/graph/maximal_is.rs +++ b/src/models/graph/maximal_is.rs @@ -215,8 +215,12 @@ where G: Graph + crate::variant::VariantParam, W: WeightElement + crate::variant::VariantParam, { - fn dimensions(&self) -> Vec { - vec![2; self.graph.num_vertices()] + fn num_variables(&self) -> Result { + Ok(self.graph.num_vertices()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } diff --git a/src/models/graph/maximum_achromatic_number.rs b/src/models/graph/maximum_achromatic_number.rs index 6046f97e1..f5fc3d2e4 100644 --- a/src/models/graph/maximum_achromatic_number.rs +++ b/src/models/graph/maximum_achromatic_number.rs @@ -172,8 +172,12 @@ impl crate::solvers::BruteForceProblem for MaximumAchromaticNumber where G: Graph + crate::variant::VariantParam, { - fn dimensions(&self) -> Vec { - vec![self.graph.num_vertices(); self.graph.num_vertices()] + fn num_variables(&self) -> Result { + Ok(self.graph.num_vertices()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(self.graph.num_vertices()) } } diff --git a/src/models/graph/maximum_clique.rs b/src/models/graph/maximum_clique.rs index 423242ba1..6c3baf47d 100644 --- a/src/models/graph/maximum_clique.rs +++ b/src/models/graph/maximum_clique.rs @@ -182,8 +182,12 @@ where G: Graph + crate::variant::VariantParam, W: WeightElement + crate::variant::VariantParam, { - fn dimensions(&self) -> Vec { - vec![2; self.graph.num_vertices()] + fn num_variables(&self) -> Result { + Ok(self.graph.num_vertices()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } diff --git a/src/models/graph/maximum_co_k_plex.rs b/src/models/graph/maximum_co_k_plex.rs index 29495f0d6..a1f654a0b 100644 --- a/src/models/graph/maximum_co_k_plex.rs +++ b/src/models/graph/maximum_co_k_plex.rs @@ -241,8 +241,12 @@ where W: WeightElement + VariantParam, K: KValue, { - fn dimensions(&self) -> Vec { - vec![2; self.graph.num_vertices()] + fn num_variables(&self) -> Result { + Ok(self.graph.num_vertices()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } diff --git a/src/models/graph/maximum_common_edge_subgraph.rs b/src/models/graph/maximum_common_edge_subgraph.rs index 179151b64..2e98aa70a 100644 --- a/src/models/graph/maximum_common_edge_subgraph.rs +++ b/src/models/graph/maximum_common_edge_subgraph.rs @@ -136,7 +136,7 @@ impl LabelledDigraph { /// /// # Configuration encoding /// -/// `dims()` returns `vec![graph_2.num_vertices + 1; graph_1.num_vertices]`. +/// The coordinate cardinalities are `vec![graph_2.num_vertices + 1; graph_1.num_vertices]`. /// For each source vertex `u in V1`, `config[u]` is either an index in /// `0..graph_2.num_vertices` (the matched target vertex) or the sentinel /// value `graph_2.num_vertices` denoting `bottom` (unmatched). Feasibility @@ -293,8 +293,18 @@ impl Problem for MaximumCommonEdgeSubgraph { } impl crate::solvers::BruteForceProblem for MaximumCommonEdgeSubgraph { - fn dimensions(&self) -> Vec { - vec![self.graph_2.num_vertices() + 1; self.graph_1.num_vertices()] + fn num_variables(&self) -> Result { + Ok(self.graph_1.num_vertices()) + } + + fn dimension(&self, _variable: usize) -> Result { + (self.graph_2.num_vertices()) + .checked_add(1usize) + .ok_or_else(|| { + crate::solvers::SolveError::IntegerOverflow( + "computing a coordinate cardinality".into(), + ) + }) } } diff --git a/src/models/graph/maximum_contact_map_overlap.rs b/src/models/graph/maximum_contact_map_overlap.rs index c7f340220..5eea5ba5d 100644 --- a/src/models/graph/maximum_contact_map_overlap.rs +++ b/src/models/graph/maximum_contact_map_overlap.rs @@ -64,7 +64,7 @@ inventory::submit! { /// /// # Configuration encoding /// -/// `dims()` returns `vec![|V_2| + 1; |V_1|]`. For each source vertex `i`, +/// The coordinate cardinalities are `vec![|V_2| + 1; |V_1|]`. For each source vertex `i`, /// `config[i] = 0` denotes `bot` (unmatched) and `config[i] = j + 1` denotes /// "matched to vertex `j in V_2`". Feasibility requires that the nonzero /// entries are pairwise distinct (injectivity) and strictly increasing along @@ -261,8 +261,14 @@ impl Problem for MaximumContactMapOverlap { } impl crate::solvers::BruteForceProblem for MaximumContactMapOverlap { - fn dimensions(&self) -> Vec { - vec![self.num_vertices_2 + 1; self.num_vertices_1] + fn num_variables(&self) -> Result { + Ok(self.num_vertices_1) + } + + fn dimension(&self, _variable: usize) -> Result { + (self.num_vertices_2).checked_add(1usize).ok_or_else(|| { + crate::solvers::SolveError::IntegerOverflow("computing a coordinate cardinality".into()) + }) } } diff --git a/src/models/graph/maximum_domatic_number.rs b/src/models/graph/maximum_domatic_number.rs index fce59b0e1..add7a727a 100644 --- a/src/models/graph/maximum_domatic_number.rs +++ b/src/models/graph/maximum_domatic_number.rs @@ -177,9 +177,12 @@ impl crate::solvers::BruteForceProblem for MaximumDomaticNumber where G: Graph + crate::variant::VariantParam, { - fn dimensions(&self) -> Vec { - let n = self.graph.num_vertices(); - vec![n; n] + fn num_variables(&self) -> Result { + Ok(self.graph.num_vertices()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(self.graph.num_vertices()) } } diff --git a/src/models/graph/maximum_edge_weighted_k_clique.rs b/src/models/graph/maximum_edge_weighted_k_clique.rs index 9ba384908..0cbf9d585 100644 --- a/src/models/graph/maximum_edge_weighted_k_clique.rs +++ b/src/models/graph/maximum_edge_weighted_k_clique.rs @@ -220,8 +220,12 @@ impl crate::solvers::BruteForceProblem for MaximumEdgeWeightedKClique where W: WeightElement + crate::variant::VariantParam, { - fn dimensions(&self) -> Vec { - vec![2; self.graph.num_vertices()] + fn num_variables(&self) -> Result { + Ok(self.graph.num_vertices()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } diff --git a/src/models/graph/maximum_independent_set.rs b/src/models/graph/maximum_independent_set.rs index 55723eb26..4d5663cdc 100644 --- a/src/models/graph/maximum_independent_set.rs +++ b/src/models/graph/maximum_independent_set.rs @@ -309,8 +309,12 @@ where G: Graph + crate::variant::VariantParam, W: WeightElement + crate::variant::VariantParam, { - fn dimensions(&self) -> Vec { - vec![2; self.graph.num_vertices()] + fn num_variables(&self) -> Result { + Ok(self.graph.num_vertices()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } diff --git a/src/models/graph/maximum_leaf_spanning_tree.rs b/src/models/graph/maximum_leaf_spanning_tree.rs index 9a8c8d3bd..cc63bf7f5 100644 --- a/src/models/graph/maximum_leaf_spanning_tree.rs +++ b/src/models/graph/maximum_leaf_spanning_tree.rs @@ -183,8 +183,12 @@ impl crate::solvers::BruteForceProblem for MaximumLeafSpanningTree where G: Graph + crate::variant::VariantParam, { - fn dimensions(&self) -> Vec { - vec![2; self.graph.num_edges()] + fn num_variables(&self) -> Result { + Ok(self.graph.num_edges()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } diff --git a/src/models/graph/maximum_matching.rs b/src/models/graph/maximum_matching.rs index 6e68acfd5..c031ab507 100644 --- a/src/models/graph/maximum_matching.rs +++ b/src/models/graph/maximum_matching.rs @@ -289,8 +289,12 @@ where G: Graph + crate::variant::VariantParam, W: WeightElement + crate::variant::VariantParam, { - fn dimensions(&self) -> Vec { - vec![2; self.graph.num_edges()] + fn num_variables(&self) -> Result { + Ok(self.graph.num_edges()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } diff --git a/src/models/graph/min_max_multicenter.rs b/src/models/graph/min_max_multicenter.rs index bcc697aff..7af2ec0cd 100644 --- a/src/models/graph/min_max_multicenter.rs +++ b/src/models/graph/min_max_multicenter.rs @@ -381,8 +381,12 @@ where G: Graph + crate::variant::VariantParam, W: WeightElement + crate::variant::VariantParam, { - fn dimensions(&self) -> Vec { - vec![2; self.graph.num_vertices()] + fn num_variables(&self) -> Result { + Ok(self.graph.num_vertices()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } diff --git a/src/models/graph/minimum_capacitated_spanning_tree.rs b/src/models/graph/minimum_capacitated_spanning_tree.rs index 631a960b6..97893e491 100644 --- a/src/models/graph/minimum_capacitated_spanning_tree.rs +++ b/src/models/graph/minimum_capacitated_spanning_tree.rs @@ -389,8 +389,12 @@ where G: Graph + crate::variant::VariantParam, W: WeightElement + crate::variant::VariantParam, { - fn dimensions(&self) -> Vec { - vec![2; self.graph.num_edges()] + fn num_variables(&self) -> Result { + Ok(self.graph.num_edges()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } diff --git a/src/models/graph/minimum_cost_circulation.rs b/src/models/graph/minimum_cost_circulation.rs index a61cea112..be297fbad 100644 --- a/src/models/graph/minimum_cost_circulation.rs +++ b/src/models/graph/minimum_cost_circulation.rs @@ -255,8 +255,12 @@ impl Problem for MinimumCostCirculation { } impl crate::solvers::BruteForceProblem for MinimumCostCirculation { - fn dimensions(&self) -> Vec { - self.capacities.iter().map(|&c| (c as usize) + 1).collect() + fn num_variables(&self) -> Result { + Ok(self.capacities.len()) + } + + fn dimension(&self, variable: usize) -> Result { + Ok(usize::try_from(i128::from(self.capacities[variable]) + 1)?) } } diff --git a/src/models/graph/minimum_cost_maximum_flow.rs b/src/models/graph/minimum_cost_maximum_flow.rs index 78cf0e271..8915d9287 100644 --- a/src/models/graph/minimum_cost_maximum_flow.rs +++ b/src/models/graph/minimum_cost_maximum_flow.rs @@ -436,8 +436,12 @@ impl Problem for MinimumCostMaximumFlow { } impl crate::solvers::BruteForceProblem for MinimumCostMaximumFlow { - fn dimensions(&self) -> Vec { - self.capacities.iter().map(|&c| (c as usize) + 1).collect() + fn num_variables(&self) -> Result { + Ok(self.capacities.len()) + } + + fn dimension(&self, variable: usize) -> Result { + Ok(usize::try_from(i128::from(self.capacities[variable]) + 1)?) } } diff --git a/src/models/graph/minimum_covering_by_cliques.rs b/src/models/graph/minimum_covering_by_cliques.rs index 1f59f2bae..a96498573 100644 --- a/src/models/graph/minimum_covering_by_cliques.rs +++ b/src/models/graph/minimum_covering_by_cliques.rs @@ -170,8 +170,12 @@ impl crate::solvers::BruteForceProblem for MinimumCoveringByCliques where G: Graph + crate::variant::VariantParam, { - fn dimensions(&self) -> Vec { - vec![self.graph.num_edges(); self.graph.num_edges()] + fn num_variables(&self) -> Result { + Ok(self.graph.num_edges()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(self.graph.num_edges()) } } diff --git a/src/models/graph/minimum_cut_into_bounded_sets.rs b/src/models/graph/minimum_cut_into_bounded_sets.rs index 2f5167a65..2ea6548b3 100644 --- a/src/models/graph/minimum_cut_into_bounded_sets.rs +++ b/src/models/graph/minimum_cut_into_bounded_sets.rs @@ -245,8 +245,12 @@ where G: Graph + crate::variant::VariantParam, W: WeightElement + crate::variant::VariantParam, { - fn dimensions(&self) -> Vec { - vec![2; self.graph.num_vertices()] + fn num_variables(&self) -> Result { + Ok(self.graph.num_vertices()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } diff --git a/src/models/graph/minimum_dominating_set.rs b/src/models/graph/minimum_dominating_set.rs index b8c85a1a4..500fa3b39 100644 --- a/src/models/graph/minimum_dominating_set.rs +++ b/src/models/graph/minimum_dominating_set.rs @@ -205,8 +205,12 @@ where G: Graph + crate::variant::VariantParam, W: WeightElement + crate::variant::VariantParam, { - fn dimensions(&self) -> Vec { - vec![2; self.graph.num_vertices()] + fn num_variables(&self) -> Result { + Ok(self.graph.num_vertices()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } diff --git a/src/models/graph/minimum_dummy_activities_pert.rs b/src/models/graph/minimum_dummy_activities_pert.rs index c272044d6..18bef5734 100644 --- a/src/models/graph/minimum_dummy_activities_pert.rs +++ b/src/models/graph/minimum_dummy_activities_pert.rs @@ -240,8 +240,12 @@ impl Problem for MinimumDummyActivitiesPert { } impl crate::solvers::BruteForceProblem for MinimumDummyActivitiesPert { - fn dimensions(&self) -> Vec { - vec![2; self.graph.num_arcs()] + fn num_variables(&self) -> Result { + Ok(self.graph.num_arcs()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } diff --git a/src/models/graph/minimum_edge_cost_flow.rs b/src/models/graph/minimum_edge_cost_flow.rs index 79e0ab8dd..ec861b944 100644 --- a/src/models/graph/minimum_edge_cost_flow.rs +++ b/src/models/graph/minimum_edge_cost_flow.rs @@ -302,8 +302,12 @@ impl Problem for MinimumEdgeCostFlow { } impl crate::solvers::BruteForceProblem for MinimumEdgeCostFlow { - fn dimensions(&self) -> Vec { - self.capacities.iter().map(|&c| (c as usize) + 1).collect() + fn num_variables(&self) -> Result { + Ok(self.capacities.len()) + } + + fn dimension(&self, variable: usize) -> Result { + Ok(usize::try_from(i128::from(self.capacities[variable]) + 1)?) } } diff --git a/src/models/graph/minimum_feedback_arc_set.rs b/src/models/graph/minimum_feedback_arc_set.rs index 7c337a1d5..a37211c8e 100644 --- a/src/models/graph/minimum_feedback_arc_set.rs +++ b/src/models/graph/minimum_feedback_arc_set.rs @@ -184,8 +184,12 @@ impl crate::solvers::BruteForceProblem for MinimumFeedbackArcSet where W: WeightElement + crate::variant::VariantParam, { - fn dimensions(&self) -> Vec { - vec![2; self.graph.num_arcs()] + fn num_variables(&self) -> Result { + Ok(self.graph.num_arcs()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } diff --git a/src/models/graph/minimum_feedback_vertex_set.rs b/src/models/graph/minimum_feedback_vertex_set.rs index 762efe3da..06853fd1f 100644 --- a/src/models/graph/minimum_feedback_vertex_set.rs +++ b/src/models/graph/minimum_feedback_vertex_set.rs @@ -185,8 +185,12 @@ impl crate::solvers::BruteForceProblem for MinimumFeedbackVertexSet where W: WeightElement + crate::variant::VariantParam, { - fn dimensions(&self) -> Vec { - vec![2; self.graph.num_vertices()] + fn num_variables(&self) -> Result { + Ok(self.graph.num_vertices()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } diff --git a/src/models/graph/minimum_geometric_connected_dominating_set.rs b/src/models/graph/minimum_geometric_connected_dominating_set.rs index 393b20b6c..cdf97e4c9 100644 --- a/src/models/graph/minimum_geometric_connected_dominating_set.rs +++ b/src/models/graph/minimum_geometric_connected_dominating_set.rs @@ -242,8 +242,12 @@ impl Problem for MinimumGeometricConnectedDominatingSet { } impl crate::solvers::BruteForceProblem for MinimumGeometricConnectedDominatingSet { - fn dimensions(&self) -> Vec { - vec![2; self.num_points()] + fn num_variables(&self) -> Result { + Ok(self.num_points()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } diff --git a/src/models/graph/minimum_graph_bandwidth.rs b/src/models/graph/minimum_graph_bandwidth.rs index d214d03e8..4da4735f0 100644 --- a/src/models/graph/minimum_graph_bandwidth.rs +++ b/src/models/graph/minimum_graph_bandwidth.rs @@ -170,9 +170,12 @@ impl crate::solvers::BruteForceProblem for MinimumGraphBandwidth where G: Graph + crate::variant::VariantParam, { - fn dimensions(&self) -> Vec { - let n = self.graph.num_vertices(); - vec![n; n] + fn num_variables(&self) -> Result { + Ok(self.graph.num_vertices()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(self.graph.num_vertices()) } } diff --git a/src/models/graph/minimum_intersection_graph_basis.rs b/src/models/graph/minimum_intersection_graph_basis.rs index 2f26294c4..9780abdbd 100644 --- a/src/models/graph/minimum_intersection_graph_basis.rs +++ b/src/models/graph/minimum_intersection_graph_basis.rs @@ -162,14 +162,18 @@ impl crate::solvers::BruteForceProblem for MinimumIntersectionGraphBasis where G: Graph + crate::variant::VariantParam, { - fn dimensions(&self) -> Vec { - let n = self.graph.num_vertices(); - let m = self.graph.num_edges(); - if m == 0 { - // No edges: no variables needed; empty assignment is trivially valid. - return vec![]; - } - vec![2; n * m] + fn num_variables(&self) -> Result { + (self.graph.num_vertices()) + .checked_mul(self.graph.num_edges()) + .ok_or_else(|| { + crate::solvers::SolveError::IntegerOverflow( + "computing a search coordinate size".into(), + ) + }) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2) } } diff --git a/src/models/graph/minimum_maximal_matching.rs b/src/models/graph/minimum_maximal_matching.rs index d559fb9c1..a6c66caac 100644 --- a/src/models/graph/minimum_maximal_matching.rs +++ b/src/models/graph/minimum_maximal_matching.rs @@ -160,8 +160,12 @@ impl crate::solvers::BruteForceProblem for MinimumMaximalMatching where G: Graph + crate::variant::VariantParam, { - fn dimensions(&self) -> Vec { - vec![2; self.graph.num_edges()] + fn num_variables(&self) -> Result { + Ok(self.graph.num_edges()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } diff --git a/src/models/graph/minimum_metric_dimension.rs b/src/models/graph/minimum_metric_dimension.rs index 42261c5c3..c97b1d286 100644 --- a/src/models/graph/minimum_metric_dimension.rs +++ b/src/models/graph/minimum_metric_dimension.rs @@ -190,8 +190,12 @@ impl crate::solvers::BruteForceProblem for MinimumMetricDimension where G: Graph + crate::variant::VariantParam, { - fn dimensions(&self) -> Vec { - vec![2; self.graph.num_vertices()] + fn num_variables(&self) -> Result { + Ok(self.graph.num_vertices()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } diff --git a/src/models/graph/minimum_multiway_cut.rs b/src/models/graph/minimum_multiway_cut.rs index 6b1b66cec..6223b7e43 100644 --- a/src/models/graph/minimum_multiway_cut.rs +++ b/src/models/graph/minimum_multiway_cut.rs @@ -251,8 +251,12 @@ where G: Graph + crate::variant::VariantParam, W: WeightElement + crate::variant::VariantParam, { - fn dimensions(&self) -> Vec { - vec![2; self.graph.num_edges()] + fn num_variables(&self) -> Result { + Ok(self.graph.num_edges()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } diff --git a/src/models/graph/minimum_sum_multicenter.rs b/src/models/graph/minimum_sum_multicenter.rs index 2ba567460..25a925762 100644 --- a/src/models/graph/minimum_sum_multicenter.rs +++ b/src/models/graph/minimum_sum_multicenter.rs @@ -352,8 +352,12 @@ where G: Graph + crate::variant::VariantParam, W: WeightElement + crate::variant::VariantParam, { - fn dimensions(&self) -> Vec { - vec![2; self.graph.num_vertices()] + fn num_variables(&self) -> Result { + Ok(self.graph.num_vertices()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } diff --git a/src/models/graph/minimum_vertex_cover.rs b/src/models/graph/minimum_vertex_cover.rs index 5f511118b..33af2207d 100644 --- a/src/models/graph/minimum_vertex_cover.rs +++ b/src/models/graph/minimum_vertex_cover.rs @@ -183,8 +183,12 @@ where G: Graph + crate::variant::VariantParam, W: WeightElement + crate::variant::VariantParam, { - fn dimensions(&self) -> Vec { - vec![2; self.graph.num_vertices()] + fn num_variables(&self) -> Result { + Ok(self.graph.num_vertices()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } @@ -240,7 +244,11 @@ where const DECISION_NAME: &'static str = "DecisionMinimumVertexCover"; } -impl Decision> { +impl Decision> +where + W: WeightElement + crate::variant::VariantParam, + W::Sum: std::fmt::Debug + serde::Serialize + serde::de::DeserializeOwned, +{ /// Number of vertices in the underlying graph. pub fn num_vertices(&self) -> usize { self.inner().num_vertices() @@ -250,11 +258,6 @@ impl Decision> { pub fn num_edges(&self) -> usize { self.inner().num_edges() } - - /// Decision bound as a nonnegative integer. - pub fn k(&self) -> usize { - (*self.bound()).try_into().unwrap_or(0) - } } #[derive(Debug, Deserialize, crate::CreateSpec)] @@ -298,13 +301,14 @@ crate::register_decision_variant!( category: crate::registry::ProblemCategory::Graph, dims: [ VariantDimension::new("graph", "SimpleGraph", &["SimpleGraph"]), - VariantDimension::new("weight", "i64", &["i64"]), + VariantDimension::new("weight", "i64", &["i64", "One"]), ], fields: [ FieldInfo { name: "graph", type_name: "G", description: "The underlying graph G=(V,E)" }, FieldInfo { name: "weights", type_name: "Vec", description: "Vertex weights w: V -> R" }, FieldInfo { name: "bound", type_name: "W::Sum", description: "Decision bound (maximum allowed cover cost)" }, ], + additional: [MinimumVertexCover => "1.1996^num_vertices"], decode: |_, indices: Vec| crate::config::config_to_bits(&indices), random ); @@ -325,52 +329,73 @@ pub(crate) fn canonical_model_example_specs() -> Vec Vec { - vec![crate::example_db::specs::ModelExampleSpec { - id: "decision_minimum_vertex_cover_simplegraph", - instance: Box::new(crate::models::decision::Decision::new( - MinimumVertexCover::new( - SimpleGraph::new(4, vec![(0, 1), (1, 2), (0, 2), (2, 3)]), - vec![1i64; 4], - ), - 2, - )), - optimal_config: serde_json::json!(vec![true, false, true, false]), - optimal_value: serde_json::json!(true), - }] -} - -#[cfg(feature = "example-db")] -pub(crate) fn decision_canonical_rule_example_specs( -) -> Vec { - vec![crate::example_db::specs::RuleExampleSpec { - id: "decision_minimum_vertex_cover_to_minimum_vertex_cover", - build: || { - use crate::example_db::specs::assemble_rule_example; - use crate::export::SolutionPair; - use crate::rules::{AggregateReductionResult, ReduceToAggregate}; - - let source = crate::models::decision::Decision::new( + vec![ + crate::example_db::specs::ModelExampleSpec { + id: "decision_minimum_vertex_cover_simplegraph", + instance: Box::new(crate::models::decision::Decision::new( MinimumVertexCover::new( SimpleGraph::new(4, vec![(0, 1), (1, 2), (0, 2), (2, 3)]), vec![1i64; 4], ), 2, - ); - let result = source - .reduce_to_aggregate() - .expect("reduction should succeed"); - let target = result.target_problem(); - let config = vec![true, false, true, false]; - assemble_rule_example( - &source, - target, - vec![SolutionPair { - source_config: serde_json::json!(config.clone()), - target_config: serde_json::json!(config), - }], - ) + )), + optimal_config: serde_json::json!(vec![true, false, true, false]), + optimal_value: serde_json::json!(true), }, - }] + crate::example_db::specs::ModelExampleSpec { + id: "decision_minimum_vertex_cover_unit", + instance: Box::new(Decision::new( + MinimumVertexCover::new(SimpleGraph::path(3), vec![One; 3]), + 1, + )), + optimal_config: serde_json::json!([false, true, false]), + optimal_value: serde_json::json!(true), + }, + ] +} + +#[cfg(feature = "example-db")] +pub(crate) fn decision_canonical_rule_example_specs( +) -> Vec { + use crate::example_db::specs::{rule_example_with_witness, RuleExampleSpec}; + use crate::export::SolutionPair; + vec![ + RuleExampleSpec { + id: "decision_minimum_vertex_cover_to_minimum_vertex_cover", + build: || { + let source = Decision::new( + MinimumVertexCover::new( + SimpleGraph::new(4, vec![(0, 1), (1, 2), (0, 2), (2, 3)]), + vec![1i64; 4], + ), + 2, + ); + rule_example_with_witness::<_, MinimumVertexCover>( + source, + SolutionPair { + source_config: serde_json::json!([true, false, true, false]), + target_config: serde_json::json!([true, false, true, false]), + }, + ) + }, + }, + RuleExampleSpec { + id: "decision_minimum_vertex_cover_unit_to_minimum_vertex_cover", + build: || { + let source = Decision::new( + MinimumVertexCover::new(SimpleGraph::path(3), vec![One; 3]), + 1, + ); + rule_example_with_witness::<_, MinimumVertexCover>( + source, + SolutionPair { + source_config: serde_json::json!([false, true, false]), + target_config: serde_json::json!([false, true, false]), + }, + ) + }, + }, + ] } /// Check if a set of vertices forms a vertex cover. diff --git a/src/models/graph/mixed_chinese_postman.rs b/src/models/graph/mixed_chinese_postman.rs index 8bd02cb1e..450959b53 100644 --- a/src/models/graph/mixed_chinese_postman.rs +++ b/src/models/graph/mixed_chinese_postman.rs @@ -349,8 +349,12 @@ impl crate::solvers::BruteForceProblem for MixedChinesePostman where W: WeightElement + crate::variant::VariantParam, { - fn dimensions(&self) -> Vec { - vec![2; self.graph.num_edges()] + fn num_variables(&self) -> Result { + Ok(self.graph.num_edges()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } diff --git a/src/models/graph/mod.rs b/src/models/graph/mod.rs index 7835dd74f..720ca1dc6 100644 --- a/src/models/graph/mod.rs +++ b/src/models/graph/mod.rs @@ -54,7 +54,6 @@ //! - [`PartitionIntoPathsOfLength2`]: Partition vertices into triples with at least two edges each //! - [`PrizeCollectingSteinerForest`]: Forest minimizing omitted-prize plus edge-cost plus omega times the number of tree components //! - [`BicliqueCover`]: Biclique cover on bipartite graphs -//! - [`SteinerTreeInGraphs`]: Minimum weight Steiner tree connecting terminal vertices //! - [`BalancedCompleteBipartiteSubgraph`]: Balanced biclique decision problem //! - [`BiconnectivityAugmentation`]: Biconnectivity augmentation with weighted potential edges //! - [`BoundedComponentSpanningForest`]: Partition vertices into bounded-weight connected components @@ -161,7 +160,6 @@ pub(crate) mod rural_postman; pub(crate) mod shortest_weight_constrained_path; pub(crate) mod spin_glass; pub(crate) mod steiner_tree; -pub(crate) mod steiner_tree_in_graphs; pub(crate) mod strong_connectivity_augmentation; pub(crate) mod subgraph_isomorphism; pub(crate) mod traveling_salesman; @@ -245,7 +243,6 @@ pub use rural_postman::RuralPostman; pub use shortest_weight_constrained_path::ShortestWeightConstrainedPath; pub use spin_glass::SpinGlass; pub use steiner_tree::SteinerTree; -pub use steiner_tree_in_graphs::SteinerTreeInGraphs; pub use strong_connectivity_augmentation::StrongConnectivityAugmentation; pub use subgraph_isomorphism::SubgraphIsomorphism; pub use traveling_salesman::TravelingSalesman; @@ -322,7 +319,6 @@ pub(crate) fn canonical_model_example_specs() -> Vec crate::solvers::BruteForceProblem for MonochromaticTriangle where G: Graph + VariantParam, { - fn dimensions(&self) -> Vec { - vec![2; self.edge_list.len()] + fn num_variables(&self) -> Result { + Ok(self.edge_list.len()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } diff --git a/src/models/graph/multiple_choice_branching.rs b/src/models/graph/multiple_choice_branching.rs index b04671ee2..5c1bb5677 100644 --- a/src/models/graph/multiple_choice_branching.rs +++ b/src/models/graph/multiple_choice_branching.rs @@ -268,8 +268,12 @@ impl crate::solvers::BruteForceProblem for MultipleChoiceBranching where W: WeightElement + crate::variant::VariantParam, { - fn dimensions(&self) -> Vec { - vec![2; self.graph.num_arcs()] + fn num_variables(&self) -> Result { + Ok(self.graph.num_arcs()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } diff --git a/src/models/graph/multiple_copy_file_allocation.rs b/src/models/graph/multiple_copy_file_allocation.rs index 0813b0113..2c73b07e5 100644 --- a/src/models/graph/multiple_copy_file_allocation.rs +++ b/src/models/graph/multiple_copy_file_allocation.rs @@ -270,8 +270,12 @@ impl Problem for MultipleCopyFileAllocation { } impl crate::solvers::BruteForceProblem for MultipleCopyFileAllocation { - fn dimensions(&self) -> Vec { - vec![2; self.graph.num_vertices()] + fn num_variables(&self) -> Result { + Ok(self.graph.num_vertices()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } diff --git a/src/models/graph/optimal_linear_arrangement.rs b/src/models/graph/optimal_linear_arrangement.rs index c50552603..6224884e3 100644 --- a/src/models/graph/optimal_linear_arrangement.rs +++ b/src/models/graph/optimal_linear_arrangement.rs @@ -184,9 +184,12 @@ impl crate::solvers::BruteForceProblem for OptimalLinearArrangement where G: Graph + crate::variant::VariantParam, { - fn dimensions(&self) -> Vec { - let n = self.graph.num_vertices(); - vec![n; n] + fn num_variables(&self) -> Result { + Ok(self.graph.num_vertices()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(self.graph.num_vertices()) } } diff --git a/src/models/graph/partial_feedback_edge_set.rs b/src/models/graph/partial_feedback_edge_set.rs index c62ad3c72..4ed0a737d 100644 --- a/src/models/graph/partial_feedback_edge_set.rs +++ b/src/models/graph/partial_feedback_edge_set.rs @@ -147,8 +147,12 @@ impl crate::solvers::BruteForceProblem for PartialFeedbackEdgeSet where G: Graph + crate::variant::VariantParam, { - fn dimensions(&self) -> Vec { - vec![2; self.num_edges()] + fn num_variables(&self) -> Result { + Ok(self.num_edges()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } diff --git a/src/models/graph/partition_into_cliques.rs b/src/models/graph/partition_into_cliques.rs index 5bb87de80..c1767b6cc 100644 --- a/src/models/graph/partition_into_cliques.rs +++ b/src/models/graph/partition_into_cliques.rs @@ -139,8 +139,12 @@ impl crate::solvers::BruteForceProblem for PartitionIntoCliques where G: Graph + VariantParam, { - fn dimensions(&self) -> Vec { - vec![self.num_cliques; self.graph.num_vertices()] + fn num_variables(&self) -> Result { + Ok(self.graph.num_vertices()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(self.num_cliques) } } diff --git a/src/models/graph/partition_into_forests.rs b/src/models/graph/partition_into_forests.rs index 09fe3a156..a7c7356a9 100644 --- a/src/models/graph/partition_into_forests.rs +++ b/src/models/graph/partition_into_forests.rs @@ -139,8 +139,12 @@ impl crate::solvers::BruteForceProblem for PartitionIntoForests where G: Graph + VariantParam, { - fn dimensions(&self) -> Vec { - vec![self.num_forests; self.graph.num_vertices()] + fn num_variables(&self) -> Result { + Ok(self.graph.num_vertices()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(self.num_forests) } } diff --git a/src/models/graph/partition_into_paths_of_length_2.rs b/src/models/graph/partition_into_paths_of_length_2.rs index 5c198c54f..807040694 100644 --- a/src/models/graph/partition_into_paths_of_length_2.rs +++ b/src/models/graph/partition_into_paths_of_length_2.rs @@ -180,9 +180,12 @@ impl crate::solvers::BruteForceProblem for PartitionIntoPathsOfLength2 where G: Graph + VariantParam, { - fn dimensions(&self) -> Vec { - let q = self.num_groups(); - vec![q; self.graph.num_vertices()] + fn num_variables(&self) -> Result { + Ok(self.graph.num_vertices()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(self.num_groups()) } } diff --git a/src/models/graph/partition_into_perfect_matchings.rs b/src/models/graph/partition_into_perfect_matchings.rs index e93976e53..8fd16d380 100644 --- a/src/models/graph/partition_into_perfect_matchings.rs +++ b/src/models/graph/partition_into_perfect_matchings.rs @@ -148,8 +148,12 @@ impl crate::solvers::BruteForceProblem for PartitionIntoPerfectMatchings where G: Graph + VariantParam, { - fn dimensions(&self) -> Vec { - vec![self.num_matchings; self.graph.num_vertices()] + fn num_variables(&self) -> Result { + Ok(self.graph.num_vertices()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(self.num_matchings) } } diff --git a/src/models/graph/partition_into_triangles.rs b/src/models/graph/partition_into_triangles.rs index 8638705d2..816009a41 100644 --- a/src/models/graph/partition_into_triangles.rs +++ b/src/models/graph/partition_into_triangles.rs @@ -168,9 +168,12 @@ impl crate::solvers::BruteForceProblem for PartitionIntoTriangles where G: Graph + VariantParam, { - fn dimensions(&self) -> Vec { - let q = self.graph.num_vertices() / 3; - vec![q; self.graph.num_vertices()] + fn num_variables(&self) -> Result { + Ok(self.graph.num_vertices()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(self.graph.num_vertices() / 3) } } diff --git a/src/models/graph/path_constrained_network_flow.rs b/src/models/graph/path_constrained_network_flow.rs index e9f324664..d88aa7ceb 100644 --- a/src/models/graph/path_constrained_network_flow.rs +++ b/src/models/graph/path_constrained_network_flow.rs @@ -327,11 +327,14 @@ impl Problem for PathConstrainedNetworkFlow { } impl crate::solvers::BruteForceProblem for PathConstrainedNetworkFlow { - fn dimensions(&self) -> Vec { - self.paths - .iter() - .map(|path| (self.path_bottleneck(path) as usize) + 1) - .collect() + fn num_variables(&self) -> Result { + Ok(self.paths.len()) + } + + fn dimension(&self, variable: usize) -> Result { + Ok(usize::try_from( + i128::from(self.path_bottleneck(&self.paths[variable])) + 1, + )?) } } diff --git a/src/models/graph/prize_collecting_steiner_forest.rs b/src/models/graph/prize_collecting_steiner_forest.rs index 1503614e1..265ce1688 100644 --- a/src/models/graph/prize_collecting_steiner_forest.rs +++ b/src/models/graph/prize_collecting_steiner_forest.rs @@ -223,12 +223,27 @@ impl PrizeCollectingSteinerForest { } for (index, prize) in vertex_prizes.iter().enumerate() { prize.validate_element(&format!("vertex prize at index {index}"))?; + if prize.to_sum() < W::Sum::zero() { + return Err(ConstructionError::InvalidInput(format!( + "vertex prize at index {index} must be nonnegative" + ))); + } } for (index, cost) in edge_costs.iter().enumerate() { cost.validate_element(&format!("edge cost at index {index}"))?; + if cost.to_sum() < W::Sum::zero() { + return Err(ConstructionError::InvalidInput(format!( + "edge cost at index {index} must be nonnegative" + ))); + } } beta.validate_element("beta")?; omega.validate_element("omega")?; + if beta.to_sum() < W::Sum::zero() || omega.to_sum() < W::Sum::zero() { + return Err(ConstructionError::InvalidInput( + "beta and omega must be nonnegative".into(), + )); + } Ok(Self { graph, vertex_prizes, @@ -389,8 +404,16 @@ where G: Graph + VariantParam, W: WeightElement + VariantParam, { - fn dimensions(&self) -> Vec { - vec![2; self.graph.num_vertices() + self.graph.num_edges()] + fn num_variables(&self) -> Result { + (self.graph.num_vertices()) + .checked_add(self.graph.num_edges()) + .ok_or_else(|| { + crate::solvers::SolveError::IntegerOverflow("computing the coordinate count".into()) + }) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } diff --git a/src/models/graph/rooted_tree_arrangement.rs b/src/models/graph/rooted_tree_arrangement.rs index 316e28187..6dd796133 100644 --- a/src/models/graph/rooted_tree_arrangement.rs +++ b/src/models/graph/rooted_tree_arrangement.rs @@ -160,9 +160,16 @@ impl crate::solvers::BruteForceProblem for RootedTreeArrangement where G: Graph + VariantParam, { - fn dimensions(&self) -> Vec { - let n = self.graph.num_vertices(); - vec![n; 2 * n] + fn num_variables(&self) -> Result { + (2usize) + .checked_mul(self.graph.num_vertices()) + .ok_or_else(|| { + crate::solvers::SolveError::IntegerOverflow("computing the coordinate count".into()) + }) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(self.graph.num_vertices()) } } diff --git a/src/models/graph/rural_postman.rs b/src/models/graph/rural_postman.rs index 2561b04e1..7a098c101 100644 --- a/src/models/graph/rural_postman.rs +++ b/src/models/graph/rural_postman.rs @@ -354,8 +354,12 @@ where G: Graph + crate::variant::VariantParam, W: WeightElement + crate::variant::VariantParam, { - fn dimensions(&self) -> Vec { - vec![3; self.graph.num_edges()] + fn num_variables(&self) -> Result { + Ok(self.graph.num_edges()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(3usize) } } diff --git a/src/models/graph/shortest_weight_constrained_path.rs b/src/models/graph/shortest_weight_constrained_path.rs index 0dab498cf..5b9aa0eee 100644 --- a/src/models/graph/shortest_weight_constrained_path.rs +++ b/src/models/graph/shortest_weight_constrained_path.rs @@ -356,8 +356,12 @@ where G: Graph + crate::variant::VariantParam, N: WeightElement + crate::variant::VariantParam, { - fn dimensions(&self) -> Vec { - vec![2; self.graph.num_edges()] + fn num_variables(&self) -> Result { + Ok(self.graph.num_edges()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } diff --git a/src/models/graph/spin_glass.rs b/src/models/graph/spin_glass.rs index b99974051..fe3a3d53f 100644 --- a/src/models/graph/spin_glass.rs +++ b/src/models/graph/spin_glass.rs @@ -392,8 +392,12 @@ where + num_traits::Zero + num_traits::Bounded, { - fn dimensions(&self) -> Vec { - vec![2; self.graph.num_vertices()] + fn num_variables(&self) -> Result { + Ok(self.graph.num_vertices()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } diff --git a/src/models/graph/steiner_tree.rs b/src/models/graph/steiner_tree.rs index 6f819ecae..712378a56 100644 --- a/src/models/graph/steiner_tree.rs +++ b/src/models/graph/steiner_tree.rs @@ -47,6 +47,10 @@ inventory::submit! { /// - Selected edges form a tree (connected + acyclic) /// - All terminal vertices are included /// +/// At least one terminal is required. With one terminal, selecting no edges +/// represents the tree consisting of that terminal alone. Signed edge weights +/// are allowed; additional edges must still form a tree containing the terminal. +/// /// # Type Parameters /// /// * `G` - The graph type (e.g., `SimpleGraph`) @@ -105,8 +109,8 @@ impl SteinerTree { if edge_weights.len() != graph.num_edges() { return Err("edge_weights length must match num_edges".into()); } - if terminals.len() < 2 { - return Err("at least 2 terminals required".into()); + if terminals.is_empty() { + return Err("at least one terminal required".into()); } let distinct_terminals: BTreeSet<_> = terminals.iter().copied().collect(); if distinct_terminals.len() != terminals.len() { @@ -222,7 +226,7 @@ fn is_valid_steiner_tree(graph: &G, terminals: &[usize], config: &[boo } if selected_count == 0 { - return false; + return terminals.len() == 1; } // BFS from first terminal to check connectivity @@ -290,13 +294,11 @@ where let mut total = W::Sum::zero(); for (idx, &selected) in config.iter().enumerate() { if selected { - if let Some(w) = self.edge_weights.get(idx) { - total = W::checked_add_to_sum( - total, - w.to_sum(), - "summing Steiner tree edge weights", - )?; - } + total = W::checked_add_to_sum( + total, + self.edge_weights[idx].to_sum(), + "summing Steiner tree edge weights", + )?; } } Min(Some(total)) @@ -309,8 +311,12 @@ where G: Graph + crate::variant::VariantParam, W: WeightElement + crate::variant::VariantParam, { - fn dimensions(&self) -> Vec { - vec![2; self.graph.num_edges()] + fn num_variables(&self) -> Result { + Ok(self.graph.num_edges()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } @@ -345,8 +351,10 @@ impl TryFrom for SteinerTree { } } +// For signed weights, enumerate nonterminal vertex subsets and compute an MST +// on each induced graph. Terminal-subset shortest-path DP assumes nonnegative weights. crate::declare_variants! { - default SteinerTree => "3^num_terminals * num_vertices + 2^num_terminals * num_vertices^2" create SteinerTreeCreateSpec random, + default SteinerTree => "2^num_vertices * 0.5^num_terminals * num_vertices^2" create SteinerTreeCreateSpec random, SteinerTree => "3^num_terminals * num_vertices + 2^num_terminals * num_vertices^2" create SteinerTreeOneCreateSpec, } diff --git a/src/models/graph/steiner_tree_in_graphs.rs b/src/models/graph/steiner_tree_in_graphs.rs deleted file mode 100644 index fe7e42f5d..000000000 --- a/src/models/graph/steiner_tree_in_graphs.rs +++ /dev/null @@ -1,396 +0,0 @@ -//! Steiner Tree in Graphs problem implementation. -//! -//! The Steiner Tree problem asks for a minimum-weight subtree of a graph -//! that connects all terminal vertices. - -use crate::registry::{CreateSpec, ProblemSchemaEntry, VariantDimension}; -use crate::topology::{Graph, SimpleGraph}; -use crate::traits::Problem; -use crate::types::{Min, One, WeightElement}; -use num_traits::Zero; -use serde::{Deserialize, Serialize}; - -inventory::submit! { - ProblemSchemaEntry { - name: "SteinerTreeInGraphs", - display_name: "Steiner Tree in Graphs", - aliases: &[], - dimensions: &[ - VariantDimension::new("graph", "SimpleGraph", &["SimpleGraph"]), - VariantDimension::new("weight", "i64", &["One", "i64"]), - ], - category: crate::registry::ProblemCategory::Graph, - module_path: module_path!(), - description: "Find minimum weight subtree connecting all terminal vertices", - fields: SteinerTreeInGraphsCreateSpec::::FIELDS, - } -} - -/// The Steiner Tree in Graphs problem. -/// -/// Given a weighted graph G = (V, E) with edge weights w_e and a -/// subset R ⊆ V of required terminal vertices, find a subtree T of G -/// that includes all vertices of R and minimizes the total edge weight -/// Σ_{e ∈ T} w(e). -/// -/// # Representation -/// -/// Each edge is assigned a binary variable: -/// - 0: edge is not in the tree -/// - 1: edge is in the tree -/// -/// A valid Steiner tree requires: -/// - All terminal vertices are connected through selected edges -/// - Selected edges form a connected subgraph (optimally a tree) -/// -/// # Type Parameters -/// -/// * `G` - The graph type (e.g., `SimpleGraph`) -/// * `W` - The weight type for edges (e.g., `i64`, `f64`) -/// -/// # Example -/// -/// ``` -/// use problemreductions::models::graph::SteinerTreeInGraphs; -/// use problemreductions::topology::SimpleGraph; -/// use problemreductions::{Problem, BruteForce}; -/// -/// // Path graph 0-1-2-3, terminals {0, 3} -/// let graph = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]); -/// let problem = SteinerTreeInGraphs::new(graph, vec![0, 3], vec![1, 1, 1]); -/// -/// let solver = BruteForce::new(); -/// let solution = solver.solve(&problem).unwrap().unwrap(); -/// // Optimal: select all 3 edges (the only path from 0 to 3) -/// assert_eq!(solution, vec![true, true, true]); -/// ``` -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct SteinerTreeInGraphs { - /// The underlying graph. - graph: G, - /// Required terminal vertices. - terminals: Vec, - /// Weights for each edge (in edge index order). - edge_weights: Vec, -} - -#[derive(Debug, Deserialize, crate::CreateSpec)] -struct SteinerTreeInGraphsCreateSpec { - /// The underlying graph. - graph: SimpleGraph, - /// Required terminal vertices. - terminals: Vec, - /// Edge weights; defaults to one per edge. - edge_weights: Option>, -} -impl TryFrom> for SteinerTreeInGraphs -where - W: WeightElement, -{ - type Error = crate::registry::ConstructionError; - fn try_from(spec: SteinerTreeInGraphsCreateSpec) -> Result { - let count = spec.graph.num_edges(); - let edge_weights = spec - .edge_weights - .unwrap_or_else(|| (0..count).map(|_| W::unit()).collect()); - if edge_weights.len() != count { - return Err(format!( - "edge_weights has {} entries, expected {count}", - edge_weights.len() - ) - .into()); - } - if let Some(&terminal) = spec - .terminals - .iter() - .find(|&&t| t >= spec.graph.num_vertices()) - { - return Err(format!("terminal {terminal} is outside the graph").into()); - } - Ok(Self::new(spec.graph, spec.terminals, edge_weights)) - } -} - -impl SteinerTreeInGraphs { - /// Create a SteinerTreeInGraphs problem from a graph, terminals, and edge weights. - /// - /// # Panics - /// Panics if `edge_weights.len() != graph.num_edges()` or any terminal index is out of bounds. - pub fn new(graph: G, terminals: Vec, edge_weights: Vec) -> Self { - assert_eq!( - edge_weights.len(), - graph.num_edges(), - "edge_weights length must match num_edges" - ); - for &t in &terminals { - assert!( - t < graph.num_vertices(), - "terminal vertex {} out of bounds (num_vertices = {})", - t, - graph.num_vertices() - ); - } - Self { - graph, - terminals, - edge_weights, - } - } - - /// Get a reference to the underlying graph. - pub fn graph(&self) -> &G { - &self.graph - } - - /// Get the terminal vertices. - pub fn terminals(&self) -> &[usize] { - &self.terminals - } - - /// Get all edges with their weights. - pub fn edges(&self) -> Vec<(usize, usize, W)> { - self.graph - .edges() - .into_iter() - .zip(self.edge_weights.iter().cloned()) - .map(|((u, v), w)| (u, v, w)) - .collect() - } - - /// Set new weights for the problem. - pub fn set_weights(&mut self, weights: Vec) { - assert_eq!(weights.len(), self.graph.num_edges()); - self.edge_weights = weights; - } - - /// Get the weights for the problem. - pub fn weights(&self) -> Vec { - self.edge_weights.clone() - } - - /// Check if the problem uses a non-unit weight type. - pub fn is_weighted(&self) -> bool - where - W: WeightElement, - { - !W::IS_UNIT - } - - /// Check if a configuration is a valid Steiner tree. - pub fn is_valid_solution(&self, config: &[usize]) -> bool { - if config.len() != self.graph.num_edges() { - return false; - } - let selected: Vec = config.iter().map(|&s| s == 1).collect(); - is_steiner_tree(&self.graph, &self.terminals, &selected) - } -} - -impl SteinerTreeInGraphs { - /// Get the number of vertices in the underlying graph. - pub fn num_vertices(&self) -> usize { - self.graph().num_vertices() - } - - /// Get the number of edges in the underlying graph. - pub fn num_edges(&self) -> usize { - self.graph().num_edges() - } - - /// Get the number of terminal vertices. - pub fn num_terminals(&self) -> usize { - self.terminals.len() - } -} - -impl Problem for SteinerTreeInGraphs -where - G: Graph + crate::variant::VariantParam, - W: WeightElement + crate::variant::VariantParam, -{ - const NAME: &'static str = "SteinerTreeInGraphs"; - type Solution = Vec; - type Value = Min; - - crate::problem_parameters![ - ("num_edges", num_edges), - ("num_terminals", num_terminals), - ("num_vertices", num_vertices), - ]; - - fn variant() -> Vec<(&'static str, &'static str)> { - crate::variant_params![G, W] - } - - fn evaluate( - &self, - config: &Self::Solution, - ) -> Result, crate::traits::EvaluationError> { - Ok({ - if config.len() != self.graph.num_edges() { - return Err(crate::traits::EvaluationError::InvalidConfiguration( - "edge-selection length does not match the graph".into(), - )); - } - let selected = config; - if !is_steiner_tree(&self.graph, &self.terminals, selected) { - return Ok(Min(None)); - } - let mut total = W::Sum::zero(); - for (idx, &sel) in config.iter().enumerate() { - if sel { - if let Some(w) = self.edge_weights.get(idx) { - total = W::checked_add_to_sum( - total, - w.to_sum(), - "summing Steiner tree edge weights", - )?; - } - } - } - Min(Some(total)) - }) - } -} - -impl crate::solvers::BruteForceProblem for SteinerTreeInGraphs -where - G: Graph + crate::variant::VariantParam, - W: WeightElement + crate::variant::VariantParam, -{ - fn dimensions(&self) -> Vec { - vec![2; self.graph.num_edges()] - } -} - -/// Check if a selection of edges forms a valid Steiner tree (connected subgraph spanning all terminals). -/// -/// A valid Steiner tree requires: -/// 1. All terminal vertices are reachable from each other through selected edges. -/// 2. The selected edges form a connected subgraph that includes all terminals. -/// -/// Note: The optimal solution is always a tree, but we accept any connected subgraph -/// spanning all terminals (the brute-force solver will find the minimum-weight one). -/// -/// # Panics -/// Panics if `selected.len() != graph.num_edges()`. -pub(crate) fn is_steiner_tree(graph: &G, terminals: &[usize], selected: &[bool]) -> bool { - assert_eq!( - selected.len(), - graph.num_edges(), - "selected length must match num_edges" - ); - - // If no terminals, any selection is trivially valid (including empty) - if terminals.is_empty() { - return true; - } - - // If only one terminal, it's valid as long as that terminal exists - // (no edges needed to connect a single vertex) - if terminals.len() == 1 { - return true; - } - - // Build adjacency list from selected edges - let n = graph.num_vertices(); - let edges = graph.edges(); - let mut adj: Vec> = vec![vec![]; n]; - - let mut has_any_edge = false; - for (idx, &sel) in selected.iter().enumerate() { - if sel { - let (u, v) = edges[idx]; - adj[u].push(v); - adj[v].push(u); - has_any_edge = true; - } - } - - if !has_any_edge { - return false; - } - - // BFS from the first terminal to check connectivity of all terminals - let start = terminals[0]; - let mut visited = vec![false; n]; - let mut queue = std::collections::VecDeque::new(); - visited[start] = true; - queue.push_back(start); - - while let Some(node) = queue.pop_front() { - for &neighbor in &adj[node] { - if !visited[neighbor] { - visited[neighbor] = true; - queue.push_back(neighbor); - } - } - } - - // All terminals must be reachable - terminals.iter().all(|&t| visited[t]) -} - -crate::impl_random_generate!(SteinerTreeInGraphs, crate::random::SimpleGraphRandomSpec, |spec| { - if spec.num_vertices < 2 { - return Err("num_vertices must be at least 2".to_string().into()); - } - let graph = spec.graph()?; - let terminals = (0..std::cmp::max(2, spec.num_vertices / 2)).collect(); - let weights = vec![1; graph.num_edges()]; - Ok(SteinerTreeInGraphs::new(graph, terminals, weights)) -}); - -#[derive(Debug, Deserialize, crate::CreateSpec)] -struct SteinerTreeInGraphsOneCreateSpec { - /// The underlying graph. - graph: SimpleGraph, - terminals: Vec, -} - -impl TryFrom for SteinerTreeInGraphs { - type Error = crate::registry::ConstructionError; - fn try_from(spec: SteinerTreeInGraphsOneCreateSpec) -> Result { - let weights = vec![One; spec.graph.num_edges()]; - if let Some(&terminal) = spec - .terminals - .iter() - .find(|&&t| t >= spec.graph.num_vertices()) - { - return Err(format!("terminal {terminal} is outside the graph").into()); - } - Ok(Self::new(spec.graph, spec.terminals, weights)) - } -} - -crate::declare_variants! { - default SteinerTreeInGraphs => "2^num_terminals * num_vertices^3" create SteinerTreeInGraphsCreateSpec random, - SteinerTreeInGraphs => "2^num_terminals * num_vertices^3" create SteinerTreeInGraphsOneCreateSpec, -} - -crate::register_brute_force! { - SteinerTreeInGraphs decode |_, indices: Vec| crate::config::config_to_bits(&indices), - SteinerTreeInGraphs decode |_, indices: Vec| crate::config::config_to_bits(&indices), -} - -#[cfg(feature = "example-db")] -pub(crate) fn canonical_model_example_specs() -> Vec { - vec![crate::example_db::specs::ModelExampleSpec { - id: "steiner_tree_in_graphs_simplegraph", - instance: Box::new(SteinerTreeInGraphs::new( - SimpleGraph::new( - 6, - vec![(0, 1), (0, 2), (1, 3), (2, 3), (2, 5), (3, 4), (4, 5)], - ), - vec![0, 3, 5], - vec![3, 2, 4, 1, 2, 3, 1], - )), - // Optimal: edges {0,2}(w=2), {2,3}(w=1), {2,5}(w=2) = weight 5 - optimal_config: serde_json::json!(vec![false, true, false, true, true, false, false]), - optimal_value: serde_json::json!(5), - }] -} - -#[cfg(test)] -#[path = "../../unit_tests/models/graph/steiner_tree_in_graphs.rs"] -mod tests; diff --git a/src/models/graph/strong_connectivity_augmentation.rs b/src/models/graph/strong_connectivity_augmentation.rs index 83c2c6cdf..eb60bec1a 100644 --- a/src/models/graph/strong_connectivity_augmentation.rs +++ b/src/models/graph/strong_connectivity_augmentation.rs @@ -213,8 +213,12 @@ impl crate::solvers::BruteForceProblem for StrongConnectivityAugmentation where W: WeightElement + crate::variant::VariantParam, { - fn dimensions(&self) -> Vec { - vec![2; self.candidate_arcs.len()] + fn num_variables(&self) -> Result { + Ok(self.candidate_arcs.len()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } diff --git a/src/models/graph/subgraph_isomorphism.rs b/src/models/graph/subgraph_isomorphism.rs index def3018fc..a5a35b2b6 100644 --- a/src/models/graph/subgraph_isomorphism.rs +++ b/src/models/graph/subgraph_isomorphism.rs @@ -175,16 +175,18 @@ impl Problem for SubgraphIsomorphism { } impl crate::solvers::BruteForceProblem for SubgraphIsomorphism { - fn dimensions(&self) -> Vec { - let n_host = self.host_graph.num_vertices(); - let n_pattern = self.pattern_graph.num_vertices(); + fn num_variables(&self) -> Result { + Ok(self.pattern_graph.num_vertices()) + } - if n_pattern > n_host { - // No injective mapping possible: each variable gets an empty domain. - vec![0; n_pattern] - } else { - vec![n_host; n_pattern] - } + fn dimension(&self, _variable: usize) -> Result { + Ok( + if self.pattern_graph.num_vertices() > self.host_graph.num_vertices() { + 0 + } else { + self.host_graph.num_vertices() + }, + ) } } diff --git a/src/models/graph/traveling_salesman.rs b/src/models/graph/traveling_salesman.rs index b658067da..6d38980d4 100644 --- a/src/models/graph/traveling_salesman.rs +++ b/src/models/graph/traveling_salesman.rs @@ -252,8 +252,12 @@ where G: Graph + crate::variant::VariantParam, W: WeightElement + crate::variant::VariantParam, { - fn dimensions(&self) -> Vec { - vec![2; self.graph.num_edges()] + fn num_variables(&self) -> Result { + Ok(self.graph.num_edges()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } diff --git a/src/models/graph/undirected_flow_lower_bounds.rs b/src/models/graph/undirected_flow_lower_bounds.rs index ff9b0657f..e2bb0f979 100644 --- a/src/models/graph/undirected_flow_lower_bounds.rs +++ b/src/models/graph/undirected_flow_lower_bounds.rs @@ -305,8 +305,12 @@ impl Problem for UndirectedFlowLowerBounds { } impl crate::solvers::BruteForceProblem for UndirectedFlowLowerBounds { - fn dimensions(&self) -> Vec { - vec![2; self.num_edges()] + fn num_variables(&self) -> Result { + Ok(self.num_edges()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } diff --git a/src/models/graph/undirected_two_commodity_integral_flow.rs b/src/models/graph/undirected_two_commodity_integral_flow.rs index ad8a584f1..975155bc6 100644 --- a/src/models/graph/undirected_two_commodity_integral_flow.rs +++ b/src/models/graph/undirected_two_commodity_integral_flow.rs @@ -148,15 +148,10 @@ impl UndirectedTwoCommodityIntegralFlow { ); } - for &capacity in &capacities { - let domain = usize::try_from(capacity) - .ok() - .and_then(|value| value.checked_add(1)); - assert!( - domain.is_some(), - "edge capacities must fit into usize for dims()" - ); - } + assert!( + capacities.iter().all(|&capacity| capacity >= 0), + "capacities must be nonnegative" + ); Self { graph, @@ -228,13 +223,6 @@ impl UndirectedTwoCommodityIntegralFlow { self.num_edges() * 4 } - fn domain_size(capacity: i64) -> usize { - usize::try_from(capacity) - .ok() - .and_then(|value| value.checked_add(1)) - .expect("capacity already validated to fit into usize") - } - fn edge_flows(&self, config: &[usize], edge_index: usize) -> Option<[usize; 4]> { let start = edge_index.checked_mul(4)?; Some([ @@ -403,14 +391,14 @@ impl Problem for UndirectedTwoCommodityIntegralFlow { } impl crate::solvers::BruteForceProblem for UndirectedTwoCommodityIntegralFlow { - fn dimensions(&self) -> Vec { - self.capacities - .iter() - .flat_map(|&capacity| { - let domain = Self::domain_size(capacity); - std::iter::repeat_n(domain, 4) - }) - .collect() + fn num_variables(&self) -> Result { + Ok(4 * self.capacities.len()) + } + + fn dimension(&self, variable: usize) -> Result { + Ok(usize::try_from( + i128::from(self.capacities[variable / 4]) + 1, + )?) } } diff --git a/src/models/misc/additional_key.rs b/src/models/misc/additional_key.rs index d15016c1b..dc48e4a05 100644 --- a/src/models/misc/additional_key.rs +++ b/src/models/misc/additional_key.rs @@ -265,8 +265,12 @@ impl Problem for AdditionalKey { } impl crate::solvers::BruteForceProblem for AdditionalKey { - fn dimensions(&self) -> Vec { - vec![2; self.relation_attrs.len()] + fn num_variables(&self) -> Result { + Ok(self.relation_attrs.len()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } diff --git a/src/models/misc/betweenness.rs b/src/models/misc/betweenness.rs index bf55a1c12..64778ea16 100644 --- a/src/models/misc/betweenness.rs +++ b/src/models/misc/betweenness.rs @@ -170,8 +170,12 @@ impl Problem for Betweenness { } impl crate::solvers::BruteForceProblem for Betweenness { - fn dimensions(&self) -> Vec { - vec![self.num_elements; self.num_elements] + fn num_variables(&self) -> Result { + Ok(self.num_elements) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(self.num_elements) } } diff --git a/src/models/misc/bin_packing.rs b/src/models/misc/bin_packing.rs index 0f952f549..d307109c4 100644 --- a/src/models/misc/bin_packing.rs +++ b/src/models/misc/bin_packing.rs @@ -155,9 +155,12 @@ where W: WeightElement + crate::variant::VariantParam, W::Sum: PartialOrd, { - fn dimensions(&self) -> Vec { - let n = self.sizes.len(); - vec![n; n] + fn num_variables(&self) -> Result { + Ok(self.sizes.len()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(self.sizes.len()) } } diff --git a/src/models/misc/boyce_codd_normal_form_violation.rs b/src/models/misc/boyce_codd_normal_form_violation.rs index a09631428..a6e2ddeb7 100644 --- a/src/models/misc/boyce_codd_normal_form_violation.rs +++ b/src/models/misc/boyce_codd_normal_form_violation.rs @@ -269,8 +269,12 @@ impl Problem for BoyceCoddNormalFormViolation { } impl crate::solvers::BruteForceProblem for BoyceCoddNormalFormViolation { - fn dimensions(&self) -> Vec { - vec![2; self.target_subset.len()] + fn num_variables(&self) -> Result { + Ok(self.target_subset.len()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } diff --git a/src/models/misc/capacity_assignment.rs b/src/models/misc/capacity_assignment.rs index f397cdb4d..061e5dbb8 100644 --- a/src/models/misc/capacity_assignment.rs +++ b/src/models/misc/capacity_assignment.rs @@ -245,8 +245,12 @@ impl Problem for CapacityAssignment { } impl crate::solvers::BruteForceProblem for CapacityAssignment { - fn dimensions(&self) -> Vec { - vec![self.num_capacities(); self.num_links()] + fn num_variables(&self) -> Result { + Ok(self.num_links()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(self.num_capacities()) } } diff --git a/src/models/misc/closest_string.rs b/src/models/misc/closest_string.rs index 722d23dfd..b0d7cf8b0 100644 --- a/src/models/misc/closest_string.rs +++ b/src/models/misc/closest_string.rs @@ -169,8 +169,12 @@ impl Problem for ClosestString { } impl crate::solvers::BruteForceProblem for ClosestString { - fn dimensions(&self) -> Vec { - vec![self.alphabet_size; self.string_length()] + fn num_variables(&self) -> Result { + Ok(self.string_length()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(self.alphabet_size) } } diff --git a/src/models/misc/closest_substring.rs b/src/models/misc/closest_substring.rs index 56a504e5a..04680a18f 100644 --- a/src/models/misc/closest_substring.rs +++ b/src/models/misc/closest_substring.rs @@ -113,11 +113,6 @@ impl ClosestSubstring { .map(|string| string.len() - substring_length + 1) .try_fold(0_usize, usize::checked_add) .ok_or("total number of windows exceeds usize")?; - strings - .iter() - .map(|string| string.len() - substring_length + 1) - .try_fold(1_usize, usize::checked_mul) - .ok_or("window-choice count exceeds usize")?; Ok(Self { alphabet_size, strings, @@ -157,16 +152,6 @@ impl ClosestSubstring { .map(|s| s.len() - self.substring_length + 1) .sum() } - - /// Returns `prod_i W_i`, the number of distinct window-selection tuples. - /// - pub fn num_window_choice_product(&self) -> usize { - self.strings - .iter() - .map(|s| s.len() - self.substring_length + 1) - .try_fold(1usize, usize::checked_mul) - .expect("validated window-choice count must fit usize") - } } impl Problem for ClosestSubstring { @@ -180,7 +165,6 @@ impl Problem for ClosestSubstring { ("substring_length", substring_length), ("total_length", total_length), ("total_num_windows", total_num_windows), - ("num_window_choice_product", num_window_choice_product), ]; fn variant() -> Vec<(&'static str, &'static str)> { @@ -235,16 +219,22 @@ impl Problem for ClosestSubstring { } impl crate::solvers::BruteForceProblem for ClosestSubstring { - fn dimensions(&self) -> Vec { - let ell = self.substring_length; - let mut dims = vec![self.alphabet_size; ell]; - dims.extend(self.strings.iter().map(|s| s.len() - ell + 1)); - dims + fn num_variables(&self) -> Result { + Ok(self.substring_length + self.strings.len()) + } + + fn dimension(&self, variable: usize) -> Result { + if variable < self.substring_length { + Ok(self.alphabet_size) + } else { + Ok(self.strings[variable - self.substring_length].len() - self.substring_length + 1) + } } } crate::declare_variants! { - default ClosestSubstring => "alphabet_size ^ substring_length * num_window_choice_product", + // AM-GM bounds the product of window counts by their mean raised to num_strings. + default ClosestSubstring => "alphabet_size ^ substring_length * (total_num_windows / num_strings)^num_strings", } crate::register_brute_force! { diff --git a/src/models/misc/clustering.rs b/src/models/misc/clustering.rs index e87998c73..691ba00be 100644 --- a/src/models/misc/clustering.rs +++ b/src/models/misc/clustering.rs @@ -192,8 +192,12 @@ impl Problem for Clustering { } impl crate::solvers::BruteForceProblem for Clustering { - fn dimensions(&self) -> Vec { - vec![self.num_clusters; self.num_elements()] + fn num_variables(&self) -> Result { + Ok(self.num_elements()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(self.num_clusters) } } diff --git a/src/models/misc/conjunctive_boolean_query.rs b/src/models/misc/conjunctive_boolean_query.rs index a6ce31f50..26004ce7a 100644 --- a/src/models/misc/conjunctive_boolean_query.rs +++ b/src/models/misc/conjunctive_boolean_query.rs @@ -317,8 +317,12 @@ impl Problem for ConjunctiveBooleanQuery { } impl crate::solvers::BruteForceProblem for ConjunctiveBooleanQuery { - fn dimensions(&self) -> Vec { - vec![self.domain_size; self.num_variables] + fn num_variables(&self) -> Result { + Ok(self.num_variables) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(self.domain_size) } } diff --git a/src/models/misc/conjunctive_query_foldability.rs b/src/models/misc/conjunctive_query_foldability.rs index d8f6ed074..08721a619 100644 --- a/src/models/misc/conjunctive_query_foldability.rs +++ b/src/models/misc/conjunctive_query_foldability.rs @@ -325,9 +325,22 @@ impl Problem for ConjunctiveQueryFoldability { impl crate::solvers::BruteForceProblem for ConjunctiveQueryFoldability { /// Each undistinguished variable can map to any element of `D ∪ X ∪ Y`. - fn dimensions(&self) -> Vec { - let range = self.domain_size + self.num_distinguished + self.num_undistinguished; - vec![range; self.num_undistinguished] + fn num_variables(&self) -> Result { + Ok(self.num_undistinguished) + } + + fn dimension(&self, _variable: usize) -> Result { + ((self.domain_size) + .checked_add(self.num_distinguished) + .ok_or_else(|| { + crate::solvers::SolveError::IntegerOverflow( + "computing a coordinate cardinality".into(), + ) + })?) + .checked_add(self.num_undistinguished) + .ok_or_else(|| { + crate::solvers::SolveError::IntegerOverflow("computing a coordinate cardinality".into()) + }) } } diff --git a/src/models/misc/consistency_of_database_frequency_tables.rs b/src/models/misc/consistency_of_database_frequency_tables.rs index b177ed320..b4b0c73c5 100644 --- a/src/models/misc/consistency_of_database_frequency_tables.rs +++ b/src/models/misc/consistency_of_database_frequency_tables.rs @@ -261,9 +261,9 @@ impl ConsistencyOfDatabaseFrequencyTables { &self.known_values } - /// Returns the product of attribute domain sizes. - pub fn domain_size_product(&self) -> usize { - self.attribute_domains.iter().copied().product() + /// Largest attribute domain, or one for the empty attribute list. + pub fn max_domain_size(&self) -> usize { + self.attribute_domains.iter().copied().max().unwrap_or(1) } /// Returns the sum of all attribute-domain sizes. @@ -318,7 +318,7 @@ impl Problem for ConsistencyOfDatabaseFrequencyTables { ("num_objects", num_objects), ("num_attributes", num_attributes), ("total_domain_size", total_domain_size), - ("domain_size_product", domain_size_product), + ("max_domain_size", max_domain_size), ("num_frequency_tables", num_frequency_tables), ("num_frequency_cells", num_frequency_cells), ("num_known_values", num_known_values), @@ -386,17 +386,23 @@ impl Problem for ConsistencyOfDatabaseFrequencyTables { } impl crate::solvers::BruteForceProblem for ConsistencyOfDatabaseFrequencyTables { - fn dimensions(&self) -> Vec { - let mut dims = Vec::with_capacity(self.num_assignment_variables()); - for _ in 0..self.num_objects { - dims.extend(self.attribute_domains.iter().copied()); - } - dims + fn num_variables(&self) -> Result { + (self.num_objects) + .checked_mul(self.attribute_domains.len()) + .ok_or_else(|| { + crate::solvers::SolveError::IntegerOverflow( + "computing a search coordinate size".into(), + ) + }) + } + + fn dimension(&self, variable: usize) -> Result { + Ok(self.attribute_domains[variable % self.attribute_domains.len()]) } } crate::declare_variants! { - default ConsistencyOfDatabaseFrequencyTables => "domain_size_product^num_objects" create ConsistencyOfDatabaseFrequencyTablesCreateSpec, + default ConsistencyOfDatabaseFrequencyTables => "max_domain_size^(num_attributes * num_objects)" create ConsistencyOfDatabaseFrequencyTablesCreateSpec, } crate::register_brute_force! { diff --git a/src/models/misc/cosine_product_integration.rs b/src/models/misc/cosine_product_integration.rs index 824e46e09..30e4de5f7 100644 --- a/src/models/misc/cosine_product_integration.rs +++ b/src/models/misc/cosine_product_integration.rs @@ -132,8 +132,12 @@ impl Problem for CosineProductIntegration { } impl crate::solvers::BruteForceProblem for CosineProductIntegration { - fn dimensions(&self) -> Vec { - vec![2; self.num_coefficients()] + fn num_variables(&self) -> Result { + Ok(self.num_coefficients()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } diff --git a/src/models/misc/cyclic_ordering.rs b/src/models/misc/cyclic_ordering.rs index 0edaffe5f..14d70565b 100644 --- a/src/models/misc/cyclic_ordering.rs +++ b/src/models/misc/cyclic_ordering.rs @@ -175,8 +175,12 @@ impl Problem for CyclicOrdering { } impl crate::solvers::BruteForceProblem for CyclicOrdering { - fn dimensions(&self) -> Vec { - vec![self.num_elements; self.num_elements] + fn num_variables(&self) -> Result { + Ok(self.num_elements) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(self.num_elements) } } diff --git a/src/models/misc/dynamic_storage_allocation.rs b/src/models/misc/dynamic_storage_allocation.rs index 37ccb3c60..81ff0e69d 100644 --- a/src/models/misc/dynamic_storage_allocation.rs +++ b/src/models/misc/dynamic_storage_allocation.rs @@ -172,11 +172,18 @@ impl Problem for DynamicStorageAllocation { } impl crate::solvers::BruteForceProblem for DynamicStorageAllocation { - fn dimensions(&self) -> Vec { - self.items - .iter() - .map(|&(_, _, s)| self.memory_size - s + 1) - .collect() + fn num_variables(&self) -> Result { + Ok(self.items.len()) + } + + fn dimension(&self, variable: usize) -> Result { + (self.memory_size - self.items[variable].2) + .checked_add(1usize) + .ok_or_else(|| { + crate::solvers::SolveError::IntegerOverflow( + "computing a search coordinate size".into(), + ) + }) } } diff --git a/src/models/misc/ensemble_computation.rs b/src/models/misc/ensemble_computation.rs index abe93a81c..dd7bb7e15 100644 --- a/src/models/misc/ensemble_computation.rs +++ b/src/models/misc/ensemble_computation.rs @@ -228,8 +228,20 @@ impl Problem for EnsembleComputation { } impl crate::solvers::BruteForceProblem for EnsembleComputation { - fn dimensions(&self) -> Vec { - vec![self.universe_size + self.budget; 2 * self.budget] + fn num_variables(&self) -> Result { + (2usize).checked_mul(self.budget).ok_or_else(|| { + crate::solvers::SolveError::IntegerOverflow("computing the coordinate count".into()) + }) + } + + fn dimension(&self, _variable: usize) -> Result { + (self.universe_size) + .checked_add(self.budget) + .ok_or_else(|| { + crate::solvers::SolveError::IntegerOverflow( + "computing a coordinate cardinality".into(), + ) + }) } } diff --git a/src/models/misc/expected_retrieval_cost.rs b/src/models/misc/expected_retrieval_cost.rs index a904aa39b..a86f3efcd 100644 --- a/src/models/misc/expected_retrieval_cost.rs +++ b/src/models/misc/expected_retrieval_cost.rs @@ -109,6 +109,15 @@ impl ExpectedRetrievalCost { Ok(Some(masses)) } + /// Number of intervening sectors, wrapping around the device. + pub(crate) fn latency_distance(&self, source: usize, target: usize) -> usize { + if source < target { + target - source - 1 + } else { + self.num_sectors - source + target - 1 + } + } + pub fn expected_cost( &self, config: &[usize], @@ -119,17 +128,7 @@ impl ExpectedRetrievalCost { let mut total = 0.0; for source in 0..self.num_sectors { for target in 0..self.num_sectors { - let latency = i64::try_from(latency_distance(self.num_sectors, source, target)) - .map_err(|_| { - crate::traits::EvaluationError::IntegerOverflow( - "converting expected-retrieval latency to i64".to_string(), - ) - })?; - let latency = crate::types::i64_to_exact_f64(latency).map_err(|_| { - crate::traits::EvaluationError::InexactFloatConversion( - "converting expected-retrieval latency to f64".to_string(), - ) - })?; + let latency = self.latency_distance(source, target) as f64; let term = masses[source] * masses[target] * latency; let next = total + term; if !term.is_finite() || !next.is_finite() { @@ -202,16 +201,12 @@ impl Problem for ExpectedRetrievalCost { } impl crate::solvers::BruteForceProblem for ExpectedRetrievalCost { - fn dimensions(&self) -> Vec { - vec![self.num_sectors; self.num_records()] + fn num_variables(&self) -> Result { + Ok(self.num_records()) } -} -fn latency_distance(num_sectors: usize, source: usize, target: usize) -> usize { - if source < target { - target - source - 1 - } else { - num_sectors - source + target - 1 + fn dimension(&self, _variable: usize) -> Result { + Ok(self.num_sectors) } } diff --git a/src/models/misc/factoring.rs b/src/models/misc/factoring.rs index eae8f35da..e943c45ba 100644 --- a/src/models/misc/factoring.rs +++ b/src/models/misc/factoring.rs @@ -238,8 +238,14 @@ impl Problem for Factoring { } impl crate::solvers::BruteForceProblem for Factoring { - fn dimensions(&self) -> Vec { - vec![2; self.m + self.n] + fn num_variables(&self) -> Result { + (self.m).checked_add(self.n).ok_or_else(|| { + crate::solvers::SolveError::IntegerOverflow("computing the coordinate count".into()) + }) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } diff --git a/src/models/misc/feasible_register_assignment.rs b/src/models/misc/feasible_register_assignment.rs index 2e485061c..d68da6521 100644 --- a/src/models/misc/feasible_register_assignment.rs +++ b/src/models/misc/feasible_register_assignment.rs @@ -303,8 +303,12 @@ impl Problem for FeasibleRegisterAssignment { } impl crate::solvers::BruteForceProblem for FeasibleRegisterAssignment { - fn dimensions(&self) -> Vec { - vec![self.num_vertices; self.num_vertices] + fn num_variables(&self) -> Result { + Ok(self.num_vertices) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(self.num_vertices) } } diff --git a/src/models/misc/flow_shop_scheduling.rs b/src/models/misc/flow_shop_scheduling.rs index bd9ee27eb..20724b953 100644 --- a/src/models/misc/flow_shop_scheduling.rs +++ b/src/models/misc/flow_shop_scheduling.rs @@ -35,7 +35,7 @@ inventory::submit! { /// /// # Representation /// -/// Configurations use Lehmer code encoding with `dims() = [n, n-1, ..., 1]`. +/// Configurations use Lehmer code encoding with `coordinate cardinalities = [n, n-1, ..., 1]`. /// A config `[c_0, c_1, ..., c_{n-1}]` where `c_i < n - i` is decoded by /// maintaining a list of available jobs and picking the `c_i`-th element: /// @@ -214,8 +214,12 @@ impl Problem for FlowShopScheduling { } impl crate::solvers::BruteForceProblem for FlowShopScheduling { - fn dimensions(&self) -> Vec { - super::lehmer_dims(self.num_jobs()) + fn num_variables(&self) -> Result { + Ok(self.num_jobs()) + } + + fn dimension(&self, variable: usize) -> Result { + Ok(self.num_jobs() - variable) } } diff --git a/src/models/misc/grouping_by_swapping.rs b/src/models/misc/grouping_by_swapping.rs index 863c0fec6..87639b30e 100644 --- a/src/models/misc/grouping_by_swapping.rs +++ b/src/models/misc/grouping_by_swapping.rs @@ -227,8 +227,12 @@ impl Problem for GroupingBySwapping { } impl crate::solvers::BruteForceProblem for GroupingBySwapping { - fn dimensions(&self) -> Vec { - vec![self.string_len(); self.budget] + fn num_variables(&self) -> Result { + Ok(self.budget) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(self.string_len()) } } diff --git a/src/models/misc/integer_expression_membership.rs b/src/models/misc/integer_expression_membership.rs index e526e32b3..83ca4e717 100644 --- a/src/models/misc/integer_expression_membership.rs +++ b/src/models/misc/integer_expression_membership.rs @@ -245,8 +245,12 @@ impl Problem for IntegerExpressionMembership { } impl crate::solvers::BruteForceProblem for IntegerExpressionMembership { - fn dimensions(&self) -> Vec { - vec![2; self.num_union_nodes()] + fn num_variables(&self) -> Result { + Ok(self.num_union_nodes()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } diff --git a/src/models/misc/job_shop_scheduling.rs b/src/models/misc/job_shop_scheduling.rs index 2be0b6f7e..911ca0eb0 100644 --- a/src/models/misc/job_shop_scheduling.rs +++ b/src/models/misc/job_shop_scheduling.rs @@ -317,12 +317,25 @@ impl Problem for JobShopScheduling { } impl crate::solvers::BruteForceProblem for JobShopScheduling { - fn dimensions(&self) -> Vec { - self.flatten_tasks() - .machine_task_ids - .into_iter() - .flat_map(|machine_tasks| super::lehmer_dims(machine_tasks.len())) - .collect() + fn num_variables(&self) -> Result { + Ok(self.num_tasks()) + } + + fn dimension(&self, variable: usize) -> Result { + let mut offset = variable; + for processor in 0..self.num_processors { + let count = self + .jobs + .iter() + .flatten() + .filter(|&&(machine, _)| machine == processor) + .count(); + if offset < count { + return Ok(count - offset); + } + offset -= count; + } + unreachable!("coordinate index is in range") } } diff --git a/src/models/misc/knapsack.rs b/src/models/misc/knapsack.rs index 95d7fd023..4678efa07 100644 --- a/src/models/misc/knapsack.rs +++ b/src/models/misc/knapsack.rs @@ -197,8 +197,12 @@ impl Problem for Knapsack { } impl crate::solvers::BruteForceProblem for Knapsack { - fn dimensions(&self) -> Vec { - vec![2; self.num_items()] + fn num_variables(&self) -> Result { + Ok(self.num_items()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } diff --git a/src/models/misc/kth_largest_m_tuple.rs b/src/models/misc/kth_largest_m_tuple.rs index 8f6137c42..1bee02dae 100644 --- a/src/models/misc/kth_largest_m_tuple.rs +++ b/src/models/misc/kth_largest_m_tuple.rs @@ -141,12 +141,9 @@ impl KthLargestMTuple { self.sets.len() } - /// Returns the total number of m-tuples (product of set sizes). - pub fn total_tuples(&self) -> usize { - self.sets - .iter() - .try_fold(1usize, |total, set| total.checked_mul(set.len())) - .expect("KthLargestMTuple total tuple count exceeds usize") + /// Returns the total number of elements across the input sets. + pub fn num_elements(&self) -> usize { + self.sets.iter().map(Vec::len).sum() } fn has_at_least_k_qualifying_tuples(&self) -> Result { @@ -208,7 +205,7 @@ impl Problem for KthLargestMTuple { type Solution = (); type Value = Or; - crate::problem_parameters![("num_sets", num_sets), ("total_tuples", total_tuples),]; + crate::problem_parameters![("num_sets", num_sets), ("num_elements", num_elements),]; fn variant() -> Vec<(&'static str, &'static str)> { crate::variant_params![] @@ -220,15 +217,19 @@ impl Problem for KthLargestMTuple { } impl crate::solvers::BruteForceProblem for KthLargestMTuple { - fn dimensions(&self) -> Vec { - vec![] + fn num_variables(&self) -> Result { + Ok(0) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(0) } } -// Best known: brute-force enumeration of all tuples, O(total_tuples * num_sets). +// Best known: brute-force enumeration of all tuples, O(product_i |X_i| * num_sets), bounded by AM-GM. // No sub-exponential exact algorithm is known for the general case. crate::declare_variants! { - default KthLargestMTuple => "total_tuples * num_sets" create KthLargestMTupleCreateSpec, + default KthLargestMTuple => "(num_elements / num_sets)^num_sets * num_sets" create KthLargestMTupleCreateSpec, } crate::register_brute_force! { diff --git a/src/models/misc/longest_common_subsequence.rs b/src/models/misc/longest_common_subsequence.rs index 15bb0c0d2..802897bf7 100644 --- a/src/models/misc/longest_common_subsequence.rs +++ b/src/models/misc/longest_common_subsequence.rs @@ -274,8 +274,14 @@ impl Problem for LongestCommonSubsequence { } impl crate::solvers::BruteForceProblem for LongestCommonSubsequence { - fn dimensions(&self) -> Vec { - vec![self.alphabet_size + 1; self.max_length] + fn num_variables(&self) -> Result { + Ok(self.max_length) + } + + fn dimension(&self, _variable: usize) -> Result { + (self.alphabet_size).checked_add(1usize).ok_or_else(|| { + crate::solvers::SolveError::IntegerOverflow("computing a coordinate cardinality".into()) + }) } } diff --git a/src/models/misc/maximum_likelihood_ranking.rs b/src/models/misc/maximum_likelihood_ranking.rs index 6483aa1a8..687b3d776 100644 --- a/src/models/misc/maximum_likelihood_ranking.rs +++ b/src/models/misc/maximum_likelihood_ranking.rs @@ -182,9 +182,12 @@ impl Problem for MaximumLikelihoodRanking { } impl crate::solvers::BruteForceProblem for MaximumLikelihoodRanking { - fn dimensions(&self) -> Vec { - let n = self.num_items(); - vec![n; n] + fn num_variables(&self) -> Result { + Ok(self.num_items()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(self.num_items()) } } diff --git a/src/models/misc/minimum_axiom_set.rs b/src/models/misc/minimum_axiom_set.rs index cbbe0fd03..8ba882ff9 100644 --- a/src/models/misc/minimum_axiom_set.rs +++ b/src/models/misc/minimum_axiom_set.rs @@ -217,8 +217,12 @@ impl Problem for MinimumAxiomSet { } impl crate::solvers::BruteForceProblem for MinimumAxiomSet { - fn dimensions(&self) -> Vec { - vec![2; self.num_true_sentences()] + fn num_variables(&self) -> Result { + Ok(self.num_true_sentences()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } diff --git a/src/models/misc/minimum_code_generation_one_register.rs b/src/models/misc/minimum_code_generation_one_register.rs index c0b1c6d4c..4cfcc0d5d 100644 --- a/src/models/misc/minimum_code_generation_one_register.rs +++ b/src/models/misc/minimum_code_generation_one_register.rs @@ -338,9 +338,12 @@ impl Problem for MinimumCodeGenerationOneRegister { } impl crate::solvers::BruteForceProblem for MinimumCodeGenerationOneRegister { - fn dimensions(&self) -> Vec { - let n_internal = self.num_internal(); - vec![n_internal; n_internal] + fn num_variables(&self) -> Result { + Ok(self.num_internal()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(self.num_internal()) } } diff --git a/src/models/misc/minimum_code_generation_parallel_assignments.rs b/src/models/misc/minimum_code_generation_parallel_assignments.rs index 405f7a962..2e668c04c 100644 --- a/src/models/misc/minimum_code_generation_parallel_assignments.rs +++ b/src/models/misc/minimum_code_generation_parallel_assignments.rs @@ -175,9 +175,12 @@ impl Problem for MinimumCodeGenerationParallelAssignments { } impl crate::solvers::BruteForceProblem for MinimumCodeGenerationParallelAssignments { - fn dimensions(&self) -> Vec { - let m = self.num_assignments(); - vec![m; m] + fn num_variables(&self) -> Result { + Ok(self.num_assignments()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(self.num_assignments()) } } diff --git a/src/models/misc/minimum_code_generation_unlimited_registers.rs b/src/models/misc/minimum_code_generation_unlimited_registers.rs index fbf5f84ee..4eda7c15a 100644 --- a/src/models/misc/minimum_code_generation_unlimited_registers.rs +++ b/src/models/misc/minimum_code_generation_unlimited_registers.rs @@ -364,9 +364,12 @@ impl Problem for MinimumCodeGenerationUnlimitedRegisters { } impl crate::solvers::BruteForceProblem for MinimumCodeGenerationUnlimitedRegisters { - fn dimensions(&self) -> Vec { - let n_internal = self.num_internal(); - vec![n_internal; n_internal] + fn num_variables(&self) -> Result { + Ok(self.num_internal()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(self.num_internal()) } } diff --git a/src/models/misc/minimum_decision_tree.rs b/src/models/misc/minimum_decision_tree.rs index da7297a4e..f04cf05ec 100644 --- a/src/models/misc/minimum_decision_tree.rs +++ b/src/models/misc/minimum_decision_tree.rs @@ -163,8 +163,17 @@ impl MinimumDecisionTree { } /// Number of internal node slots in the flattened complete binary tree. - fn num_tree_slots(&self) -> usize { - (1usize << (self.num_objects - 1)) - 1 + fn num_tree_slots(&self) -> Result { + self.num_objects + .checked_sub(1) + .and_then(|depth| u32::try_from(depth).ok()) + .and_then(|depth| 1usize.checked_shl(depth)) + .map(|leaves| leaves - 1) + .ok_or_else(|| { + crate::traits::EvaluationError::IntegerOverflow( + "representing the decision-tree witness slots".into(), + ) + }) } /// Sentinel value meaning "this node is a leaf". @@ -176,7 +185,7 @@ impl MinimumDecisionTree { /// or None if the tree is invalid (doesn't identify all objects uniquely). fn simulate(&self, config: &[usize]) -> Result, crate::traits::EvaluationError> { let sentinel = self.leaf_sentinel(); - let max_slots = self.num_tree_slots(); + let max_slots = self.num_tree_slots()?; let mut seen_leaves = std::collections::HashSet::new(); let mut total_depth = 0_i64; @@ -232,7 +241,7 @@ impl Problem for MinimumDecisionTree { config: &Self::Solution, ) -> Result, crate::traits::EvaluationError> { Ok({ - if config.len() != self.num_tree_slots() { + if config.len() != self.num_tree_slots()? { return Err(crate::traits::EvaluationError::InvalidConfiguration( "decision-tree encoding length does not match the instance".into(), )); @@ -247,9 +256,14 @@ impl Problem for MinimumDecisionTree { } impl crate::solvers::BruteForceProblem for MinimumDecisionTree { - fn dimensions(&self) -> Vec { - // Each internal node can hold test 0..num_tests-1 or sentinel (leaf) - vec![self.num_tests + 1; self.num_tree_slots()] + fn num_variables(&self) -> Result { + Ok(self.num_tree_slots()?) + } + + fn dimension(&self, _variable: usize) -> Result { + (self.num_tests).checked_add(1usize).ok_or_else(|| { + crate::solvers::SolveError::IntegerOverflow("computing a coordinate cardinality".into()) + }) } } diff --git a/src/models/misc/minimum_discrete_planar_inverse_kinematics.rs b/src/models/misc/minimum_discrete_planar_inverse_kinematics.rs index f002f1bc5..c6ab52e24 100644 --- a/src/models/misc/minimum_discrete_planar_inverse_kinematics.rs +++ b/src/models/misc/minimum_discrete_planar_inverse_kinematics.rs @@ -113,16 +113,12 @@ impl MinimumDiscretePlanarInverseKinematics { if orientation_samples.len() != n { return Err("orientation_samples must have one entry per link".into()); } - let mut total_configurations = 1_usize; for (link, samples) in orientation_samples.iter().enumerate() { if samples.is_empty() { return Err( format!("link {link} must have at least one candidate orientation").into(), ); } - total_configurations = total_configurations - .checked_mul(samples.len()) - .ok_or("orientation configuration count exceeds usize")?; for (sample, &angle) in samples.iter().enumerate() { if !angle.is_finite() { return Err(format!( @@ -180,16 +176,6 @@ impl MinimumDiscretePlanarInverseKinematics { self.link_lengths.len() } - /// Total number of configurations (product of per-link sample counts): - /// `prod_{j=1}^n m_j`. This is the size of the brute-force search space. - pub fn total_configurations(&self) -> usize { - self.orientation_samples - .iter() - .map(|samples| samples.len()) - .try_fold(1_usize, usize::checked_mul) - .expect("validated orientation configuration count must fit usize") - } - /// Total number of sampled orientations across all links: /// `sum_{j=1}^n m_j`. This is the QUBO variable count for the one-hot /// encoding used by the QUBO reduction. @@ -276,7 +262,6 @@ impl Problem for MinimumDiscretePlanarInverseKinematics { type Value = Min; crate::problem_parameters![ - ("total_configurations", total_configurations), ("num_links", num_links), ("num_orientation_samples", num_orientation_samples), ]; @@ -313,16 +298,17 @@ impl Problem for MinimumDiscretePlanarInverseKinematics { } impl crate::solvers::BruteForceProblem for MinimumDiscretePlanarInverseKinematics { - fn dimensions(&self) -> Vec { - self.orientation_samples - .iter() - .map(|samples| samples.len()) - .collect() + fn num_variables(&self) -> Result { + Ok(self.orientation_samples.len()) + } + + fn dimension(&self, variable: usize) -> Result { + Ok(self.orientation_samples[variable].len()) } } crate::declare_variants! { - default MinimumDiscretePlanarInverseKinematics => "total_configurations", + default MinimumDiscretePlanarInverseKinematics => "(num_orientation_samples / num_links)^num_links", } crate::register_brute_force! { diff --git a/src/models/misc/minimum_disjunctive_normal_form.rs b/src/models/misc/minimum_disjunctive_normal_form.rs index ac2b429c0..7f1bc5ac4 100644 --- a/src/models/misc/minimum_disjunctive_normal_form.rs +++ b/src/models/misc/minimum_disjunctive_normal_form.rs @@ -197,8 +197,12 @@ impl Problem for MinimumDisjunctiveNormalForm { } impl crate::solvers::BruteForceProblem for MinimumDisjunctiveNormalForm { - fn dimensions(&self) -> Vec { - vec![2; self.prime_implicants.len()] + fn num_variables(&self) -> Result { + Ok(self.prime_implicants.len()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } diff --git a/src/models/misc/minimum_external_macro_data_compression.rs b/src/models/misc/minimum_external_macro_data_compression.rs index dbaa37c8f..9141e9061 100644 --- a/src/models/misc/minimum_external_macro_data_compression.rs +++ b/src/models/misc/minimum_external_macro_data_compression.rs @@ -158,17 +158,6 @@ impl MinimumExternalMacroDataCompression { &self.string } - /// Returns the number of valid pointers into D (|s|*(|s|+1)/2). - fn num_pointers(&self) -> usize { - let n = self.string.len(); - n * (n + 1) / 2 - } - - /// Returns the C-slot domain size: alphabet_size + 1 (empty) + num_pointers. - fn c_domain_size(&self) -> usize { - self.alphabet_size + 1 + self.num_pointers() - } - /// Decode a pointer index (offset from alphabet_size+1) into (start, len) /// in the dictionary. Pointers are enumerated as: /// index 0 -> (0, 1), 1 -> (0, 2), ..., n-1 -> (0, n), @@ -322,13 +311,22 @@ impl Problem for MinimumExternalMacroDataCompression { } impl crate::solvers::BruteForceProblem for MinimumExternalMacroDataCompression { - fn dimensions(&self) -> Vec { - let n = self.string.len(); - let d_domain = self.alphabet_size + 1; // symbols + empty - let c_domain = self.c_domain_size(); // symbols + empty + pointers - let mut dims = vec![d_domain; n]; // D-slots - dims.extend(vec![c_domain; n]); // C-slots - dims + fn num_variables(&self) -> Result { + Ok(2 * self.string.len()) + } + + fn dimension(&self, variable: usize) -> Result { + if variable < self.string.len() { + Ok((self.alphabet_size).checked_add(1usize).ok_or_else(|| { + crate::solvers::SolveError::IntegerOverflow( + "computing a search coordinate size".into(), + ) + })?) + } else { + let n = self.string.len() as u128; + let cardinality = self.alphabet_size as u128 + 1 + n * (n + 1) / 2; + Ok(usize::try_from(cardinality)?) + } } } diff --git a/src/models/misc/minimum_fault_detection_test_set.rs b/src/models/misc/minimum_fault_detection_test_set.rs index daadcd56d..51ceacd14 100644 --- a/src/models/misc/minimum_fault_detection_test_set.rs +++ b/src/models/misc/minimum_fault_detection_test_set.rs @@ -335,8 +335,16 @@ impl Problem for MinimumFaultDetectionTestSet { } impl crate::solvers::BruteForceProblem for MinimumFaultDetectionTestSet { - fn dimensions(&self) -> Vec { - vec![2; self.inputs.len() * self.outputs.len()] + fn num_variables(&self) -> Result { + (self.inputs.len()) + .checked_mul(self.outputs.len()) + .ok_or_else(|| { + crate::solvers::SolveError::IntegerOverflow("computing the coordinate count".into()) + }) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } diff --git a/src/models/misc/minimum_internal_macro_data_compression.rs b/src/models/misc/minimum_internal_macro_data_compression.rs index 611981db0..028f09e0d 100644 --- a/src/models/misc/minimum_internal_macro_data_compression.rs +++ b/src/models/misc/minimum_internal_macro_data_compression.rs @@ -285,10 +285,22 @@ impl Problem for MinimumInternalMacroDataCompression { } impl crate::solvers::BruteForceProblem for MinimumInternalMacroDataCompression { - fn dimensions(&self) -> Vec { - let n = self.string.len(); - let domain = self.alphabet_size + n + 1; // literals + EOS + pointers - vec![domain; n] + fn num_variables(&self) -> Result { + Ok(self.string.len()) + } + + fn dimension(&self, _variable: usize) -> Result { + ((self.alphabet_size) + .checked_add(self.string.len()) + .ok_or_else(|| { + crate::solvers::SolveError::IntegerOverflow( + "computing a coordinate cardinality".into(), + ) + })?) + .checked_add(1usize) + .ok_or_else(|| { + crate::solvers::SolveError::IntegerOverflow("computing a coordinate cardinality".into()) + }) } } diff --git a/src/models/misc/minimum_register_sufficiency_for_loops.rs b/src/models/misc/minimum_register_sufficiency_for_loops.rs index fd2bf3465..081f0cd17 100644 --- a/src/models/misc/minimum_register_sufficiency_for_loops.rs +++ b/src/models/misc/minimum_register_sufficiency_for_loops.rs @@ -212,9 +212,12 @@ impl Problem for MinimumRegisterSufficiencyForLoops { } impl crate::solvers::BruteForceProblem for MinimumRegisterSufficiencyForLoops { - fn dimensions(&self) -> Vec { - let n = self.variables.len(); - vec![n; n] + fn num_variables(&self) -> Result { + Ok(self.variables.len()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(self.variables.len()) } } diff --git a/src/models/misc/minimum_tardiness_sequencing.rs b/src/models/misc/minimum_tardiness_sequencing.rs index c8865a5e2..fe9ee747b 100644 --- a/src/models/misc/minimum_tardiness_sequencing.rs +++ b/src/models/misc/minimum_tardiness_sequencing.rs @@ -308,8 +308,12 @@ impl Problem for MinimumTardinessSequencing { } impl crate::solvers::BruteForceProblem for MinimumTardinessSequencing { - fn dimensions(&self) -> Vec { - super::lehmer_dims(self.num_tasks()) + fn num_variables(&self) -> Result { + Ok(self.num_tasks()) + } + + fn dimension(&self, variable: usize) -> Result { + Ok(self.num_tasks() - variable) } } @@ -382,8 +386,12 @@ impl Problem for MinimumTardinessSequencing { } impl crate::solvers::BruteForceProblem for MinimumTardinessSequencing { - fn dimensions(&self) -> Vec { - super::lehmer_dims(self.num_tasks()) + fn num_variables(&self) -> Result { + Ok(self.num_tasks()) + } + + fn dimension(&self, variable: usize) -> Result { + Ok(self.num_tasks() - variable) } } diff --git a/src/models/misc/minimum_weight_and_or_graph.rs b/src/models/misc/minimum_weight_and_or_graph.rs index 879879967..346bbe243 100644 --- a/src/models/misc/minimum_weight_and_or_graph.rs +++ b/src/models/misc/minimum_weight_and_or_graph.rs @@ -350,8 +350,12 @@ impl Problem for MinimumWeightAndOrGraph { } impl crate::solvers::BruteForceProblem for MinimumWeightAndOrGraph { - fn dimensions(&self) -> Vec { - vec![2; self.arcs.len()] + fn num_variables(&self) -> Result { + Ok(self.arcs.len()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } diff --git a/src/models/misc/multiprocessor_scheduling.rs b/src/models/misc/multiprocessor_scheduling.rs index 0cfb11a7c..c9271d6f6 100644 --- a/src/models/misc/multiprocessor_scheduling.rs +++ b/src/models/misc/multiprocessor_scheduling.rs @@ -170,8 +170,12 @@ impl Problem for MultiprocessorScheduling { } impl crate::solvers::BruteForceProblem for MultiprocessorScheduling { - fn dimensions(&self) -> Vec { - vec![self.num_processors; self.num_tasks()] + fn num_variables(&self) -> Result { + Ok(self.num_tasks()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(self.num_processors) } } diff --git a/src/models/misc/non_liveness_free_petri_net.rs b/src/models/misc/non_liveness_free_petri_net.rs index 66fca114a..70c643ee4 100644 --- a/src/models/misc/non_liveness_free_petri_net.rs +++ b/src/models/misc/non_liveness_free_petri_net.rs @@ -429,8 +429,12 @@ impl Problem for NonLivenessFreePetriNet { } impl crate::solvers::BruteForceProblem for NonLivenessFreePetriNet { - fn dimensions(&self) -> Vec { - vec![2; self.num_transitions] + fn num_variables(&self) -> Result { + Ok(self.num_transitions) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } diff --git a/src/models/misc/numerical_3_dimensional_matching.rs b/src/models/misc/numerical_3_dimensional_matching.rs index 2a07faf11..9e3d72ff1 100644 --- a/src/models/misc/numerical_3_dimensional_matching.rs +++ b/src/models/misc/numerical_3_dimensional_matching.rs @@ -217,8 +217,12 @@ impl Problem for Numerical3DimensionalMatching { } impl crate::solvers::BruteForceProblem for Numerical3DimensionalMatching { - fn dimensions(&self) -> Vec { - vec![self.num_groups(); 2 * self.num_groups()] + fn num_variables(&self) -> Result { + Ok(2 * self.num_groups()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(self.num_groups()) } } diff --git a/src/models/misc/numerical_matching_with_target_sums.rs b/src/models/misc/numerical_matching_with_target_sums.rs index bccdc2f7c..c7e653acf 100644 --- a/src/models/misc/numerical_matching_with_target_sums.rs +++ b/src/models/misc/numerical_matching_with_target_sums.rs @@ -173,9 +173,12 @@ impl Problem for NumericalMatchingWithTargetSums { } impl crate::solvers::BruteForceProblem for NumericalMatchingWithTargetSums { - fn dimensions(&self) -> Vec { - let m = self.num_pairs(); - vec![m; m] + fn num_variables(&self) -> Result { + Ok(self.num_pairs()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(self.num_pairs()) } } diff --git a/src/models/misc/open_shop_scheduling.rs b/src/models/misc/open_shop_scheduling.rs index 30680b8cc..9aad80df4 100644 --- a/src/models/misc/open_shop_scheduling.rs +++ b/src/models/misc/open_shop_scheduling.rs @@ -138,7 +138,7 @@ impl OpenShopScheduling { "operation count overflows usize".into(), ) })?; - let horizon = processing_times + processing_times .iter() .flatten() .try_fold(0i64, |total, &time| total.checked_add(time)) @@ -147,14 +147,6 @@ impl OpenShopScheduling { "schedule horizon overflows i64".into(), ) })?; - usize::try_from(horizon) - .ok() - .and_then(|value| value.checked_add(1)) - .ok_or_else(|| { - crate::registry::ConstructionError::IntegerOverflow( - "schedule horizon domain overflows usize".into(), - ) - })?; Ok(Self { num_machines, processing_times, @@ -177,16 +169,8 @@ impl OpenShopScheduling { } /// Return the sum of all processing times, a valid serial-schedule horizon. - pub fn schedule_horizon(&self) -> usize { - self.processing_times - .iter() - .flatten() - .try_fold(0usize, |total, &time| { - usize::try_from(time) - .ok() - .and_then(|time| total.checked_add(time)) - }) - .expect("processing times must fit the brute-force schedule horizon") + pub fn schedule_horizon(&self) -> i64 { + self.processing_times.iter().flatten().sum() } fn finish_time( @@ -284,12 +268,12 @@ impl Problem for OpenShopScheduling { } impl crate::solvers::BruteForceProblem for OpenShopScheduling { - fn dimensions(&self) -> Vec { - let domain = self - .schedule_horizon() - .checked_add(1) - .expect("schedule horizon overflow"); - vec![domain; self.num_jobs() * self.num_machines] + fn num_variables(&self) -> Result { + Ok(self.num_jobs() * self.num_machines) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(usize::try_from(i128::from(self.schedule_horizon()) + 1)?) } } diff --git a/src/models/misc/optimum_communication_spanning_tree.rs b/src/models/misc/optimum_communication_spanning_tree.rs index a2732e7a1..1eb9ec3f0 100644 --- a/src/models/misc/optimum_communication_spanning_tree.rs +++ b/src/models/misc/optimum_communication_spanning_tree.rs @@ -369,8 +369,12 @@ impl Problem for OptimumCommunicationSpanningTree { } impl crate::solvers::BruteForceProblem for OptimumCommunicationSpanningTree { - fn dimensions(&self) -> Vec { - vec![2; self.num_edges()] + fn num_variables(&self) -> Result { + Ok(self.num_edges()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } diff --git a/src/models/misc/paintshop.rs b/src/models/misc/paintshop.rs index e758c9faf..3524e7bb1 100644 --- a/src/models/misc/paintshop.rs +++ b/src/models/misc/paintshop.rs @@ -214,8 +214,12 @@ impl Problem for PaintShop { } impl crate::solvers::BruteForceProblem for PaintShop { - fn dimensions(&self) -> Vec { - vec![2; self.num_cars] + fn num_variables(&self) -> Result { + Ok(self.num_cars) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } diff --git a/src/models/misc/partially_ordered_knapsack.rs b/src/models/misc/partially_ordered_knapsack.rs index af4e40f5b..375a82d22 100644 --- a/src/models/misc/partially_ordered_knapsack.rs +++ b/src/models/misc/partially_ordered_knapsack.rs @@ -344,8 +344,12 @@ impl Problem for PartiallyOrderedKnapsack { } impl crate::solvers::BruteForceProblem for PartiallyOrderedKnapsack { - fn dimensions(&self) -> Vec { - vec![2; self.num_items()] + fn num_variables(&self) -> Result { + Ok(self.num_items()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } diff --git a/src/models/misc/partition.rs b/src/models/misc/partition.rs index 032f4eccc..dba5f04be 100644 --- a/src/models/misc/partition.rs +++ b/src/models/misc/partition.rs @@ -137,8 +137,12 @@ impl Problem for Partition { } impl crate::solvers::BruteForceProblem for Partition { - fn dimensions(&self) -> Vec { - vec![2; self.num_elements()] + fn num_variables(&self) -> Result { + Ok(self.num_elements()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } diff --git a/src/models/misc/precedence_constrained_scheduling.rs b/src/models/misc/precedence_constrained_scheduling.rs index b062b26a2..bb60830be 100644 --- a/src/models/misc/precedence_constrained_scheduling.rs +++ b/src/models/misc/precedence_constrained_scheduling.rs @@ -76,10 +76,8 @@ impl TryFrom for PrecedenceConstraine .to_string() .into()); } - if spec.deadline < 0 || usize::try_from(spec.deadline).is_err() { - return Err("deadline must be nonnegative and fit usize" - .to_string() - .into()); + if spec.deadline < 0 { + return Err("deadline must be nonnegative".to_string().into()); } let precedences = spec.precedences.unwrap_or_default(); if let Some(&(pred, succ)) = precedences @@ -121,10 +119,7 @@ impl PrecedenceConstrainedScheduling { ); assert!(deadline > 0, "deadline must be > 0 when there are tasks"); } - assert!( - deadline >= 0 && usize::try_from(deadline).is_ok(), - "deadline must be nonnegative and fit usize" - ); + assert!(deadline >= 0, "deadline must be nonnegative"); for &(i, j) in &precedences { assert!( i < num_tasks && j < num_tasks, @@ -194,24 +189,26 @@ impl Problem for PrecedenceConstrainedScheduling { "schedule length does not match the tasks".into(), )); } - let deadline = - usize::try_from(self.deadline).expect("validated deadline must fit usize"); - if config.iter().any(|&v| v >= deadline) { + if config + .iter() + .any(|&v| v as i128 >= i128::from(self.deadline)) + { return Err(crate::traits::EvaluationError::InvalidConfiguration( "schedule contains an out-of-range time slot".into(), )); } // Check processor capacity: at most num_processors tasks per time slot - let mut slot_count = vec![0usize; deadline]; + let mut slot_count = std::collections::BTreeMap::new(); for &slot in config { - slot_count[slot] += 1; - if slot_count[slot] > self.num_processors { + let count = slot_count.entry(slot).or_insert(0usize); + *count += 1; + if *count > self.num_processors { return Ok(crate::types::Or(false)); } } // Check precedence constraints: for (i, j), slot[j] >= slot[i] + 1 for &(i, j) in &self.precedences { - if config[j] < config[i] + 1 { + if config[j] <= config[i] { return Ok(crate::types::Or(false)); } } @@ -222,11 +219,12 @@ impl Problem for PrecedenceConstrainedScheduling { } impl crate::solvers::BruteForceProblem for PrecedenceConstrainedScheduling { - fn dimensions(&self) -> Vec { - vec![ - usize::try_from(self.deadline).expect("validated deadline must fit usize"); - self.num_tasks - ] + fn num_variables(&self) -> Result { + Ok(self.num_tasks) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(usize::try_from(self.deadline)?) } } diff --git a/src/models/misc/preemptive_scheduling.rs b/src/models/misc/preemptive_scheduling.rs index 119fae84d..7a55c3308 100644 --- a/src/models/misc/preemptive_scheduling.rs +++ b/src/models/misc/preemptive_scheduling.rs @@ -275,9 +275,14 @@ impl Problem for PreemptiveScheduling { } impl crate::solvers::BruteForceProblem for PreemptiveScheduling { - fn dimensions(&self) -> Vec { - let d = self.d_max(); - vec![2; self.num_tasks() * d] + fn num_variables(&self) -> Result { + (self.num_tasks()).checked_mul(self.d_max()).ok_or_else(|| { + crate::solvers::SolveError::IntegerOverflow("computing the coordinate count".into()) + }) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } diff --git a/src/models/misc/production_planning.rs b/src/models/misc/production_planning.rs index 58e0449cc..0b7d9ce85 100644 --- a/src/models/misc/production_planning.rs +++ b/src/models/misc/production_planning.rs @@ -71,13 +71,8 @@ impl TryFrom for ProductionPlanning { ); } } - if spec.capacities.iter().any(|&capacity| { - usize::try_from(capacity) - .ok() - .and_then(|v| v.checked_add(1)) - .is_none() - }) { - return Err("capacities must fit in usize for dims()".to_string().into()); + if spec.capacities.iter().any(|&capacity| capacity < 0) { + return Err("capacities must be nonnegative".into()); } Ok(Self::new( spec.num_periods, @@ -114,15 +109,6 @@ impl ProductionPlanning { "all per-period vectors must have length num_periods" ); } - assert!( - capacities.iter().all(|&capacity| { - usize::try_from(capacity) - .ok() - .and_then(|value| value.checked_add(1)) - .is_some() - }), - "capacities must fit in usize for dims()" - ); assert!( demands .iter() @@ -200,11 +186,7 @@ impl Problem for ProductionPlanning { let mut total_cost = 0_i64; for (i, &production) in config.iter().enumerate() { - let capacity = match usize::try_from(self.capacities[i]) { - Ok(value) => value, - Err(_) => return Ok(Or(false)), - }; - if production > capacity { + if production as i128 > i128::from(self.capacities[i]) { return Ok(Or(false)); } @@ -289,16 +271,12 @@ impl Problem for ProductionPlanning { } impl crate::solvers::BruteForceProblem for ProductionPlanning { - fn dimensions(&self) -> Vec { - self.capacities - .iter() - .map(|&capacity| { - usize::try_from(capacity) - .ok() - .and_then(|value| value.checked_add(1)) - .expect("capacities validated in constructor") - }) - .collect() + fn num_variables(&self) -> Result { + Ok(self.capacities.len()) + } + + fn dimension(&self, variable: usize) -> Result { + Ok(usize::try_from(i128::from(self.capacities[variable]) + 1)?) } } diff --git a/src/models/misc/rectilinear_picture_compression.rs b/src/models/misc/rectilinear_picture_compression.rs index cd399f591..3266e3a39 100644 --- a/src/models/misc/rectilinear_picture_compression.rs +++ b/src/models/misc/rectilinear_picture_compression.rs @@ -297,8 +297,12 @@ impl Problem for RectilinearPictureCompression { } impl crate::solvers::BruteForceProblem for RectilinearPictureCompression { - fn dimensions(&self) -> Vec { - vec![2; self.maximal_rects.len()] + fn num_variables(&self) -> Result { + Ok(self.maximal_rects.len()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } diff --git a/src/models/misc/register_sufficiency.rs b/src/models/misc/register_sufficiency.rs index ebf2972f7..d478fb965 100644 --- a/src/models/misc/register_sufficiency.rs +++ b/src/models/misc/register_sufficiency.rs @@ -391,8 +391,12 @@ impl Problem for RegisterSufficiency { } impl crate::solvers::BruteForceProblem for RegisterSufficiency { - fn dimensions(&self) -> Vec { - vec![self.num_vertices; self.num_vertices] + fn num_variables(&self) -> Result { + Ok(self.num_vertices) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(self.num_vertices) } } diff --git a/src/models/misc/resource_constrained_scheduling.rs b/src/models/misc/resource_constrained_scheduling.rs index c43d4bdca..1e9a2d205 100644 --- a/src/models/misc/resource_constrained_scheduling.rs +++ b/src/models/misc/resource_constrained_scheduling.rs @@ -254,8 +254,12 @@ impl Problem for ResourceConstrainedScheduling { } impl crate::solvers::BruteForceProblem for ResourceConstrainedScheduling { - fn dimensions(&self) -> Vec { - vec![self.deadline as usize; self.num_tasks()] + fn num_variables(&self) -> Result { + Ok(self.num_tasks()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(usize::try_from(self.deadline)?) } } 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..67a83a47a 100644 --- a/src/models/misc/scheduling_to_minimize_weighted_completion_time.rs +++ b/src/models/misc/scheduling_to_minimize_weighted_completion_time.rs @@ -37,7 +37,7 @@ inventory::submit! { /// # Representation /// /// Each task has a variable in `{0, ..., m-1}` representing its processor -/// assignment, giving `dims() = [m; n]`. +/// assignment, giving `coordinate cardinalities = [m; n]`. /// /// # Example /// @@ -303,8 +303,12 @@ impl Problem for SchedulingToMinimizeWeightedCompletionTime { } impl crate::solvers::BruteForceProblem for SchedulingToMinimizeWeightedCompletionTime { - fn dimensions(&self) -> Vec { - vec![self.num_processors; self.num_tasks()] + fn num_variables(&self) -> Result { + Ok(self.num_tasks()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(self.num_processors) } } diff --git a/src/models/misc/scheduling_with_individual_deadlines.rs b/src/models/misc/scheduling_with_individual_deadlines.rs index 6f8b21658..0dc746fa5 100644 --- a/src/models/misc/scheduling_with_individual_deadlines.rs +++ b/src/models/misc/scheduling_with_individual_deadlines.rs @@ -61,15 +61,6 @@ impl TryFrom for SchedulingWithIndi if spec.deadlines.iter().any(|&deadline| deadline < 0) { return Err("deadlines must be nonnegative".to_string().into()); } - if spec - .deadlines - .iter() - .any(|&deadline| usize::try_from(deadline).is_err()) - { - return Err("deadlines must fit usize to define schedule slots" - .to_string() - .into()); - } let precedences = spec.precedences.unwrap_or_default(); if let Some(&(pred, succ)) = precedences .iter() @@ -106,12 +97,6 @@ impl SchedulingWithIndividualDeadlines { deadlines.iter().all(|&deadline| deadline >= 0), "deadlines must be nonnegative" ); - assert!( - deadlines - .iter() - .all(|&deadline| usize::try_from(deadline).is_ok()), - "deadlines must fit usize to define schedule slots" - ); for &(pred, succ) in &precedences { assert!( pred < num_tasks, @@ -188,15 +173,13 @@ impl Problem for SchedulingWithIndividualDeadlines { } for (&start, &deadline) in config.iter().zip(&self.deadlines) { - let deadline = - usize::try_from(deadline).expect("validated deadline must fit usize"); - if start >= deadline { + if start as i128 >= i128::from(deadline) { return Ok(crate::types::Or(false)); } } for &(pred, succ) in &self.precedences { - if config[pred] + 1 > config[succ] { + if config[pred] >= config[succ] { return Ok(crate::types::Or(false)); } } @@ -217,11 +200,12 @@ impl Problem for SchedulingWithIndividualDeadlines { } impl crate::solvers::BruteForceProblem for SchedulingWithIndividualDeadlines { - fn dimensions(&self) -> Vec { - self.deadlines - .iter() - .map(|&deadline| usize::try_from(deadline).expect("validated deadline must fit usize")) - .collect() + fn num_variables(&self) -> Result { + Ok(self.deadlines.len()) + } + + fn dimension(&self, variable: usize) -> Result { + Ok(usize::try_from(self.deadlines[variable])?) } } 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..47c71350e 100644 --- a/src/models/misc/sequencing_to_minimize_maximum_cumulative_cost.rs +++ b/src/models/misc/sequencing_to_minimize_maximum_cumulative_cost.rs @@ -209,8 +209,12 @@ impl Problem for SequencingToMinimizeMaximumCumulativeCost { } impl crate::solvers::BruteForceProblem for SequencingToMinimizeMaximumCumulativeCost { - fn dimensions(&self) -> Vec { - super::lehmer_dims(self.num_tasks()) + fn num_variables(&self) -> Result { + Ok(self.num_tasks()) + } + + fn dimension(&self, variable: usize) -> Result { + Ok(self.num_tasks() - variable) } } 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 c60474b40..56119e70a 100644 --- a/src/models/misc/sequencing_to_minimize_tardy_task_weight.rs +++ b/src/models/misc/sequencing_to_minimize_tardy_task_weight.rs @@ -31,7 +31,7 @@ inventory::submit! { /// This is the weighted generalization of minimizing the number of tardy tasks /// (problem SS8 in Garey & Johnson, 1979, written $1 || sum w_j U_j$). /// -/// Configurations are direct permutation encodings with `dims() = [n; n]`: +/// Configurations are direct permutation encodings with `coordinate cardinalities = [n; n]`: /// each position holds the index of the task scheduled at that position. /// A configuration is valid iff it is a permutation of `0..n`. #[derive(Debug, Clone, Serialize)] @@ -221,9 +221,12 @@ impl Problem for SequencingToMinimizeTardyTaskWeight { } impl crate::solvers::BruteForceProblem for SequencingToMinimizeTardyTaskWeight { - fn dimensions(&self) -> Vec { - let n = self.num_tasks(); - vec![n; n] + fn num_variables(&self) -> Result { + Ok(self.num_tasks()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(self.num_tasks()) } } 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..eb8143890 100644 --- a/src/models/misc/sequencing_to_minimize_weighted_completion_time.rs +++ b/src/models/misc/sequencing_to_minimize_weighted_completion_time.rs @@ -35,7 +35,7 @@ inventory::submit! { /// and minimizes `sum_t w(t) * C(t)`, where `C(t)` is the completion time of /// task `t`. /// -/// Configurations use Lehmer code with `dims() = [n, n-1, ..., 1]`. +/// Configurations use Lehmer code with `coordinate cardinalities = [n, n-1, ..., 1]`. #[derive(Debug, Clone, Serialize)] pub struct SequencingToMinimizeWeightedCompletionTime { lengths: Vec, @@ -256,8 +256,12 @@ impl Problem for SequencingToMinimizeWeightedCompletionTime { } impl crate::solvers::BruteForceProblem for SequencingToMinimizeWeightedCompletionTime { - fn dimensions(&self) -> Vec { - super::lehmer_dims(self.num_tasks()) + fn num_variables(&self) -> Result { + Ok(self.num_tasks()) + } + + fn dimension(&self, variable: usize) -> Result { + Ok(self.num_tasks() - variable) } } diff --git a/src/models/misc/sequencing_to_minimize_weighted_tardiness.rs b/src/models/misc/sequencing_to_minimize_weighted_tardiness.rs index 08b6d8419..076009147 100644 --- a/src/models/misc/sequencing_to_minimize_weighted_tardiness.rs +++ b/src/models/misc/sequencing_to_minimize_weighted_tardiness.rs @@ -249,8 +249,12 @@ impl Problem for SequencingToMinimizeWeightedTardiness { } impl crate::solvers::BruteForceProblem for SequencingToMinimizeWeightedTardiness { - fn dimensions(&self) -> Vec { - super::lehmer_dims(self.num_tasks()) + fn num_variables(&self) -> Result { + Ok(self.num_tasks()) + } + + fn dimension(&self, variable: usize) -> Result { + Ok(self.num_tasks() - variable) } } 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..e1e56be82 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 @@ -39,7 +39,7 @@ inventory::submit! { /// This is problem SS14 in Garey & Johnson (1979), written /// $1 | s_{ij} | \text{feasibility}$. /// -/// Configurations are direct permutation encodings with `dims() = [n; n]`: +/// Configurations are direct permutation encodings with `coordinate cardinalities = [n; n]`: /// each position holds the index of the task scheduled at that position. /// A configuration is valid iff it is a permutation of `0..n`. #[derive(Debug, Clone, Serialize)] @@ -239,9 +239,12 @@ impl Problem for SequencingWithDeadlinesAndSetUpTimes { } impl crate::solvers::BruteForceProblem for SequencingWithDeadlinesAndSetUpTimes { - fn dimensions(&self) -> Vec { - let n = self.num_tasks(); - vec![n; n] + fn num_variables(&self) -> Result { + Ok(self.num_tasks()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(self.num_tasks()) } } 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 3bc6449ea..af6a27405 100644 --- a/src/models/misc/sequencing_with_release_times_and_deadlines.rs +++ b/src/models/misc/sequencing_with_release_times_and_deadlines.rs @@ -37,7 +37,7 @@ inventory::submit! { /// /// Uses a permutation encoding (Lehmer code), where `config[i]` selects which /// remaining task to schedule next from the pool of unscheduled tasks. -/// `dims() = [n, n-1, ..., 2, 1]`. Tasks are scheduled left-to-right: each +/// `coordinate cardinalities = [n, n-1, ..., 2, 1]`. Tasks are scheduled left-to-right: each /// task starts at `max(release_time, current_time)`. The schedule is feasible /// iff every task finishes by its deadline. /// @@ -167,8 +167,12 @@ impl Problem for SequencingWithReleaseTimesAndDeadlines { } impl crate::solvers::BruteForceProblem for SequencingWithReleaseTimesAndDeadlines { - fn dimensions(&self) -> Vec { - super::lehmer_dims(self.num_tasks()) + fn num_variables(&self) -> Result { + Ok(self.num_tasks()) + } + + fn dimension(&self, variable: usize) -> Result { + Ok(self.num_tasks() - variable) } } diff --git a/src/models/misc/sequencing_within_intervals.rs b/src/models/misc/sequencing_within_intervals.rs index dd7d3deb3..8119c48d7 100644 --- a/src/models/misc/sequencing_within_intervals.rs +++ b/src/models/misc/sequencing_within_intervals.rs @@ -261,8 +261,15 @@ impl Problem for SequencingWithinIntervals { } impl crate::solvers::BruteForceProblem for SequencingWithinIntervals { - fn dimensions(&self) -> Vec { - self.start_slot_counts().collect() + fn num_variables(&self) -> Result { + Ok(self.num_tasks()) + } + + fn dimension(&self, variable: usize) -> Result { + Ok(self + .start_slot_counts() + .nth(variable) + .expect("coordinate index is in range")) } } diff --git a/src/models/misc/shortest_common_supersequence.rs b/src/models/misc/shortest_common_supersequence.rs index b158a5347..e15e8d169 100644 --- a/src/models/misc/shortest_common_supersequence.rs +++ b/src/models/misc/shortest_common_supersequence.rs @@ -238,8 +238,14 @@ impl Problem for ShortestCommonSupersequence { } impl crate::solvers::BruteForceProblem for ShortestCommonSupersequence { - fn dimensions(&self) -> Vec { - vec![self.alphabet_size + 1; self.max_length] + fn num_variables(&self) -> Result { + Ok(self.max_length) + } + + fn dimension(&self, _variable: usize) -> Result { + (self.alphabet_size).checked_add(1usize).ok_or_else(|| { + crate::solvers::SolveError::IntegerOverflow("computing a coordinate cardinality".into()) + }) } } diff --git a/src/models/misc/shortest_common_superstring.rs b/src/models/misc/shortest_common_superstring.rs index 81fee2a1c..00b98217d 100644 --- a/src/models/misc/shortest_common_superstring.rs +++ b/src/models/misc/shortest_common_superstring.rs @@ -202,8 +202,14 @@ impl Problem for ShortestCommonSuperstring { } impl crate::solvers::BruteForceProblem for ShortestCommonSuperstring { - fn dimensions(&self) -> Vec { - vec![self.alphabet_size + 1; self.max_length] + fn num_variables(&self) -> Result { + Ok(self.max_length) + } + + fn dimension(&self, _variable: usize) -> Result { + (self.alphabet_size).checked_add(1usize).ok_or_else(|| { + crate::solvers::SolveError::IntegerOverflow("computing a coordinate cardinality".into()) + }) } } diff --git a/src/models/misc/square_tiling.rs b/src/models/misc/square_tiling.rs index 4f8b4b951..aaf699686 100644 --- a/src/models/misc/square_tiling.rs +++ b/src/models/misc/square_tiling.rs @@ -207,8 +207,14 @@ impl Problem for SquareTiling { } impl crate::solvers::BruteForceProblem for SquareTiling { - fn dimensions(&self) -> Vec { - vec![self.tiles.len(); self.grid_size * self.grid_size] + fn num_variables(&self) -> Result { + (self.grid_size).checked_mul(self.grid_size).ok_or_else(|| { + crate::solvers::SolveError::IntegerOverflow("computing the coordinate count".into()) + }) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(self.tiles.len()) } } diff --git a/src/models/misc/stacker_crane.rs b/src/models/misc/stacker_crane.rs index 6d56ca1b8..a04445f40 100644 --- a/src/models/misc/stacker_crane.rs +++ b/src/models/misc/stacker_crane.rs @@ -368,8 +368,12 @@ impl Problem for StackerCrane { } impl crate::solvers::BruteForceProblem for StackerCrane { - fn dimensions(&self) -> Vec { - vec![self.num_arcs(); self.num_arcs()] + fn num_variables(&self) -> Result { + Ok(self.num_arcs()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(self.num_arcs()) } } diff --git a/src/models/misc/staff_scheduling.rs b/src/models/misc/staff_scheduling.rs index 1d5d60a0d..ce658db02 100644 --- a/src/models/misc/staff_scheduling.rs +++ b/src/models/misc/staff_scheduling.rs @@ -50,14 +50,8 @@ impl TryFrom for StaffScheduling { type Error = crate::registry::ConstructionError; fn try_from(spec: StaffSchedulingCreateSpec) -> Result { - if usize::try_from(spec.num_workers) - .ok() - .and_then(|workers| workers.checked_add(1)) - .is_none() - { - return Err("num_workers must be nonnegative and encodable by dims()" - .to_string() - .into()); + if spec.num_workers < 0 { + return Err("num_workers must be nonnegative".into()); } for (schedule_index, schedule) in spec.schedules.iter().enumerate() { if schedule.len() != spec.requirements.len() { @@ -101,13 +95,7 @@ impl StaffScheduling { requirements: Vec, num_workers: i64, ) -> Self { - assert!( - usize::try_from(num_workers) - .ok() - .and_then(|workers| workers.checked_add(1)) - .is_some(), - "num_workers must be nonnegative and encodable by dims()" - ); + assert!(num_workers >= 0, "num_workers must be nonnegative"); let num_periods = requirements.len(); for (index, schedule) in schedules.iter().enumerate() { @@ -165,13 +153,10 @@ impl StaffScheduling { self.schedules.len() } - fn worker_limit(&self) -> usize { - usize::try_from(self.num_workers) - .expect("validated nonnegative worker count must fit usize") - } - fn worker_counts_valid(&self, config: &[usize]) -> bool { - config.iter().all(|&count| count <= self.worker_limit()) + config + .iter() + .all(|&count| count as i128 <= i128::from(self.num_workers)) } fn within_budget(&self, config: &[usize]) -> Result { @@ -255,8 +240,12 @@ impl Problem for StaffScheduling { } impl crate::solvers::BruteForceProblem for StaffScheduling { - fn dimensions(&self) -> Vec { - vec![self.worker_limit() + 1; self.num_schedules()] + fn num_variables(&self) -> Result { + Ok(self.num_schedules()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(usize::try_from(i128::from(self.num_workers) + 1)?) } } diff --git a/src/models/misc/string_to_string_correction.rs b/src/models/misc/string_to_string_correction.rs index 0343806e8..965b13120 100644 --- a/src/models/misc/string_to_string_correction.rs +++ b/src/models/misc/string_to_string_correction.rs @@ -246,8 +246,12 @@ impl Problem for StringToStringCorrection { } impl crate::solvers::BruteForceProblem for StringToStringCorrection { - fn dimensions(&self) -> Vec { - vec![2 * self.source.len() + 1; self.bound] + fn num_variables(&self) -> Result { + Ok(self.bound) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2 * self.source.len() + 1) } } diff --git a/src/models/misc/subset_product.rs b/src/models/misc/subset_product.rs index 71cd0f4c1..06f5aa938 100644 --- a/src/models/misc/subset_product.rs +++ b/src/models/misc/subset_product.rs @@ -144,8 +144,12 @@ impl Problem for SubsetProduct { } impl crate::solvers::BruteForceProblem for SubsetProduct { - fn dimensions(&self) -> Vec { - vec![2; self.num_elements()] + fn num_variables(&self) -> Result { + Ok(self.num_elements()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } diff --git a/src/models/misc/subset_sum.rs b/src/models/misc/subset_sum.rs index 95611a696..5a8d771c2 100644 --- a/src/models/misc/subset_sum.rs +++ b/src/models/misc/subset_sum.rs @@ -142,8 +142,12 @@ impl Problem for SubsetSum { } impl crate::solvers::BruteForceProblem for SubsetSum { - fn dimensions(&self) -> Vec { - vec![2; self.num_elements()] + fn num_variables(&self) -> Result { + Ok(self.num_elements()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } diff --git a/src/models/misc/sum_of_squares_partition.rs b/src/models/misc/sum_of_squares_partition.rs index 0edae8f31..5275aa009 100644 --- a/src/models/misc/sum_of_squares_partition.rs +++ b/src/models/misc/sum_of_squares_partition.rs @@ -197,8 +197,12 @@ impl Problem for SumOfSquaresPartition { } impl crate::solvers::BruteForceProblem for SumOfSquaresPartition { - fn dimensions(&self) -> Vec { - vec![self.num_groups; self.sizes.len()] + fn num_variables(&self) -> Result { + Ok(self.sizes.len()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(self.num_groups) } } diff --git a/src/models/misc/three_partition.rs b/src/models/misc/three_partition.rs index 9a57d336c..81739756f 100644 --- a/src/models/misc/three_partition.rs +++ b/src/models/misc/three_partition.rs @@ -214,8 +214,12 @@ impl Problem for ThreePartition { } impl crate::solvers::BruteForceProblem for ThreePartition { - fn dimensions(&self) -> Vec { - vec![self.num_groups(); self.num_elements()] + fn num_variables(&self) -> Result { + Ok(self.num_elements()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(self.num_groups()) } } diff --git a/src/models/misc/timetable_design.rs b/src/models/misc/timetable_design.rs index b96bbe294..44aa4dcec 100644 --- a/src/models/misc/timetable_design.rs +++ b/src/models/misc/timetable_design.rs @@ -475,8 +475,12 @@ impl Problem for TimetableDesign { } impl crate::solvers::BruteForceProblem for TimetableDesign { - fn dimensions(&self) -> Vec { - vec![2; self.config_len()] + fn num_variables(&self) -> Result { + Ok(self.config_len()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } diff --git a/src/models/mod.rs b/src/models/mod.rs index 21220aa30..b01593468 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -43,9 +43,8 @@ pub use graph::{ PartialFeedbackEdgeSet, PartitionIntoCliques, PartitionIntoForests, PartitionIntoPathsOfLength2, PartitionIntoPerfectMatchings, PartitionIntoTriangles, PathConstrainedNetworkFlow, RootedTreeArrangement, RuralPostman, ShortestWeightConstrainedPath, - SpinGlass, SteinerTree, SteinerTreeInGraphs, StrongConnectivityAugmentation, - SubgraphIsomorphism, TravelingSalesman, UndirectedFlowLowerBounds, - UndirectedTwoCommodityIntegralFlow, + SpinGlass, SteinerTree, StrongConnectivityAugmentation, SubgraphIsomorphism, TravelingSalesman, + UndirectedFlowLowerBounds, UndirectedTwoCommodityIntegralFlow, }; pub use misc::PartiallyOrderedKnapsack; pub use misc::{ diff --git a/src/models/set/comparative_containment.rs b/src/models/set/comparative_containment.rs index f628c9744..398136d5c 100644 --- a/src/models/set/comparative_containment.rs +++ b/src/models/set/comparative_containment.rs @@ -340,8 +340,12 @@ impl crate::solvers::BruteForceProblem for ComparativeContainment where W: WeightElement + crate::variant::VariantParam, { - fn dimensions(&self) -> Vec { - vec![2; self.universe_size] + fn num_variables(&self) -> Result { + Ok(self.universe_size) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } diff --git a/src/models/set/consecutive_sets.rs b/src/models/set/consecutive_sets.rs index 9fc0aa171..babec6e76 100644 --- a/src/models/set/consecutive_sets.rs +++ b/src/models/set/consecutive_sets.rs @@ -244,9 +244,14 @@ impl Problem for ConsecutiveSets { } impl crate::solvers::BruteForceProblem for ConsecutiveSets { - fn dimensions(&self) -> Vec { - // Each position can be any symbol (0..alphabet_size-1) or "unused" (alphabet_size) - vec![self.alphabet_size + 1; self.bound_k] + fn num_variables(&self) -> Result { + Ok(self.bound_k) + } + + fn dimension(&self, _variable: usize) -> Result { + (self.alphabet_size).checked_add(1usize).ok_or_else(|| { + crate::solvers::SolveError::IntegerOverflow("computing a coordinate cardinality".into()) + }) } } diff --git a/src/models/set/exact_cover_by_3_sets.rs b/src/models/set/exact_cover_by_3_sets.rs index ac6f64852..d9f13cce8 100644 --- a/src/models/set/exact_cover_by_3_sets.rs +++ b/src/models/set/exact_cover_by_3_sets.rs @@ -239,8 +239,12 @@ impl Problem for ExactCoverBy3Sets { } impl crate::solvers::BruteForceProblem for ExactCoverBy3Sets { - fn dimensions(&self) -> Vec { - vec![2; self.subsets.len()] + fn num_variables(&self) -> Result { + Ok(self.subsets.len()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } diff --git a/src/models/set/integer_knapsack.rs b/src/models/set/integer_knapsack.rs index 6080cbdff..a0a38ad05 100644 --- a/src/models/set/integer_knapsack.rs +++ b/src/models/set/integer_knapsack.rs @@ -5,7 +5,6 @@ use crate::registry::ConstructionError; use crate::registry::{FieldInfo, ProblemSchemaEntry}; -use crate::solvers::BruteForceProblem as _; use crate::traits::Problem; use crate::types::Max; use serde::{Deserialize, Serialize}; @@ -115,16 +114,6 @@ impl Problem for IntegerKnapsack { "multiplicity-vector length does not match the items".into(), )); } - let dims = self.dimensions(); - if config - .iter() - .zip(&dims) - .any(|(&count, &dimension)| count >= dimension) - { - return Err(crate::traits::EvaluationError::InvalidConfiguration( - "multiplicity vector contains an out-of-range count".into(), - )); - } let total_size = config .iter() .enumerate() @@ -174,15 +163,14 @@ impl Problem for IntegerKnapsack { } impl crate::solvers::BruteForceProblem for IntegerKnapsack { - fn dimensions(&self) -> Vec { - self.sizes - .iter() - .map(|&s| { - let dimension = i128::from(self.capacity) / i128::from(s) + 1; - usize::try_from(dimension) - .expect("validated integer-knapsack dimension must fit usize") - }) - .collect() + fn num_variables(&self) -> Result { + Ok(self.num_items()) + } + + fn dimension(&self, variable: usize) -> Result { + Ok(usize::try_from( + i128::from(self.capacity / self.sizes[variable]) + 1, + )?) } } @@ -239,15 +227,6 @@ impl TryFrom for IntegerKnapsack { raw.capacity ))); } - for &size in &raw.sizes { - let dimension = i128::from(raw.capacity) / i128::from(size) + 1; - usize::try_from(dimension).map_err(|_| { - ConstructionError::IntegerOverflow(format!( - "knapsack dimension for capacity {} and item size {size} does not fit usize", - raw.capacity - )) - })?; - } Ok(IntegerKnapsack { sizes: raw.sizes, values: raw.values, diff --git a/src/models/set/maximum_set_packing.rs b/src/models/set/maximum_set_packing.rs index 40542427c..798220bf5 100644 --- a/src/models/set/maximum_set_packing.rs +++ b/src/models/set/maximum_set_packing.rs @@ -225,8 +225,12 @@ impl crate::solvers::BruteForceProblem for MaximumSetPacking where W: WeightElement + crate::variant::VariantParam, { - fn dimensions(&self) -> Vec { - vec![2; self.sets.len()] + fn num_variables(&self) -> Result { + Ok(self.sets.len()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } diff --git a/src/models/set/minimum_cardinality_key.rs b/src/models/set/minimum_cardinality_key.rs index 89b3a21ac..81c143fae 100644 --- a/src/models/set/minimum_cardinality_key.rs +++ b/src/models/set/minimum_cardinality_key.rs @@ -156,8 +156,12 @@ impl Problem for MinimumCardinalityKey { } impl crate::solvers::BruteForceProblem for MinimumCardinalityKey { - fn dimensions(&self) -> Vec { - vec![2; self.num_attributes] + fn num_variables(&self) -> Result { + Ok(self.num_attributes) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } diff --git a/src/models/set/minimum_hitting_set.rs b/src/models/set/minimum_hitting_set.rs index ca2aa38d2..6b2b69e35 100644 --- a/src/models/set/minimum_hitting_set.rs +++ b/src/models/set/minimum_hitting_set.rs @@ -168,8 +168,12 @@ impl Problem for MinimumHittingSet { } impl crate::solvers::BruteForceProblem for MinimumHittingSet { - fn dimensions(&self) -> Vec { - vec![2; self.universe_size] + fn num_variables(&self) -> Result { + Ok(self.universe_size) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } diff --git a/src/models/set/minimum_set_covering.rs b/src/models/set/minimum_set_covering.rs index 81748fc00..f1f7de407 100644 --- a/src/models/set/minimum_set_covering.rs +++ b/src/models/set/minimum_set_covering.rs @@ -223,8 +223,12 @@ impl crate::solvers::BruteForceProblem for MinimumSetCovering where W: WeightElement + crate::variant::VariantParam, { - fn dimensions(&self) -> Vec { - vec![2; self.sets.len()] + fn num_variables(&self) -> Result { + Ok(self.sets.len()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } diff --git a/src/models/set/prime_attribute_name.rs b/src/models/set/prime_attribute_name.rs index 42a3c1839..c552c51b0 100644 --- a/src/models/set/prime_attribute_name.rs +++ b/src/models/set/prime_attribute_name.rs @@ -255,8 +255,12 @@ impl Problem for PrimeAttributeName { } impl crate::solvers::BruteForceProblem for PrimeAttributeName { - fn dimensions(&self) -> Vec { - vec![2; self.num_attributes] + fn num_variables(&self) -> Result { + Ok(self.num_attributes) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } diff --git a/src/models/set/rooted_tree_storage_assignment.rs b/src/models/set/rooted_tree_storage_assignment.rs index 1593976d0..c609f70af 100644 --- a/src/models/set/rooted_tree_storage_assignment.rs +++ b/src/models/set/rooted_tree_storage_assignment.rs @@ -238,8 +238,12 @@ impl Problem for RootedTreeStorageAssignment { } impl crate::solvers::BruteForceProblem for RootedTreeStorageAssignment { - fn dimensions(&self) -> Vec { - vec![self.universe_size; self.universe_size] + fn num_variables(&self) -> Result { + Ok(self.universe_size) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(self.universe_size) } } diff --git a/src/models/set/set_basis.rs b/src/models/set/set_basis.rs index be76ee298..8eaba1aa1 100644 --- a/src/models/set/set_basis.rs +++ b/src/models/set/set_basis.rs @@ -201,8 +201,14 @@ impl Problem for SetBasis { } impl crate::solvers::BruteForceProblem for SetBasis { - fn dimensions(&self) -> Vec { - vec![2; self.k * self.universe_size] + fn num_variables(&self) -> Result { + (self.k).checked_mul(self.universe_size).ok_or_else(|| { + crate::solvers::SolveError::IntegerOverflow("computing the coordinate count".into()) + }) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } diff --git a/src/models/set/set_splitting.rs b/src/models/set/set_splitting.rs index 1c7f46a76..adf234449 100644 --- a/src/models/set/set_splitting.rs +++ b/src/models/set/set_splitting.rs @@ -210,8 +210,12 @@ impl Problem for SetSplitting { } impl crate::solvers::BruteForceProblem for SetSplitting { - fn dimensions(&self) -> Vec { - vec![2; self.universe_size] + fn num_variables(&self) -> Result { + Ok(self.universe_size) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } diff --git a/src/models/set/three_dimensional_matching.rs b/src/models/set/three_dimensional_matching.rs index 6b7fb2641..47f38ac38 100644 --- a/src/models/set/three_dimensional_matching.rs +++ b/src/models/set/three_dimensional_matching.rs @@ -179,8 +179,12 @@ impl Problem for ThreeDimensionalMatching { } impl crate::solvers::BruteForceProblem for ThreeDimensionalMatching { - fn dimensions(&self) -> Vec { - vec![2; self.triples.len()] + fn num_variables(&self) -> Result { + Ok(self.triples.len()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } diff --git a/src/models/set/two_dimensional_consecutive_sets.rs b/src/models/set/two_dimensional_consecutive_sets.rs index e14af706f..d0bde6b50 100644 --- a/src/models/set/two_dimensional_consecutive_sets.rs +++ b/src/models/set/two_dimensional_consecutive_sets.rs @@ -224,8 +224,12 @@ impl Problem for TwoDimensionalConsecutiveSets { } impl crate::solvers::BruteForceProblem for TwoDimensionalConsecutiveSets { - fn dimensions(&self) -> Vec { - vec![self.alphabet_size; self.alphabet_size] + fn num_variables(&self) -> Result { + Ok(self.alphabet_size) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(self.alphabet_size) } } diff --git a/src/registry/dyn_problem.rs b/src/registry/dyn_problem.rs index e5938bdc4..16f4595e7 100644 --- a/src/registry/dyn_problem.rs +++ b/src/registry/dyn_problem.rs @@ -1,11 +1,9 @@ -use serde::Serialize; use serde_json::Value; use std::any::Any; use std::collections::BTreeMap; use std::fmt; -use crate::traits::{EvaluationError, Problem}; -use crate::types::SolutionAggregate; +use crate::traits::EvaluationError; /// Format a metric for CLI- and registry-facing dynamic dispatch. /// @@ -19,13 +17,10 @@ where /// Type-erased problem interface for dynamic dispatch. /// -/// Implemented for serializable problems whose values support solution witnesses. +/// Generated for concrete variants at the registration boundary. pub trait DynProblem: Any { - /// Evaluate a configuration and return the CLI-facing metric string. - fn evaluate_dyn(&self, solution: &Value) -> Result; - /// Evaluate a candidate witness, returning `None` when it is infeasible. - /// This validates feasibility, not global optimality. - fn evaluate_witness_dyn(&self, solution: &Value) -> Result, EvaluationError>; + /// Evaluate once and return the display value and whether the configuration is feasible. + fn evaluate_dyn(&self, solution: &Value) -> Result<(String, bool), EvaluationError>; /// Evaluate a configuration and return the result as a serializable JSON value. fn evaluate_json(&self, solution: &Value) -> Result; /// Serialize the problem to a JSON value. @@ -42,57 +37,68 @@ pub trait DynProblem: Any { fn parameters_dyn(&self) -> crate::types::ProblemParameters; } -impl DynProblem for T -where - T: Problem + Serialize + 'static, - T::Solution: serde::de::DeserializeOwned, - T::Value: SolutionAggregate + fmt::Display + Serialize, -{ - fn evaluate_dyn(&self, solution: &Value) -> Result { - let solution = serde::Deserialize::deserialize(solution).map_err(|error| { - EvaluationError::InvalidConfiguration(format!("invalid solution JSON: {error}")) - })?; - Ok(format_metric(&self.evaluate(&solution)?)) - } - - fn evaluate_json(&self, solution: &Value) -> Result { - let solution = serde::Deserialize::deserialize(solution).map_err(|error| { - EvaluationError::InvalidConfiguration(format!("invalid solution JSON: {error}")) - })?; - Ok(serde_json::to_value(self.evaluate(&solution)?).expect("serialize metric failed")) - } - - fn evaluate_witness_dyn(&self, solution: &Value) -> Result, EvaluationError> { - let solution = serde::Deserialize::deserialize(solution).map_err(|error| { - EvaluationError::InvalidConfiguration(format!("invalid solution JSON: {error}")) - })?; - let value = self.evaluate(&solution)?; - Ok(T::Value::contributes_to_solution(&value, &value).then(|| format_metric(&value))) - } - - fn serialize_json(&self) -> Value { - serde_json::to_value(self).expect("serialize failed") - } - - fn as_any(&self) -> &dyn Any { - self - } - - fn problem_name(&self) -> &'static str { - T::NAME - } - - fn variant_map(&self) -> BTreeMap { - crate::export::variant_to_map(T::variant()) - } - - fn parameter_names_dyn(&self) -> &'static [&'static str] { - T::parameter_names() - } - - fn parameters_dyn(&self) -> crate::types::ProblemParameters { - self.parameters() - } +/// Implement the existing dynamic transport boundary for a concrete problem type. +/// +/// Concrete value semantics determine feasibility; no solver capability is required. +#[macro_export] +macro_rules! impl_dyn_problem { + ($ty:ty) => { + impl $crate::registry::DynProblem for $ty { + fn evaluate_dyn( + &self, + solution: &serde_json::Value, + ) -> Result<(String, bool), $crate::traits::EvaluationError> { + let solution = serde::Deserialize::deserialize(solution).map_err(|error| { + $crate::traits::EvaluationError::InvalidConfiguration(format!( + "invalid solution JSON: {error}" + )) + })?; + let value = <$ty as $crate::traits::Problem>::evaluate(self, &solution)?; + Ok(($crate::registry::format_metric(&value), value.is_valid())) + } + + fn evaluate_json( + &self, + solution: &serde_json::Value, + ) -> Result { + let solution = serde::Deserialize::deserialize(solution).map_err(|error| { + $crate::traits::EvaluationError::InvalidConfiguration(format!( + "invalid solution JSON: {error}" + )) + })?; + Ok( + serde_json::to_value(<$ty as $crate::traits::Problem>::evaluate( + self, &solution, + )?) + .expect("serialize metric failed"), + ) + } + + fn serialize_json(&self) -> serde_json::Value { + serde_json::to_value(self).expect("serialize failed") + } + + fn as_any(&self) -> &dyn std::any::Any { + self + } + + fn problem_name(&self) -> &'static str { + <$ty as $crate::traits::Problem>::NAME + } + + fn variant_map(&self) -> std::collections::BTreeMap { + $crate::export::variant_to_map(<$ty as $crate::traits::Problem>::variant()) + } + + fn parameter_names_dyn(&self) -> &'static [&'static str] { + <$ty as $crate::traits::Problem>::parameter_names() + } + + fn parameters_dyn(&self) -> $crate::types::ProblemParameters { + <$ty as $crate::traits::Problem>::parameters(self) + } + } + }; } /// A loaded type-erased problem. diff --git a/src/rules/acyclicpartition_ilp.rs b/src/rules/acyclicpartition_ilp.rs index df8b781e2..7877c3fc7 100644 --- a/src/rules/acyclicpartition_ilp.rs +++ b/src/rules/acyclicpartition_ilp.rs @@ -29,9 +29,12 @@ impl ReductionResult for ReductionAcyclicPartitionToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - - crate::rules::ilp_helpers::one_hot_decode_rows(target_solution, self.n, self.n, 0) + Ok(crate::rules::ilp_helpers::one_hot_decode_rows( + target_solution, + self.n, + self.n, + 0, + )) } } diff --git a/src/rules/balancedcompletebipartitesubgraph_ilp.rs b/src/rules/balancedcompletebipartitesubgraph_ilp.rs index 8c220ba38..008bf47b0 100644 --- a/src/rules/balancedcompletebipartitesubgraph_ilp.rs +++ b/src/rules/balancedcompletebipartitesubgraph_ilp.rs @@ -28,8 +28,6 @@ impl ReductionResult for ReductionBCBSToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution[..self.num_vertices] .iter() .map(|&value| value == 1) diff --git a/src/rules/bicliquecover_bmf.rs b/src/rules/bicliquecover_bmf.rs index bcd5bd822..75a20dba8 100644 --- a/src/rules/bicliquecover_bmf.rs +++ b/src/rules/bicliquecover_bmf.rs @@ -40,8 +40,6 @@ impl ReductionResult for ReductionBicliqueCoverToBMF { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(config_bmf_to_bc(target_solution, self.m, self.n, self.k)) } } diff --git a/src/rules/biconnectivityaugmentation_ilp.rs b/src/rules/biconnectivityaugmentation_ilp.rs index 6fc7dd3bc..009c94c47 100644 --- a/src/rules/biconnectivityaugmentation_ilp.rs +++ b/src/rules/biconnectivityaugmentation_ilp.rs @@ -58,15 +58,6 @@ impl ReductionResult for ReductionBiconnAugToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - if crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)? - .value - .is_none() - { - return Err(crate::rules::ExtractionError::invalid( - "target ILP assignment is infeasible", - )); - } - Ok(target_solution[..self.num_candidates] .iter() .map(|&value| value == 1) diff --git a/src/rules/binpacking_ilp.rs b/src/rules/binpacking_ilp.rs index 24d3cc5a8..5e96b3559 100644 --- a/src/rules/binpacking_ilp.rs +++ b/src/rules/binpacking_ilp.rs @@ -41,9 +41,7 @@ impl ReductionResult for ReductionBPToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - - one_hot_decode_rows(target_solution, self.n, self.n, 0) + Ok(one_hot_decode_rows(target_solution, self.n, self.n, 0)) } } diff --git a/src/rules/bmf_bicliquecover.rs b/src/rules/bmf_bicliquecover.rs index 8f9790636..7c88d5fa2 100644 --- a/src/rules/bmf_bicliquecover.rs +++ b/src/rules/bmf_bicliquecover.rs @@ -86,8 +86,6 @@ impl ReductionResult for ReductionBMFToBicliqueCover { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(config_bc_to_bmf(target_solution, self.m, self.n, self.k)) } } diff --git a/src/rules/bmf_ilp.rs b/src/rules/bmf_ilp.rs index 8944c45e5..33e03f5a2 100644 --- a/src/rules/bmf_ilp.rs +++ b/src/rules/bmf_ilp.rs @@ -29,8 +29,6 @@ impl ReductionResult for ReductionBMFToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - let b = (0..self.m) .map(|i| { (0..self.k) diff --git a/src/rules/bottlenecktravelingsalesman_ilp.rs b/src/rules/bottlenecktravelingsalesman_ilp.rs index 6951b619a..2ba4659dc 100644 --- a/src/rules/bottlenecktravelingsalesman_ilp.rs +++ b/src/rules/bottlenecktravelingsalesman_ilp.rs @@ -27,13 +27,6 @@ impl ReductionResult for ReductionBTSPToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - let value = - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - if !value.is_valid() { - return Err(crate::rules::ExtractionError::invalid( - "target ILP assignment is infeasible", - )); - } let n = self.num_vertices; Ok((0..self.num_edges) .map(|edge| { diff --git a/src/rules/boundedcomponentspanningforest_ilp.rs b/src/rules/boundedcomponentspanningforest_ilp.rs index 96c32f915..bc90dda01 100644 --- a/src/rules/boundedcomponentspanningforest_ilp.rs +++ b/src/rules/boundedcomponentspanningforest_ilp.rs @@ -31,9 +31,7 @@ impl ReductionResult for ReductionBCSFToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - - one_hot_decode_rows(target_solution, self.n, self.k, 0) + Ok(one_hot_decode_rows(target_solution, self.n, self.k, 0)) } } diff --git a/src/rules/capacityassignment_ilp.rs b/src/rules/capacityassignment_ilp.rs index 1b348b59a..e95e31566 100644 --- a/src/rules/capacityassignment_ilp.rs +++ b/src/rules/capacityassignment_ilp.rs @@ -38,14 +38,12 @@ impl ReductionResult for ReductionCAToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - - crate::rules::ilp_helpers::one_hot_decode_rows( + Ok(crate::rules::ilp_helpers::one_hot_decode_rows( target_solution, self.num_links, self.num_capacities, 0, - ) + )) } } diff --git a/src/rules/circuit_ilp.rs b/src/rules/circuit_ilp.rs index 6fa63d452..a86bda996 100644 --- a/src/rules/circuit_ilp.rs +++ b/src/rules/circuit_ilp.rs @@ -40,15 +40,6 @@ impl ReductionResult for ReductionCircuitToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - if crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)? - .value - .is_none() - { - return Err(crate::rules::ExtractionError::invalid( - "target ILP assignment is infeasible", - )); - } - Ok({ self.source_variables .iter() diff --git a/src/rules/circuit_sat.rs b/src/rules/circuit_sat.rs index 34811903a..93a36c07a 100644 --- a/src/rules/circuit_sat.rs +++ b/src/rules/circuit_sat.rs @@ -293,8 +293,6 @@ impl ReductionResult for ReductionCircuitSATToSAT { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution[..self.source_var_count].to_vec()) } } diff --git a/src/rules/circuit_spinglass.rs b/src/rules/circuit_spinglass.rs index 92ee614d4..0bc7c3997 100644 --- a/src/rules/circuit_spinglass.rs +++ b/src/rules/circuit_spinglass.rs @@ -230,14 +230,6 @@ impl ReductionResult for ReductionCircuitToSG { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - let value = - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - if !crate::rules::AggregateReductionResult::extract_value(self, value).0 { - return Err(crate::rules::ExtractionError::invalid( - "SpinGlass energy does not meet the circuit zero-penalty threshold", - )); - } - Ok(self .source_variables .iter() diff --git a/src/rules/closeststring_ilp.rs b/src/rules/closeststring_ilp.rs index 1f7d2a922..97565041f 100644 --- a/src/rules/closeststring_ilp.rs +++ b/src/rules/closeststring_ilp.rs @@ -55,26 +55,12 @@ impl ReductionResult for ReductionClosestStringToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - - let q = self.alphabet_size; - let mut center = Vec::with_capacity(self.string_length); - for position in 0..self.string_length { - let block = &target_solution[position * q..(position + 1) * q]; - let mut selected = block.iter().enumerate().filter(|(_, value)| **value == 1); - let symbol = selected.next().map(|(symbol, _)| symbol).ok_or_else(|| { - crate::rules::ExtractionError::invalid(format!( - "center position {position} has no selected symbol" - )) - })?; - if selected.next().is_some() || block.iter().any(|&value| value > 1) { - return Err(crate::rules::ExtractionError::invalid(format!( - "center position {position} is not one-hot" - ))); - } - center.push(symbol); - } - Ok(center) + Ok(crate::rules::ilp_helpers::one_hot_decode_rows( + target_solution, + self.string_length, + self.alphabet_size, + 0, + )) } } diff --git a/src/rules/closestsubstring_ilp.rs b/src/rules/closestsubstring_ilp.rs index 65416c54c..de280add8 100644 --- a/src/rules/closestsubstring_ilp.rs +++ b/src/rules/closestsubstring_ilp.rs @@ -75,49 +75,31 @@ impl ReductionResult for ReductionClosestSubstringToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - let q = self.alphabet_size; let ell = self.substring_length; let y_base = q * ell; let mut out = Vec::with_capacity(ell + self.window_counts.len()); - for position in 0..ell { - let block = &target_solution[position * q..(position + 1) * q]; - out.push(decode_one_hot(block, "center position", position)?); - } + out.extend(crate::rules::ilp_helpers::one_hot_decode_rows( + target_solution, + ell, + q, + 0, + )); for (string, &window_count) in self.window_counts.iter().enumerate() { let start = y_base + self.window_offsets[string]; - out.push(decode_one_hot( - &target_solution[start..start + window_count], - "string window", - string, - )?); + out.extend(crate::rules::ilp_helpers::one_hot_decode_rows( + target_solution, + 1, + window_count, + start, + )); } Ok(out) } } -fn decode_one_hot( - block: &[i64], - block_name: &str, - block_index: usize, -) -> crate::rules::ExtractionResult { - let mut selected = block.iter().enumerate().filter(|(_, value)| **value == 1); - let index = selected.next().map(|(index, _)| index).ok_or_else(|| { - crate::rules::ExtractionError::invalid(format!( - "{block_name} {block_index} has no selected value" - )) - })?; - if selected.next().is_some() || block.iter().any(|&value| value > 1) { - return Err(crate::rules::ExtractionError::invalid(format!( - "{block_name} {block_index} is not one-hot" - ))); - } - Ok(index) -} - #[reduction( transform = exact { num_vars = "alphabet_size * substring_length + total_num_windows + 1", @@ -166,11 +148,8 @@ impl ReduceTo> for ClosestSubstring { } // Tight upper bound on R: the worst-case Hamming distance over a - // length-ell window is at most ell. Added as a single-term `<=` - // constraint so the solver's bound-tightening pass (which scans for - // exactly this pattern) picks it up. Without this, R defaults to the - // full i64 domain, which severely degrades HiGHS performance even on - // tiny instances. + // length-ell window is at most ell. Restricting R to this range + // preserves every optimal solution. constraints.push(LinearConstraint::le(vec![(r_idx, 1)], ell_i64)); // Window-choice constraints: exactly one window per input string. diff --git a/src/rules/closestvectorproblem_qubo.rs b/src/rules/closestvectorproblem_qubo.rs index 3efa14979..a5f16848f 100644 --- a/src/rules/closestvectorproblem_qubo.rs +++ b/src/rules/closestvectorproblem_qubo.rs @@ -41,28 +41,17 @@ impl ReductionResult for ReductionCVPToQUBO { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - self.encodings .iter() .map(|encoding| { - let offset = encoding.weights.iter().enumerate().try_fold( - 0_i64, - |offset, (index, &weight)| { - if target_solution[encoding.start + index] { - offset.checked_add(weight) - } else { - Some(offset) - } - }, - ); - offset - .and_then(|offset| encoding.lower.checked_add(offset)) - .ok_or_else(|| { - crate::rules::ExtractionError::invalid( - "decoded closest-vector coefficient overflows i64", - ) - }) + let offset: i64 = encoding + .weights + .iter() + .enumerate() + .filter(|(index, _)| target_solution[encoding.start + index]) + .map(|(_, &weight)| weight) + .sum(); + Ok(encoding.lower + offset) }) .collect() } @@ -110,9 +99,7 @@ fn determinant(matrix: &[Vec]) -> Result } fn coefficient_bounds(problem: &Source) -> Result, crate::rules::ReductionError> { - let rows = problem - .independent_rows() - .map_err(crate::rules::ReductionError::construction::)?; + let rows = problem.independent_rows(); let size = problem.num_basis_vectors(); if size == 0 { return Ok(Vec::new()); @@ -293,7 +280,7 @@ impl ReduceTo> for ClosestVectorProblem { .map(move |&weight| (coefficient, weight)) }) .collect::>(); - let mut integer_matrix = vec![vec![0_i64; total_bits]; total_bits]; + let mut integer_matrix = vec![std::collections::BTreeMap::new(); total_bits]; for u in 0..total_bits { let (coefficient_u, weight_u) = bit_terms[u]; let quadratic = gram[coefficient_u][coefficient_u] @@ -304,22 +291,27 @@ impl ReduceTo> for ClosestVectorProblem { .checked_mul(weight_u) .and_then(|value| value.checked_mul(2)) .ok_or_else(|| overflow("computing a closest-vector QUBO diagonal"))?; - integer_matrix[u][u] = quadratic - .checked_add(linear_term) - .ok_or_else(|| overflow("computing a closest-vector QUBO diagonal"))?; + integer_matrix[u].insert( + u, + quadratic + .checked_add(linear_term) + .ok_or_else(|| overflow("computing a closest-vector QUBO diagonal"))?, + ); - for v in (u + 1)..total_bits { - let (coefficient_v, weight_v) = bit_terms[v]; - integer_matrix[u][v] = gram[coefficient_u][coefficient_v] + for (v, &(coefficient_v, weight_v)) in bit_terms.iter().enumerate().skip(u + 1) { + let coefficient = gram[coefficient_u][coefficient_v] .checked_mul(weight_u) .and_then(|value| value.checked_mul(weight_v)) .and_then(|value| value.checked_mul(2)) .ok_or_else(|| overflow("computing a closest-vector QUBO interaction"))?; + if coefficient != 0 { + integer_matrix[u].insert(v, coefficient); + } } } Ok(ReductionCVPToQUBO { - target: QUBO::from_matrix(integer_matrix) + target: QUBO::from_rows(integer_matrix) .map_err(crate::rules::ReductionError::construction::)?, encodings, }) diff --git a/src/rules/clustering_ilp.rs b/src/rules/clustering_ilp.rs index bb3149ae0..2e09cd1ed 100644 --- a/src/rules/clustering_ilp.rs +++ b/src/rules/clustering_ilp.rs @@ -30,14 +30,12 @@ impl ReductionResult for ReductionClusteringToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - - crate::rules::ilp_helpers::one_hot_decode_rows( + Ok(crate::rules::ilp_helpers::one_hot_decode_rows( target_solution, self.num_elements, self.num_clusters, 0, - ) + )) } } diff --git a/src/rules/coloring_ilp.rs b/src/rules/coloring_ilp.rs index 37c6f5944..9473b5fd5 100644 --- a/src/rules/coloring_ilp.rs +++ b/src/rules/coloring_ilp.rs @@ -48,9 +48,12 @@ where &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - - one_hot_decode_rows(target_solution, self.num_vertices, self.num_colors, 0) + Ok(one_hot_decode_rows( + target_solution, + self.num_vertices, + self.num_colors, + 0, + )) } } diff --git a/src/rules/coloring_qubo.rs b/src/rules/coloring_qubo.rs index c29893620..012a1d77b 100644 --- a/src/rules/coloring_qubo.rs +++ b/src/rules/coloring_qubo.rs @@ -33,34 +33,21 @@ impl ReductionResult for ReductionKColoringToQUBO { &self.target } - /// Decode one-hot: for each vertex, find which color bit is 1. + /// Decode a target witness at `feasible_energy` into a proper coloring. + /// At that energy all nonnegative penalties vanish, including one-hot. + /// An optimum above the threshold means the source is uncolorable and + /// is interpreted through `extract_value` before witness extraction. fn extract_solution( &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - let value = - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - if !crate::rules::AggregateReductionResult::extract_value(self, value).0 { - return Err(crate::rules::ExtractionError::invalid( - "target QUBO configuration does not certify a proper coloring", - )); - } - - (0..self.num_vertices) + Ok((0..self.num_vertices) .map(|vertex| { - let mut selected = (0..self.num_colors) - .filter(|&color| target_solution[vertex * self.num_colors + color]); - match (selected.next(), selected.next()) { - (Some(color), None) => Ok(color), - (None, _) => Err(crate::rules::ExtractionError::invalid(format!( - "assignment row {vertex} has no selected color" - ))), - (Some(_), Some(_)) => Err(crate::rules::ExtractionError::invalid(format!( - "assignment row {vertex} has multiple selected colors" - ))), - } + (0..self.num_colors) + .find(|&color| target_solution[vertex * self.num_colors + color]) + .unwrap() }) - .collect() + .collect()) } } @@ -125,7 +112,7 @@ fn reduce_kcoloring_to_qubo( .checked_mul(4) .ok_or_else(|| overflow("computing a one-hot interaction coefficient"))?; - let mut matrix = vec![vec![0i64; nq]; nq]; + let mut matrix = vec![std::collections::BTreeMap::new(); nq]; // Twice the former half-integral objective keeps every coefficient integral. // One-hot penalty: 2P*sum_v (1 - sum_c x_{v,c})^2 @@ -136,7 +123,8 @@ fn reduce_kcoloring_to_qubo( for c in 0..k { let idx = v * k + c; // Diagonal: -2P - matrix[idx][idx] = matrix[idx][idx] + let coefficient = matrix[idx].entry(idx).or_insert(0i64); + *coefficient = coefficient .checked_add(diagonal_penalty) .ok_or_else(|| overflow("adding a coloring diagonal coefficient"))?; } @@ -145,7 +133,8 @@ fn reduce_kcoloring_to_qubo( for c2 in (c1 + 1)..k { let idx1 = v * k + c1; let idx2 = v * k + c2; - matrix[idx1][idx2] = matrix[idx1][idx2] + let coefficient = matrix[idx1].entry(idx2).or_insert(0i64); + *coefficient = coefficient .checked_add(one_hot_interaction) .ok_or_else(|| overflow("adding a one-hot interaction coefficient"))?; } @@ -162,14 +151,15 @@ fn reduce_kcoloring_to_qubo( } else { (idx_v, idx_u) }; - matrix[i][j] = matrix[i][j] + let coefficient = matrix[i].entry(j).or_insert(0i64); + *coefficient = coefficient .checked_add(penalty) .ok_or_else(|| overflow("adding an edge-conflict coefficient"))?; } } Ok(ReductionKColoringToQUBO { - target: QUBO::from_matrix(matrix).map_err(|message| { + target: QUBO::from_rows(matrix).map_err(|message| { crate::rules::ReductionError::construction::, QUBO>( message, ) diff --git a/src/rules/consecutiveblockminimization_ilp.rs b/src/rules/consecutiveblockminimization_ilp.rs index ca25d2c2f..40780bec4 100644 --- a/src/rules/consecutiveblockminimization_ilp.rs +++ b/src/rules/consecutiveblockminimization_ilp.rs @@ -28,9 +28,12 @@ impl ReductionResult for ReductionCBMToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - - one_hot_decode(target_solution, self.num_cols, self.num_cols, 0) + Ok(one_hot_decode( + target_solution, + self.num_cols, + self.num_cols, + 0, + )) } } diff --git a/src/rules/consecutiveonesmatrixaugmentation_ilp.rs b/src/rules/consecutiveonesmatrixaugmentation_ilp.rs index 0941ff0a7..bafcf8a29 100644 --- a/src/rules/consecutiveonesmatrixaugmentation_ilp.rs +++ b/src/rules/consecutiveonesmatrixaugmentation_ilp.rs @@ -29,9 +29,12 @@ impl ReductionResult for ReductionCOMAToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - - one_hot_decode(target_solution, self.num_cols, self.num_cols, 0) + Ok(one_hot_decode( + target_solution, + self.num_cols, + self.num_cols, + 0, + )) } } diff --git a/src/rules/consecutiveonessubmatrix_ilp.rs b/src/rules/consecutiveonessubmatrix_ilp.rs index d279e0913..1d0128c6f 100644 --- a/src/rules/consecutiveonessubmatrix_ilp.rs +++ b/src/rules/consecutiveonessubmatrix_ilp.rs @@ -26,8 +26,6 @@ impl ReductionResult for ReductionCOSToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ // Output the selection bits s_c (first num_cols variables) target_solution[..self.num_cols] diff --git a/src/rules/consistencyofdatabasefrequencytables_ilp.rs b/src/rules/consistencyofdatabasefrequencytables_ilp.rs index 71710293c..5662aa61d 100644 --- a/src/rules/consistencyofdatabasefrequencytables_ilp.rs +++ b/src/rules/consistencyofdatabasefrequencytables_ilp.rs @@ -95,30 +95,17 @@ impl ReductionResult for ReductionCDFTToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ let mut source_solution = Vec::with_capacity(self.source.num_assignment_variables()); for object in 0..self.source.num_objects() { for (attribute, &domain_size) in self.source.attribute_domains().iter().enumerate() { - let mut selected = (0..domain_size).filter(|&candidate| { - target_solution[self.assignment_var_index(object, attribute, candidate)] - == 1 - }); - let value = match (selected.next(), selected.next()) { - (Some(value), None) => value, - (None, _) => { - return Err(crate::rules::ExtractionError::invalid(format!( - "object {object}, attribute {attribute} has no selected value" - ))) - } - (Some(_), Some(_)) => { - return Err(crate::rules::ExtractionError::invalid(format!( - "object {object}, attribute {attribute} has multiple selected values" - ))) - } - }; + let value = (0..domain_size) + .filter(|&candidate| { + target_solution[self.assignment_var_index(object, attribute, candidate)] + == 1 + }) + .sum(); source_solution.push(value); } } diff --git a/src/rules/decisionmaximumindependentset_integralflowbundles.rs b/src/rules/decisionmaximumindependentset_integralflowbundles.rs index 7fdeab6b4..13fd0419a 100644 --- a/src/rules/decisionmaximumindependentset_integralflowbundles.rs +++ b/src/rules/decisionmaximumindependentset_integralflowbundles.rs @@ -31,13 +31,6 @@ impl ReductionResult for ReductionDecisionMISToIFB { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - let feasible = - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - if !feasible.0 { - return Err(crate::rules::ExtractionError::invalid( - "target flow must satisfy conservation, bundle capacities, and the requirement", - )); - } Ok((0..self.num_source_vertices) .map(|i| target_solution[2 * i + 1] == 1) .collect()) diff --git a/src/rules/decisionminimumdominatingset_minimumsummulticenter.rs b/src/rules/decisionminimumdominatingset_minimumsummulticenter.rs index 60d57a4cb..fc9cd8875 100644 --- a/src/rules/decisionminimumdominatingset_minimumsummulticenter.rs +++ b/src/rules/decisionminimumdominatingset_minimumsummulticenter.rs @@ -32,13 +32,6 @@ impl ReductionResult for ReductionDecisionMinimumDominatingSetToMinimumSumMultic &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - let value = - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - if !crate::rules::AggregateReductionResult::extract_value(self, value).0 { - return Err(crate::rules::ExtractionError::invalid( - "target placement does not certify a dominating set within the source bound", - )); - } // Original vertices precede the auxiliary isolated vertices. Ok(target_solution[..self.source_num_vertices].to_vec()) } diff --git a/src/rules/decisionminimumdominatingset_minmaxmulticenter.rs b/src/rules/decisionminimumdominatingset_minmaxmulticenter.rs index 5bcb1bac1..8980e3bbb 100644 --- a/src/rules/decisionminimumdominatingset_minmaxmulticenter.rs +++ b/src/rules/decisionminimumdominatingset_minmaxmulticenter.rs @@ -30,13 +30,6 @@ impl ReductionResult for ReductionDecisionMinimumDominatingSetToMinMaxMulticente &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - let value = - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - if !crate::rules::AggregateReductionResult::extract_value(self, value).0 { - return Err(crate::rules::ExtractionError::invalid( - "target placement does not certify a dominating set: radius must be at most one", - )); - } Ok(target_solution[..self.source_num_vertices].to_vec()) } } diff --git a/src/rules/decisionminimumvertexcover_hamiltoniancircuit.rs b/src/rules/decisionminimumvertexcover_hamiltoniancircuit.rs index 201a2c437..d6d677fde 100644 --- a/src/rules/decisionminimumvertexcover_hamiltoniancircuit.rs +++ b/src/rules/decisionminimumvertexcover_hamiltoniancircuit.rs @@ -1,20 +1,19 @@ //! Reduction from Decision Minimum Vertex Cover to Hamiltonian Circuit. //! //! This implements the gadget construction from Garey & Johnson, Theorem 3.4, -//! on the unit-weight `Decision>` model. +//! on the unit-weight `Decision>` model. use crate::models::decision::Decision; use crate::models::graph::{HamiltonianCircuit, MinimumVertexCover}; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; use crate::topology::{Graph, SimpleGraph}; -use crate::traits::Problem; +use crate::types::One; use std::collections::BTreeSet; #[derive(Debug, Clone)] enum ConstructionKind { - FixedYes { source_cover: Vec }, - FixedNo, + Fixed { source_cover: Vec }, Theorem(TheoremConstruction), } @@ -184,24 +183,12 @@ impl TheoremConstruction { fn decode_solution( &self, - target_problem: &HamiltonianCircuit, - target_solution: &Vec, + target_solution: &[usize], ) -> crate::rules::ExtractionResult> { Ok({ let mut source_cover = vec![false; self.num_source_vertices]; - if !target_problem.evaluate(target_solution)?.0 { - return Err(crate::rules::ExtractionError::invalid( - "target configuration is not a Hamiltonian circuit", - )); - } - let mut positions = vec![usize::MAX; target_solution.len()]; for (idx, &vertex) in target_solution.iter().enumerate() { - if vertex >= positions.len() || positions[vertex] != usize::MAX { - return Err(crate::rules::ExtractionError::invalid( - "target circuit contains an invalid or repeated vertex", - )); - } positions[vertex] = idx; } @@ -222,19 +209,12 @@ impl TheoremConstruction { } } - let selected_count = source_cover.iter().filter(|&&x| x).count(); - if selected_count != self.selector_count || !self.covers_all_edges(&source_cover) { - return Err(crate::rules::ExtractionError::invalid( - "target circuit does not encode a source vertex cover of the required size", - )); - } - source_cover }) } } -/// Result of reducing Decision> to +/// Result of reducing Decision> to /// HamiltonianCircuit. #[derive(Debug, Clone)] pub struct ReductionDecisionMinimumVertexCoverToHamiltonianCircuit { @@ -246,8 +226,7 @@ impl ReductionDecisionMinimumVertexCoverToHamiltonianCircuit { #[cfg(any(test, feature = "example-db"))] fn build_target_witness(&self, source_cover: &[bool]) -> Vec { match &self.construction { - ConstructionKind::FixedYes { .. } => vec![0, 1, 2], - ConstructionKind::FixedNo => Vec::new(), + ConstructionKind::Fixed { .. } => vec![0, 1, 2], ConstructionKind::Theorem(construction) => { construction.build_target_witness(source_cover) } @@ -256,7 +235,7 @@ impl ReductionDecisionMinimumVertexCoverToHamiltonianCircuit { } impl ReductionResult for ReductionDecisionMinimumVertexCoverToHamiltonianCircuit { - type Source = Decision>; + type Source = Decision>; type Target = HamiltonianCircuit; fn target_problem(&self) -> &Self::Target { @@ -267,26 +246,11 @@ impl ReductionResult for ReductionDecisionMinimumVertexCoverToHamiltonianCircuit &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ match &self.construction { - ConstructionKind::FixedYes { source_cover } => { - if self.target.evaluate(target_solution)?.0 { - source_cover.clone() - } else { - return Err(crate::rules::ExtractionError::invalid( - "target configuration is not the fixed Hamiltonian circuit", - )); - } - } - ConstructionKind::FixedNo => { - return Err(crate::rules::ExtractionError::invalid( - "the fixed negative target instance has no extractable witness", - )) - } + ConstructionKind::Fixed { source_cover } => source_cover.clone(), ConstructionKind::Theorem(construction) => { - construction.decode_solution(&self.target, target_solution)? + construction.decode_solution(target_solution)? } } }) @@ -313,30 +277,21 @@ fn insert_edge(edges: &mut BTreeSet<(usize, usize)>, a: usize, b: usize) { num_edges = "the construction size depends on the decision threshold, which is not a problem parameter", } )] -impl ReduceTo> for Decision> { +impl ReduceTo> for Decision> { type Result = ReductionDecisionMinimumVertexCoverToHamiltonianCircuit; fn reduce_to(&self) -> Result { - let weights = self.inner().weights(); - if weights.iter().any(|&weight| weight != 1) { - return Err(crate::rules::ReductionError::invalid_target::< - Decision>, - HamiltonianCircuit, - >( - "Garey-Johnson construction requires unit vertex weights" - )); - } - let num_source_vertices = self.inner().graph().num_vertices(); let raw_bound = *self.bound(); if raw_bound < 0 { return Ok(ReductionDecisionMinimumVertexCoverToHamiltonianCircuit { target: HamiltonianCircuit::new(SimpleGraph::path(3)), - construction: ConstructionKind::FixedNo, + construction: ConstructionKind::Fixed { + source_cover: vec![false; num_source_vertices], + }, }); } - let k = self.k(); let edges = normalize_edges(self.inner().graph().edges()); let mut incident_edges = vec![Vec::new(); num_source_vertices]; for (edge_idx, &(u, v)) in edges.iter().enumerate() { @@ -352,27 +307,30 @@ impl ReduceTo> for Decision= active_count { + if i128::from(raw_bound) >= active_count as i128 { let mut source_cover = vec![false; num_source_vertices]; for vertex in active_vertices { source_cover[vertex] = true; } return Ok(ReductionDecisionMinimumVertexCoverToHamiltonianCircuit { target: HamiltonianCircuit::new(SimpleGraph::cycle(3)), - construction: ConstructionKind::FixedYes { source_cover }, + construction: ConstructionKind::Fixed { source_cover }, }); } - if k == 0 { + if raw_bound == 0 { return Ok(ReductionDecisionMinimumVertexCoverToHamiltonianCircuit { target: HamiltonianCircuit::new(SimpleGraph::path(3)), - construction: ConstructionKind::FixedNo, + construction: ConstructionKind::Fixed { + source_cover: vec![false; num_source_vertices], + }, }); } let construction = TheoremConstruction { num_source_vertices, - selector_count: k, + selector_count: usize::try_from(raw_bound) + .expect("nonnegative bound is smaller than the active vertex count"), edges, incident_edges, }; @@ -426,7 +384,7 @@ impl ReduceTo> for Decision>, + Decision>, HamiltonianCircuit, >("active source vertex has no Hamiltonian gadget path endpoints") })?; @@ -457,7 +415,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ let n = self.num_vertices; // Decode one-hot assignment: permutation[k] = v where x_{v,k} = 1 - one_hot_decode(target_solution, n, n, 0)? + one_hot_decode(target_solution, n, n, 0) }) } } diff --git a/src/rules/directedtwocommodityintegralflow_ilp.rs b/src/rules/directedtwocommodityintegralflow_ilp.rs index 6e644d21f..22194abbf 100644 --- a/src/rules/directedtwocommodityintegralflow_ilp.rs +++ b/src/rules/directedtwocommodityintegralflow_ilp.rs @@ -41,8 +41,6 @@ impl ReductionResult for ReductionD2CIFToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - crate::rules::ilp_helpers::decode_usize_values(&target_solution[..2 * self.num_arcs]) } } diff --git a/src/rules/disjointconnectingpaths_ilp.rs b/src/rules/disjointconnectingpaths_ilp.rs index 5d1269c13..72c8dd44f 100644 --- a/src/rules/disjointconnectingpaths_ilp.rs +++ b/src/rules/disjointconnectingpaths_ilp.rs @@ -40,8 +40,6 @@ impl ReductionResult for ReductionDCPToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - let mut result = vec![false; self.edges.len()]; for (k, &(source, sink)) in self.terminal_pairs.iter().enumerate() { let offset = k * self.num_edge_vars_per_commodity; @@ -71,12 +69,7 @@ impl ReductionResult for ReductionDCPToILP { } } let mut vertex = sink; - while vertex != source { - let (previous, edge) = predecessor[vertex].ok_or_else(|| { - crate::rules::ExtractionError::invalid( - "commodity flow does not connect its terminal pair", - ) - })?; + while let Some((previous, edge)) = predecessor[vertex] { result[edge] = true; vertex = previous; } diff --git a/src/rules/ensemblecomputation_ilp.rs b/src/rules/ensemblecomputation_ilp.rs index 2ed359613..5c342c690 100644 --- a/src/rules/ensemblecomputation_ilp.rs +++ b/src/rules/ensemblecomputation_ilp.rs @@ -42,30 +42,20 @@ impl ReductionResult for ReductionEnsembleComputationToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; let mut config = Vec::with_capacity(2 * self.budget); - let mut inactive = false; for step in 0..self.budget { let active = target_solution[self.activity_base + step]; if active == 0 { - inactive = true; - continue; - } - if active != 1 || inactive { - return Err(crate::rules::ExtractionError::invalid( - "active ensemble-operation slots must form a binary prefix", - )); + break; } for left in [true, false] { - let selected = (0..self.universe_size + step) - .filter(|&operand| target_solution[self.selector_var(left, step, operand)] == 1) - .collect::>(); - if selected.len() != 1 { - return Err(crate::rules::ExtractionError::invalid( - "each active ensemble operation must select exactly one operand per side", - )); - } - config.push(selected[0]); + config.push( + (0..self.universe_size + step) + .filter(|&operand| { + target_solution[self.selector_var(left, step, operand)] == 1 + }) + .sum(), + ); } } let filler = if self.universe_size >= 2 { diff --git a/src/rules/eulerianpath_ilp.rs b/src/rules/eulerianpath_ilp.rs index ce8ec318b..63008d824 100644 --- a/src/rules/eulerianpath_ilp.rs +++ b/src/rules/eulerianpath_ilp.rs @@ -68,56 +68,30 @@ impl ReductionResult for ReductionEulerianPathToILP { /// /// Reads the unique active start arc (`s_a = 1`) and walks the active /// successor relation (`y_{a,b} = 1`) one step at a time, producing an arc - /// permutation of length `m`. Malformed assignments return an extraction - /// error instead of fabricating an ordering. + /// permutation of length `m` under the target path constraints. fn extract_solution( &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ let m = self.num_arcs; if m == 0 { return Ok(Vec::new()); } - // Find the unique active start arc. - let mut current = match (0..m).find(|&a| target_solution[self.s_idx(a)] == 1) { - Some(a) => a, - None => { - return Err(crate::rules::ExtractionError::invalid( - "ILP witness has no active Eulerian-path start arc", - )); - } - }; - - // Walk the active successor relation, recording each visited arc. + let mut current = (0..m) + .filter(|&a| target_solution[self.s_idx(a)] == 1) + .sum(); let mut order = Vec::with_capacity(m); - let mut visited = vec![false; m]; - order.push(current); - visited[current] = true; - - for _ in 1..m { - let next = self + for _ in 0..m { + order.push(current); + current = self .pairs .iter() .enumerate() - .find(|&(k, &(a, _))| a == current && target_solution[k] == 1) - .map(|(_, &(_, b))| b); - - match next { - Some(b) if !visited[b] => { - order.push(b); - visited[b] = true; - current = b; - } - _ => { - return Err(crate::rules::ExtractionError::invalid(format!( - "ILP witness has no unvisited successor for arc {current}", - ))); - } - } + .filter(|&(k, &(a, _))| a == current && target_solution[k] == 1) + .map(|(_, &(_, b))| b) + .sum(); } order }) diff --git a/src/rules/exactcoverby3sets_algebraicequationsovergf2.rs b/src/rules/exactcoverby3sets_algebraicequationsovergf2.rs index ace09c92b..22ee0d325 100644 --- a/src/rules/exactcoverby3sets_algebraicequationsovergf2.rs +++ b/src/rules/exactcoverby3sets_algebraicequationsovergf2.rs @@ -22,8 +22,6 @@ impl ReductionResult for ReductionX3CToAlgebraicEquationsOverGF2 { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution.to_vec()) } } diff --git a/src/rules/exactcoverby3sets_boundeddiameterspanningtree.rs b/src/rules/exactcoverby3sets_boundeddiameterspanningtree.rs index f87ba0c2a..f09cd0fdf 100644 --- a/src/rules/exactcoverby3sets_boundeddiameterspanningtree.rs +++ b/src/rules/exactcoverby3sets_boundeddiameterspanningtree.rs @@ -98,14 +98,6 @@ impl ReductionResult for ReductionX3CToBoundedDiameterSpanningTree { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - let value = - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - if !value.0 { - return Err(crate::rules::ExtractionError::invalid( - "target edge selection is not a feasible bounded-diameter spanning tree", - )); - } - Ok({ let m = self.source_num_subsets; let root_to_set_offset = 2; diff --git a/src/rules/exactcoverby3sets_ilp.rs b/src/rules/exactcoverby3sets_ilp.rs index b42f93864..2c5169224 100644 --- a/src/rules/exactcoverby3sets_ilp.rs +++ b/src/rules/exactcoverby3sets_ilp.rs @@ -25,8 +25,6 @@ impl ReductionResult for ReductionX3CToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution.iter().map(|&value| value == 1).collect()) } } diff --git a/src/rules/exactcoverby3sets_maximumsetpacking.rs b/src/rules/exactcoverby3sets_maximumsetpacking.rs index 60536abc0..cfe4556ee 100644 --- a/src/rules/exactcoverby3sets_maximumsetpacking.rs +++ b/src/rules/exactcoverby3sets_maximumsetpacking.rs @@ -33,8 +33,6 @@ impl ReductionResult for ReductionXC3SToMaximumSetPacking { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution.to_vec()) } } diff --git a/src/rules/exactcoverby3sets_minimumaxiomset.rs b/src/rules/exactcoverby3sets_minimumaxiomset.rs index 5c9827351..b0c481aeb 100644 --- a/src/rules/exactcoverby3sets_minimumaxiomset.rs +++ b/src/rules/exactcoverby3sets_minimumaxiomset.rs @@ -33,8 +33,6 @@ impl ReductionResult for ReductionXC3SToMinimumAxiomSet { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ let set_offset = self.source_universe_size; (0..self.source_num_subsets) diff --git a/src/rules/exactcoverby3sets_minimumfaultdetectiontestset.rs b/src/rules/exactcoverby3sets_minimumfaultdetectiontestset.rs index eb2846373..4c80f5d04 100644 --- a/src/rules/exactcoverby3sets_minimumfaultdetectiontestset.rs +++ b/src/rules/exactcoverby3sets_minimumfaultdetectiontestset.rs @@ -28,8 +28,6 @@ impl ReductionResult for ReductionXC3SToMinimumFaultDetectionTestSet { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution.iter().map(|row| row[0]).collect()) } } diff --git a/src/rules/exactcoverby3sets_staffscheduling.rs b/src/rules/exactcoverby3sets_staffscheduling.rs index 3721b7fb9..980578d21 100644 --- a/src/rules/exactcoverby3sets_staffscheduling.rs +++ b/src/rules/exactcoverby3sets_staffscheduling.rs @@ -37,8 +37,6 @@ impl ReductionResult for ReductionXC3SToStaffScheduling { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution.iter().map(|&count| count > 0).collect()) } } diff --git a/src/rules/exactcoverby3sets_subsetproduct.rs b/src/rules/exactcoverby3sets_subsetproduct.rs index 4084f1a40..3027d4baa 100644 --- a/src/rules/exactcoverby3sets_subsetproduct.rs +++ b/src/rules/exactcoverby3sets_subsetproduct.rs @@ -30,8 +30,6 @@ impl ReductionResult for ReductionX3CToSubsetProduct { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution.to_vec()) } } diff --git a/src/rules/expectedretrievalcost_ilp.rs b/src/rules/expectedretrievalcost_ilp.rs index a2d76cefb..f6968422d 100644 --- a/src/rules/expectedretrievalcost_ilp.rs +++ b/src/rules/expectedretrievalcost_ilp.rs @@ -20,18 +20,6 @@ use crate::reduction; use crate::rules::ilp_helpers::{mccormick_product, one_hot_decode_rows}; use crate::rules::traits::{ReduceTo, ReductionResult}; -/// Compute the latency distance between sectors on a circular device. -/// -/// Returns the number of sectors between source and target (not counting source itself), -/// wrapping around. This matches the `latency_distance` function in the model. -fn latency_distance(num_sectors: usize, source: usize, target: usize) -> usize { - if source < target { - target - source - 1 - } else { - num_sectors - source + target - 1 - } -} - /// Result of reducing ExpectedRetrievalCost to ILP. /// /// Variable layout: @@ -70,9 +58,12 @@ impl ReductionResult for ReductionERCToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - - one_hot_decode_rows(target_solution, self.num_records, self.num_sectors, 0) + Ok(one_hot_decode_rows( + target_solution, + self.num_records, + self.num_sectors, + 0, + )) } } @@ -135,7 +126,7 @@ impl ReduceTo> for ExpectedRetrievalCost { for s in 0..num_sectors { for r2 in 0..num_records { for s2 in 0..num_sectors { - let lat = latency_distance(num_sectors, s, s2) as f64; + let lat = self.latency_distance(s, s2) as f64; if lat > 0.0 { let coeff = lat * probabilities[r] * probabilities[r2]; if coeff.abs() > 0.0 { diff --git a/src/rules/factoring_circuit.rs b/src/rules/factoring_circuit.rs index 330406035..f62be444c 100644 --- a/src/rules/factoring_circuit.rs +++ b/src/rules/factoring_circuit.rs @@ -47,14 +47,6 @@ impl ReductionResult for ReductionFactoringToCircuit { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - let value = - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - if !value.0 { - return Err(crate::rules::ExtractionError::invalid( - "target assignment does not satisfy the multiplication circuit", - )); - } - Ok({ let var_names = self.target.variable_names(); @@ -69,21 +61,16 @@ impl ReductionResult for ReductionFactoringToCircuit { names .iter() .enumerate() - .try_fold(BigUint::zero(), |value, (index, name)| { - let bit = var_map.get(name.as_str()).copied().ok_or_else(|| { - crate::rules::ExtractionError::invalid(format!( - "target circuit does not contain factor variable {name}" - )) - })?; - Ok::(if bit { + .fold(BigUint::zero(), |value, (index, name)| { + if var_map[name.as_str()] { value + (BigUint::one() << index) } else { value - }) + } }) }; - let left = decode(&self.p_vars)?; - let right = decode(&self.q_vars)?; + let left = decode(&self.p_vars); + let right = decode(&self.q_vars); if left <= right { (left, right) } else { diff --git a/src/rules/factoring_ilp.rs b/src/rules/factoring_ilp.rs index eef11666f..10c3006be 100644 --- a/src/rules/factoring_ilp.rs +++ b/src/rules/factoring_ilp.rs @@ -80,8 +80,6 @@ impl ReductionResult for ReductionFactoringToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ // Extract p bits (first factor) let p = (0..self.m) diff --git a/src/rules/feasibleregisterassignment_ilp.rs b/src/rules/feasibleregisterassignment_ilp.rs index 69b181c5d..d43e3a889 100644 --- a/src/rules/feasibleregisterassignment_ilp.rs +++ b/src/rules/feasibleregisterassignment_ilp.rs @@ -33,8 +33,6 @@ impl ReductionResult for ReductionFeasibleRegisterAssignmentToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - crate::rules::ilp_helpers::decode_usize_values(&target_solution[..self.num_vertices]) } } diff --git a/src/rules/flowshopscheduling_ilp.rs b/src/rules/flowshopscheduling_ilp.rs index d5a4d60ee..334c4fec1 100644 --- a/src/rules/flowshopscheduling_ilp.rs +++ b/src/rules/flowshopscheduling_ilp.rs @@ -39,8 +39,6 @@ impl ReductionResult for ReductionFSSToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ let n = self.num_jobs; let m = self.num_machines; diff --git a/src/rules/graph.rs b/src/rules/graph.rs index f89ffbb49..4a302bb4c 100644 --- a/src/rules/graph.rs +++ b/src/rules/graph.rs @@ -11,8 +11,8 @@ //! - JSON export for documentation and visualization use crate::rules::registry::{ - AggregateReduceFn, EdgeCapabilities, ParameterContractError, ReduceFn, ReductionEntry, - ReductionParameterContract, + AggregateReduceFn, EdgeCapabilities, ExecutedStep, ParameterContractError, ReduceFn, + ReductionEntry, ReductionParameterContract, }; use crate::rules::traits::{DynAggregateReductionResult, DynReductionResult}; use crate::types::ProblemParameters; @@ -22,7 +22,6 @@ use petgraph::visit::EdgeRef; use serde::Serialize; use std::any::Any; use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet, VecDeque}; -use std::rc::Rc; type NodePathOrderKey<'a> = (usize, Vec<(&'static str, &'a BTreeMap)>); @@ -1528,21 +1527,51 @@ pub struct MatchedEntry { pub parameter_contract: Result, } +/// Apply already-constructed witness mappings in reverse order. +fn map_solution<'a>( + steps: impl DoubleEndedIterator, + target_solution: &dyn Any, +) -> crate::rules::ExtractionResult> { + let mut steps = steps.rev(); + let first = steps.next().expect("reduction path has no steps"); + let mut solution = first.extract_solution_dyn(target_solution)?; + for step in steps { + solution = step.extract_solution_dyn(solution.as_ref())?; + } + Ok(solution) +} + /// A composed reduction chain produced by [`ReductionGraph::reduce_along_path`]. /// /// Holds the intermediate reduction results from executing a multi-step /// reduction path. Provides access to the final target problem and /// solution extraction back to the source problem space. pub struct ReductionChain { - steps: Vec>, + pub(crate) steps: Vec, } impl ReductionChain { + pub(crate) fn execute( + source: &dyn Any, + reducers: &[ReduceFn], + ) -> Result { + let mut steps: Vec = Vec::with_capacity(reducers.len()); + for reduce in reducers { + let input = steps + .last() + .map(|step| step.witness.target_problem_any()) + .unwrap_or(source); + steps.push(reduce(input)?); + } + Ok(Self { steps }) + } + /// Get the final target problem as a type-erased reference. pub fn target_problem_any(&self) -> &dyn Any { self.steps .last() .expect("ReductionChain has no steps") + .witness .target_problem_any() } @@ -1560,12 +1589,10 @@ impl ReductionChain { &self, target_solution: &T, ) -> crate::rules::ExtractionResult { - let mut steps = self.steps.iter().rev(); - let first = steps.next().expect("ReductionChain has no steps"); - let mut solution = first.extract_solution_dyn(target_solution)?; - for step in steps { - solution = step.extract_solution_dyn(solution.as_ref())?; - } + let solution = map_solution( + self.steps.iter().map(|step| step.witness.as_ref()), + target_solution, + )?; solution .downcast::() .map(|solution| *solution) @@ -1578,11 +1605,14 @@ impl ReductionChain { target_solution: serde_json::Value, ) -> crate::rules::ExtractionResult { let last = self.steps.last().expect("ReductionChain has no steps"); - let mut solution = last.target_solution_from_json(target_solution)?; - for step in self.steps.iter().rev() { - solution = step.extract_solution_dyn(solution.as_ref())?; - } - self.steps[0].source_solution_json(solution.as_ref()) + let solution = last.witness.target_solution_from_json(target_solution)?; + let solution = map_solution( + self.steps.iter().map(|step| step.witness.as_ref()), + solution.as_ref(), + )?; + self.steps[0] + .witness + .source_solution_json(solution.as_ref()) } } @@ -1679,18 +1709,7 @@ impl ReductionGraph { }; edge_fns.push(reduce); } - // Execute the chain - let mut steps: Vec> = Vec::new(); - let step = (edge_fns[0])(source)?; - steps.push(step); - for edge_fn in &edge_fns[1..] { - let step = { - let prev_target = steps.last().unwrap().target_problem_any(); - edge_fn(prev_target)? - }; - steps.push(step); - } - Ok(Some(ReductionChain { steps })) + Ok(Some(ReductionChain::execute(source, &edge_fns)?)) } /// Execute an aggregate-value reduction path on a source problem instance. @@ -1744,7 +1763,7 @@ pub struct ExecutedPath { /// The variant-level path. pub path: ReductionPath, /// The executed reduction steps (one per hop), shared via `Rc`. - steps: Vec>, + steps: Vec, } impl ExecutedPath { @@ -1753,6 +1772,7 @@ impl ExecutedPath { self.steps .last() .expect("ExecutedPath has no steps") + .witness .target_problem_any() } @@ -1765,7 +1785,7 @@ impl ExecutedPath { ReductionGraph::compute_problem_parameters( &target.name, &target.variant, - result.target_problem_any(), + result.witness.target_problem_any(), ) }) .collect() @@ -1776,12 +1796,10 @@ impl ExecutedPath { &self, target_solution: &T, ) -> crate::rules::ExtractionResult { - let mut steps = self.steps.iter().rev(); - let first = steps.next().expect("ExecutedPath has no steps"); - let mut solution = first.extract_solution_dyn(target_solution)?; - for step in steps { - solution = step.extract_solution_dyn(solution.as_ref())?; - } + let solution = map_solution( + self.steps.iter().map(|step| step.witness.as_ref()), + target_solution, + )?; solution .downcast::() .map(|solution| *solution) @@ -1796,8 +1814,7 @@ impl ReductionGraph { paths: &[ReductionPath], source_instance: &dyn Any, ) -> Result, ExecutePathsError> { - let mut prefixes: HashMap, Vec>> = - HashMap::new(); + let mut prefixes: HashMap, ExecutedStep> = HashMap::new(); let mut executed = Vec::with_capacity(paths.len()); let mut batch_source: Option<&ReductionStep> = None; for (path_index, path) in paths.iter().enumerate() { @@ -1815,14 +1832,12 @@ impl ReductionGraph { } else { batch_source = Some(source); } - let source_prefix = vec![source.clone()]; - let mut chain = prefixes.get(&source_prefix).cloned().unwrap_or_default(); - prefixes.entry(source_prefix.clone()).or_default(); - let mut prefix = source_prefix; + let mut chain: Vec = Vec::with_capacity(path.len()); + let mut prefix = vec![source.clone()]; for pair in path.steps.windows(2) { prefix.push(pair[1].clone()); if let Some(cached) = prefixes.get(&prefix) { - chain = cached.clone(); + chain.push(cached.clone()); continue; } let source_node = self @@ -1857,12 +1872,12 @@ impl ReductionGraph { }; let current = chain .last() - .map(|step| step.target_problem_any()) + .map(|step| step.witness.target_problem_any()) .unwrap_or(source_instance); let result = reduce_fn(current) .map_err(|cause| ExecutePathsError::Reduction { path_index, cause })?; - chain.push(Rc::from(result)); - prefixes.insert(prefix.clone(), chain.clone()); + prefixes.insert(prefix.clone(), result.clone()); + chain.push(result); } executed.push(ExecutedPath { path: path.clone(), diff --git a/src/rules/graph_helpers.rs b/src/rules/graph_helpers.rs index 288c734c1..f9e74619e 100644 --- a/src/rules/graph_helpers.rs +++ b/src/rules/graph_helpers.rs @@ -2,82 +2,28 @@ use crate::topology::{Graph, SimpleGraph}; -/// Extract a Hamiltonian cycle vertex ordering from edge-selection configs on complete graphs. -/// -/// Given a graph and a binary `target_solution` over its edges (1 = selected), -/// walks the selected edges to produce a vertex permutation representing the cycle. -/// Returns an error if the selection does not form a valid Hamiltonian cycle. -pub(crate) fn edges_to_cycle_order( - graph: &G, - target_solution: &[bool], -) -> crate::rules::ExtractionResult> { +/// Order the vertices of a selected Hamiltonian cycle. +/// Target feasibility and the reduction's premises establish a single cycle. +pub(crate) fn edges_to_cycle_order(graph: &G, target_solution: &[bool]) -> Vec { let n = graph.num_vertices(); - if n == 0 { - return Ok(vec![]); - } - - let edges = graph.edges(); - if target_solution.len() != edges.len() { - return Err(crate::rules::ExtractionError::invalid(format!( - "expected {} edge-selection values, got {}", - edges.len(), - target_solution.len() - ))); - } - let mut adjacency = vec![Vec::new(); n]; - let mut selected_count = 0usize; - for (idx, &selected) in target_solution.iter().enumerate() { - if !selected { - continue; + for ((u, v), &selected) in graph.edges().into_iter().zip(target_solution) { + if selected { + adjacency[u].push(v); + adjacency[v].push(u); } - let (u, v) = edges[idx]; - adjacency[u].push(v); - adjacency[v].push(u); - selected_count += 1; } - - if selected_count != n || adjacency.iter().any(|neighbors| neighbors.len() != 2) { - return Err(crate::rules::ExtractionError::invalid( - "selected edges do not form a Hamiltonian cycle", - )); - } - let mut order = Vec::with_capacity(n); - let mut visited = vec![false; n]; - let mut prev = None; - let mut current = 0usize; - + let mut previous = n; + let mut current = 0; for _ in 0..n { - if visited[current] { - return Err(crate::rules::ExtractionError::invalid( - "selected edges contain multiple disjoint cycles", - )); - } - visited[current] = true; order.push(current); let neighbors = &adjacency[current]; - let next = match prev { - Some(previous) => { - if neighbors[0] == previous { - neighbors[1] - } else { - neighbors[0] - } - } - None => neighbors[0], - }; - prev = Some(current); + let next = neighbors[usize::from(neighbors[0] == previous)]; + previous = current; current = next; } - - if current != 0 || visited.iter().any(|seen| !seen) { - return Err(crate::rules::ExtractionError::invalid( - "selected edges do not form one Hamiltonian cycle", - )); - } - - Ok(order) + order } /// Build the complement graph edges: edges between all non-adjacent vertex pairs. @@ -93,15 +39,3 @@ pub(crate) fn complement_edges(graph: &SimpleGraph) -> Vec<(usize, usize)> { } edges } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn rejects_disjoint_selected_cycles() { - let graph = SimpleGraph::new(6, vec![(0, 1), (1, 2), (0, 2), (3, 4), (4, 5), (3, 5)]); - - assert!(edges_to_cycle_order(&graph, &[true; 6]).is_err()); - } -} diff --git a/src/rules/graphpartitioning_ilp.rs b/src/rules/graphpartitioning_ilp.rs index 3186696a2..f23d9bbb7 100644 --- a/src/rules/graphpartitioning_ilp.rs +++ b/src/rules/graphpartitioning_ilp.rs @@ -34,8 +34,6 @@ impl ReductionResult for ReductionGraphPartitioningToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution[..self.num_vertices] .iter() .map(|&value| value == 1) diff --git a/src/rules/graphpartitioning_maxcut.rs b/src/rules/graphpartitioning_maxcut.rs index 658bfe136..007ded318 100644 --- a/src/rules/graphpartitioning_maxcut.rs +++ b/src/rules/graphpartitioning_maxcut.rs @@ -26,8 +26,6 @@ impl ReductionResult for ReductionGPToMaxCut { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution.to_vec()) } } diff --git a/src/rules/graphpartitioning_qubo.rs b/src/rules/graphpartitioning_qubo.rs index 9840bc491..6d2ae01e1 100644 --- a/src/rules/graphpartitioning_qubo.rs +++ b/src/rules/graphpartitioning_qubo.rs @@ -28,8 +28,6 @@ impl ReductionResult for ReductionGraphPartitioningToQUBO { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution.to_vec()) } } @@ -52,7 +50,7 @@ impl ReduceTo> for GraphPartitioning { let penalty = edge_count .checked_add(1) .ok_or_else(|| overflow("computing the balance penalty"))?; - let mut matrix = vec![vec![0i64; n]; n]; + let mut matrix = vec![std::collections::BTreeMap::new(); n]; let mut degrees = vec![0usize; n]; let edges = self.graph().edges(); @@ -70,11 +68,11 @@ impl ReduceTo> for GraphPartitioning { .ok_or_else(|| overflow("computing a balance coefficient"))?, ) .ok_or_else(|| overflow("computing a balance coefficient"))?; - row[i] = degree + *row.entry(i).or_insert(0i64) = degree .checked_add(balance_linear) .ok_or_else(|| overflow("combining QUBO diagonal coefficients"))?; - for value in row.iter_mut().skip(i + 1) { - *value = penalty + for j in (i + 1)..n { + *row.entry(j).or_insert(0i64) = penalty .checked_mul(2) .ok_or_else(|| overflow("computing a balance interaction coefficient"))?; } @@ -82,13 +80,14 @@ impl ReduceTo> for GraphPartitioning { for (u, v) in edges { let (lo, hi) = if u < v { (u, v) } else { (v, u) }; - matrix[lo][hi] = matrix[lo][hi] + let coefficient = matrix[lo].entry(hi).or_insert(0i64); + *coefficient = coefficient .checked_sub(2) .ok_or_else(|| overflow("adding a cut interaction coefficient"))?; } Ok(ReductionGraphPartitioningToQUBO { - target: QUBO::from_matrix(matrix).map_err(|message| { + target: QUBO::from_rows(matrix).map_err(|message| { crate::rules::ReductionError::construction::< GraphPartitioning, QUBO, diff --git a/src/rules/hamiltoniancircuit_biconnectivityaugmentation.rs b/src/rules/hamiltoniancircuit_biconnectivityaugmentation.rs index 07576fe0e..1b1a7d4f2 100644 --- a/src/rules/hamiltoniancircuit_biconnectivityaugmentation.rs +++ b/src/rules/hamiltoniancircuit_biconnectivityaugmentation.rs @@ -51,22 +51,8 @@ impl ReductionResult for ReductionHamiltonianCircuitToBiconnectivityAugmentation &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - if !crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)? - .0 - { - return Err(crate::rules::ExtractionError::invalid( - "target augmentation is infeasible", - )); - } - Ok({ let n = self.num_vertices; - if n < 3 { - return Err(crate::rules::ExtractionError::invalid( - "a Hamiltonian circuit requires at least three vertices", - )); - } - // Collect selected edges (those with config value 1) let mut adj: Vec> = vec![vec![]; n]; for (i, &(u, v)) in self.potential_edges.iter().enumerate() { @@ -76,43 +62,17 @@ impl ReductionResult for ReductionHamiltonianCircuitToBiconnectivityAugmentation } } - // Check that every vertex has exactly degree 2 (Hamiltonian cycle) - if adj.iter().any(|neighbors| neighbors.len() != 2) { - return Err(crate::rules::ExtractionError::invalid( - "selected edges do not give every source vertex degree two", - )); - } - - // Walk the cycle starting from vertex 0 let mut circuit = Vec::with_capacity(n); - circuit.push(0); - let mut prev = 0; - let mut current = adj[0][0]; - while current != 0 { + let mut previous = n; + let mut current = 0; + for _ in 0..n { circuit.push(current); - let next = if adj[current][0] == prev { - adj[current][1] - } else { - adj[current][0] - }; - prev = current; + let neighbors = &adj[current]; + let next = neighbors[usize::from(neighbors[0] == previous)]; + previous = current; current = next; - - // Safety: if we've visited more than n vertices, something is wrong - if circuit.len() > n { - return Err(crate::rules::ExtractionError::invalid( - "selected edges revisit a source vertex", - )); - } - } - - if circuit.len() == n { - circuit - } else { - return Err(crate::rules::ExtractionError::invalid( - "selected edges do not form a spanning circuit", - )); } + circuit }) } } diff --git a/src/rules/hamiltoniancircuit_bottlenecktravelingsalesman.rs b/src/rules/hamiltoniancircuit_bottlenecktravelingsalesman.rs index abe6c3783..81056862d 100644 --- a/src/rules/hamiltoniancircuit_bottlenecktravelingsalesman.rs +++ b/src/rules/hamiltoniancircuit_bottlenecktravelingsalesman.rs @@ -27,9 +27,10 @@ impl ReductionResult for ReductionHamiltonianCircuitToBottleneckTravelingSalesma &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - - crate::rules::graph_helpers::edges_to_cycle_order(self.target.graph(), target_solution) + Ok(crate::rules::graph_helpers::edges_to_cycle_order( + self.target.graph(), + target_solution, + )) } } diff --git a/src/rules/hamiltoniancircuit_hamiltonianpath.rs b/src/rules/hamiltoniancircuit_hamiltonianpath.rs index 4d15d9227..85826ae97 100644 --- a/src/rules/hamiltoniancircuit_hamiltonianpath.rs +++ b/src/rules/hamiltoniancircuit_hamiltonianpath.rs @@ -40,40 +40,25 @@ impl ReductionResult for ReductionHamiltonianCircuitToHamiltonianPath { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ let n = self.num_original_vertices; if n == 0 { return Ok(vec![]); } - let v_prime = n; // index of duplicated vertex v' - let s = n + 1; // pendant attached to v=0 - let t = n + 2; // pendant attached to v' - - // The two pendants force any valid witness to have endpoints s and t. - let reversed; - let oriented = match (target_solution.first(), target_solution.last()) { - (Some(&start), Some(&end)) if start == s && end == t => target_solution, - (Some(&start), Some(&end)) if start == t && end == s => { - reversed = target_solution.iter().copied().rev().collect::>(); - reversed.as_slice() - } - _ => { - return Err(crate::rules::ExtractionError::invalid( - "target path does not have the required pendant endpoints", - )) - } - }; - - if oriented.get(1) != Some(&0) || oriented.get(n + 1) != Some(&v_prime) { - return Err(crate::rules::ExtractionError::invalid( - "target path does not traverse the duplicated source vertex correctly", - )); + let s = n + 1; + // Pendant vertices force the path's endpoints; orient from s. + if target_solution[0] == s { + target_solution[1..=n].to_vec() + } else { + target_solution + .iter() + .rev() + .skip(1) + .take(n) + .copied() + .collect() } - - oriented[1..=n].to_vec() }) } } diff --git a/src/rules/hamiltoniancircuit_longestcircuit.rs b/src/rules/hamiltoniancircuit_longestcircuit.rs index 34ae58de9..faa29346c 100644 --- a/src/rules/hamiltoniancircuit_longestcircuit.rs +++ b/src/rules/hamiltoniancircuit_longestcircuit.rs @@ -27,15 +27,10 @@ impl ReductionResult for ReductionHamiltonianCircuitToLongestCircuit { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - let value = - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - if !crate::rules::AggregateReductionResult::extract_value(self, value).0 { - return Err(crate::rules::ExtractionError::invalid( - "target circuit does not certify a Hamiltonian circuit", - )); - } - - crate::rules::graph_helpers::edges_to_cycle_order(self.target.graph(), target_solution) + Ok(crate::rules::graph_helpers::edges_to_cycle_order( + self.target.graph(), + target_solution, + )) } } diff --git a/src/rules/hamiltoniancircuit_quadraticassignment.rs b/src/rules/hamiltoniancircuit_quadraticassignment.rs index 4cd1e5264..a9cf105fd 100644 --- a/src/rules/hamiltoniancircuit_quadraticassignment.rs +++ b/src/rules/hamiltoniancircuit_quadraticassignment.rs @@ -29,14 +29,6 @@ impl ReductionResult for ReductionHamiltonianCircuitToQuadraticAssignment { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - let value = - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - if !crate::rules::AggregateReductionResult::extract_value(self, value).0 { - return Err(crate::rules::ExtractionError::invalid( - "target assignment does not certify a Hamiltonian circuit", - )); - } - // Zero cost makes this permutation itself a Hamiltonian circuit. Ok(target_solution.to_vec()) } diff --git a/src/rules/hamiltoniancircuit_ruralpostman.rs b/src/rules/hamiltoniancircuit_ruralpostman.rs index e1e2d321d..8b81f2b5c 100644 --- a/src/rules/hamiltoniancircuit_ruralpostman.rs +++ b/src/rules/hamiltoniancircuit_ruralpostman.rs @@ -50,8 +50,6 @@ impl ReductionResult for ReductionHamiltonianCircuitToRuralPostman { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ // The target solution is edge multiplicities. // Required edges are indices 0..n (the {v_i^a, v_i^b} edges). @@ -90,11 +88,6 @@ impl ReductionResult for ReductionHamiltonianCircuitToRuralPostman { for _ in 0..n { cycle.push(current); let next = successor[current]; - if next == usize::MAX { - return Err(crate::rules::ExtractionError::invalid( - "target tour does not provide one successor for every source vertex", - )); - } current = next; } @@ -103,7 +96,21 @@ impl ReductionResult for ReductionHamiltonianCircuitToRuralPostman { } } +impl crate::rules::AggregateReductionResult for ReductionHamiltonianCircuitToRuralPostman { + type Source = HamiltonianCircuit; + type Target = RuralPostman; + + fn target_problem(&self) -> &Self::Target { + &self.target + } + + fn extract_value(&self, value: crate::types::Min) -> crate::types::Or { + crate::types::Or(self.n >= 3 && value.0 == Some(2 * self.n as i64)) + } +} + #[reduction( + aggregate = custom, transform = exact { num_vertices = "2 * num_vertices", num_edges = "num_vertices + 2 * num_edges", diff --git a/src/rules/hamiltoniancircuit_stackercrane.rs b/src/rules/hamiltoniancircuit_stackercrane.rs index bccffa686..34f232865 100644 --- a/src/rules/hamiltoniancircuit_stackercrane.rs +++ b/src/rules/hamiltoniancircuit_stackercrane.rs @@ -36,13 +36,6 @@ impl ReductionResult for ReductionHamiltonianCircuitToStackerCrane { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - let value = - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - if !crate::rules::AggregateReductionResult::extract_value(self, value).0 { - return Err(crate::rules::ExtractionError::invalid( - "target tour does not certify a Hamiltonian circuit", - )); - } // Service arc i corresponds to source vertex i. Ok(target_solution.to_vec()) } diff --git a/src/rules/hamiltoniancircuit_strongconnectivityaugmentation.rs b/src/rules/hamiltoniancircuit_strongconnectivityaugmentation.rs index e2592e77e..9fac217bc 100644 --- a/src/rules/hamiltoniancircuit_strongconnectivityaugmentation.rs +++ b/src/rules/hamiltoniancircuit_strongconnectivityaugmentation.rs @@ -31,14 +31,8 @@ impl ReductionResult for ReductionHamiltonianCircuitToStrongConnectivityAugmenta &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ let n = self.n; - if n == 0 { - return Ok(vec![]); - } - // Build directed adjacency from selected arcs. let candidate_arcs = self.target.candidate_arcs(); let mut successors = vec![Vec::new(); n]; @@ -52,20 +46,8 @@ impl ReductionResult for ReductionHamiltonianCircuitToStrongConnectivityAugmenta // Walk the directed cycle starting from vertex 0. let mut order = Vec::with_capacity(n); let mut current = 0; - let mut visited = vec![false; n]; for _ in 0..n { - if visited[current] { - return Err(crate::rules::ExtractionError::invalid( - "selected arcs revisit a source vertex", - )); - } - visited[current] = true; order.push(current); - if successors[current].len() != 1 { - return Err(crate::rules::ExtractionError::invalid( - "selected arcs do not provide one successor for every source vertex", - )); - } current = successors[current][0]; } diff --git a/src/rules/hamiltoniancircuit_travelingsalesman.rs b/src/rules/hamiltoniancircuit_travelingsalesman.rs index 8e90e4ac8..f12128789 100644 --- a/src/rules/hamiltoniancircuit_travelingsalesman.rs +++ b/src/rules/hamiltoniancircuit_travelingsalesman.rs @@ -27,9 +27,10 @@ impl ReductionResult for ReductionHamiltonianCircuitToTravelingSalesman { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - - crate::rules::graph_helpers::edges_to_cycle_order(self.target.graph(), target_solution) + Ok(crate::rules::graph_helpers::edges_to_cycle_order( + self.target.graph(), + target_solution, + )) } } diff --git a/src/rules/hamiltonianpath_degreeconstrainedspanningtree.rs b/src/rules/hamiltonianpath_degreeconstrainedspanningtree.rs index c783517cb..fd12968b1 100644 --- a/src/rules/hamiltonianpath_degreeconstrainedspanningtree.rs +++ b/src/rules/hamiltonianpath_degreeconstrainedspanningtree.rs @@ -25,9 +25,10 @@ impl ReductionResult for ReductionHamiltonianPathToDegreeConstrainedSpanningTree &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - - extract_hamiltonian_order(self.target.graph(), target_solution) + Ok(extract_hamiltonian_order( + self.target.graph(), + target_solution, + )) } } @@ -49,13 +50,10 @@ impl ReduceTo> for HamiltonianPath crate::rules::ExtractionResult> { +fn extract_hamiltonian_order(graph: &SimpleGraph, target_solution: &[bool]) -> Vec { let num_vertices = graph.num_vertices(); if num_vertices < 2 { - return Ok((0..num_vertices).collect()); + return (0..num_vertices).collect(); } let edges = graph.edges(); @@ -74,46 +72,24 @@ fn extract_hamiltonian_order( .filter_map(|(vertex, neighbors)| (neighbors.len() == 1).then_some(vertex)) .collect(); endpoints.sort_unstable(); - if endpoints.len() != 2 { - return Err(crate::rules::ExtractionError::invalid( - "selected edges do not form a Hamiltonian path", - )); - } - let mut order = Vec::with_capacity(num_vertices); - let mut visited = vec![false; num_vertices]; let mut previous = None; let mut current = endpoints[0]; - loop { - if visited[current] { - return Err(crate::rules::ExtractionError::invalid( - "selected edges contain a cycle", - )); - } - visited[current] = true; order.push(current); - - let next = adjacency[current] + match adjacency[current] .iter() .copied() - .find(|&neighbor| Some(neighbor) != previous && !visited[neighbor]); - match next { - Some(next_vertex) => { + .find(|&neighbor| Some(neighbor) != previous) + { + Some(next) => { previous = Some(current); - current = next_vertex; + current = next; } None => break, } } - - if order.len() == num_vertices { - Ok(order) - } else { - Err(crate::rules::ExtractionError::invalid( - "selected edges do not span every source vertex", - )) - } + order } #[cfg(feature = "example-db")] diff --git a/src/rules/hamiltonianpath_ilp.rs b/src/rules/hamiltonianpath_ilp.rs index 378545040..e9001806c 100644 --- a/src/rules/hamiltonianpath_ilp.rs +++ b/src/rules/hamiltonianpath_ilp.rs @@ -39,9 +39,12 @@ impl ReductionResult for ReductionHamiltonianPathToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - - one_hot_decode(target_solution, self.num_vertices, self.num_vertices, 0) + Ok(one_hot_decode( + target_solution, + self.num_vertices, + self.num_vertices, + 0, + )) } } diff --git a/src/rules/hamiltonianpath_isomorphicspanningtree.rs b/src/rules/hamiltonianpath_isomorphicspanningtree.rs index b95b555d4..dd7802124 100644 --- a/src/rules/hamiltonianpath_isomorphicspanningtree.rs +++ b/src/rules/hamiltonianpath_isomorphicspanningtree.rs @@ -32,8 +32,6 @@ impl ReductionResult for ReductionHPToIST { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution.to_vec()) } } diff --git a/src/rules/hamiltonianpathbetweentwovertices_longestpath.rs b/src/rules/hamiltonianpathbetweentwovertices_longestpath.rs index 0a9186ee1..f4d773f77 100644 --- a/src/rules/hamiltonianpathbetweentwovertices_longestpath.rs +++ b/src/rules/hamiltonianpathbetweentwovertices_longestpath.rs @@ -33,14 +33,6 @@ impl ReductionResult for ReductionHPBTVToLP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - let value = - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - if !crate::rules::AggregateReductionResult::extract_value(self, value).0 { - return Err(crate::rules::ExtractionError::invalid( - "target path does not certify a Hamiltonian source-target path", - )); - } - let mut adjacency = vec![Vec::new(); self.target.num_vertices()]; for (&selected, (u, v)) in target_solution.iter().zip(self.target.graph().edges()) { if selected { diff --git a/src/rules/highlyconnecteddeletion_ilp.rs b/src/rules/highlyconnecteddeletion_ilp.rs index 1fb2c0b47..0d72c1f87 100644 --- a/src/rules/highlyconnecteddeletion_ilp.rs +++ b/src/rules/highlyconnecteddeletion_ilp.rs @@ -64,32 +64,15 @@ impl ReductionResult for ReductionHighlyConnectedDeletionToILP { &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" - ))); - } - Ok(self .edges .iter() @@ -117,13 +100,16 @@ fn vertex_count(clusters: &[Vec]) -> usize { /// 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) -> Vec> { +fn enumerate_feasible_clusters( + graph: &SimpleGraph, +) -> Result>, crate::rules::ReductionError> { let n = graph.num_vertices(); - debug_assert!( - n < 64, - "enumerate_feasible_clusters requires n < 64 due to u64 subset mask; got n={}", - n - ); + 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. @@ -132,7 +118,7 @@ fn enumerate_feasible_clusters(graph: &SimpleGraph) -> Vec> { } if n < 3 { - return clusters; + return Ok(clusters); } // Larger feasible clusters by ascending subset bitmask. @@ -147,7 +133,7 @@ fn enumerate_feasible_clusters(graph: &SimpleGraph) -> Vec> { } } - clusters + Ok(clusters) } #[reduction( @@ -165,7 +151,7 @@ impl ReduceTo> for HighlyConnectedDeletion { fn reduce_to(&self) -> Result { let graph = self.graph(); let n = graph.num_vertices(); - let clusters = enumerate_feasible_clusters(graph); + 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. diff --git a/src/rules/ilp_bool_ilp_i64.rs b/src/rules/ilp_bool_ilp_i64.rs index c5bb1df86..bb2a8f9fa 100644 --- a/src/rules/ilp_bool_ilp_i64.rs +++ b/src/rules/ilp_bool_ilp_i64.rs @@ -25,8 +25,6 @@ impl ReductionResult for ReductionBinaryILPToIntILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution.to_vec()) } } diff --git a/src/rules/ilp_casts.rs b/src/rules/ilp_casts.rs deleted file mode 100644 index 586fe6605..000000000 --- a/src/rules/ilp_casts.rs +++ /dev/null @@ -1,110 +0,0 @@ -//! Numeric variant reductions for ILP. - -use crate::models::algebraic::{Comparison, LinearConstraint, VariableDomain, ILP}; -use crate::reduction; -use crate::rules::{ReduceTo, ReductionError, ReductionResult}; -use crate::types::i64_to_exact_f64; - -#[derive(Debug, Clone)] -pub struct ReductionILPToFloat { - source: ILP, - target: ILP, -} - -impl ReductionILPToFloat { - fn new(source: &ILP) -> Result { - let convert = |coefficient: i64| { - i64_to_exact_f64(coefficient) - .map_err(ReductionError::inexact_float_conversion::, ILP>) - }; - let constraints = source - .constraints() - .iter() - .map(|constraint| { - let terms = constraint - .terms() - .iter() - .map(|&(variable, coefficient)| Ok((variable, convert(coefficient)?))) - .collect::, ReductionError>>()?; - let rhs = convert(constraint.rhs())?; - Ok(match constraint.comparison() { - Comparison::Le => LinearConstraint::le(terms, rhs), - Comparison::Ge => LinearConstraint::ge(terms, rhs), - Comparison::Eq => LinearConstraint::eq(terms, rhs), - }) - }) - .collect::, ReductionError>>()?; - let objective = source - .objective() - .iter() - .map(|&(variable, coefficient)| Ok((variable, convert(coefficient)?))) - .collect::, ReductionError>>()?; - let target = ILP::with_variables( - source.variables().to_vec(), - constraints, - objective, - source.sense(), - ) - .map_err(ReductionError::construction::, ILP>)?; - Ok(Self { - source: source.clone(), - target, - }) - } -} - -impl ReductionResult for ReductionILPToFloat { - type Source = ILP; - type Target = ILP; - - fn target_problem(&self) -> &Self::Target { - &self.target - } - - fn extract_solution( - &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - if !self.source.is_feasible(target_solution)? { - return Err(crate::rules::ExtractionError::invalid( - "the floating-point assignment violates the source integer ILP", - )); - } - Ok(target_solution.clone()) - } -} - -#[reduction( - transform = exact { - num_vars = "num_vars", - num_constraints = "num_constraints", - num_nonzeros = "num_nonzeros", - }, -)] -impl ReduceTo> for ILP { - type Result = ReductionILPToFloat; - - fn reduce_to(&self) -> Result { - ReductionILPToFloat::new(self) - } -} - -#[reduction( - transform = exact { - num_vars = "num_vars", - num_constraints = "num_constraints", - num_nonzeros = "num_nonzeros", - }, -)] -impl ReduceTo> for ILP { - type Result = ReductionILPToFloat; - - fn reduce_to(&self) -> Result { - ReductionILPToFloat::new(self) - } -} - -#[cfg(test)] -#[path = "../unit_tests/rules/ilp_casts.rs"] -mod tests; diff --git a/src/rules/ilp_helpers.rs b/src/rules/ilp_helpers.rs index 4f982bdc5..48d8dd728 100644 --- a/src/rules/ilp_helpers.rs +++ b/src/rules/ilp_helpers.rs @@ -43,62 +43,34 @@ pub fn mccormick_product>( ] } -/// Decode one selected item from each slot of a column-major one-hot matrix. +/// Decode a column-major assignment whose constraints select one item per slot. pub fn one_hot_decode( solution: &[i64], num_items: usize, num_slots: usize, var_offset: usize, -) -> crate::rules::ExtractionResult> { - let assignment: Vec = (0..num_slots) +) -> Vec { + (0..num_slots) .map(|slot| { - let mut selected = - (0..num_items).filter(|&item| solution[var_offset + item * num_slots + slot] == 1); - let item = selected.next().ok_or_else(|| { - crate::rules::ExtractionError::invalid(format!( - "assignment slot {slot} has no selected item" - )) - })?; - if selected.next().is_some() { - return Err(crate::rules::ExtractionError::invalid(format!( - "assignment slot {slot} has multiple selected items" - ))); - } - Ok(item) + (0..num_items) + .filter(|&item| solution[var_offset + item * num_slots + slot] == 1) + .sum() }) - .collect::>()?; - - let mut assigned = vec![false; num_items]; - for &item in &assignment { - if std::mem::replace(&mut assigned[item], true) { - return Err(crate::rules::ExtractionError::invalid(format!( - "item {item} is selected for multiple assignment slots" - ))); - } - } - Ok(assignment) + .collect() } -/// Decode one selected column from each row of a row-major one-hot matrix. +/// Decode a row-major assignment whose constraints select one column per row. pub fn one_hot_decode_rows( solution: &[i64], num_rows: usize, num_columns: usize, var_offset: usize, -) -> crate::rules::ExtractionResult> { +) -> Vec { (0..num_rows) .map(|row| { - let mut selected = (0..num_columns) - .filter(|&column| solution[var_offset + row * num_columns + column] == 1); - match (selected.next(), selected.next()) { - (Some(column), None) => Ok(column), - (None, _) => Err(crate::rules::ExtractionError::invalid(format!( - "assignment row {row} has no selected column" - ))), - (Some(_), Some(_)) => Err(crate::rules::ExtractionError::invalid(format!( - "assignment row {row} has multiple selected columns" - ))), - } + (0..num_columns) + .filter(|&column| solution[var_offset + row * num_columns + column] == 1) + .sum() }) .collect() } diff --git a/src/rules/ilp_i64_ilp_bool.rs b/src/rules/ilp_i64_ilp_bool.rs index ea783e12f..2d53911cf 100644 --- a/src/rules/ilp_i64_ilp_bool.rs +++ b/src/rules/ilp_i64_ilp_bool.rs @@ -84,29 +84,20 @@ impl ReductionResult for ReductionIntILPToBinaryILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - self.encodings + Ok(self + .encodings .iter() .map(|encoding| { - encoding.weights.iter().enumerate().try_fold( - encoding.lower_bound, - |value, (offset, &weight)| { - let term = weight - .checked_mul(target_solution[encoding.start + offset]) - .ok_or_else(|| { - crate::rules::ExtractionError::invalid( - "binary ILP decoding multiplication overflowed i64", - ) - })?; - value.checked_add(term).ok_or_else(|| { - crate::rules::ExtractionError::invalid( - "binary ILP decoding sum overflowed i64", - ) - }) - }, - ) + let offset: i64 = encoding + .weights + .iter() + .enumerate() + .filter(|(offset, _)| target_solution[encoding.start + offset] == 1) + .map(|(_, &weight)| weight) + .sum(); + encoding.lower_bound + offset }) - .collect() + .collect()) } } diff --git a/src/rules/ilp_i64_ilp_f64.rs b/src/rules/ilp_i64_ilp_f64.rs new file mode 100644 index 000000000..8098a115d --- /dev/null +++ b/src/rules/ilp_i64_ilp_f64.rs @@ -0,0 +1,86 @@ +//! Exact integer-to-floating coefficient reductions for ILP. +//! +//! Preserve variable domains and every formal linear expression by converting +//! coefficients and right-hand sides exactly within the supported numeric range. +//! Solution extraction is the identity map. Numerical backend capabilities are +//! independent of this reduction. + +use crate::models::algebraic::{Comparison, LinearConstraint, VariableDomain, ILP}; +use crate::reduction; +use crate::rules::{ReduceTo, ReductionError, VariantReductionResult}; +use crate::types::i64_to_exact_f64; + +pub type ReductionILPToFloat = VariantReductionResult, ILP>; + +fn reduce_coefficients( + source: &ILP, +) -> Result, ReductionError> { + let convert = |coefficient: i64| { + i64_to_exact_f64(coefficient) + .map_err(ReductionError::inexact_float_conversion::, ILP>) + }; + let constraints = source + .constraints() + .iter() + .map(|constraint| { + let terms = constraint + .terms() + .iter() + .map(|&(variable, coefficient)| Ok((variable, convert(coefficient)?))) + .collect::, ReductionError>>()?; + let rhs = convert(constraint.rhs())?; + Ok(match constraint.comparison() { + Comparison::Le => LinearConstraint::le(terms, rhs), + Comparison::Ge => LinearConstraint::ge(terms, rhs), + Comparison::Eq => LinearConstraint::eq(terms, rhs), + }) + }) + .collect::, ReductionError>>()?; + let objective = source + .objective() + .iter() + .map(|&(variable, coefficient)| Ok((variable, convert(coefficient)?))) + .collect::, ReductionError>>()?; + let target = ILP::with_variables( + source.variables().to_vec(), + constraints, + objective, + source.sense(), + ) + .map_err(ReductionError::construction::, ILP>)?; + Ok(VariantReductionResult::new(target)) +} + +#[reduction( + transform = exact { + num_vars = "num_vars", + num_constraints = "num_constraints", + num_nonzeros = "num_nonzeros", + }, +)] +impl ReduceTo> for ILP { + type Result = ReductionILPToFloat; + + fn reduce_to(&self) -> Result { + reduce_coefficients(self) + } +} + +#[reduction( + transform = exact { + num_vars = "num_vars", + num_constraints = "num_constraints", + num_nonzeros = "num_nonzeros", + }, +)] +impl ReduceTo> for ILP { + type Result = ReductionILPToFloat; + + fn reduce_to(&self) -> Result { + reduce_coefficients(self) + } +} + +#[cfg(test)] +#[path = "../unit_tests/rules/ilp_i64_ilp_f64.rs"] +mod tests; diff --git a/src/rules/ilp_qubo.rs b/src/rules/ilp_qubo.rs index fbe885057..f6dd95dcd 100644 --- a/src/rules/ilp_qubo.rs +++ b/src/rules/ilp_qubo.rs @@ -39,14 +39,6 @@ impl ReductionResult for ReductionILPToQUBO { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - let value = - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - if !crate::rules::AggregateReductionResult::extract_value(self, value).is_valid() { - return Err(crate::rules::ExtractionError::invalid( - "target QUBO configuration does not certify a feasible ILP assignment", - )); - } - Ok(target_solution[..self.num_original_vars] .iter() .map(|&value| i64::from(value)) @@ -252,7 +244,7 @@ impl ReduceTo> for ILP { feasible_energy_range(&c_vec, &b_vec, penalty)?; // QUBO = -diag(c + 2·P·b·A) + P·A^T·A - let mut matrix = vec![vec![0_i64; nq]; nq]; + let mut matrix = vec![std::collections::BTreeMap::new(); nq]; // Compute b·A (b_vec dot each column of a_ext) let mut ba = vec![0_i64; nq]; @@ -281,14 +273,17 @@ impl ReduceTo> for ILP { "computing a QUBO diagonal penalty", ) })?; - matrix[j][j] = c_vec[j] - .checked_add(penalty_term) - .and_then(i64::checked_neg) - .ok_or_else(|| { - crate::rules::ReductionError::integer_overflow::, QUBO>( - "computing a QUBO diagonal coefficient", - ) - })?; + matrix[j].insert( + j, + c_vec[j] + .checked_add(penalty_term) + .and_then(i64::checked_neg) + .ok_or_else(|| { + crate::rules::ReductionError::integer_overflow::, QUBO>( + "computing a QUBO diagonal coefficient", + ) + })?, + ); } // A^T·A contribution (upper-triangular convention) @@ -308,13 +303,17 @@ impl ReduceTo> for ILP { "computing a quadratic QUBO diagonal penalty", ) })?; - row_i[i] = row_i[i].checked_add(diagonal).ok_or_else(|| { + let coefficient = row_i.entry(i).or_insert(0i64); + *coefficient = coefficient.checked_add(diagonal).ok_or_else(|| { crate::rules::ReductionError::integer_overflow::, QUBO>( "adding a quadratic QUBO diagonal penalty", ) })?; // Off-diagonal for j in (i + 1)..nq { + if row[j] == 0 { + continue; + } let interaction = penalty .checked_mul(row[i]) .and_then(|value| value.checked_mul(row[j])) @@ -324,7 +323,8 @@ impl ReduceTo> for ILP { "computing a quadratic QUBO interaction penalty", ) })?; - row_i[j] = row_i[j].checked_add(interaction).ok_or_else(|| { + let coefficient = row_i.entry(j).or_insert(0i64); + *coefficient = coefficient.checked_add(interaction).ok_or_else(|| { crate::rules::ReductionError::integer_overflow::, QUBO>( "adding a quadratic QUBO interaction penalty", ) @@ -334,7 +334,7 @@ impl ReduceTo> for ILP { } Ok(ReductionILPToQUBO { - target: QUBO::from_matrix(matrix) + target: QUBO::from_rows(matrix) .map_err(crate::rules::ReductionError::construction::, QUBO>)?, num_original_vars: n, sense: self.sense(), diff --git a/src/rules/integerknapsack_ilp.rs b/src/rules/integerknapsack_ilp.rs index 8eb5b2aca..e74dddbdc 100644 --- a/src/rules/integerknapsack_ilp.rs +++ b/src/rules/integerknapsack_ilp.rs @@ -26,8 +26,6 @@ impl ReductionResult for ReductionIntegerKnapsackToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - crate::rules::ilp_helpers::decode_usize_values(target_solution) } } diff --git a/src/rules/integralflowbundles_ilp.rs b/src/rules/integralflowbundles_ilp.rs index d4220c41e..33122f747 100644 --- a/src/rules/integralflowbundles_ilp.rs +++ b/src/rules/integralflowbundles_ilp.rs @@ -27,8 +27,6 @@ impl ReductionResult for ReductionIFBToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - crate::rules::ilp_helpers::decode_usize_values(target_solution) } } diff --git a/src/rules/integralflowhomologousarcs_ilp.rs b/src/rules/integralflowhomologousarcs_ilp.rs index 006ecdb56..5ef8ec0ef 100644 --- a/src/rules/integralflowhomologousarcs_ilp.rs +++ b/src/rules/integralflowhomologousarcs_ilp.rs @@ -26,8 +26,6 @@ impl ReductionResult for ReductionIFHAToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - crate::rules::ilp_helpers::decode_usize_values(target_solution) } } diff --git a/src/rules/integralflowwithmultipliers_ilp.rs b/src/rules/integralflowwithmultipliers_ilp.rs index 6c700a3ba..4d8b18294 100644 --- a/src/rules/integralflowwithmultipliers_ilp.rs +++ b/src/rules/integralflowwithmultipliers_ilp.rs @@ -26,8 +26,6 @@ impl ReductionResult for ReductionIFWMToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - crate::rules::ilp_helpers::decode_usize_values(target_solution) } } diff --git a/src/rules/isomorphicspanningtree_ilp.rs b/src/rules/isomorphicspanningtree_ilp.rs index e2d9e2ec4..ee9aa2d04 100644 --- a/src/rules/isomorphicspanningtree_ilp.rs +++ b/src/rules/isomorphicspanningtree_ilp.rs @@ -28,9 +28,12 @@ impl ReductionResult for ReductionISTToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - - crate::rules::ilp_helpers::one_hot_decode_rows(target_solution, self.n, self.n, 0) + Ok(crate::rules::ilp_helpers::one_hot_decode_rows( + target_solution, + self.n, + self.n, + 0, + )) } } diff --git a/src/rules/kclique_balancedcompletebipartitesubgraph.rs b/src/rules/kclique_balancedcompletebipartitesubgraph.rs index b8285dbf6..7dfc7af23 100644 --- a/src/rules/kclique_balancedcompletebipartitesubgraph.rs +++ b/src/rules/kclique_balancedcompletebipartitesubgraph.rs @@ -38,8 +38,6 @@ impl ReductionResult for ReductionKCliqueToBCBS { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ (0..self.num_original_vertices) .map(|v| !target_solution[v]) diff --git a/src/rules/kclique_conjunctivebooleanquery.rs b/src/rules/kclique_conjunctivebooleanquery.rs index e0c66f76b..c8f1241ae 100644 --- a/src/rules/kclique_conjunctivebooleanquery.rs +++ b/src/rules/kclique_conjunctivebooleanquery.rs @@ -38,8 +38,6 @@ impl ReductionResult for ReductionKCliqueToCBQ { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(KClique::::config_from_vertices( self.num_vertices, target_solution, diff --git a/src/rules/kclique_ilp.rs b/src/rules/kclique_ilp.rs index 1c3e0f962..3c7377041 100644 --- a/src/rules/kclique_ilp.rs +++ b/src/rules/kclique_ilp.rs @@ -43,8 +43,6 @@ impl ReductionResult for ReductionKCliqueToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution.iter().map(|&value| value == 1).collect()) } } diff --git a/src/rules/kclique_subgraphisomorphism.rs b/src/rules/kclique_subgraphisomorphism.rs index f39f27561..775fb94b8 100644 --- a/src/rules/kclique_subgraphisomorphism.rs +++ b/src/rules/kclique_subgraphisomorphism.rs @@ -38,8 +38,6 @@ impl ReductionResult for ReductionKCliqueToSubIso { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(KClique::::config_from_vertices( self.num_source_vertices, target_solution, diff --git a/src/rules/kcoloring_bicliquecover.rs b/src/rules/kcoloring_bicliquecover.rs index 46be9a726..c4391c0e6 100644 --- a/src/rules/kcoloring_bicliquecover.rs +++ b/src/rules/kcoloring_bicliquecover.rs @@ -44,9 +44,6 @@ pub struct ReductionKColoringToBicliqueCover { /// the diagonal indices of each source vertex without re-reading the /// reduction parameters. num_vertices: usize, - /// Number of source colors `q`. Used as the upper bound on the number of - /// color bicliques recovered during extraction. - num_colors: usize, } impl ReductionResult for ReductionKColoringToBicliqueCover { @@ -72,14 +69,6 @@ impl ReductionResult for ReductionKColoringToBicliqueCover { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - let value = - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - if value.0.is_none() { - return Err(crate::rules::ExtractionError::invalid( - "target configuration is not a biclique cover", - )); - } - Ok({ let n = self.num_vertices; let k = self.target.k(); @@ -91,14 +80,11 @@ impl ReductionResult for ReductionKColoringToBicliqueCover { for v in 0..n { let a_v = v; let b_v = left_size + v; - let biclique = (0..k) - .find(|&r| target_solution[r][a_v] && target_solution[r][b_v]) - .ok_or_else(|| { - crate::rules::ExtractionError::invalid(format!( - "target cover leaves diagonal gadget edge {v} uncovered" - )) - })?; - diagonal_biclique.push(biclique); + diagonal_biclique.extend( + (0..k) + .filter(|&r| target_solution[r][a_v] && target_solution[r][b_v]) + .take(1), + ); } // Compact distinct biclique indices into colors 0..q-1 in first-seen order. @@ -108,12 +94,6 @@ impl ReductionResult for ReductionKColoringToBicliqueCover { for biclique in diagonal_biclique { let next_color = color_of_biclique.len(); let color = *color_of_biclique.entry(biclique).or_insert(next_color); - if color >= self.num_colors { - return Err(crate::rules::ExtractionError::invalid(format!( - "target cover uses more than {} diagonal bicliques", - self.num_colors - ))); - } coloring.push(color); } coloring @@ -145,7 +125,6 @@ impl ReduceTo for KColoring { return Ok(ReductionKColoringToBicliqueCover { target: BicliqueCover::new(BipartiteGraph::new(1, 1, vec![(0, 0)]), 0), num_vertices: n, - num_colors: q, }); } @@ -216,7 +195,6 @@ impl ReduceTo for KColoring { Ok(ReductionKColoringToBicliqueCover { target, num_vertices: n, - num_colors: q, }) } } diff --git a/src/rules/kcoloring_clustering.rs b/src/rules/kcoloring_clustering.rs index 6311eb3af..40ddffec3 100644 --- a/src/rules/kcoloring_clustering.rs +++ b/src/rules/kcoloring_clustering.rs @@ -32,8 +32,6 @@ impl ReductionResult for ReductionKColoringToClustering { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution[..self.source_num_vertices].to_vec()) } } diff --git a/src/rules/kcoloring_partitionintocliques.rs b/src/rules/kcoloring_partitionintocliques.rs index bb3dd69bd..b1656eace 100644 --- a/src/rules/kcoloring_partitionintocliques.rs +++ b/src/rules/kcoloring_partitionintocliques.rs @@ -29,8 +29,6 @@ impl ReductionResult for ReductionKColoringToPartitionIntoCliques { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution.to_vec()) } } diff --git a/src/rules/kcoloring_twodimensionalconsecutivesets.rs b/src/rules/kcoloring_twodimensionalconsecutivesets.rs index da2a2318e..5e1b715f0 100644 --- a/src/rules/kcoloring_twodimensionalconsecutivesets.rs +++ b/src/rules/kcoloring_twodimensionalconsecutivesets.rs @@ -45,14 +45,6 @@ impl ReductionResult for ReductionKColoringToTDCS { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - let value = - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - if !value.0 { - return Err(crate::rules::ExtractionError::invalid( - "target grouping is not a consecutive-set partition", - )); - } - Ok({ // The target solution is config[symbol] = group_index. // Vertex symbols are indices 0..num_vertices. diff --git a/src/rules/knapsack_ilp.rs b/src/rules/knapsack_ilp.rs index 2601ce5f9..a45de39ce 100644 --- a/src/rules/knapsack_ilp.rs +++ b/src/rules/knapsack_ilp.rs @@ -28,8 +28,6 @@ impl ReductionResult for ReductionKnapsackToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution.iter().map(|&value| value == 1).collect()) } } diff --git a/src/rules/knapsack_qubo.rs b/src/rules/knapsack_qubo.rs index 63a0e208d..d2c29d291 100644 --- a/src/rules/knapsack_qubo.rs +++ b/src/rules/knapsack_qubo.rs @@ -38,8 +38,6 @@ impl ReductionResult for ReductionKnapsackToQUBO { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution[..self.num_items].to_vec()) } } @@ -108,7 +106,7 @@ impl ReduceTo> for Knapsack { coeffs[n + j] = weight; } - let mut matrix = vec![vec![0_i64; total]; total]; + let mut matrix = vec![std::collections::BTreeMap::new(); total]; // Diagonal: P * a_k^2 - 2P * C * a_k - v_k (for items) for k in 0..total { @@ -121,29 +119,33 @@ impl ReduceTo> for Knapsack { .and_then(|value| value.checked_mul(coeffs[k])) .and_then(|value| value.checked_mul(2)) .ok_or_else(|| overflow("computing a knapsack QUBO linear penalty"))?; - matrix[k][k] = square + let mut diagonal = square .checked_sub(linear) .ok_or_else(|| overflow("combining knapsack QUBO diagonal penalties"))?; if k < n { - matrix[k][k] = matrix[k][k] + diagonal = diagonal .checked_sub(values[k]) .ok_or_else(|| overflow("adding a knapsack value to the QUBO objective"))?; } + matrix[k].insert(k, diagonal); } // Off-diagonal (upper triangular): 2P * a_i * a_j for i in 0..total { for j in (i + 1)..total { - matrix[i][j] = penalty - .checked_mul(coeffs[i]) - .and_then(|value| value.checked_mul(coeffs[j])) - .and_then(|value| value.checked_mul(2)) - .ok_or_else(|| overflow("computing a knapsack QUBO interaction"))?; + matrix[i].insert( + j, + penalty + .checked_mul(coeffs[i]) + .and_then(|value| value.checked_mul(coeffs[j])) + .and_then(|value| value.checked_mul(2)) + .ok_or_else(|| overflow("computing a knapsack QUBO interaction"))?, + ); } } Ok(ReductionKnapsackToQUBO { - target: QUBO::from_matrix(matrix).map_err(|message| { + target: QUBO::from_rows(matrix).map_err(|message| { crate::rules::ReductionError::construction::>(message) })?, num_items: n, diff --git a/src/rules/ksatisfiability_acyclicpartition.rs b/src/rules/ksatisfiability_acyclicpartition.rs index 257516807..9f1d71490 100644 --- a/src/rules/ksatisfiability_acyclicpartition.rs +++ b/src/rules/ksatisfiability_acyclicpartition.rs @@ -33,13 +33,6 @@ impl ReductionResult for Reduction3SATToAcyclicPartition { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - if !crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)? - .0 - { - return Err(crate::rules::ExtractionError::invalid( - "target partition does not satisfy the acyclic partition constraints", - )); - } let source_label = target_solution[self.source_vertex]; let selected = target_solution[..self.sat_to_clique.target_problem().num_vertices()] .iter() diff --git a/src/rules/ksatisfiability_bicliquecover.rs b/src/rules/ksatisfiability_bicliquecover.rs index 5caf355a0..297321a6a 100644 --- a/src/rules/ksatisfiability_bicliquecover.rs +++ b/src/rules/ksatisfiability_bicliquecover.rs @@ -89,18 +89,11 @@ impl ReductionResult for ReductionKSatisfiabilityToBicliqueCover { /// The rank budget forces a unique row covering the first domino anchor. /// Its left crown memberships give the normalized truth assignment. /// Map appearing variables back to their original indices and assign false - /// to variables absent from the formula. Infeasible covers are rejected. + /// to variables absent from the formula. fn extract_solution( &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - let value = - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - if value.0.is_none() { - return Err(crate::rules::ExtractionError::invalid( - "target configuration is not a biclique cover", - )); - } // Variables absent from every clause may be assigned false. // This also defines the inverse map for the empty-formula YES target. let mut source_assignment = vec![false; self.source_num_vars]; @@ -111,18 +104,14 @@ impl ReductionResult for ReductionKSatisfiabilityToBicliqueCover { let s11_v = self.target.left_size() + self.s1_right_offset; // The Y matching and the important induced matching use the entire // rank budget. Exactly one row covers this important anchor edge. - let b1_index = target_solution + for row in target_solution .iter() - .position(|row| row[s11_u] && row[s11_v]); - - let b1_index = b1_index.ok_or_else(|| { - crate::rules::ExtractionError::invalid( - "target configuration has no important-edge biclique B_1", - ) - })?; - // Pair i corresponds to source_variables[i]; its t variable is 2*i. - for (i, &source_index) in self.source_variables.iter().enumerate() { - source_assignment[source_index] = target_solution[b1_index][2 * i]; + .filter(|row| row[s11_u] && row[s11_v]) + { + // Pair i corresponds to source_variables[i]; its t variable is 2*i. + for (i, &source_index) in self.source_variables.iter().enumerate() { + source_assignment[source_index] = row[2 * i]; + } } Ok(source_assignment) } diff --git a/src/rules/ksatisfiability_cyclicordering.rs b/src/rules/ksatisfiability_cyclicordering.rs index 704a26f66..7a43dc164 100644 --- a/src/rules/ksatisfiability_cyclicordering.rs +++ b/src/rules/ksatisfiability_cyclicordering.rs @@ -43,13 +43,6 @@ impl ReductionResult for Reduction3SATToCyclicOrdering { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - let value = - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - if !value.0 { - return Err(crate::rules::ExtractionError::invalid( - "target configuration is not a feasible cyclic ordering", - )); - } let mut assignment = vec![false; self.source_num_vars]; for (compact, &original) in self.source_variables.iter().enumerate() { let (alpha, beta, gamma) = variable_triple(compact); diff --git a/src/rules/ksatisfiability_directedtwocommodityintegralflow.rs b/src/rules/ksatisfiability_directedtwocommodityintegralflow.rs index a9e5d8bcb..2f5b282ef 100644 --- a/src/rules/ksatisfiability_directedtwocommodityintegralflow.rs +++ b/src/rules/ksatisfiability_directedtwocommodityintegralflow.rs @@ -175,8 +175,6 @@ impl ReductionResult for Reduction3SATToDirectedTwoCommodityIntegralFlow { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ self.variable_paths .iter() diff --git a/src/rules/ksatisfiability_feasibleregisterassignment.rs b/src/rules/ksatisfiability_feasibleregisterassignment.rs index 23176c5ed..5d9861aaf 100644 --- a/src/rules/ksatisfiability_feasibleregisterassignment.rs +++ b/src/rules/ksatisfiability_feasibleregisterassignment.rs @@ -78,13 +78,6 @@ impl ReductionResult for Reduction3SATToFeasibleRegisterAssignment { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - let value = - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - if !value.0 { - return Err(crate::rules::ExtractionError::invalid( - "target configuration is not a feasible register assignment realization", - )); - } let mut assignment = vec![false; self.num_vars]; let compact_vars = self.source_variables.len(); for (compact, &original) in self.source_variables.iter().enumerate() { diff --git a/src/rules/ksatisfiability_kclique.rs b/src/rules/ksatisfiability_kclique.rs index 19c92416b..29d8e86c9 100644 --- a/src/rules/ksatisfiability_kclique.rs +++ b/src/rules/ksatisfiability_kclique.rs @@ -33,13 +33,6 @@ impl ReductionResult for Reduction3SATToKClique { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - if !crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)? - .0 - { - return Err(crate::rules::ExtractionError::invalid( - "target selection is not a clique meeting the threshold", - )); - } // Variables absent from the selected literals are free; choose false. let mut assignment = vec![false; self.source_num_vars]; for (&selected, &(variable, positive)) in target_solution[..self.literal_assignments.len()] diff --git a/src/rules/ksatisfiability_kernel.rs b/src/rules/ksatisfiability_kernel.rs index d426e0025..c79e71fce 100644 --- a/src/rules/ksatisfiability_kernel.rs +++ b/src/rules/ksatisfiability_kernel.rs @@ -33,13 +33,6 @@ impl ReductionResult for Reduction3SatToKernel { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - let value = - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - if !value.0 { - return Err(crate::rules::ExtractionError::invalid( - "target vertex selection is not a kernel", - )); - } let mut assignment = vec![false; self.source_num_vars]; for (compact, &original) in self.source_variables.iter().enumerate() { assignment[original] = target_solution[2 * compact]; diff --git a/src/rules/ksatisfiability_minimumvertexcover.rs b/src/rules/ksatisfiability_minimumvertexcover.rs index 38d133bbe..b3af90d00 100644 --- a/src/rules/ksatisfiability_minimumvertexcover.rs +++ b/src/rules/ksatisfiability_minimumvertexcover.rs @@ -23,6 +23,7 @@ use crate::variant::K3; pub struct Reduction3SATToMVC { target: MinimumVertexCover, source_num_vars: usize, + target_bound: i64, } impl ReductionResult for Reduction3SATToMVC { @@ -37,15 +38,13 @@ impl ReductionResult for Reduction3SATToMVC { /// /// Vertex layout: indices 0..2n are literal vertices (even = positive, /// odd = negated). For variable i, vertex 2*i is u_i and vertex 2*i+1 - /// is not-u_i. Each truth-setting edge forces exactly one of these two - /// into any minimum vertex cover. If u_i is in the cover, set x_i = 1; + /// is not-u_i. A cover meeting the n + 2m bound contains exactly one of these two + /// for each variable. If u_i is in the cover, set x_i = 1; /// if not-u_i is in the cover, set x_i = 0. fn extract_solution( &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ (0..self.source_num_vars) .map(|i| { @@ -57,7 +56,21 @@ impl ReductionResult for Reduction3SATToMVC { } } +impl crate::rules::AggregateReductionResult for Reduction3SATToMVC { + type Source = KSatisfiability; + type Target = MinimumVertexCover; + + fn target_problem(&self) -> &Self::Target { + &self.target + } + + fn extract_value(&self, value: crate::types::Min) -> crate::types::Or { + crate::types::Or(value.0.is_some_and(|cost| cost <= self.target_bound)) + } +} + #[reduction( + aggregate = custom, transform = exact { num_vertices = "2 * num_vars + 3 * num_clauses", num_edges = "num_vars + 6 * num_clauses", @@ -69,6 +82,16 @@ impl ReduceTo> for KSatisfiability { fn reduce_to(&self) -> Result { let n = self.num_vars(); let m = self.num_clauses(); + let target_bound = m + .checked_mul(2) + .and_then(|value| value.checked_add(n)) + .and_then(|value| i64::try_from(value).ok()) + .ok_or_else(|| { + crate::rules::ReductionError::integer_overflow::< + KSatisfiability, + MinimumVertexCover, + >("computing the target cover bound") + })?; let total_vertices = 2 * n + 3 * m; let mut edges: Vec<(usize, usize)> = Vec::with_capacity(n + 6 * m); @@ -107,6 +130,7 @@ impl ReduceTo> for KSatisfiability { Ok(Reduction3SATToMVC { target, source_num_vars: n, + target_bound, }) } } diff --git a/src/rules/ksatisfiability_monochromatictriangle.rs b/src/rules/ksatisfiability_monochromatictriangle.rs index 61a8de78a..9ffe3f0e4 100644 --- a/src/rules/ksatisfiability_monochromatictriangle.rs +++ b/src/rules/ksatisfiability_monochromatictriangle.rs @@ -55,7 +55,6 @@ impl ReductionResult for Reduction3SATToMonochromaticTriangle { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; let nae_solution = (0..self.nae_reduction.target_problem().num_vars()) .map(|index| target_solution[2 * index]) .collect(); diff --git a/src/rules/ksatisfiability_oneinthreesatisfiability.rs b/src/rules/ksatisfiability_oneinthreesatisfiability.rs index 3c9faa76c..e8e1fb4b6 100644 --- a/src/rules/ksatisfiability_oneinthreesatisfiability.rs +++ b/src/rules/ksatisfiability_oneinthreesatisfiability.rs @@ -31,13 +31,6 @@ impl ReductionResult for Reduction3SATToOneInThreeSAT { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - let value = - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - if !value.0 { - return Err(crate::rules::ExtractionError::invalid( - "target assignment does not satisfy every one-in-three clause", - )); - } let mut assignment = vec![false; self.source_num_vars]; for (compact, &original) in self.source_variables.iter().enumerate() { assignment[original] = target_solution[compact]; diff --git a/src/rules/ksatisfiability_preemptivescheduling.rs b/src/rules/ksatisfiability_preemptivescheduling.rs index e994b9cda..dd378edad 100644 --- a/src/rules/ksatisfiability_preemptivescheduling.rs +++ b/src/rules/ksatisfiability_preemptivescheduling.rs @@ -339,8 +339,6 @@ impl ReductionResult for Reduction3SATToPreemptiveScheduling { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ let d_max = self.target.d_max(); self.positive_start_jobs diff --git a/src/rules/ksatisfiability_quadraticcongruences.rs b/src/rules/ksatisfiability_quadraticcongruences.rs index 15320498f..f1ac10d28 100644 --- a/src/rules/ksatisfiability_quadraticcongruences.rs +++ b/src/rules/ksatisfiability_quadraticcongruences.rs @@ -40,13 +40,6 @@ impl ReductionResult for Reduction3SATToQuadraticCongruences { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - let value = - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - if !value.0 { - return Err(crate::rules::ExtractionError::invalid( - "target integer does not satisfy the bounded quadratic congruence", - )); - } // Validation gives 0 < x <= H. Each prime power divides exactly one // of H-x and H+x. The coordinate zero sign chooses x or -x so that // the odd linear target, rather than its negative, is recovered. diff --git a/src/rules/ksatisfiability_quadraticdiophantineequations.rs b/src/rules/ksatisfiability_quadraticdiophantineequations.rs index e8e4053c8..f6e8b9cb8 100644 --- a/src/rules/ksatisfiability_quadraticdiophantineequations.rs +++ b/src/rules/ksatisfiability_quadraticdiophantineequations.rs @@ -32,8 +32,6 @@ impl ReductionResult for Reduction3SATToQuadraticDiophantineEquations { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ self.congruence_reduction .extract_solution(target_solution)? diff --git a/src/rules/ksatisfiability_qubo.rs b/src/rules/ksatisfiability_qubo.rs index 4a7b8b21c..47adbae75 100644 --- a/src/rules/ksatisfiability_qubo.rs +++ b/src/rules/ksatisfiability_qubo.rs @@ -37,13 +37,6 @@ impl ReductionResult for ReductionKSatToQUBO { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - let value = - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - if !crate::rules::AggregateReductionResult::extract_value(self, value).0 { - return Err(crate::rules::ExtractionError::invalid( - "QUBO energy does not meet the SAT zero-penalty threshold", - )); - } Ok(target_solution[..self.source_num_vars].to_vec()) } } @@ -68,13 +61,6 @@ impl ReductionResult for Reduction3SATToQUBO { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - let value = - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - if !crate::rules::AggregateReductionResult::extract_value(self, value).0 { - return Err(crate::rules::ExtractionError::invalid( - "QUBO energy does not meet the SAT zero-penalty threshold", - )); - } Ok(target_solution[..self.source_num_vars].to_vec()) } } @@ -84,19 +70,20 @@ impl ReductionResult for Reduction3SATToQUBO { /// For clause (l_i ∨ l_j), the penalty for the clause being unsatisfied is /// the product of the complemented literals. fn add_coefficient( - matrix: &mut [Vec], + matrix: &mut [std::collections::BTreeMap], row: usize, column: usize, coefficient: i64, ) -> Result<(), &'static str> { - matrix[row][column] = matrix[row][column] + let entry = matrix[row].entry(column).or_insert(0i64); + *entry = entry .checked_add(coefficient) .ok_or("adding a SAT QUBO coefficient")?; Ok(()) } fn add_2sat_clause_penalty( - matrix: &mut [Vec], + matrix: &mut [std::collections::BTreeMap], lits: &[(usize, bool)], ) -> Result<(), &'static str> { assert_eq!(lits.len(), 2, "Expected 2-literal clause"); @@ -151,7 +138,7 @@ fn add_2sat_clause_penalty( /// /// `aux_var` is the 0-indexed auxiliary variable. fn add_3sat_clause_penalty( - matrix: &mut [Vec], + matrix: &mut [std::collections::BTreeMap], lits: &[(usize, bool)], aux_var: usize, ) -> Result<(), &'static str> { @@ -176,7 +163,7 @@ fn add_3sat_clause_penalty( // Helper: add coefficient * yi * yj to the matrix // where yi depends on variable vi and negation ni - let add_yy = |matrix: &mut [Vec], + let add_yy = |matrix: &mut [std::collections::BTreeMap], vi: usize, ni: bool, vj: usize, @@ -241,7 +228,7 @@ fn add_3sat_clause_penalty( // Helper: add coefficient * yi * a to the matrix // where yi depends on variable vi and negation ni, a is aux variable - let add_ya = |matrix: &mut [Vec], + let add_ya = |matrix: &mut [std::collections::BTreeMap], vi: usize, ni: bool, a: usize, @@ -280,6 +267,8 @@ fn add_3sat_clause_penalty( Ok(()) } +type CoefficientRows = Vec>; + /// Expand clause penalties and retain the constant omitted by QUBO. /// K3 reserves one auxiliary per clause, including free auxiliaries for short /// clauses; K2 reserves none. The source constructor validates clause widths. @@ -287,14 +276,11 @@ fn build_qubo_matrix( num_vars: usize, clauses: &[crate::models::formula::CNFClause], num_aux: usize, -) -> Result<(Vec>, i64), &'static str> { +) -> Result<(CoefficientRows, i64), &'static str> { let total = num_vars .checked_add(num_aux) .ok_or("computing the number of SAT QUBO variables")?; - total - .checked_mul(total) - .ok_or("computing the SAT QUBO dense matrix entry count")?; - let mut matrix = vec![vec![0; total]; total]; + let mut matrix = vec![std::collections::BTreeMap::new(); total]; let mut constant = 0i64; for (idx, clause) in clauses.iter().enumerate() { let literals: Vec<_> = clause @@ -366,7 +352,7 @@ impl ReduceTo> for KSatisfiability { })?; Ok(ReductionKSatToQUBO { - target: QUBO::from_matrix(matrix).map_err(|message| { + target: QUBO::from_rows(matrix).map_err(|message| { crate::rules::ReductionError::construction::, QUBO>( message, ) @@ -396,7 +382,7 @@ impl ReduceTo> for KSatisfiability { })?; Ok(Reduction3SATToQUBO { - target: QUBO::from_matrix(matrix).map_err(|message| { + target: QUBO::from_rows(matrix).map_err(|message| { crate::rules::ReductionError::construction::, QUBO>( message, ) diff --git a/src/rules/ksatisfiability_registersufficiency.rs b/src/rules/ksatisfiability_registersufficiency.rs index 8cde3e058..9bed847ed 100644 --- a/src/rules/ksatisfiability_registersufficiency.rs +++ b/src/rules/ksatisfiability_registersufficiency.rs @@ -296,13 +296,6 @@ impl ReductionResult for Reduction3SATToRegisterSufficiency { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - let value = - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - if !value.0 { - return Err(crate::rules::ExtractionError::invalid( - "target ordering does not satisfy the register bound and dependencies", - )); - } let mut assignment = vec![false; self.source_num_vars]; let Some(layout) = &self.layout else { // Only the empty-conjunction target has a feasible witness here. @@ -311,12 +304,6 @@ impl ReductionResult for Reduction3SATToRegisterSufficiency { let cutoff = target_solution[layout.w(layout.num_vars - 1)]; for (variable, &original) in self.source_variables.iter().enumerate() { let positive = target_solution[layout.x_pos(variable)] < cutoff; - let negative = target_solution[layout.x_neg(variable)] < cutoff; - if positive && negative { - return Err(crate::rules::ExtractionError::invalid(format!( - "both literals of variable {original} precede the extraction cutoff" - ))); - } assignment[original] = positive; } Ok(assignment) diff --git a/src/rules/ksatisfiability_simultaneousincongruences.rs b/src/rules/ksatisfiability_simultaneousincongruences.rs index 785e808b8..8d6b58b99 100644 --- a/src/rules/ksatisfiability_simultaneousincongruences.rs +++ b/src/rules/ksatisfiability_simultaneousincongruences.rs @@ -30,14 +30,8 @@ impl ReductionResult for Reduction3SATToSimultaneousIncongruences { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ - let x = u64::try_from(*target_solution).map_err(|_| { - crate::rules::ExtractionError::invalid( - "target value cannot be represented in the CRT implementation domain", - ) - })?; + let x = *target_solution as u64; self.variable_primes .iter() .map(|&prime| x % prime == 1) diff --git a/src/rules/ksatisfiability_subsetsum.rs b/src/rules/ksatisfiability_subsetsum.rs index 3c62e5d14..cb780b17a 100644 --- a/src/rules/ksatisfiability_subsetsum.rs +++ b/src/rules/ksatisfiability_subsetsum.rs @@ -39,8 +39,6 @@ impl ReductionResult for Reduction3SATToSubsetSum { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ // Variable integers are the first 2n elements in 0-based indexing: // for variable i (0 <= i < n), y_i is stored at index 2*i and z_i at index 2*i + 1. diff --git a/src/rules/ksatisfiability_timetabledesign.rs b/src/rules/ksatisfiability_timetabledesign.rs index 112eabbb6..30846529f 100644 --- a/src/rules/ksatisfiability_timetabledesign.rs +++ b/src/rules/ksatisfiability_timetabledesign.rs @@ -748,8 +748,6 @@ impl ReductionResult for Reduction3SATToTimetableDesign { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ let num_periods = self.target.num_periods(); diff --git a/src/rules/lengthboundeddisjointpaths_ilp.rs b/src/rules/lengthboundeddisjointpaths_ilp.rs index 5745f463e..404f32270 100644 --- a/src/rules/lengthboundeddisjointpaths_ilp.rs +++ b/src/rules/lengthboundeddisjointpaths_ilp.rs @@ -40,22 +40,12 @@ impl ReductionResult for ReductionLBDPToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - let m = self.edges.len(); let flow_vars_per_k = 2 * m; let activation_offset = self.num_paths * flow_vars_per_k; let mut result = vec![vec![false; m]; self.num_paths]; for (k, path) in result.iter_mut().enumerate() { if target_solution[activation_offset + k] == 0 { - if target_solution[k * flow_vars_per_k..(k + 1) * flow_vars_per_k] - .iter() - .any(|&flow| flow != 0) - { - return Err(crate::rules::ExtractionError::invalid( - "inactive path slot contains flow", - )); - } continue; } let mut adjacency = vec![Vec::new(); self.num_vertices]; @@ -86,12 +76,7 @@ impl ReductionResult for ReductionLBDPToILP { } } let mut vertex = self.sink; - while vertex != self.source { - let (previous, edge) = predecessor[vertex].ok_or_else(|| { - crate::rules::ExtractionError::invalid( - "active path flow does not connect source to sink", - ) - })?; + while let Some((previous, edge)) = predecessor[vertex] { path[edge] = true; vertex = previous; } diff --git a/src/rules/longestcircuit_ilp.rs b/src/rules/longestcircuit_ilp.rs index c67c9a2e7..d21f3812a 100644 --- a/src/rules/longestcircuit_ilp.rs +++ b/src/rules/longestcircuit_ilp.rs @@ -40,8 +40,6 @@ impl ReductionResult for ReductionLongestCircuitToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution[..self.num_edges] .iter() .map(|&value| value == 1) diff --git a/src/rules/longestcommonsubsequence_ilp.rs b/src/rules/longestcommonsubsequence_ilp.rs index 11225f470..362ecfdee 100644 --- a/src/rules/longestcommonsubsequence_ilp.rs +++ b/src/rules/longestcommonsubsequence_ilp.rs @@ -35,14 +35,12 @@ impl ReductionResult for ReductionLCSToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(crate::rules::ilp_helpers::one_hot_decode_rows( target_solution, self.max_length, self.alphabet_size + 1, 0, - )? + ) .into_iter() .map(|symbol| (symbol < self.alphabet_size).then_some(symbol)) .collect()) diff --git a/src/rules/longestcommonsubsequence_maximumindependentset.rs b/src/rules/longestcommonsubsequence_maximumindependentset.rs index be5454a14..980fe3657 100644 --- a/src/rules/longestcommonsubsequence_maximumindependentset.rs +++ b/src/rules/longestcommonsubsequence_maximumindependentset.rs @@ -50,8 +50,6 @@ impl ReductionResult for ReductionLCSToIS { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ // Collect selected match nodes with their characters let mut selected: Vec<(usize, usize)> = target_solution diff --git a/src/rules/longestpath_ilp.rs b/src/rules/longestpath_ilp.rs index ef0246564..83e18cbe2 100644 --- a/src/rules/longestpath_ilp.rs +++ b/src/rules/longestpath_ilp.rs @@ -35,8 +35,6 @@ impl ReductionResult for ReductionLongestPathToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ (0..self.num_edges) .map(|edge_idx| { diff --git a/src/rules/maxcut_minimumcutintoboundedsets.rs b/src/rules/maxcut_minimumcutintoboundedsets.rs index 795c46ffb..0e50c80aa 100644 --- a/src/rules/maxcut_minimumcutintoboundedsets.rs +++ b/src/rules/maxcut_minimumcutintoboundedsets.rs @@ -34,8 +34,6 @@ impl ReductionResult for ReductionMaxCutToMinCutBounded { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution[..self.original_n].to_vec()) } } diff --git a/src/rules/maxcut_minimummatrixcover.rs b/src/rules/maxcut_minimummatrixcover.rs index a1e8f50c7..aa817ecba 100644 --- a/src/rules/maxcut_minimummatrixcover.rs +++ b/src/rules/maxcut_minimummatrixcover.rs @@ -13,9 +13,9 @@ //! sign encoding (`config[i] = 1 ⇔ f(i) = +1 ⇔ i ∈ S`). //! //! **Precondition:** all edge weights must be nonnegative. The reduction -//! panics on any negative weight, since `MinimumMatrixCover` requires a +//! returns an error on any negative weight, since `MinimumMatrixCover` requires a //! nonnegative integer matrix. Negative-weight `MaxCut` instances are out -//! of scope and must use a different (preprocessing) reduction. +//! of scope for this reduction. //! //! Reference: Garey & Johnson, *Computers and Intractability* (1979), //! Appendix A1.2, MS13 ("Transformation from MAXIMUM CUT"). @@ -52,8 +52,6 @@ impl ReductionResult for ReductionMaxCutToMMC { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution.to_vec()) } } @@ -79,13 +77,13 @@ impl ReduceTo for MaxCut { "edge ({u}, {v}) has negative weight {w}" ))); } - let w64 = w; - matrix[u][v] = w64; - matrix[v][u] = w64; + matrix[u][v] = w; + matrix[v][u] = w; } Ok(ReductionMaxCutToMMC { - target: MinimumMatrixCover::new(matrix), + target: MinimumMatrixCover::new(matrix) + .map_err(>::target_construction)?, }) } } diff --git a/src/rules/maximalis_ilp.rs b/src/rules/maximalis_ilp.rs index 1d2e3f93c..8496d67b0 100644 --- a/src/rules/maximalis_ilp.rs +++ b/src/rules/maximalis_ilp.rs @@ -26,8 +26,6 @@ impl ReductionResult for ReductionMxISToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution.iter().map(|&value| value == 1).collect()) } } diff --git a/src/rules/maximum2satisfiability_ilp.rs b/src/rules/maximum2satisfiability_ilp.rs index cfa12df13..76fbc49cc 100644 --- a/src/rules/maximum2satisfiability_ilp.rs +++ b/src/rules/maximum2satisfiability_ilp.rs @@ -31,8 +31,6 @@ impl ReductionResult for ReductionMaximum2SatisfiabilityToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution[..self.num_vars] .iter() .map(|&value| value == 1) diff --git a/src/rules/maximum2satisfiability_maxcut.rs b/src/rules/maximum2satisfiability_maxcut.rs index e153b5d4f..59d3c047d 100644 --- a/src/rules/maximum2satisfiability_maxcut.rs +++ b/src/rules/maximum2satisfiability_maxcut.rs @@ -37,8 +37,6 @@ impl ReductionResult for ReductionMaximum2SatisfiabilityToMaxCut { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ let reference_side = target_solution[0]; (0..self.source_num_vars) diff --git a/src/rules/maximumclique_ilp.rs b/src/rules/maximumclique_ilp.rs index 7f62e15b1..60f2f9af3 100644 --- a/src/rules/maximumclique_ilp.rs +++ b/src/rules/maximumclique_ilp.rs @@ -39,8 +39,6 @@ impl ReductionResult for ReductionCliqueToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution.iter().map(|&value| value == 1).collect()) } } diff --git a/src/rules/maximumclique_maximumindependentset.rs b/src/rules/maximumclique_maximumindependentset.rs index 7a42f27f4..63c8534e1 100644 --- a/src/rules/maximumclique_maximumindependentset.rs +++ b/src/rules/maximumclique_maximumindependentset.rs @@ -32,8 +32,6 @@ where &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution.to_vec()) } } diff --git a/src/rules/maximumcokplex_ilp.rs b/src/rules/maximumcokplex_ilp.rs index 8021bb316..5c8521c92 100644 --- a/src/rules/maximumcokplex_ilp.rs +++ b/src/rules/maximumcokplex_ilp.rs @@ -35,8 +35,6 @@ where &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution.iter().map(|&value| value == 1).collect()) } } diff --git a/src/rules/maximumcommonedgesubgraph_ilp.rs b/src/rules/maximumcommonedgesubgraph_ilp.rs index 4ab7d8858..d01b63d53 100644 --- a/src/rules/maximumcommonedgesubgraph_ilp.rs +++ b/src/rules/maximumcommonedgesubgraph_ilp.rs @@ -47,22 +47,15 @@ impl ReductionResult for ReductionMCESToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - let n2 = self.num_vertices_2; - (0..self.num_vertices_1) + Ok((0..self.num_vertices_1) .map(|vertex| { - let mut selected = - (0..n2).filter(|&mapped| target_solution[vertex * n2 + mapped] == 1); - match (selected.next(), selected.next()) { - (Some(mapped), None) => Ok(mapped), - (None, _) => Ok(n2), - (Some(_), Some(_)) => Err(crate::rules::ExtractionError::invalid(format!( - "source vertex {vertex} maps to multiple target vertices" - ))), + match (0..n2).find(|&mapped| target_solution[vertex * n2 + mapped] == 1) { + Some(mapped) => mapped, + None => n2, } }) - .collect() + .collect()) } } diff --git a/src/rules/maximumcontactmapoverlap_ilp.rs b/src/rules/maximumcontactmapoverlap_ilp.rs index 709e0053f..150309bfa 100644 --- a/src/rules/maximumcontactmapoverlap_ilp.rs +++ b/src/rules/maximumcontactmapoverlap_ilp.rs @@ -50,22 +50,15 @@ impl ReductionResult for ReductionCMOToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - let n2 = self.num_vertices_2; - (0..self.num_vertices_1) + Ok((0..self.num_vertices_1) .map(|residue| { - let mut selected = - (0..n2).filter(|&mapped| target_solution[residue * n2 + mapped] == 1); - match (selected.next(), selected.next()) { - (Some(mapped), None) => Ok(mapped + 1), - (None, _) => Ok(0), - (Some(_), Some(_)) => Err(crate::rules::ExtractionError::invalid(format!( - "source residue {residue} maps to multiple target residues" - ))), + match (0..n2).find(|&mapped| target_solution[residue * n2 + mapped] == 1) { + Some(mapped) => mapped + 1, + None => 0, } }) - .collect() + .collect()) } } diff --git a/src/rules/maximumdomaticnumber_ilp.rs b/src/rules/maximumdomaticnumber_ilp.rs index 3a0221a6e..a4e092a3a 100644 --- a/src/rules/maximumdomaticnumber_ilp.rs +++ b/src/rules/maximumdomaticnumber_ilp.rs @@ -40,8 +40,6 @@ impl ReductionResult for ReductionDomaticNumberToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ let n = self.n; let mut config = vec![0; n]; diff --git a/src/rules/maximumedgeweightedkclique_ilp.rs b/src/rules/maximumedgeweightedkclique_ilp.rs index c7909601b..64389a0a9 100644 --- a/src/rules/maximumedgeweightedkclique_ilp.rs +++ b/src/rules/maximumedgeweightedkclique_ilp.rs @@ -62,8 +62,6 @@ where &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution[..self.num_vertices] .iter() .map(|&value| value == 1) diff --git a/src/rules/maximumindependentset_gridgraph.rs b/src/rules/maximumindependentset_gridgraph.rs index 1542a67fb..d09644424 100644 --- a/src/rules/maximumindependentset_gridgraph.rs +++ b/src/rules/maximumindependentset_gridgraph.rs @@ -29,8 +29,6 @@ impl ReductionResult for ReductionISSimpleOneToGridOne { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - let encoded = crate::config::bits_to_config(target_solution); let mapped = self.mapping_result.map_config_back(&encoded)?; Ok(crate::config::config_to_bits(&mapped)) diff --git a/src/rules/maximumindependentset_maximumclique.rs b/src/rules/maximumindependentset_maximumclique.rs index 98adc9190..d8d21dda3 100644 --- a/src/rules/maximumindependentset_maximumclique.rs +++ b/src/rules/maximumindependentset_maximumclique.rs @@ -32,8 +32,6 @@ where &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution.to_vec()) } } diff --git a/src/rules/maximumindependentset_maximumsetpacking.rs b/src/rules/maximumindependentset_maximumsetpacking.rs index 6695e87f4..1e657ce60 100644 --- a/src/rules/maximumindependentset_maximumsetpacking.rs +++ b/src/rules/maximumindependentset_maximumsetpacking.rs @@ -33,8 +33,6 @@ where &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution.to_vec()) } } @@ -95,8 +93,6 @@ where &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution.to_vec()) } } diff --git a/src/rules/maximumindependentset_triangular.rs b/src/rules/maximumindependentset_triangular.rs index 644c75bae..2dec63e97 100644 --- a/src/rules/maximumindependentset_triangular.rs +++ b/src/rules/maximumindependentset_triangular.rs @@ -31,8 +31,6 @@ impl ReductionResult for ReductionISSimpleToTriangular { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - let encoded = crate::config::bits_to_config(target_solution); let mapped = triangular::map_config_back(&self.mapping_result, &encoded)?; Ok(crate::config::config_to_bits(&mapped)) diff --git a/src/rules/maximumleafspanningtree_ilp.rs b/src/rules/maximumleafspanningtree_ilp.rs index 2623018b7..21502bd13 100644 --- a/src/rules/maximumleafspanningtree_ilp.rs +++ b/src/rules/maximumleafspanningtree_ilp.rs @@ -43,8 +43,6 @@ impl ReductionResult for ReductionMaximumLeafSpanningTreeToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ // First m variables are edge selectors target_solution[..self.num_edges] diff --git a/src/rules/maximumlikelihoodranking_ilp.rs b/src/rules/maximumlikelihoodranking_ilp.rs index 954b09627..ffa7518e9 100644 --- a/src/rules/maximumlikelihoodranking_ilp.rs +++ b/src/rules/maximumlikelihoodranking_ilp.rs @@ -43,8 +43,6 @@ impl ReductionResult for ReductionMaximumLikelihoodRankingToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ let n = self.n; if n == 0 { diff --git a/src/rules/maximummatching_ilp.rs b/src/rules/maximummatching_ilp.rs index a4e27b0d9..7a5b6af52 100644 --- a/src/rules/maximummatching_ilp.rs +++ b/src/rules/maximummatching_ilp.rs @@ -39,8 +39,6 @@ impl ReductionResult for ReductionMatchingToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution.iter().map(|&value| value == 1).collect()) } } diff --git a/src/rules/maximummatching_maximumsetpacking.rs b/src/rules/maximummatching_maximumsetpacking.rs index 1d98ef7a1..6eb6c97a4 100644 --- a/src/rules/maximummatching_maximumsetpacking.rs +++ b/src/rules/maximummatching_maximumsetpacking.rs @@ -34,8 +34,6 @@ where &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution.to_vec()) } } diff --git a/src/rules/maximumsetpacking_ilp.rs b/src/rules/maximumsetpacking_ilp.rs index 82766a60e..d600aef91 100644 --- a/src/rules/maximumsetpacking_ilp.rs +++ b/src/rules/maximumsetpacking_ilp.rs @@ -33,14 +33,12 @@ impl ReductionResult for ReductionSPToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution.iter().map(|&value| value == 1).collect()) } } #[reduction( - transform = exact { + transform = upper_bound { num_vars = "num_sets", num_constraints = "universe_size", }, diff --git a/src/rules/maximumsetpacking_qubo.rs b/src/rules/maximumsetpacking_qubo.rs index 2a7559884..24028e8fd 100644 --- a/src/rules/maximumsetpacking_qubo.rs +++ b/src/rules/maximumsetpacking_qubo.rs @@ -34,8 +34,6 @@ impl ReductionResult for ReductionSPToQUBO { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution.to_vec()) } } @@ -62,21 +60,21 @@ impl ReduceTo> for MaximumSetPacking { >("computing the set-packing conflict penalty")); } - let mut matrix = vec![vec![0.0; n]; n]; + let mut matrix = vec![std::collections::BTreeMap::new(); n]; // Diagonal: -w_i for i in 0..n { - matrix[i][i] = -weights[i]; + matrix[i].insert(i, -weights[i]); } // Off-diagonal: P for overlapping pairs for (i, j) in self.overlapping_pairs() { let (a, b) = if i < j { (i, j) } else { (j, i) }; - matrix[a][b] += penalty; + *matrix[a].entry(b).or_insert(0.0) += penalty; } Ok(ReductionSPToQUBO { - target: QUBO::from_matrix(matrix).map_err(|message| { + target: QUBO::from_rows(matrix).map_err(|message| { crate::rules::ReductionError::construction::, QUBO>( message, ) diff --git a/src/rules/minimumcapacitatedspanningtree_ilp.rs b/src/rules/minimumcapacitatedspanningtree_ilp.rs index 0886ad247..cbc01dd4a 100644 --- a/src/rules/minimumcapacitatedspanningtree_ilp.rs +++ b/src/rules/minimumcapacitatedspanningtree_ilp.rs @@ -50,8 +50,6 @@ impl ReductionResult for ReductionMinimumCapacitatedSpanningTreeToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ // First m variables are edge selectors target_solution[..self.num_edges] diff --git a/src/rules/minimumcostmaximumflow_minimumcostcirculation.rs b/src/rules/minimumcostmaximumflow_minimumcostcirculation.rs index 0d6df7ddb..4c04d4e76 100644 --- a/src/rules/minimumcostmaximumflow_minimumcostcirculation.rs +++ b/src/rules/minimumcostmaximumflow_minimumcostcirculation.rs @@ -47,8 +47,6 @@ impl ReductionResult for ReductionMCMFToMCC { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution[..self.num_original_arcs].to_vec()) } } diff --git a/src/rules/minimumcoveringbycliques_ilp.rs b/src/rules/minimumcoveringbycliques_ilp.rs index fc139660b..7fe76bc6d 100644 --- a/src/rules/minimumcoveringbycliques_ilp.rs +++ b/src/rules/minimumcoveringbycliques_ilp.rs @@ -43,21 +43,15 @@ impl ReductionResult for ReductionMinimumCoveringByCliquesToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - - (0..self.num_edges) - .map(|edge| { + Ok((0..self.num_edges) + .flat_map(|edge| { (0..self.num_edges) - .find(|&clique| { + .filter(move |&clique| { target_solution[self.y_offset + edge * self.num_edges + clique] == 1 }) - .ok_or_else(|| { - crate::rules::ExtractionError::invalid(format!( - "edge {edge} is not covered by any clique" - )) - }) + .take(1) }) - .collect() + .collect()) } } diff --git a/src/rules/minimumcoveringbycliques_minimumintersectiongraphbasis.rs b/src/rules/minimumcoveringbycliques_minimumintersectiongraphbasis.rs index 892f31b03..fe6a20594 100644 --- a/src/rules/minimumcoveringbycliques_minimumintersectiongraphbasis.rs +++ b/src/rules/minimumcoveringbycliques_minimumintersectiongraphbasis.rs @@ -8,7 +8,6 @@ use crate::models::graph::{MinimumCoveringByCliques, MinimumIntersectionGraphBas use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; use crate::topology::{Graph, SimpleGraph}; -use crate::traits::Problem; use std::collections::BTreeMap; #[derive(Debug, Clone)] @@ -16,37 +15,20 @@ pub struct ReductionMinimumCoveringByCliquesToMinimumIntersectionGraphBasis { target: MinimumIntersectionGraphBasis, } -fn extract_edge_clique_cover( - graph: &SimpleGraph, - target_solution: &[Vec], -) -> Option> { - let n = graph.num_vertices(); +fn extract_edge_clique_cover(graph: &SimpleGraph, target_solution: &[Vec]) -> Vec { let m = graph.num_edges(); - - if target_solution.len() != n || target_solution.iter().any(|row| row.len() != m) { - return None; - } - - if m == 0 { - return Some(Vec::new()); - } - let mut label_map = BTreeMap::new(); - let mut next_label = 0usize; let mut source_solution = Vec::with_capacity(m); - for (u, v) in graph.edges() { - let shared_label = - (0..m).find(|&slot| target_solution[u][slot] && target_solution[v][slot])?; - let compressed = *label_map.entry(shared_label).or_insert_with(|| { - let label = next_label; - next_label += 1; - label - }); - source_solution.push(compressed); + for shared_label in (0..m) + .filter(|&slot| target_solution[u][slot] && target_solution[v][slot]) + .take(1) + { + let next_label = label_map.len(); + source_solution.push(*label_map.entry(shared_label).or_insert(next_label)); + } } - - Some(source_solution) + source_solution } #[cfg(any(test, feature = "example-db"))] @@ -86,21 +68,10 @@ impl ReductionResult for ReductionMinimumCoveringByCliquesToMinimumIntersectionG &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - - Ok({ - if !self.target.evaluate(target_solution)?.is_valid() { - return Err(crate::rules::ExtractionError::invalid( - "target configuration is not a valid intersection graph basis", - )); - } - - extract_edge_clique_cover(self.target.graph(), target_solution).ok_or_else(|| { - crate::rules::ExtractionError::invalid( - "target basis does not assign a shared label to every source edge", - ) - })? - }) + Ok(extract_edge_clique_cover( + self.target.graph(), + target_solution, + )) } } diff --git a/src/rules/minimumcutintoboundedsets_ilp.rs b/src/rules/minimumcutintoboundedsets_ilp.rs index bb0331be8..3a52a6520 100644 --- a/src/rules/minimumcutintoboundedsets_ilp.rs +++ b/src/rules/minimumcutintoboundedsets_ilp.rs @@ -30,8 +30,6 @@ impl ReductionResult for ReductionMinCutBSToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution[..self.num_vertices] .iter() .map(|&value| value == 1) diff --git a/src/rules/minimumdiscreteplanarinversekinematics_qubo.rs b/src/rules/minimumdiscreteplanarinversekinematics_qubo.rs index a00026804..198214b0d 100644 --- a/src/rules/minimumdiscreteplanarinversekinematics_qubo.rs +++ b/src/rules/minimumdiscreteplanarinversekinematics_qubo.rs @@ -29,6 +29,8 @@ pub struct ReductionMinimumDiscretePlanarInverseKinematicsToQUBO { target: QUBO, block_offsets: Vec, block_sizes: Vec, + omitted_constant: f64, + feasible_energy_upper: f64, } impl ReductionResult for ReductionMinimumDiscretePlanarInverseKinematicsToQUBO { @@ -39,36 +41,47 @@ impl ReductionResult for ReductionMinimumDiscretePlanarInverseKinematicsToQUBO { &self.target } + /// Decode a qualifying optimum after the energy relation establishes source + /// feasibility. Such an optimum is one-hot and obeys every allowed pair. fn extract_solution( &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - - self.block_offsets + Ok(self + .block_offsets .iter() .zip(&self.block_sizes) - .enumerate() - .map(|(link, (&start, &size))| { - let mut selected = target_solution[start..start + size] + .map(|(&start, &size)| { + target_solution[start..start + size] .iter() - .enumerate() - .filter_map(|(orientation, &bit)| bit.then_some(orientation)); - match (selected.next(), selected.next()) { - (Some(orientation), None) => Ok(orientation), - (None, _) => Err(crate::rules::ExtractionError::invalid(format!( - "link {link} has no selected orientation" - ))), - (Some(_), Some(_)) => Err(crate::rules::ExtractionError::invalid(format!( - "link {link} has multiple selected orientations" - ))), - } + .position(|&bit| bit) + .unwrap() }) - .collect() + .collect()) } } -#[reduction(transform = exact { +impl crate::rules::AggregateReductionResult + for ReductionMinimumDiscretePlanarInverseKinematicsToQUBO +{ + type Source = MinimumDiscretePlanarInverseKinematics; + type Target = QUBO; + + fn target_problem(&self) -> &Self::Target { + &self.target + } + + fn extract_value(&self, value: crate::types::Min) -> crate::types::Min { + crate::types::Min( + value + .0 + .filter(|&energy| energy < self.feasible_energy_upper) + .map(|energy| energy + self.omitted_constant), + ) + } +} + +#[reduction(aggregate = custom, transform = exact { num_vars = "num_orientation_samples", })] impl ReduceTo> for MinimumDiscretePlanarInverseKinematics { @@ -95,12 +108,25 @@ impl ReduceTo> for MinimumDiscretePlanarInverseKinematics { // instance is one-hot and pair-feasible. let sum_abs_x: f64 = x_coeffs.iter().map(|coeff| coeff.abs()).sum(); let sum_abs_y: f64 = y_coeffs.iter().map(|coeff| coeff.abs()).sum(); - let penalty = 1.0 + (sum_abs_x + gx.abs()).powi(2) + (sum_abs_y + gy.abs()).powi(2); + let distance_bound = (sum_abs_x + gx.abs()).powi(2) + (sum_abs_y + gy.abs()).powi(2); + // Leave a gap proportional to the scale, rather than adding one to a + // large floating-point number that may round back to itself. + let penalty = 2.0 * (1.0 + distance_bound); + let omitted_constant = gx * gx + gy * gy + penalty * block_sizes.len() as f64; + let feasible_energy_upper = 1.5 * (1.0 + distance_bound) - omitted_constant; + if !omitted_constant.is_finite() || !feasible_energy_upper.is_finite() { + return Err(crate::rules::ReductionError::non_finite_result::< + Self, + QUBO, + >( + "computing the inverse-kinematics energy relation" + )); + } - let mut matrix = vec![vec![0.0; total_vars]; total_vars]; + let mut matrix = vec![std::collections::BTreeMap::new(); total_vars]; let mut add_upper = |i: usize, j: usize, value: f64| { let (lo, hi) = if i <= j { (i, j) } else { (j, i) }; - matrix[lo][hi] += value; + *matrix[lo].entry(hi).or_insert(0.0) += value; }; // Position objective: (X - g_x)^2 + (Y - g_y)^2, dropping the @@ -156,14 +182,12 @@ impl ReduceTo> for MinimumDiscretePlanarInverseKinematics { } Ok(ReductionMinimumDiscretePlanarInverseKinematicsToQUBO { - target: QUBO::from_matrix(matrix).map_err(|message| { - crate::rules::ReductionError::construction::< - MinimumDiscretePlanarInverseKinematics, - QUBO, - >(message) - })?, + target: QUBO::from_rows(matrix) + .map_err(>>::target_construction)?, block_offsets, block_sizes, + omitted_constant, + feasible_energy_upper, }) } } diff --git a/src/rules/minimumdominatingset_ilp.rs b/src/rules/minimumdominatingset_ilp.rs index 5b1d3f945..49b9c517a 100644 --- a/src/rules/minimumdominatingset_ilp.rs +++ b/src/rules/minimumdominatingset_ilp.rs @@ -40,8 +40,6 @@ impl ReductionResult for ReductionDSToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution.iter().map(|&value| value == 1).collect()) } } diff --git a/src/rules/minimumedgecostflow_ilp.rs b/src/rules/minimumedgecostflow_ilp.rs index dd30eb04a..4b55f0b60 100644 --- a/src/rules/minimumedgecostflow_ilp.rs +++ b/src/rules/minimumedgecostflow_ilp.rs @@ -47,8 +47,6 @@ impl ReductionResult for ReductionMECFToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - crate::rules::ilp_helpers::decode_usize_values(&target_solution[..self.num_edges]) } } diff --git a/src/rules/minimumexternalmacrodatacompression_ilp.rs b/src/rules/minimumexternalmacrodatacompression_ilp.rs index ba34bba53..19b7e911c 100644 --- a/src/rules/minimumexternalmacrodatacompression_ilp.rs +++ b/src/rules/minimumexternalmacrodatacompression_ilp.rs @@ -125,8 +125,6 @@ impl ReductionResult for ReductionEMDCToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ let n = self.layout.n; let k = self.alphabet_size; @@ -135,27 +133,10 @@ impl ReductionResult for ReductionEMDCToILP { // Build D-slots let mut d_slots = vec![empty; n]; for j in 0..n { - let symbols: Vec<_> = (0..k) - .filter(|&c| target_solution[self.layout.d_var(j, c)] == 1) - .collect(); if target_solution[self.layout.d_used_var(j)] == 1 { - match symbols.as_slice() { - [symbol] => d_slots[j] = *symbol, - [] => { - return Err(crate::rules::ExtractionError::invalid(format!( - "dictionary slot {j} is active without a symbol" - ))) - } - _ => { - return Err(crate::rules::ExtractionError::invalid(format!( - "dictionary slot {j} selects multiple symbols" - ))) - } - } - } else if !symbols.is_empty() { - return Err(crate::rules::ExtractionError::invalid(format!( - "inactive dictionary slot {j} selects a symbol" - ))); + d_slots[j] = (0..k) + .filter(|&c| target_solution[self.layout.d_var(j, c)] == 1) + .sum(); } } @@ -173,23 +154,14 @@ impl ReductionResult for ReductionEMDCToILP { }) .collect(); if target_solution[self.layout.lit_var(pos)] == 1 { - if !pointers.is_empty() { - return Err(crate::rules::ExtractionError::invalid(format!( - "position {pos} selects both a literal and a pointer" - ))); - } // Literal at position pos c_slots[c_pos] = self.source_string[pos]; c_pos += 1; pos += 1; continue; } - let [(d_start, length)] = pointers.as_slice() else { - return Err(crate::rules::ExtractionError::invalid(format!( - "position {pos} must select exactly one pointer" - ))); - }; - let ptr_idx = encode_pointer(n, *d_start, *length); + let (d_start, length) = pointers[0]; + let ptr_idx = encode_pointer(n, d_start, length); c_slots[c_pos] = k + 1 + ptr_idx; c_pos += 1; pos += length; diff --git a/src/rules/minimumfaultdetectiontestset_ilp.rs b/src/rules/minimumfaultdetectiontestset_ilp.rs index bf0893c70..47d6d683e 100644 --- a/src/rules/minimumfaultdetectiontestset_ilp.rs +++ b/src/rules/minimumfaultdetectiontestset_ilp.rs @@ -31,8 +31,6 @@ impl ReductionResult for ReductionMFDTSToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok((0..self.num_inputs) .map(|input| { (0..self.num_outputs) diff --git a/src/rules/minimumfeedbackarcset_ilp.rs b/src/rules/minimumfeedbackarcset_ilp.rs index 2de455d85..6b6d1d932 100644 --- a/src/rules/minimumfeedbackarcset_ilp.rs +++ b/src/rules/minimumfeedbackarcset_ilp.rs @@ -45,8 +45,6 @@ impl ReductionResult for ReductionFASToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution[..self.num_arcs] .iter() .map(|&value| value == 1) diff --git a/src/rules/minimumfeedbackvertexset_ilp.rs b/src/rules/minimumfeedbackvertexset_ilp.rs index 8b712b1a1..8c07f9793 100644 --- a/src/rules/minimumfeedbackvertexset_ilp.rs +++ b/src/rules/minimumfeedbackvertexset_ilp.rs @@ -42,8 +42,6 @@ impl ReductionResult for ReductionMFVSToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution[..self.num_vertices] .iter() .map(|&value| value == 1) diff --git a/src/rules/minimumfeedbackvertexset_minimumcodegenerationunlimitedregisters.rs b/src/rules/minimumfeedbackvertexset_minimumcodegenerationunlimitedregisters.rs index 1e336e56b..8d88975e0 100644 --- a/src/rules/minimumfeedbackvertexset_minimumcodegenerationunlimitedregisters.rs +++ b/src/rules/minimumfeedbackvertexset_minimumcodegenerationunlimitedregisters.rs @@ -34,13 +34,6 @@ impl ReductionResult for ReductionFVSToCodeGen { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - let value = - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - if value.0.is_none() { - return Err(crate::rules::ExtractionError::invalid( - "target order must be a permutation respecting expression dependencies", - )); - } Ok(self .chain_start .iter() diff --git a/src/rules/minimumgraphbandwidth_ilp.rs b/src/rules/minimumgraphbandwidth_ilp.rs index 76e1b5692..1a584e782 100644 --- a/src/rules/minimumgraphbandwidth_ilp.rs +++ b/src/rules/minimumgraphbandwidth_ilp.rs @@ -38,14 +38,12 @@ impl ReductionResult for ReductionMGBToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - - crate::rules::ilp_helpers::one_hot_decode_rows( + Ok(crate::rules::ilp_helpers::one_hot_decode_rows( target_solution, self.num_vertices, self.num_vertices, 0, - ) + )) } } diff --git a/src/rules/minimumhittingset_ilp.rs b/src/rules/minimumhittingset_ilp.rs index fe8f498d9..03bc54555 100644 --- a/src/rules/minimumhittingset_ilp.rs +++ b/src/rules/minimumhittingset_ilp.rs @@ -25,8 +25,6 @@ impl ReductionResult for ReductionHSToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution.iter().map(|&value| value == 1).collect()) } } diff --git a/src/rules/minimuminternalmacrodatacompression_ilp.rs b/src/rules/minimuminternalmacrodatacompression_ilp.rs index 0ff986409..6008e3f77 100644 --- a/src/rules/minimuminternalmacrodatacompression_ilp.rs +++ b/src/rules/minimuminternalmacrodatacompression_ilp.rs @@ -99,8 +99,6 @@ impl ReductionResult for ReductionIMDCToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ let n = self.layout.n; let k = self.alphabet_size; diff --git a/src/rules/minimummatrixcover_ilp.rs b/src/rules/minimummatrixcover_ilp.rs index d279e1b67..c39aadd19 100644 --- a/src/rules/minimummatrixcover_ilp.rs +++ b/src/rules/minimummatrixcover_ilp.rs @@ -32,8 +32,6 @@ impl ReductionResult for ReductionMinimumMatrixCoverToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ // First n variables are the sign variables x_0,...,x_{n-1} target_solution[..self.n] @@ -160,7 +158,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution.iter().map(|&value| value == 1).collect()) } } diff --git a/src/rules/minimummaximalmatching_maximumachromaticnumber.rs b/src/rules/minimummaximalmatching_maximumachromaticnumber.rs index 65615aa27..ca773ac50 100644 --- a/src/rules/minimummaximalmatching_maximumachromaticnumber.rs +++ b/src/rules/minimummaximalmatching_maximumachromaticnumber.rs @@ -46,8 +46,6 @@ impl ReductionResult for ReductionMMMToAchromatic { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ self.source_edges .iter() diff --git a/src/rules/minimummaximalmatching_minimummatrixdomination.rs b/src/rules/minimummaximalmatching_minimummatrixdomination.rs index 03b0c98d7..25749ece1 100644 --- a/src/rules/minimummaximalmatching_minimummatrixdomination.rs +++ b/src/rules/minimummaximalmatching_minimummatrixdomination.rs @@ -80,7 +80,6 @@ impl ReductionResult for ReductionMMMToMatrixDomination { /// /// - **Drop:** if every edge of `B` incident to `u` is already dominated /// by `D \ {e1}`, set `D := D \ {e1}` (size strictly decreases). - /// Symmetric for `w` and `e2`. /// - **Swap:** otherwise, some edge `(u, x)` of `B` is currently dominated /// only by `e1`. This `x` must lie outside `V(D \ {e1})` and is /// therefore distinct from `w`, so `(u, x)` is not adjacent to `e2`. @@ -89,16 +88,14 @@ impl ReductionResult for ReductionMMMToMatrixDomination { /// /// Each iteration strictly decreases either `|D|` or the number of /// adjacent pairs, so the loop terminates in `O(|F|^2)` iterations. Each - /// iteration scans `O(|F|)` edges to find an adjacent pair, an EDS check, - /// and a swap candidate, for a total of `O(|F|^3)` time. The result is a + /// iteration scans `O(|F|)` edges to find an adjacent pair and an + /// undominated edge, for a total of `O(|F|^3)` time. The result is a /// matching that is an EDS, i.e. an independent EDS, which is precisely a /// maximal matching. fn extract_solution( &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ let graph = self.source.graph(); let edges = graph.edges(); @@ -124,87 +121,28 @@ impl ReductionResult for ReductionMMMToMatrixDomination { .collect(); let mut d: Vec = target_solution .iter() - .zip(target_ones.iter()) - .filter_map(|(&sel, &cell)| { - if sel { - Some(cell_to_source_edge.get(&cell).copied().ok_or_else(|| { - crate::rules::ExtractionError::invalid(format!( - "selected matrix cell {cell:?} has no source edge" - )) - })) - } else { - None - } - }) - .collect::>()?; - - // Step 2: Yannakakis-Gavril EDS -> independent EDS (maximal matching). - // Loop invariants: `d` is an EDS of the source graph; each iteration - // strictly decreases either |d| or the number of (unordered) pairs of - // adjacent edges inside `d`. - loop { - // Find an adjacent pair (e1_idx, e2_idx) inside `d`, sharing vertex v. - let pair = find_adjacent_pair(&d, &edges); - let Some((e1_idx, e2_idx, _shared)) = pair else { - break; // `d` is a matching; we are done. - }; - - // Try dropping e1_idx or e2_idx if the remainder is still an EDS. - let mut without_e1 = d.clone(); - let e1_position = d.iter().position(|&x| x == e1_idx).ok_or_else(|| { - crate::rules::ExtractionError::invalid( - "edge-domination transformation lost its selected edge", - ) - })?; - without_e1.swap_remove(e1_position); - if is_edge_dominating_set(&without_e1, &edges) { - d = without_e1; - continue; - } - let mut without_e2 = d.clone(); - let e2_position = d.iter().position(|&x| x == e2_idx).ok_or_else(|| { - crate::rules::ExtractionError::invalid( - "edge-domination transformation lost its selected edge", - ) - })?; - without_e2.swap_remove(e2_position); - if is_edge_dominating_set(&without_e2, &edges) { - d = without_e2; - continue; - } - - // Neither drop works -> perform a swap on one of e1 or e2. - // Choose endpoint not shared with the other edge: for e1=(u, v), - // e2=(v, w), the "non-shared" endpoint of e1 is u. - let (e1_a, e1_b) = edges[e1_idx]; - let (e2_a, e2_b) = edges[e2_idx]; - let shared = if e1_a == e2_a || e1_a == e2_b { - e1_a - } else { - e1_b - }; - let u = if e1_a == shared { e1_b } else { e1_a }; - let w = if e2_a == shared { e2_b } else { e2_a }; + .zip(target_ones) + .filter(|(selected, _)| **selected) + .map(|(_, cell)| cell_to_source_edge[cell]) + .collect(); - // Try to swap e1 := (u, x) where x ∉ V(d \ {e1}). The YG proof - // guarantees such x exists when neither drop succeeded. - if let Some(new_idx) = find_swap_edge(u, e1_idx, &d, &edges) { - d[e1_position] = new_idx; - continue; - } - // Symmetric swap on e2. - if let Some(new_idx) = find_swap_edge(w, e2_idx, &d, &edges) { - d[e2_position] = new_idx; - continue; + // Remove one of two adjacent selected edges. Any edge no longer + // dominated is private to its non-shared endpoint; replacing the + // removed edge with it preserves domination and reduces adjacency. + while let Some(position) = find_adjacent_pair(&d, &edges) { + let mut remaining = d.clone(); + remaining.swap_remove(position); + let covered: std::collections::HashSet<_> = remaining + .iter() + .flat_map(|&edge| [edges[edge].0, edges[edge].1]) + .collect(); + match edges + .iter() + .position(|&(u, v)| !covered.contains(&u) && !covered.contains(&v)) + { + Some(edge) => d[position] = edge, + None => d = remaining, } - - // YG guarantees that for an EDS at least one of the four moves - // above succeeds. Reaching this point implies the input was not - // a valid EDS (i.e., not a feasible MMD witness on the constructed - // instance), which violates the reduction's precondition. - return Err(crate::rules::ExtractionError::invalid( - "target matrix entries do not encode an edge-dominating set", - )); } // Step 3: encode the matching as a binary configuration over source edges. @@ -217,81 +155,19 @@ impl ReductionResult for ReductionMMMToMatrixDomination { } } -/// Return `Some((i, j, v))` where `i`, `j` are indices in `d` of two edges that -/// share vertex `v`, or `None` if all edges in `d` are pairwise independent. -fn find_adjacent_pair(d: &[usize], edges: &[(usize, usize)]) -> Option<(usize, usize, usize)> { - for (a_pos, &i) in d.iter().enumerate() { - let (iu, iv) = edges[i]; - for &j in &d[a_pos + 1..] { - let (ju, jv) = edges[j]; - if iu == ju || iu == jv { - return Some((i, j, iu)); - } - if iv == ju || iv == jv { - return Some((i, j, iv)); +/// Find the position of a selected edge adjacent to another selected edge. +fn find_adjacent_pair(d: &[usize], edges: &[(usize, usize)]) -> Option { + let mut incident = std::collections::HashMap::new(); + for (position, &edge) in d.iter().enumerate() { + for vertex in [edges[edge].0, edges[edge].1] { + if let Some(previous) = incident.insert(vertex, position) { + return Some(previous); } } } None } -/// Check whether the edge set `d` (indices into `edges`) dominates every edge -/// of `edges`. An edge `f` is dominated iff `f ∈ d` or `f` shares an endpoint -/// with some edge in `d`. -fn is_edge_dominating_set(d: &[usize], edges: &[(usize, usize)]) -> bool { - // Vertex cover of the candidate EDS. - let mut covered_vertices: std::collections::HashSet = std::collections::HashSet::new(); - for &i in d { - let (u, v) = edges[i]; - covered_vertices.insert(u); - covered_vertices.insert(v); - } - edges.iter().enumerate().all(|(f_idx, (u, v))| { - d.contains(&f_idx) || covered_vertices.contains(u) || covered_vertices.contains(v) - }) -} - -/// Find an edge index in `edges` that is (i) incident to vertex `endpoint`, -/// (ii) different from `excluded_idx`, and (iii) whose other endpoint lies -/// outside `V(d \ {excluded_idx})`. -/// -/// This is the swap candidate `(u, x)` from the Yannakakis-Gavril argument -/// when the drop move is not available for `excluded_idx`. -fn find_swap_edge( - endpoint: usize, - excluded_idx: usize, - d: &[usize], - edges: &[(usize, usize)], -) -> Option { - // Vertex cover of d \ {excluded_idx}. - let mut other_cover: std::collections::HashSet = std::collections::HashSet::new(); - for &i in d { - if i == excluded_idx { - continue; - } - let (u, v) = edges[i]; - other_cover.insert(u); - other_cover.insert(v); - } - for (k, &(u, v)) in edges.iter().enumerate() { - if k == excluded_idx { - continue; - } - let (e_endpoint, other) = if u == endpoint { - (u, v) - } else if v == endpoint { - (v, u) - } else { - continue; - }; - debug_assert_eq!(e_endpoint, endpoint); - if !other_cover.contains(&other) { - return Some(k); - } - } - None -} - #[reduction( transform = exact { num_rows = "num_vertices", diff --git a/src/rules/minimummetricdimension_ilp.rs b/src/rules/minimummetricdimension_ilp.rs index a10a1e9a5..ea01ef90e 100644 --- a/src/rules/minimummetricdimension_ilp.rs +++ b/src/rules/minimummetricdimension_ilp.rs @@ -42,8 +42,6 @@ impl ReductionResult for ReductionMDToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution.iter().map(|&value| value == 1).collect()) } } diff --git a/src/rules/minimummultiwaycut_ilp.rs b/src/rules/minimummultiwaycut_ilp.rs index 51c157007..ce9f18853 100644 --- a/src/rules/minimummultiwaycut_ilp.rs +++ b/src/rules/minimummultiwaycut_ilp.rs @@ -46,8 +46,6 @@ impl ReductionResult for ReductionMMCToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ let offset = self.k * self.n; (0..self.m) diff --git a/src/rules/minimummultiwaycut_qubo.rs b/src/rules/minimummultiwaycut_qubo.rs index 8679c2285..faf2c054a 100644 --- a/src/rules/minimummultiwaycut_qubo.rs +++ b/src/rules/minimummultiwaycut_qubo.rs @@ -7,7 +7,8 @@ //! QUBO Hamiltonian: H = H_A + H_B //! //! H_A enforces valid partition (one-hot per vertex) and terminal pinning. -//! H_B encodes the cut cost objective. +//! H_B encodes nonnegative cut costs. Negative edges are always deleted during +//! extraction: deleting them improves the objective and preserves separation. //! //! Reference: Heidari, Dinneen & Delmas (2022). @@ -24,6 +25,7 @@ pub struct ReductionMinimumMultiwayCutToQUBO { num_vertices: usize, num_terminals: usize, edges: Vec<(usize, usize)>, + negative_edges: Vec, } impl ReductionResult for ReductionMinimumMultiwayCutToQUBO { @@ -34,38 +36,27 @@ impl ReductionResult for ReductionMinimumMultiwayCutToQUBO { &self.target } - /// Decode one-hot assignment: for each vertex find its terminal, then - /// for each edge check if endpoints are in different terminals. + /// Map an optimal target assignment to an optimal edge deletion set. + /// The penalty guarantees one-hot, terminal-pinned assignments at every + /// optimum. All source instances are feasible by deleting every edge. fn extract_solution( &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - - Ok({ - let k = self.num_terminals; - let n = self.num_vertices; - - // For each vertex, find which terminal position it is assigned to - let assignments: Vec = (0..n) - .map(|vertex| { - let mut selected = - (0..k).filter(|&terminal| target_solution[vertex * k + terminal]); - match (selected.next(), selected.next()) { - (Some(terminal), None) => Ok(terminal), - _ => Err(crate::rules::ExtractionError::invalid(format!( - "vertex {vertex} does not have exactly one terminal assignment" - ))), - } - }) - .collect::>()?; - - // For each edge, output 1 (cut) if endpoints differ, 0 (keep) otherwise - self.edges - .iter() - .map(|&(u, v)| assignments[u] != assignments[v]) - .collect() - }) + let k = self.num_terminals; + let assignments: Vec = (0..self.num_vertices) + .map(|vertex| { + (0..k) + .find(|&terminal| target_solution[vertex * k + terminal]) + .unwrap() + }) + .collect(); + Ok(self + .edges + .iter() + .zip(&self.negative_edges) + .map(|(&(u, v), &negative)| negative || assignments[u] != assignments[v]) + .collect()) } } @@ -91,26 +82,24 @@ impl ReduceTo> for MinimumMultiwayCut { .checked_mul(k) .ok_or_else(|| overflow("computing the number of QUBO variables"))?; - // Penalty: sum of all edge weights + 1 + // Nonnegative cut costs cannot reward invalid assignments. A feasible + // pinned partition costs at most their sum, below one penalty unit. let alpha = edge_weights.iter().try_fold(0i64, |total, &weight| { total - .checked_add( - weight - .checked_abs() - .ok_or_else(|| overflow("taking the absolute value of a cut weight"))?, - ) - .ok_or_else(|| overflow("summing absolute cut weights")) + .checked_add(weight.max(0)) + .ok_or_else(|| overflow("summing nonnegative cut weights")) })?; let alpha = alpha .checked_add(1) .ok_or_else(|| overflow("computing the partition penalty"))?; - let mut matrix = vec![vec![0i64; nq]; nq]; + let mut matrix = vec![std::collections::BTreeMap::new(); nq]; // Helper: add value to upper-triangular position let mut add_upper = |i: usize, j: usize, val: i64| { let (lo, hi) = if i <= j { (i, j) } else { (j, i) }; - matrix[lo][hi] = matrix[lo][hi] + let coefficient = matrix[lo].entry(hi).or_insert(0i64); + *coefficient = coefficient .checked_add(val) .ok_or_else(|| overflow("adding a multiway-cut QUBO coefficient"))?; Ok::<(), crate::rules::ReductionError>(()) @@ -158,7 +147,7 @@ impl ReduceTo> for MinimumMultiwayCut { // For each edge (u,v) with weight w, for each pair of distinct // terminal positions s != t: add w to Q[u*k+s, v*k+t] for (edge_idx, &(u, v)) in edges.iter().enumerate() { - let w = edge_weights[edge_idx]; + let w = edge_weights[edge_idx].max(0); for s in 0..k { for t in 0..k { if s != t { @@ -169,15 +158,12 @@ impl ReduceTo> for MinimumMultiwayCut { } Ok(ReductionMinimumMultiwayCutToQUBO { - target: QUBO::from_matrix(matrix).map_err(|message| { - crate::rules::ReductionError::construction::< - MinimumMultiwayCut, - QUBO, - >(message) - })?, + target: QUBO::from_rows(matrix) + .map_err(>>::target_construction)?, num_vertices: n, num_terminals: k, edges, + negative_edges: edge_weights.iter().map(|&weight| weight < 0).collect(), }) } } diff --git a/src/rules/minimumsetcovering_ilp.rs b/src/rules/minimumsetcovering_ilp.rs index 6f208be75..ae0aa423c 100644 --- a/src/rules/minimumsetcovering_ilp.rs +++ b/src/rules/minimumsetcovering_ilp.rs @@ -37,8 +37,6 @@ impl ReductionResult for ReductionSCToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution.iter().map(|&value| value == 1).collect()) } } diff --git a/src/rules/minimumsummulticenter_ilp.rs b/src/rules/minimumsummulticenter_ilp.rs index bb6f82fd6..817beae0b 100644 --- a/src/rules/minimumsummulticenter_ilp.rs +++ b/src/rules/minimumsummulticenter_ilp.rs @@ -45,8 +45,6 @@ impl ReductionResult for ReductionMSMCToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution[..self.num_vertices] .iter() .map(|&value| value == 1) diff --git a/src/rules/minimumtardinesssequencing_ilp.rs b/src/rules/minimumtardinesssequencing_ilp.rs index e8533153d..7621f3ee2 100644 --- a/src/rules/minimumtardinesssequencing_ilp.rs +++ b/src/rules/minimumtardinesssequencing_ilp.rs @@ -30,12 +30,10 @@ impl ReductionResult for ReductionMTSToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ let n = self.num_tasks; - one_hot_decode(target_solution, n, n, 0)? + one_hot_decode(target_solution, n, n, 0) }) } } @@ -59,12 +57,10 @@ impl ReductionResult for ReductionMTSWeightedToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ let n = self.num_tasks; - one_hot_decode(target_solution, n, n, 0)? + one_hot_decode(target_solution, n, n, 0) }) } } diff --git a/src/rules/minimumvertexcover_comparativecontainment.rs b/src/rules/minimumvertexcover_comparativecontainment.rs index 4e08fc68a..eb43d1596 100644 --- a/src/rules/minimumvertexcover_comparativecontainment.rs +++ b/src/rules/minimumvertexcover_comparativecontainment.rs @@ -34,13 +34,6 @@ impl ReductionResult for ReductionDecisionMVCToComparativeContainment { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - if !crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)? - .0 - { - return Err(crate::rules::ExtractionError::invalid( - "containment inequality is not satisfied", - )); - } Ok(target_solution.clone()) } } diff --git a/src/rules/minimumvertexcover_ensemblecomputation.rs b/src/rules/minimumvertexcover_ensemblecomputation.rs index 710badf1f..61b937335 100644 --- a/src/rules/minimumvertexcover_ensemblecomputation.rs +++ b/src/rules/minimumvertexcover_ensemblecomputation.rs @@ -44,18 +44,10 @@ impl ReductionResult for ReductionVCToEC { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - let value = - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - let crate::types::Min(Some(length)) = value else { - return Err(crate::rules::ExtractionError::invalid( - "target configuration does not encode a valid ensemble computation", - )); - }; - let meaningful_steps = usize::try_from(length).map_err(|_| { - crate::rules::ExtractionError::invalid( - "ensemble operation count cannot be represented as usize", - ) - })?; + let value = crate::traits::Problem::evaluate(self.target_problem(), target_solution)?; + // Evaluation supplies the meaningful prefix, which the mapping needs. + // The target witness premise already guarantees a feasible program. + let meaningful_steps = value.0.unwrap() as usize; let mut cover = vec![false; self.num_vertices]; let universe_size = self.target.universe_size(); for &[left, right] in target_solution diff --git a/src/rules/minimumvertexcover_longestcommonsubsequence.rs b/src/rules/minimumvertexcover_longestcommonsubsequence.rs index e2f0a23a8..74de6eb2e 100644 --- a/src/rules/minimumvertexcover_longestcommonsubsequence.rs +++ b/src/rules/minimumvertexcover_longestcommonsubsequence.rs @@ -25,8 +25,6 @@ impl ReductionResult for ReductionVCToLCS { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ let mut cover = vec![true; self.num_vertices]; for &symbol in target_solution { diff --git a/src/rules/minimumvertexcover_maximumindependentset.rs b/src/rules/minimumvertexcover_maximumindependentset.rs index 5c43127d7..c9922103d 100644 --- a/src/rules/minimumvertexcover_maximumindependentset.rs +++ b/src/rules/minimumvertexcover_maximumindependentset.rs @@ -31,8 +31,6 @@ where &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution.iter().map(|&x| !x).collect()) } } @@ -77,8 +75,6 @@ where &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution.iter().map(|&x| !x).collect()) } } diff --git a/src/rules/minimumvertexcover_minimumfeedbackarcset.rs b/src/rules/minimumvertexcover_minimumfeedbackarcset.rs index ef8f39d10..4318b5d67 100644 --- a/src/rules/minimumvertexcover_minimumfeedbackarcset.rs +++ b/src/rules/minimumvertexcover_minimumfeedbackarcset.rs @@ -35,8 +35,6 @@ impl ReductionResult for ReductionVCToFAS { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution[..self.num_source_vertices].to_vec()) } } diff --git a/src/rules/minimumvertexcover_minimumfeedbackvertexset.rs b/src/rules/minimumvertexcover_minimumfeedbackvertexset.rs index d5ed67e8a..9c4cbd3e7 100644 --- a/src/rules/minimumvertexcover_minimumfeedbackvertexset.rs +++ b/src/rules/minimumvertexcover_minimumfeedbackvertexset.rs @@ -30,8 +30,6 @@ where &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution.to_vec()) } } diff --git a/src/rules/minimumvertexcover_minimumhittingset.rs b/src/rules/minimumvertexcover_minimumhittingset.rs index 656cbcfd6..a0841c4f9 100644 --- a/src/rules/minimumvertexcover_minimumhittingset.rs +++ b/src/rules/minimumvertexcover_minimumhittingset.rs @@ -30,8 +30,6 @@ impl ReductionResult for ReductionVCToHS { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution.to_vec()) } } diff --git a/src/rules/minimumvertexcover_minimumsetcovering.rs b/src/rules/minimumvertexcover_minimumsetcovering.rs index 4a7baceca..048e4e31d 100644 --- a/src/rules/minimumvertexcover_minimumsetcovering.rs +++ b/src/rules/minimumvertexcover_minimumsetcovering.rs @@ -33,8 +33,6 @@ where &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution.to_vec()) } } diff --git a/src/rules/minimumvertexcover_minimumweightandorgraph.rs b/src/rules/minimumvertexcover_minimumweightandorgraph.rs index 247ce8733..f462f28f6 100644 --- a/src/rules/minimumvertexcover_minimumweightandorgraph.rs +++ b/src/rules/minimumvertexcover_minimumweightandorgraph.rs @@ -27,8 +27,6 @@ impl ReductionResult for ReductionVCToAndOrGraph { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ (0..self.num_source_vertices) .map(|j| target_solution[self.sink_arc_start + j]) diff --git a/src/rules/minimumweightdecoding_ilp.rs b/src/rules/minimumweightdecoding_ilp.rs index 970470c71..ce5ec087e 100644 --- a/src/rules/minimumweightdecoding_ilp.rs +++ b/src/rules/minimumweightdecoding_ilp.rs @@ -44,8 +44,6 @@ impl ReductionResult for ReductionMinimumWeightDecodingToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution[..self.num_cols] .iter() .map(|&value| value == 1) diff --git a/src/rules/minmaxmulticenter_ilp.rs b/src/rules/minmaxmulticenter_ilp.rs index f4360ac63..14580acdf 100644 --- a/src/rules/minmaxmulticenter_ilp.rs +++ b/src/rules/minmaxmulticenter_ilp.rs @@ -49,8 +49,6 @@ impl ReductionResult for ReductionMMCToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution[..self.num_vertices] .iter() .map(|&value| value == 1) diff --git a/src/rules/mixedchinesepostman_ilp.rs b/src/rules/mixedchinesepostman_ilp.rs index 88da8843a..fc0b840a0 100644 --- a/src/rules/mixedchinesepostman_ilp.rs +++ b/src/rules/mixedchinesepostman_ilp.rs @@ -30,8 +30,6 @@ impl ReductionResult for ReductionMCPToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ // Return the orientation bits d_k in source edge order target_solution[..self.num_undirected_edges] diff --git a/src/rules/mod.rs b/src/rules/mod.rs index 2b7d66b7c..4b7faa052 100644 --- a/src/rules/mod.rs +++ b/src/rules/mod.rs @@ -43,8 +43,8 @@ pub(crate) mod hamiltoniancircuit_travelingsalesman; pub(crate) mod hamiltonianpath_degreeconstrainedspanningtree; pub(crate) mod hamiltonianpath_isomorphicspanningtree; pub(crate) mod hamiltonianpathbetweentwovertices_longestpath; -pub(crate) mod ilp_casts; pub(crate) mod ilp_i64_ilp_bool; +pub(crate) mod ilp_i64_ilp_f64; pub(crate) mod integerknapsack_ilp; pub(crate) mod kclique_balancedcompletebipartitesubgraph; pub(crate) mod kclique_conjunctivebooleanquery; @@ -271,7 +271,6 @@ pub(crate) mod shortestweightconstrainedpath_ilp; pub(crate) mod sparsematrixcompression_ilp; pub(crate) mod stackercrane_ilp; pub(crate) mod steinertree_ilp; -pub(crate) mod steinertreeingraphs_ilp; pub(crate) mod stringtostringcorrection_ilp; pub(crate) mod strongconnectivityaugmentation_ilp; pub(crate) mod subgraphisomorphism_ilp; @@ -289,7 +288,7 @@ pub use graph::{ PathParameterError, ReductionChain, ReductionEdgeInfo, ReductionGraph, ReductionMode, ReductionPath, ReductionStep, TraversalFlow, }; -pub(crate) use traits::{validate_target_solution, DynReductionResult}; +pub(crate) use traits::DynReductionResult; pub use traits::{ AggregateReductionResult, ExtractionError, ExtractionResult, ReduceTo, ReduceToAggregate, ReductionError, ReductionResult, VariantReductionResult, @@ -571,7 +570,6 @@ pub(crate) fn canonical_rule_example_specs() -> Vec::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution.iter().map(|&value| value == 1).collect()) } } diff --git a/src/rules/multiplechoicebranching_ilp.rs b/src/rules/multiplechoicebranching_ilp.rs index cba6f76cd..206a551d8 100644 --- a/src/rules/multiplechoicebranching_ilp.rs +++ b/src/rules/multiplechoicebranching_ilp.rs @@ -23,7 +23,6 @@ impl ReductionResult for ReductionMultipleChoiceBranchingToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; Ok(target_solution[..self.num_arcs] .iter() .map(|&selected| selected == 1) diff --git a/src/rules/multiplecopyfileallocation_ilp.rs b/src/rules/multiplecopyfileallocation_ilp.rs index 335aa7a91..4c9ce90c8 100644 --- a/src/rules/multiplecopyfileallocation_ilp.rs +++ b/src/rules/multiplecopyfileallocation_ilp.rs @@ -40,8 +40,6 @@ impl ReductionResult for ReductionMCFAToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution[..self.num_vertices] .iter() .map(|&value| value == 1) diff --git a/src/rules/multiprocessorscheduling_ilp.rs b/src/rules/multiprocessorscheduling_ilp.rs index c3d8ed3cd..2e8136b60 100644 --- a/src/rules/multiprocessorscheduling_ilp.rs +++ b/src/rules/multiprocessorscheduling_ilp.rs @@ -37,14 +37,12 @@ impl ReductionResult for ReductionMSToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - - crate::rules::ilp_helpers::one_hot_decode_rows( + Ok(crate::rules::ilp_helpers::one_hot_decode_rows( target_solution, self.num_tasks, self.num_processors, 0, - ) + )) } } diff --git a/src/rules/naesatisfiability_ilp.rs b/src/rules/naesatisfiability_ilp.rs index 1c57fc3a5..2a91a142b 100644 --- a/src/rules/naesatisfiability_ilp.rs +++ b/src/rules/naesatisfiability_ilp.rs @@ -30,8 +30,6 @@ impl ReductionResult for ReductionNAESATToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution.iter().map(|&value| value == 1).collect()) } } diff --git a/src/rules/naesatisfiability_maxcut.rs b/src/rules/naesatisfiability_maxcut.rs index 55e8e81c7..5ed460e9f 100644 --- a/src/rules/naesatisfiability_maxcut.rs +++ b/src/rules/naesatisfiability_maxcut.rs @@ -42,14 +42,6 @@ impl ReductionResult for ReductionNAESATToMaxCut { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - let value = - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - if !crate::rules::AggregateReductionResult::extract_value(self, value).0 { - return Err(crate::rules::ExtractionError::invalid( - "target cut does not certify a satisfying NAE assignment", - )); - } - Ok({ (0..self.source_num_vars) .map(|i| target_solution[2 * i]) diff --git a/src/rules/naesatisfiability_partitionintoperfectmatchings.rs b/src/rules/naesatisfiability_partitionintoperfectmatchings.rs index c7a09e983..d56cc6b35 100644 --- a/src/rules/naesatisfiability_partitionintoperfectmatchings.rs +++ b/src/rules/naesatisfiability_partitionintoperfectmatchings.rs @@ -69,8 +69,6 @@ impl ReductionResult for ReductionNAESATToPartitionIntoPerfectMatchings { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ self.layout .variables diff --git a/src/rules/naesatisfiability_setsplitting.rs b/src/rules/naesatisfiability_setsplitting.rs index 854e7ed69..284a9cac4 100644 --- a/src/rules/naesatisfiability_setsplitting.rs +++ b/src/rules/naesatisfiability_setsplitting.rs @@ -29,8 +29,6 @@ impl ReductionResult for ReductionNAESATToSetSplitting { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution[..self.num_source_variables].to_vec()) } } diff --git a/src/rules/numerical3dimensionalmatching_numericalmatchingwithtargetsums.rs b/src/rules/numerical3dimensionalmatching_numericalmatchingwithtargetsums.rs index 30729b498..9f8478b8c 100644 --- a/src/rules/numerical3dimensionalmatching_numericalmatchingwithtargetsums.rs +++ b/src/rules/numerical3dimensionalmatching_numericalmatchingwithtargetsums.rs @@ -8,14 +8,11 @@ use crate::models::misc::{Numerical3DimensionalMatching, NumericalMatchingWithTargetSums}; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; -use std::collections::BTreeMap; /// Result of reducing Numerical3DimensionalMatching to NumericalMatchingWithTargetSums. #[derive(Debug, Clone)] pub struct ReductionN3DMToNMTS { target: NumericalMatchingWithTargetSums, - source_sizes_w: Vec, - source_bound: i64, } impl ReductionResult for ReductionN3DMToNMTS { @@ -30,39 +27,23 @@ impl ReductionResult for ReductionN3DMToNMTS { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ - let mut x_indices_by_pair_sum: BTreeMap> = BTreeMap::new(); - for (x_index, &y_index) in target_solution.iter().enumerate() { - let pair_sum = self.target.sizes_x()[x_index] - .checked_add(self.target.sizes_y()[y_index]) - .ok_or_else(|| { - crate::rules::ExtractionError::invalid( - "target pair sum overflows the target numeric domain", - ) - })?; - x_indices_by_pair_sum - .entry(pair_sum) - .or_default() - .push(x_index); - } + let mut pairs: Vec<_> = target_solution + .iter() + .enumerate() + .map(|(x, &y)| (self.target.sizes_x()[x] + self.target.sizes_y()[y], x, y)) + .collect(); + let mut targets: Vec<_> = self.target.targets().iter().copied().enumerate().collect(); + pairs.sort_unstable(); + targets.sort_unstable_by_key(|&(w, sum)| (sum, w)); - let mut x_perm = Vec::with_capacity(self.source_sizes_w.len()); - let mut y_perm = Vec::with_capacity(self.source_sizes_w.len()); - for &w_size in &self.source_sizes_w { - let target_sum = checked_target_sum(self.source_bound, w_size) - .map_err(crate::rules::ExtractionError::invalid)?; - let x_index = x_indices_by_pair_sum - .get_mut(&target_sum) - .and_then(Vec::pop) - .ok_or_else(|| { - crate::rules::ExtractionError::invalid(format!( - "target matching does not realize required pair sum {target_sum}" - )) - })?; - x_perm.push(x_index); - y_perm.push(target_solution[x_index]); + // Feasibility equates the pair-sum and target multisets. Target + // index w retains the source W order from construction. + let mut x_perm = vec![0; targets.len()]; + let mut y_perm = vec![0; targets.len()]; + for ((w, _), (_, x, y)) in targets.into_iter().zip(pairs) { + x_perm[w] = x; + y_perm[w] = y; } x_perm.extend(y_perm); @@ -102,11 +83,7 @@ impl ReduceTo for Numerical3DimensionalMatching .map_err(map_error)?, ); - Ok(ReductionN3DMToNMTS { - target, - source_sizes_w: self.sizes_w().to_vec(), - source_bound: self.bound(), - }) + Ok(ReductionN3DMToNMTS { target }) } } diff --git a/src/rules/numericalmatchingwithtargetsums_ilp.rs b/src/rules/numericalmatchingwithtargetsums_ilp.rs index 3f658a8bf..a4cfe5092 100644 --- a/src/rules/numericalmatchingwithtargetsums_ilp.rs +++ b/src/rules/numericalmatchingwithtargetsums_ilp.rs @@ -48,8 +48,6 @@ impl ReductionResult for ReductionNMTSToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ let mut assignment = vec![0usize; self.m]; for (var_idx, triple) in self.triples.iter().enumerate() { diff --git a/src/rules/openshopscheduling_ilp.rs b/src/rules/openshopscheduling_ilp.rs index 41f385661..28c2416fc 100644 --- a/src/rules/openshopscheduling_ilp.rs +++ b/src/rules/openshopscheduling_ilp.rs @@ -91,7 +91,6 @@ impl ReductionResult for ReductionOSSToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; let start = self.num_order_vars; let end = start + self.num_jobs * self.num_machines; crate::rules::ilp_helpers::decode_usize_values(&target_solution[start..end]) diff --git a/src/rules/optimallineararrangement_consecutiveonesmatrixaugmentation.rs b/src/rules/optimallineararrangement_consecutiveonesmatrixaugmentation.rs index 1efcd0b0b..ab1433533 100644 --- a/src/rules/optimallineararrangement_consecutiveonesmatrixaugmentation.rs +++ b/src/rules/optimallineararrangement_consecutiveonesmatrixaugmentation.rs @@ -30,13 +30,6 @@ impl ReductionResult for ReductionOptimalLinearArrangementToConsecutiveOnesMatri &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - let value = - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - if !value.0 { - return Err(crate::rules::ExtractionError::invalid( - "target column order is not a satisfying augmentation certificate", - )); - } // Validation establishes a permutation within the augmentation budget. // The NO sentinel has no such certificate; all remaining columns are // source vertices, including the empty permutation for an empty graph. diff --git a/src/rules/optimallineararrangement_ilp.rs b/src/rules/optimallineararrangement_ilp.rs index 934dd35b1..02d0f2e53 100644 --- a/src/rules/optimallineararrangement_ilp.rs +++ b/src/rules/optimallineararrangement_ilp.rs @@ -38,14 +38,12 @@ impl ReductionResult for ReductionOLAToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - - crate::rules::ilp_helpers::one_hot_decode_rows( + Ok(crate::rules::ilp_helpers::one_hot_decode_rows( target_solution, self.num_vertices, self.num_vertices, 0, - ) + )) } } diff --git a/src/rules/optimallineararrangement_sequencingtominimizeweightedcompletiontime.rs b/src/rules/optimallineararrangement_sequencingtominimizeweightedcompletiontime.rs index e7a14a680..0c8223680 100644 --- a/src/rules/optimallineararrangement_sequencingtominimizeweightedcompletiontime.rs +++ b/src/rules/optimallineararrangement_sequencingtominimizeweightedcompletiontime.rs @@ -36,8 +36,6 @@ impl ReductionResult for ReductionOLAToSequencingToMinimizeWeightedCompletionTim &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ let mut arrangement = vec![0usize; self.num_vertices]; let mut next_position = 0usize; diff --git a/src/rules/optimumcommunicationspanningtree_ilp.rs b/src/rules/optimumcommunicationspanningtree_ilp.rs index ee0c48bb7..6bc4da1ef 100644 --- a/src/rules/optimumcommunicationspanningtree_ilp.rs +++ b/src/rules/optimumcommunicationspanningtree_ilp.rs @@ -37,8 +37,6 @@ impl ReductionResult for ReductionOptimumCommunicationSpanningTreeToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution[..self.num_edges] .iter() .map(|&value| value == 1) diff --git a/src/rules/paintshop_ilp.rs b/src/rules/paintshop_ilp.rs index b5f455827..df7af6521 100644 --- a/src/rules/paintshop_ilp.rs +++ b/src/rules/paintshop_ilp.rs @@ -28,8 +28,6 @@ impl ReductionResult for ReductionPaintShopToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution[..self.num_cars] .iter() .map(|&value| value == 1) diff --git a/src/rules/paintshop_qubo.rs b/src/rules/paintshop_qubo.rs index b8a05456c..920b65fab 100644 --- a/src/rules/paintshop_qubo.rs +++ b/src/rules/paintshop_qubo.rs @@ -32,8 +32,6 @@ impl ReductionResult for ReductionPaintShopToQUBO { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution.to_vec()) } } @@ -50,7 +48,7 @@ impl ReduceTo> for PaintShop { let is_first = self.is_first(); let seq_len = seq.len(); - let mut matrix = vec![vec![0i64; n]; n]; + let mut matrix = vec![std::collections::BTreeMap::new(); n]; let overflow = |operation| { crate::rules::ReductionError::integer_overflow::>(operation) }; @@ -74,32 +72,38 @@ impl ReduceTo> for PaintShop { if parity_a == parity_b { // Same parity: color change when x_a != x_b // Contribution: +1 to Q[a][a], +1 to Q[b][b], -2 to Q[lo][hi] - matrix[a][a] = matrix[a][a] + let coefficient = matrix[a].entry(a).or_insert(0i64); + *coefficient = coefficient .checked_add(1) .ok_or_else(|| overflow("adding a PaintShop diagonal coefficient"))?; - matrix[b][b] = matrix[b][b] + let coefficient = matrix[b].entry(b).or_insert(0i64); + *coefficient = coefficient .checked_add(1) .ok_or_else(|| overflow("adding a PaintShop diagonal coefficient"))?; - matrix[lo][hi] = matrix[lo][hi] + let coefficient = matrix[lo].entry(hi).or_insert(0i64); + *coefficient = coefficient .checked_sub(2) .ok_or_else(|| overflow("adding a PaintShop interaction coefficient"))?; } else { // Different parity: color change when x_a == x_b // Contribution: -1 to Q[a][a], -1 to Q[b][b], +2 to Q[lo][hi] - matrix[a][a] = matrix[a][a] + let coefficient = matrix[a].entry(a).or_insert(0i64); + *coefficient = coefficient .checked_sub(1) .ok_or_else(|| overflow("adding a PaintShop diagonal coefficient"))?; - matrix[b][b] = matrix[b][b] + let coefficient = matrix[b].entry(b).or_insert(0i64); + *coefficient = coefficient .checked_sub(1) .ok_or_else(|| overflow("adding a PaintShop diagonal coefficient"))?; - matrix[lo][hi] = matrix[lo][hi] + let coefficient = matrix[lo].entry(hi).or_insert(0i64); + *coefficient = coefficient .checked_add(2) .ok_or_else(|| overflow("adding a PaintShop interaction coefficient"))?; } } Ok(ReductionPaintShopToQUBO { - target: QUBO::from_matrix(matrix).map_err(|message| { + target: QUBO::from_rows(matrix).map_err(|message| { crate::rules::ReductionError::construction::>(message) })?, }) diff --git a/src/rules/partiallyorderedknapsack_ilp.rs b/src/rules/partiallyorderedknapsack_ilp.rs index ecfef47b6..56c5b3c08 100644 --- a/src/rules/partiallyorderedknapsack_ilp.rs +++ b/src/rules/partiallyorderedknapsack_ilp.rs @@ -25,8 +25,6 @@ impl ReductionResult for ReductionPOKToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution.iter().map(|&value| value == 1).collect()) } } diff --git a/src/rules/partition_binpacking.rs b/src/rules/partition_binpacking.rs index 20301e8a5..639e603ec 100644 --- a/src/rules/partition_binpacking.rs +++ b/src/rules/partition_binpacking.rs @@ -34,8 +34,6 @@ impl ReductionResult for ReductionPartitionToBinPacking { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ // BinPacking may use any bin indices (0..n-1). Remap the two distinct // bins used in a 2-bin packing to Partition's {0, 1} assignment. diff --git a/src/rules/partition_cosineproductintegration.rs b/src/rules/partition_cosineproductintegration.rs index 1698a5334..3cea2660d 100644 --- a/src/rules/partition_cosineproductintegration.rs +++ b/src/rules/partition_cosineproductintegration.rs @@ -32,8 +32,6 @@ impl ReductionResult for ReductionPartitionToCPI { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution.to_vec()) } } diff --git a/src/rules/partition_integralflowwithmultipliers.rs b/src/rules/partition_integralflowwithmultipliers.rs index 8c1a96391..2bae8498b 100644 --- a/src/rules/partition_integralflowwithmultipliers.rs +++ b/src/rules/partition_integralflowwithmultipliers.rs @@ -15,7 +15,7 @@ use crate::topology::DirectedGraph; #[derive(Debug, Clone)] pub struct ReductionPartitionToIntegralFlowWithMultipliers { target: IntegralFlowWithMultipliers, - item_arc_count: Option, + item_arc_count: usize, } impl ReductionResult for ReductionPartitionToIntegralFlowWithMultipliers { @@ -31,14 +31,7 @@ impl ReductionResult for ReductionPartitionToIntegralFlowWithMultipliers { target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { Ok({ - let item_arc_count = self.item_arc_count.ok_or_else(|| { - crate::rules::ExtractionError::invalid( - "the fixed infeasible target instance has no extractable witness", - ) - })?; - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - - target_solution[..item_arc_count] + target_solution[..self.item_arc_count] .iter() .map(|&flow| flow > 0) .collect() @@ -67,7 +60,7 @@ impl ReduceTo for Partition { let graph = DirectedGraph::new(3, vec![(0, 1), (1, 2)]); return Ok(ReductionPartitionToIntegralFlowWithMultipliers { target: IntegralFlowWithMultipliers::new(graph, 0, 2, vec![1, 2, 1], vec![1, 1], 1), - item_arc_count: None, + item_arc_count: source_n, }); } @@ -106,7 +99,7 @@ impl ReduceTo for Partition { capacities, half_sum, ), - item_arc_count: Some(source_n), + item_arc_count: source_n, }) } } diff --git a/src/rules/partition_knapsack.rs b/src/rules/partition_knapsack.rs index 6ea901cca..4692151b1 100644 --- a/src/rules/partition_knapsack.rs +++ b/src/rules/partition_knapsack.rs @@ -22,8 +22,6 @@ impl ReductionResult for ReductionPartitionToKnapsack { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution.to_vec()) } } diff --git a/src/rules/partition_multiprocessorscheduling.rs b/src/rules/partition_multiprocessorscheduling.rs index 9e35ce1d2..ddccfaafb 100644 --- a/src/rules/partition_multiprocessorscheduling.rs +++ b/src/rules/partition_multiprocessorscheduling.rs @@ -36,8 +36,6 @@ impl ReductionResult for ReductionPartitionToMPS { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution .iter() .map(|&processor| processor == 1) diff --git a/src/rules/partition_openshopscheduling.rs b/src/rules/partition_openshopscheduling.rs index f3126f0c5..a660b5dc2 100644 --- a/src/rules/partition_openshopscheduling.rs +++ b/src/rules/partition_openshopscheduling.rs @@ -22,52 +22,26 @@ impl ReductionResult for ReductionPartitionToOpenShopScheduling { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - let value = - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - if !crate::rules::AggregateReductionResult::extract_value(self, value).0 { - return Err(crate::rules::ExtractionError::invalid( - "target schedule does not certify a balanced partition", - )); - } - Ok({ let num_elements = self.target.num_jobs() - 1; let mut source_config = vec![false; num_elements]; let m = self.target.num_machines(); let start_times = target_solution .chunks_exact(m) - .map(|times| { - times - .iter() - .map(|&time| { - i64::try_from(time).map_err(|_| { - crate::rules::ExtractionError::invalid( - "target schedule time does not fit i64", - ) - }) - }) - .collect::, _>>() - }) - .collect::, _>>()?; + .map(|times| times.iter().map(|&time| time as i64).collect::>()) + .collect::>(); let special_job = num_elements; let half_sum = self.target.processing_times()[special_job][0]; // Find the middle machine where the special job starts at half_sum - let middle_machine = (0..m) - .find(|&machine| start_times[special_job][machine] == half_sum) - .ok_or_else(|| { - crate::rules::ExtractionError::invalid( - "target schedule has no machine at the partition boundary", - ) - })?; + let middle_machine: usize = (0..m) + .filter(|&machine| start_times[special_job][machine] == half_sum) + .sum(); let pivot = start_times[special_job][middle_machine]; for (job, slot) in source_config.iter_mut().enumerate() { let completion = start_times[job][middle_machine] - .checked_add(self.target.processing_times()[job][middle_machine]) - .ok_or_else(|| { - crate::rules::ExtractionError::invalid("target schedule time overflows i64") - })?; + + self.target.processing_times()[job][middle_machine]; if completion <= pivot { *slot = true; } diff --git a/src/rules/partition_productionplanning.rs b/src/rules/partition_productionplanning.rs index 3dc8856fb..f9c77435a 100644 --- a/src/rules/partition_productionplanning.rs +++ b/src/rules/partition_productionplanning.rs @@ -21,8 +21,6 @@ impl ReductionResult for ReductionPartitionToProductionPlanning { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution[..self.target.num_periods() - 1] .iter() .map(|&production| production > 0) diff --git a/src/rules/partition_sequencingtominimizetardytaskweight.rs b/src/rules/partition_sequencingtominimizetardytaskweight.rs index 72827a5d0..b13ef760d 100644 --- a/src/rules/partition_sequencingtominimizetardytaskweight.rs +++ b/src/rules/partition_sequencingtominimizetardytaskweight.rs @@ -22,26 +22,12 @@ impl ReductionResult for ReductionPartitionToSequencingToMinimizeTardyTaskWeight &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - let value = - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - if !crate::rules::AggregateReductionResult::extract_value(self, value).0 { - return Err(crate::rules::ExtractionError::invalid( - "target schedule does not certify a balanced partition", - )); - } - Ok({ let mut source_config = vec![true; self.target.num_tasks()]; let mut completion_time = 0i64; for &task in target_solution { - completion_time = completion_time - .checked_add(self.target.lengths()[task]) - .ok_or_else(|| { - crate::rules::ExtractionError::invalid( - "target schedule completion time overflows i64", - ) - })?; + completion_time += self.target.lengths()[task]; if completion_time <= self.target.deadlines()[task] { source_config[task] = false; } diff --git a/src/rules/partition_subsetsum.rs b/src/rules/partition_subsetsum.rs index 57f6c0d14..358f39ac9 100644 --- a/src/rules/partition_subsetsum.rs +++ b/src/rules/partition_subsetsum.rs @@ -13,9 +13,6 @@ use num_bigint::{BigUint, ToBigUint}; #[derive(Debug, Clone)] pub struct ReductionPartitionToSubsetSum { target: SubsetSum, - /// Number of elements in the original Partition instance. - /// When the total sum is odd, the target has 0 elements but the source has n. - source_n: usize, } impl ReductionResult for ReductionPartitionToSubsetSum { @@ -30,15 +27,6 @@ impl ReductionResult for ReductionPartitionToSubsetSum { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - - if target_solution.len() != self.source_n { - return Err(crate::rules::ExtractionError::invalid(format!( - "expected {} subset-selection values, got {}", - self.source_n, - target_solution.len() - ))); - } Ok(target_solution.to_vec()) } } @@ -52,14 +40,12 @@ impl ReduceTo for Partition { fn reduce_to(&self) -> Result { let total = self.total_sum(); - let source_n = self.num_elements(); Ok(if total % 2 != 0 { // Odd total sum: no balanced partition exists. // Return a trivially infeasible SubsetSum: no elements, target = 1. ReductionPartitionToSubsetSum { target: SubsetSum::new_unchecked(vec![], BigUint::from(1u32)), - source_n, } } else { let sizes: Vec = self @@ -75,7 +61,6 @@ impl ReduceTo for Partition { .expect("validated nonnegative Partition total"); ReductionPartitionToSubsetSum { target: SubsetSum::new_unchecked(sizes, target_val), - source_n, } }) } diff --git a/src/rules/partition_sumofsquarespartition.rs b/src/rules/partition_sumofsquarespartition.rs index 8eb491c9b..6aa16d8c7 100644 --- a/src/rules/partition_sumofsquarespartition.rs +++ b/src/rules/partition_sumofsquarespartition.rs @@ -49,15 +49,6 @@ impl ReductionResult for ReductionPartitionToSumOfSquaresPartition { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - if target_solution.len() != self.target.num_elements() { - return Err(crate::rules::ExtractionError::invalid(format!( - "expected {} target group assignments, got {}", - self.target.num_elements(), - target_solution.len() - ))); - } - Ok(target_solution[..self.source_n] .iter() .map(|&group| group == 1) diff --git a/src/rules/partitionintocliques_ilp.rs b/src/rules/partitionintocliques_ilp.rs index 843c6a905..00cd30881 100644 --- a/src/rules/partitionintocliques_ilp.rs +++ b/src/rules/partitionintocliques_ilp.rs @@ -25,19 +25,12 @@ impl ReductionResult for ReductionPartitionIntoCliquesToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - - (0..self.num_vertices) - .map(|vertex| { - (0..self.num_cliques) - .find(|&clique| target_solution[vertex * self.num_cliques + clique] == 1) - .ok_or_else(|| { - crate::rules::ExtractionError::invalid(format!( - "target solution does not assign vertex {vertex} to a clique" - )) - }) - }) - .collect() + Ok(crate::rules::ilp_helpers::one_hot_decode_rows( + target_solution, + self.num_vertices, + self.num_cliques, + 0, + )) } } diff --git a/src/rules/partitionintocliques_minimumcoveringbycliques.rs b/src/rules/partitionintocliques_minimumcoveringbycliques.rs index b3078e508..143e9ef2c 100644 --- a/src/rules/partitionintocliques_minimumcoveringbycliques.rs +++ b/src/rules/partitionintocliques_minimumcoveringbycliques.rs @@ -142,7 +142,6 @@ fn target_clique_bound( pub struct ReductionPartitionIntoCliquesToMinimumCoveringByCliques { target: MinimumCoveringByCliques, num_source_vertices: usize, - source_num_cliques: usize, target_bound: i64, } @@ -158,58 +157,32 @@ impl ReductionResult for ReductionPartitionIntoCliquesToMinimumCoveringByCliques &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - let value = - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - if !Min::meets_bound(&value, &self.target_bound) { - return Err(crate::rules::ExtractionError::invalid( - "target cover does not certify the source clique bound", - )); - } - - Ok({ - let n = self.num_source_vertices; - let target_edges = self.target.graph().edges(); - let mut matching_labels = vec![None; n]; - for ((u, v), &label) in target_edges.iter().zip(target_solution.iter()) { - let matching_index = if *u < n && *v == n + *u { - Some(*u) - } else if *v < n && *u == n + *v { - Some(*v) + let n = self.num_source_vertices; + let mut matching_labels: Vec<_> = self + .target + .graph() + .edges() + .into_iter() + .zip(target_solution) + .filter_map(|((u, v), &label)| { + if u < n && v == n + u { + Some((u, label)) + } else if v < n && u == n + v { + Some((v, label)) } else { None - }; - - if let Some(i) = matching_index { - matching_labels[i] = Some(label); } - } - - let mut label_map = BTreeMap::new(); - let extracted = matching_labels - .into_iter() - .map(|label| { - let label = label.ok_or_else(|| { - crate::rules::ExtractionError::invalid( - "target cover does not label every matching gadget edge", - ) - })?; - let next = label_map.len(); - Ok(*label_map.entry(label).or_insert(next)) - }) - .collect::>>()?; - - if label_map.len() > self.source_num_cliques { - return Err(crate::rules::ExtractionError::invalid(format!( - "target cover uses {} cliques, exceeding source bound {}", - label_map.len(), - self.source_num_cliques - ))); - } - - // Equal matching-edge labels imply pairwise source adjacency. - // The target certificate leaves at most K labels for these edges. - extracted - }) + }) + .collect(); + matching_labels.sort_unstable_by_key(|&(vertex, _)| vertex); + let mut label_map = BTreeMap::new(); + Ok(matching_labels + .into_iter() + .map(|(_, label)| { + let next = label_map.len(); + *label_map.entry(label).or_insert(next) + }) + .collect()) } } @@ -287,7 +260,6 @@ impl ReduceTo> for PartitionIntoCliques::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution.to_vec()) } } diff --git a/src/rules/partitionintopathsoflength2_ilp.rs b/src/rules/partitionintopathsoflength2_ilp.rs index f652ba758..fe713d7ce 100644 --- a/src/rules/partitionintopathsoflength2_ilp.rs +++ b/src/rules/partitionintopathsoflength2_ilp.rs @@ -48,14 +48,12 @@ impl ReductionResult for ReductionPIPL2ToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - - crate::rules::ilp_helpers::one_hot_decode_rows( + Ok(crate::rules::ilp_helpers::one_hot_decode_rows( target_solution, self.num_vertices, self.num_groups, 0, - ) + )) } } diff --git a/src/rules/partitionintotriangles_ilp.rs b/src/rules/partitionintotriangles_ilp.rs index b80d3fe8c..82e21297d 100644 --- a/src/rules/partitionintotriangles_ilp.rs +++ b/src/rules/partitionintotriangles_ilp.rs @@ -41,14 +41,12 @@ impl ReductionResult for ReductionPITToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - - crate::rules::ilp_helpers::one_hot_decode_rows( + Ok(crate::rules::ilp_helpers::one_hot_decode_rows( target_solution, self.num_vertices, self.num_groups, 0, - ) + )) } } diff --git a/src/rules/pathconstrainednetworkflow_ilp.rs b/src/rules/pathconstrainednetworkflow_ilp.rs index 799cebfc8..d7868e606 100644 --- a/src/rules/pathconstrainednetworkflow_ilp.rs +++ b/src/rules/pathconstrainednetworkflow_ilp.rs @@ -26,8 +26,6 @@ impl ReductionResult for ReductionPCNFToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - crate::rules::ilp_helpers::decode_usize_values(target_solution) } } diff --git a/src/rules/precedenceconstrainedscheduling_ilp.rs b/src/rules/precedenceconstrainedscheduling_ilp.rs index c79cbc5fc..a837400c1 100644 --- a/src/rules/precedenceconstrainedscheduling_ilp.rs +++ b/src/rules/precedenceconstrainedscheduling_ilp.rs @@ -42,14 +42,12 @@ impl ReductionResult for ReductionPCSToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - - crate::rules::ilp_helpers::one_hot_decode_rows( + Ok(crate::rules::ilp_helpers::one_hot_decode_rows( target_solution, self.num_tasks, self.deadline, 0, - ) + )) } } diff --git a/src/rules/preemptivescheduling_ilp.rs b/src/rules/preemptivescheduling_ilp.rs index f6d65c971..0d390f9f3 100644 --- a/src/rules/preemptivescheduling_ilp.rs +++ b/src/rules/preemptivescheduling_ilp.rs @@ -55,8 +55,6 @@ 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) diff --git a/src/rules/prizecollectingsteinerforest_steinertree.rs b/src/rules/prizecollectingsteinerforest_steinertree.rs index 26ab8ddb6..41a1befc7 100644 --- a/src/rules/prizecollectingsteinerforest_steinertree.rs +++ b/src/rules/prizecollectingsteinerforest_steinertree.rs @@ -16,11 +16,13 @@ //! - every `v in V` is attached to `r` by an edge of cost `omega` (so each //! tree component of `F` is paid by exactly one root-attachment edge in //! `T*`), -//! - for every `v in V_p` we add `(v, t_v)` of cost `0` and `(r, t_v)` of -//! cost `beta * p(v)`, +//! - with `M = omega + 1`, for every `v in V_p` add `(v, t_v)` of cost `M` +//! and `(r, t_v)` of cost `M + beta * p(v)`, //! - the terminal set is `{r} cup {t_v : v in V_p}`. //! -//! The Steiner-tree optimum then equals the PCSF optimum. +//! The Steiner-tree optimum equals the PCSF optimum plus `M * |V_p|`. +//! In an optimum each gadget terminal is a leaf: replacing both gadget edges +//! by the include edge and a root attachment strictly reduces cost. //! //! References: //! - Bienstock, Goemans, Simchi-Levi, Williamson, "A note on the prize @@ -38,7 +40,7 @@ use crate::topology::{Graph, SimpleGraph}; /// Result of reducing PCSF to SteinerTree. /// -/// Stores the original PCSF source parameterss plus the mapping from the target +/// Stores the original PCSF source parameters plus the mapping from the target /// graph's edge list back to the source variables (the original edge index /// for each "original" edge, and the source vertex index for each gadget /// include-edge). Other target edges (root-attachment and gadget omit-edges) @@ -73,8 +75,6 @@ impl ReductionResult for ReductionPCSFToSteinerTree { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ let n = self.num_source_vertices; let m = self.num_source_edges; @@ -99,12 +99,11 @@ impl ReductionResult for ReductionPCSFToSteinerTree { // edge has an unselected endpoint, so we mark endpoints explicitly // (this also covers prize-zero endpoints, which have no gadget). let edges = self.target.graph().edges(); - for (target_idx, &(_, _)) in edges.iter().enumerate() { + for (target_idx, &(u, v)) in edges.iter().enumerate() { if !target_solution[target_idx] { continue; } - if let Some(src_edge) = self.target_to_source_edge[target_idx] { - let (u, v) = self.source_edge_pair(src_edge); + if self.target_to_source_edge[target_idx].is_some() { selected_vertices[u] = true; selected_vertices[v] = true; } @@ -115,14 +114,6 @@ impl ReductionResult for ReductionPCSFToSteinerTree { } } -impl ReductionPCSFToSteinerTree { - /// Look up the endpoint pair of the `idx`-th source edge in the target - /// graph's edge list (source edges are placed first by construction). - fn source_edge_pair(&self, src_edge_idx: usize) -> (usize, usize) { - self.target.graph().edges()[src_edge_idx] - } -} - #[reduction( transform = exact { num_vertices = "num_vertices + num_vertices_with_prize + 1", @@ -174,18 +165,34 @@ impl ReduceTo> for PrizeCollectingSteinerForest, + >("forming the Steiner gadget inclusion cost") + })?; + let omit_cost = beta + .checked_mul(source_prizes[v]) + .and_then(|penalty| penalty.checked_add(include_cost)) + .ok_or_else(|| { + crate::rules::ReductionError::integer_overflow::< + Self, + SteinerTree, + >("forming the Steiner gadget omission cost") + })?; + // The include edge records a selected prized vertex. target_edges.push((v, t_v)); - target_edge_weights.push(0); + target_edge_weights.push(include_cost); target_to_source_edge.push(None); target_to_include_vertex.push(Some(v)); - // omit-edge: pays beta * p(v) when v is excluded from V_F. + // The omit edge pays the extra beta * p(v). target_edges.push((root, t_v)); - target_edge_weights.push(beta * source_prizes[v]); + target_edge_weights.push(omit_cost); target_to_source_edge.push(None); target_to_include_vertex.push(None); } @@ -215,16 +222,10 @@ impl ReduceTo> for PrizeCollectingSteinerForest Vec { use crate::example_db::specs::RuleExampleSpec; use crate::export::SolutionPair; - use crate::solvers::BruteForce; vec![RuleExampleSpec { id: "prize_collecting_steiner_forest_to_steiner_tree", build: || { - // Issue #1027 canonical instance with the omit-edge actually - // selected at the optimum: path 0 - 1 - 2 with c(0,1)=10, - // c(1,2)=10, prizes p = (5, 1, 5), beta = 1, omega = 1. The - // optimum drops vertex 1 (paying p(1) = 1) rather than paying a - // size-10 edge to reach it. let source = PrizeCollectingSteinerForest::::new( SimpleGraph::new(3, vec![(0, 1), (1, 2)]), vec![5, 1, 5], @@ -233,25 +234,14 @@ pub(crate) fn canonical_rule_example_specs() -> Vec as ReduceTo< - SteinerTree, - >>::reduce_to(&source) - .expect("reduction should succeed"); - let target = reduction.target_problem(); - let target_config = BruteForce::new() - .solve(target) - .expect("canonical target evaluation must succeed") - .expect("canonical PCSF -> SteinerTree example must have an optimal target tree"); - let source_config = reduction.extract_solution(&target_config).unwrap(); - crate::example_db::specs::assemble_rule_example( - &source, - target, - vec![SolutionPair { - source_config: serde_json::to_value(source_config) - .expect("solution serialization must succeed"), - target_config: serde_json::to_value(target_config) - .expect("solution serialization must succeed"), - }], + crate::example_db::specs::rule_example_with_witness::<_, SteinerTree>( + source, + SolutionPair { + source_config: serde_json::json!([[true, false, true], [false, false]]), + target_config: serde_json::json!([ + false, false, true, false, true, true, false, false, true, true, false + ]), + }, ) }, }] diff --git a/src/rules/quadraticassignment_ilp.rs b/src/rules/quadraticassignment_ilp.rs index aacef5d4e..af1b433cf 100644 --- a/src/rules/quadraticassignment_ilp.rs +++ b/src/rules/quadraticassignment_ilp.rs @@ -38,14 +38,12 @@ impl ReductionResult for ReductionQAPToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - - crate::rules::ilp_helpers::one_hot_decode_rows( + Ok(crate::rules::ilp_helpers::one_hot_decode_rows( target_solution, self.num_facilities, self.num_locations, 0, - ) + )) } } diff --git a/src/rules/qubo_casts.rs b/src/rules/qubo_casts.rs index 841c432cc..08639d9be 100644 --- a/src/rules/qubo_casts.rs +++ b/src/rules/qubo_casts.rs @@ -10,20 +10,16 @@ impl_variant_reduction!( => , fields: [num_vars], |src| { - let matrix = src - .matrix() - .iter() - .map(|row| { - row.iter() - .copied() - .map(i64_to_exact_f64) - .collect::, _>>() - }) - .collect::, _>>() - .map_err(|error| { - ReductionError::inexact_float_conversion::, QUBO>(error) - })?; - QUBO::from_matrix(matrix) + let coefficients = src.matrix().data().iter().copied() + .map(i64_to_exact_f64).collect::, _>>() + .map_err(ReductionError::inexact_float_conversion::, QUBO>)?; + let matrix = sprs::CsMat::new( + src.matrix().shape(), + src.matrix().indptr().raw_storage().to_vec(), + src.matrix().indices().to_vec(), + coefficients, + ); + QUBO::from_sparse(matrix) .map_err(ReductionError::construction::, QUBO>)? } ); diff --git a/src/rules/qubo_ilp.rs b/src/rules/qubo_ilp.rs index 2dd22909c..9d2f6e1e0 100644 --- a/src/rules/qubo_ilp.rs +++ b/src/rules/qubo_ilp.rs @@ -41,8 +41,6 @@ where &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution[..self.num_original] .iter() .map(|&value| value == 1) @@ -59,9 +57,9 @@ where // Collect non-zero off-diagonal entries (i < j) let mut off_diag: Vec<(usize, usize, C)> = Vec::new(); - for (i, row) in matrix.iter().enumerate() { - for (j, &q_ij) in row.iter().enumerate().skip(i + 1) { - if q_ij != C::zero() { + for (i, row) in matrix.outer_iterator().enumerate() { + for (j, &q_ij) in row.iter() { + if j > i && q_ij != C::zero() { off_diag.push((i, j, q_ij)); } } @@ -72,8 +70,8 @@ where // Objective: minimize Σ Q_ii · x_i + Σ Q_ij · y_k let mut objective: Vec<(usize, C)> = Vec::new(); - for (i, row) in matrix.iter().enumerate() { - let q_ii = row[i]; + for (i, row) in matrix.outer_iterator().enumerate() { + let q_ii = row.get(i).copied().unwrap_or_else(C::zero); if q_ii != C::zero() { objective.push((i, q_ii)); } diff --git a/src/rules/rectilinearpicturecompression_ilp.rs b/src/rules/rectilinearpicturecompression_ilp.rs index cf75fb3a1..d5be34842 100644 --- a/src/rules/rectilinearpicturecompression_ilp.rs +++ b/src/rules/rectilinearpicturecompression_ilp.rs @@ -25,8 +25,6 @@ impl ReductionResult for ReductionRPCToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution.iter().map(|&value| value == 1).collect()) } } diff --git a/src/rules/registersufficiency_ilp.rs b/src/rules/registersufficiency_ilp.rs index 021b02edc..24b00c96d 100644 --- a/src/rules/registersufficiency_ilp.rs +++ b/src/rules/registersufficiency_ilp.rs @@ -30,8 +30,6 @@ impl ReductionResult for ReductionRegisterSufficiencyToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - crate::rules::ilp_helpers::decode_usize_values(&target_solution[..self.num_vertices]) } } diff --git a/src/rules/registry.rs b/src/rules/registry.rs index 80a10beb6..7f0b7bc1a 100644 --- a/src/rules/registry.rs +++ b/src/rules/registry.rs @@ -132,9 +132,22 @@ impl From for ParameterContractError { } } +/// Interpret an accepted target optimum using this execution's mathematical relation. +pub type InterpretOptimum = dyn Fn(&dyn Any) -> crate::rules::ExtractionResult; + +/// One executed witness reduction, with optional value mapping over the same state. +#[derive(Clone)] +pub struct ExecutedStep { + /// Target access and witness recovery for this execution. + pub witness: std::rc::Rc, + /// Value recovery sharing the witness result allocation, when supported. + pub aggregate: Option>, + /// Solver completion only: whether the mapped optimum supplies a source witness. + pub interpret_optimum: Option>, +} + /// Witness/config reduction executor stored in the inventory. -pub type ReduceFn = - fn(&dyn Any) -> Result, crate::rules::ReductionError>; +pub type ReduceFn = fn(&dyn Any) -> Result; /// Aggregate/value reduction executor stored in the inventory. pub type AggregateReduceFn = @@ -182,7 +195,7 @@ pub struct ReductionEntry { pub module_path: &'static str, /// Type-erased reduction executor. /// Takes a `&dyn Any` (must be `&SourceType`), calls `ReduceTo::reduce_to()`, - /// and returns either a boxed `DynReductionResult` or the edge's `ReductionError`. + /// and returns one `ExecutedStep` sharing the result, or the edge's `ReductionError`. pub reduce_fn: Option, /// Type-erased aggregate reduction executor. /// Takes a `&dyn Any` (must be `&SourceType`), calls diff --git a/src/rules/resourceconstrainedscheduling_ilp.rs b/src/rules/resourceconstrainedscheduling_ilp.rs index bcc1726d9..9bbbcea69 100644 --- a/src/rules/resourceconstrainedscheduling_ilp.rs +++ b/src/rules/resourceconstrainedscheduling_ilp.rs @@ -33,14 +33,12 @@ impl ReductionResult for ReductionRCSToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - - crate::rules::ilp_helpers::one_hot_decode_rows( + Ok(crate::rules::ilp_helpers::one_hot_decode_rows( target_solution, self.num_tasks, self.deadline, 0, - ) + )) } } diff --git a/src/rules/rootedtreearrangement_rootedtreestorageassignment.rs b/src/rules/rootedtreearrangement_rootedtreestorageassignment.rs index 8c40f906a..a08051394 100644 --- a/src/rules/rootedtreearrangement_rootedtreestorageassignment.rs +++ b/src/rules/rootedtreearrangement_rootedtreestorageassignment.rs @@ -40,8 +40,6 @@ impl ReductionResult for ReductionRootedTreeArrangementToRootedTreeStorageAssign &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ let n = self.num_vertices; // target_solution is the parent array of the rooted tree on X = V diff --git a/src/rules/rootedtreestorageassignment_ilp.rs b/src/rules/rootedtreestorageassignment_ilp.rs index 70b4d9c56..46d67fcb8 100644 --- a/src/rules/rootedtreestorageassignment_ilp.rs +++ b/src/rules/rootedtreestorageassignment_ilp.rs @@ -76,9 +76,7 @@ impl ReductionResult for ReductionRTSAToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - - one_hot_decode_rows(target_solution, self.n, self.n, 0) + Ok(one_hot_decode_rows(target_solution, self.n, self.n, 0)) } } diff --git a/src/rules/ruralpostman_ilp.rs b/src/rules/ruralpostman_ilp.rs index ad8e63a1d..0441e8bde 100644 --- a/src/rules/ruralpostman_ilp.rs +++ b/src/rules/ruralpostman_ilp.rs @@ -30,8 +30,6 @@ impl ReductionResult for ReductionRPToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - if self.target.num_vars() == 0 { Ok(vec![0; self.num_edges]) } else { diff --git a/src/rules/sat_circuitsat.rs b/src/rules/sat_circuitsat.rs index c6fbfab3e..e18f6ca6e 100644 --- a/src/rules/sat_circuitsat.rs +++ b/src/rules/sat_circuitsat.rs @@ -7,7 +7,6 @@ use crate::models::formula::Satisfiability; use crate::models::formula::{Assignment, BooleanExpr, Circuit, CircuitSAT}; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; -use crate::solvers::BruteForceProblem as _; use std::collections::HashSet; /// Result of reducing SAT to CircuitSAT. @@ -30,8 +29,6 @@ impl ReductionResult for ReductionSATToCircuit { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ self.source_var_indices .iter() @@ -55,7 +52,7 @@ impl ReduceTo for Satisfiability { type Result = ReductionSATToCircuit; fn reduce_to(&self) -> Result { - let num_vars = self.num_variables(); + let num_vars = self.num_vars(); let clauses = self.clauses(); let mut assignments = Vec::new(); diff --git a/src/rules/sat_coloring.rs b/src/rules/sat_coloring.rs index 09fadc3b2..4fb262177 100644 --- a/src/rules/sat_coloring.rs +++ b/src/rules/sat_coloring.rs @@ -244,33 +244,15 @@ impl ReductionResult for ReductionSATToColoring { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ // First determine which color is TRUE, FALSE, and AUX // Vertices 0, 1, 2 are TRUE, FALSE, AUX respectively let true_color = target_solution[0]; - let false_color = target_solution[1]; - let aux_color = target_solution[2]; - - if true_color == false_color || true_color == aux_color || false_color == aux_color { - return Err(crate::rules::ExtractionError::invalid( - "target coloring does not distinguish true, false, and auxiliary colors", - )); - } - let mut assignment = vec![false; self.num_source_variables]; for (i, &pos_vertex) in self.pos_vertices.iter().enumerate() { let vertex_color = target_solution[pos_vertex]; - // Sanity check: variable vertices should not have AUX color - if vertex_color == aux_color { - return Err(crate::rules::ExtractionError::invalid(format!( - "variable {i} has the auxiliary color" - ))); - } - // If positive literal has TRUE color, variable is true (1) // Otherwise, variable is false (0) assignment[i] = vertex_color == true_color; diff --git a/src/rules/sat_ksat.rs b/src/rules/sat_ksat.rs index 897c8bb5b..9d27c416f 100644 --- a/src/rules/sat_ksat.rs +++ b/src/rules/sat_ksat.rs @@ -36,8 +36,6 @@ impl ReductionResult for ReductionSATToKSAT { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ // Only return the original variables, discarding ancillas target_solution[..self.source_num_vars].to_vec() @@ -186,8 +184,6 @@ impl ReductionResult for ReductionKSATToSAT { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ // Direct mapping - no transformation needed target_solution.to_vec() diff --git a/src/rules/sat_maximumindependentset.rs b/src/rules/sat_maximumindependentset.rs index d8271f74e..4e9d647f9 100644 --- a/src/rules/sat_maximumindependentset.rs +++ b/src/rules/sat_maximumindependentset.rs @@ -82,15 +82,6 @@ impl ReductionResult for ReductionSATToIS { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - let value = - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - let certificate = crate::rules::AggregateReductionResult::extract_value(self, value); - if !certificate.0 { - return Err(crate::rules::ExtractionError::invalid( - "target independent set does not certify satisfiability", - )); - } - let mut assignment = vec![false; self.num_source_variables]; for (literal, &selected) in self.literals.iter().zip(target_solution) { if selected { diff --git a/src/rules/sat_minimumdominatingset.rs b/src/rules/sat_minimumdominatingset.rs index bed0c45e8..0897b13d7 100644 --- a/src/rules/sat_minimumdominatingset.rs +++ b/src/rules/sat_minimumdominatingset.rs @@ -62,15 +62,6 @@ impl ReductionResult for ReductionSATToDS { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - let value = - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - let certificate = crate::rules::AggregateReductionResult::extract_value(self, value); - if !certificate.0 { - return Err(crate::rules::ExtractionError::invalid( - "target dominating set does not certify satisfiability", - )); - } - let mut assignment = vec![false; self.num_literals]; for (&variable, &gadget) in &self.variables { assignment[variable] = target_solution[3 * gadget]; diff --git a/src/rules/satisfiability_integralflowhomologousarcs.rs b/src/rules/satisfiability_integralflowhomologousarcs.rs index d6a1fa8dc..c8b3484f3 100644 --- a/src/rules/satisfiability_integralflowhomologousarcs.rs +++ b/src/rules/satisfiability_integralflowhomologousarcs.rs @@ -106,8 +106,6 @@ impl ReductionResult for ReductionSATToIntegralFlowHomologousArcs { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ self.variable_paths .iter() diff --git a/src/rules/satisfiability_maximum2satisfiability.rs b/src/rules/satisfiability_maximum2satisfiability.rs index 040837da5..e477085c1 100644 --- a/src/rules/satisfiability_maximum2satisfiability.rs +++ b/src/rules/satisfiability_maximum2satisfiability.rs @@ -26,15 +26,6 @@ impl ReductionResult for ReductionSatisfiabilityToMaximum2Satisfiability { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - let value = - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - let certificate = crate::rules::AggregateReductionResult::extract_value(self, value); - if !certificate.0 { - return Err(crate::rules::ExtractionError::invalid( - "target assignment does not certify satisfiability", - )); - } - Ok(target_solution[..self.source_num_vars].to_vec()) } } diff --git a/src/rules/satisfiability_naesatisfiability.rs b/src/rules/satisfiability_naesatisfiability.rs index 0be93eb62..fd3543ed9 100644 --- a/src/rules/satisfiability_naesatisfiability.rs +++ b/src/rules/satisfiability_naesatisfiability.rs @@ -33,16 +33,7 @@ impl ReductionResult for ReductionSATToNAESAT { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - let n = self.source_num_vars; - if target_solution.len() != n + 1 { - return Err(crate::rules::ExtractionError::invalid(format!( - "expected {} target truth values, got {}", - n + 1, - target_solution.len() - ))); - } let sentinel = target_solution[n]; Ok(target_solution[..n] .iter() diff --git a/src/rules/satisfiability_nontautology.rs b/src/rules/satisfiability_nontautology.rs index 3ba7ee1c8..d932cc773 100644 --- a/src/rules/satisfiability_nontautology.rs +++ b/src/rules/satisfiability_nontautology.rs @@ -25,8 +25,6 @@ impl ReductionResult for ReductionSATToNonTautology { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution.to_vec()) } } diff --git a/src/rules/schedulingtominimizeweightedcompletiontime_ilp.rs b/src/rules/schedulingtominimizeweightedcompletiontime_ilp.rs index caefabe83..17e80dfa3 100644 --- a/src/rules/schedulingtominimizeweightedcompletiontime_ilp.rs +++ b/src/rules/schedulingtominimizeweightedcompletiontime_ilp.rs @@ -56,9 +56,12 @@ impl ReductionResult for ReductionSMWCTToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - - one_hot_decode_rows(target_solution, self.num_tasks, self.num_processors, 0) + Ok(one_hot_decode_rows( + target_solution, + self.num_tasks, + self.num_processors, + 0, + )) } } diff --git a/src/rules/schedulingwithindividualdeadlines_ilp.rs b/src/rules/schedulingwithindividualdeadlines_ilp.rs index a36ee872d..2be00352f 100644 --- a/src/rules/schedulingwithindividualdeadlines_ilp.rs +++ b/src/rules/schedulingwithindividualdeadlines_ilp.rs @@ -43,9 +43,12 @@ impl ReductionResult for ReductionSWIDToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - - one_hot_decode_rows(target_solution, self.num_tasks, self.max_deadline, 0) + Ok(one_hot_decode_rows( + target_solution, + self.num_tasks, + self.max_deadline, + 0, + )) } } diff --git a/src/rules/sequencingtominimizemaximumcumulativecost_ilp.rs b/src/rules/sequencingtominimizemaximumcumulativecost_ilp.rs index 1c87717e3..f350cd09e 100644 --- a/src/rules/sequencingtominimizemaximumcumulativecost_ilp.rs +++ b/src/rules/sequencingtominimizemaximumcumulativecost_ilp.rs @@ -35,12 +35,10 @@ impl ReductionResult for ReductionSTMMCCToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ let n = self.num_tasks; - one_hot_decode(target_solution, n, n, 0)? + one_hot_decode(target_solution, n, n, 0) }) } } diff --git a/src/rules/sequencingtominimizetardytaskweight_ilp.rs b/src/rules/sequencingtominimizetardytaskweight_ilp.rs index 5f41493d2..69413f461 100644 --- a/src/rules/sequencingtominimizetardytaskweight_ilp.rs +++ b/src/rules/sequencingtominimizetardytaskweight_ilp.rs @@ -29,20 +29,12 @@ impl ReductionResult for ReductionSTMTTWToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - let value = - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - if !value.is_valid() { - return Err(crate::rules::ExtractionError::invalid( - "target ILP assignment is infeasible", - )); - } - Ok({ let n = self.num_tasks; // Decode the n*n block of x_{j,p} variables into a schedule permutation. // The source uses direct permutation encoding (config = schedule directly), // so return the schedule as-is (it is already a permutation of 0..n). - one_hot_decode(target_solution, n, n, 0)? + one_hot_decode(target_solution, n, n, 0) }) } } diff --git a/src/rules/sequencingtominimizeweightedcompletiontime_ilp.rs b/src/rules/sequencingtominimizeweightedcompletiontime_ilp.rs index 1171c160a..dd9978ac5 100644 --- a/src/rules/sequencingtominimizeweightedcompletiontime_ilp.rs +++ b/src/rules/sequencingtominimizeweightedcompletiontime_ilp.rs @@ -41,8 +41,6 @@ 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)); diff --git a/src/rules/sequencingtominimizeweightedtardiness_ilp.rs b/src/rules/sequencingtominimizeweightedtardiness_ilp.rs index f046bd232..b9f9174ec 100644 --- a/src/rules/sequencingtominimizeweightedtardiness_ilp.rs +++ b/src/rules/sequencingtominimizeweightedtardiness_ilp.rs @@ -37,8 +37,6 @@ impl ReductionResult for ReductionSTMWTToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ let n = self.num_tasks; let c_offset = self.num_order_vars; diff --git a/src/rules/sequencingwithdeadlinesandsetuptimes_ilp.rs b/src/rules/sequencingwithdeadlinesandsetuptimes_ilp.rs index 21cd366b0..6a7a15b7c 100644 --- a/src/rules/sequencingwithdeadlinesandsetuptimes_ilp.rs +++ b/src/rules/sequencingwithdeadlinesandsetuptimes_ilp.rs @@ -40,12 +40,10 @@ impl ReductionResult for ReductionSWDSTToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ let n = self.num_tasks; // x_{j,p} occupies the first n*n variables: decode the permutation. - one_hot_decode(target_solution, n, n, 0)? + one_hot_decode(target_solution, n, n, 0) }) } } diff --git a/src/rules/sequencingwithinintervals_ilp.rs b/src/rules/sequencingwithinintervals_ilp.rs index daccfd111..8235c9af4 100644 --- a/src/rules/sequencingwithinintervals_ilp.rs +++ b/src/rules/sequencingwithinintervals_ilp.rs @@ -47,24 +47,15 @@ impl ReductionResult for ReductionSWIToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - - self.task_layout + Ok(self + .task_layout .iter() - .enumerate() - .map(|(task, &(base, count))| { - let mut selected = (0..count).filter(|&offset| target_solution[base + offset] == 1); - match (selected.next(), selected.next()) { - (Some(offset), None) => Ok(offset), - (None, _) => Err(crate::rules::ExtractionError::invalid(format!( - "task {task} has no selected start time" - ))), - (Some(_), Some(_)) => Err(crate::rules::ExtractionError::invalid(format!( - "task {task} has multiple selected start times" - ))), - } + .map(|&(base, count)| { + (0..count) + .filter(|&offset| target_solution[base + offset] == 1) + .sum() }) - .collect() + .collect()) } } diff --git a/src/rules/sequencingwithreleasetimesanddeadlines_ilp.rs b/src/rules/sequencingwithreleasetimesanddeadlines_ilp.rs index d14ad874c..d47e56d21 100644 --- a/src/rules/sequencingwithreleasetimesanddeadlines_ilp.rs +++ b/src/rules/sequencingwithreleasetimesanddeadlines_ilp.rs @@ -33,14 +33,12 @@ impl ReductionResult for ReductionSWRTDToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - 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)?; + 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)); diff --git a/src/rules/setsplitting_betweenness.rs b/src/rules/setsplitting_betweenness.rs index a62f799d4..5f5c9ff5e 100644 --- a/src/rules/setsplitting_betweenness.rs +++ b/src/rules/setsplitting_betweenness.rs @@ -32,8 +32,6 @@ impl ReductionResult for ReductionSetSplittingToBetweenness { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - let pole_position = target_solution[self.pole]; Ok(target_solution[..self.source_universe_size] .iter() diff --git a/src/rules/setsplitting_ilp.rs b/src/rules/setsplitting_ilp.rs index db0b8a61c..682e944c0 100644 --- a/src/rules/setsplitting_ilp.rs +++ b/src/rules/setsplitting_ilp.rs @@ -32,8 +32,6 @@ impl ReductionResult for ReductionSetSplittingToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution.iter().map(|&value| value == 1).collect()) } } diff --git a/src/rules/shortestcommonsupersequence_ilp.rs b/src/rules/shortestcommonsupersequence_ilp.rs index d1c713717..ef65f31b4 100644 --- a/src/rules/shortestcommonsupersequence_ilp.rs +++ b/src/rules/shortestcommonsupersequence_ilp.rs @@ -31,14 +31,12 @@ impl ReductionResult for ReductionSCSToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(crate::rules::ilp_helpers::one_hot_decode_rows( target_solution, self.max_length, self.alphabet_size + 1, 0, - )? + ) .into_iter() .map(|symbol| (symbol < self.alphabet_size).then_some(symbol)) .collect()) diff --git a/src/rules/shortestweightconstrainedpath_ilp.rs b/src/rules/shortestweightconstrainedpath_ilp.rs index fe8ff92be..4c3e38b68 100644 --- a/src/rules/shortestweightconstrainedpath_ilp.rs +++ b/src/rules/shortestweightconstrainedpath_ilp.rs @@ -44,8 +44,6 @@ impl ReductionResult for ReductionSWCPToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ (0..self.num_edges) .map(|edge_idx| { diff --git a/src/rules/sparsematrixcompression_ilp.rs b/src/rules/sparsematrixcompression_ilp.rs index b19b79368..086b312ef 100644 --- a/src/rules/sparsematrixcompression_ilp.rs +++ b/src/rules/sparsematrixcompression_ilp.rs @@ -26,14 +26,12 @@ impl ReductionResult for ReductionSMCToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - - crate::rules::ilp_helpers::one_hot_decode_rows( + Ok(crate::rules::ilp_helpers::one_hot_decode_rows( target_solution, self.num_rows, self.bound_k, 0, - ) + )) } } diff --git a/src/rules/spinglass_maxcut.rs b/src/rules/spinglass_maxcut.rs index 31e315acc..6988964a0 100644 --- a/src/rules/spinglass_maxcut.rs +++ b/src/rules/spinglass_maxcut.rs @@ -39,8 +39,6 @@ where &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution.iter().map(|&spin| spin == 1).collect()) } } @@ -125,8 +123,6 @@ where &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ match self.ancilla { None => target_solution diff --git a/src/rules/spinglass_qubo.rs b/src/rules/spinglass_qubo.rs index 1a6a900ad..2e4cb929f 100644 --- a/src/rules/spinglass_qubo.rs +++ b/src/rules/spinglass_qubo.rs @@ -30,8 +30,6 @@ impl ReductionResult for ReductionQUBOToSG { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution.iter().map(|&spin| spin == 1).collect()) } } @@ -63,10 +61,9 @@ impl ReduceTo> for QUBO { let mut interactions = Vec::new(); let mut onsite = vec![0.0; n]; - for i in 0..n { - for j in i..n { - let q = matrix[i][j]; - if q.abs() < 1e-10 { + for (i, row) in matrix.outer_iterator().enumerate() { + for (j, &q) in row.iter() { + if j < i || q == 0.0 { continue; } @@ -77,7 +74,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 { + if j_ij != 0.0 { interactions.push(((i, j), j_ij)); } // h_i and h_j contributions @@ -121,8 +118,6 @@ where &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution .iter() .map(|&bit| if bit { 1 } else { -1 }) @@ -140,7 +135,7 @@ impl ReduceTo> for SpinGlass { fn reduce_to(&self) -> Result { let n = self.num_spins(); - let mut matrix = vec![vec![0.0; n]; n]; + let mut matrix = vec![std::collections::BTreeMap::new(); n]; // Convert using s = 2x - 1: // s_i * s_j = (2x_i - 1)(2x_j - 1) = 4x_i*x_j - 2x_i - 2x_j + 1 @@ -152,19 +147,19 @@ impl ReduceTo> for SpinGlass { // h_i * s_i = h_i * (2x_i - 1) = 2*h_i*x_i - h_i for ((i, j), j_val) in self.interactions() { // Off-diagonal: 4 * J_ij - matrix[i][j] += 4.0 * j_val; + *matrix[i].entry(j).or_insert(0.0) += 4.0 * j_val; // Diagonal contributions: -2 * J_ij - matrix[i][i] -= 2.0 * j_val; - matrix[j][j] -= 2.0 * j_val; + *matrix[i].entry(i).or_insert(0.0) -= 2.0 * j_val; + *matrix[j].entry(j).or_insert(0.0) -= 2.0 * j_val; } // Convert h fields to diagonal for (i, &h) in self.fields().iter().enumerate() { // h_i * s_i -> 2*h_i*x_i - matrix[i][i] += 2.0 * h; + *matrix[i].entry(i).or_insert(0.0) += 2.0 * h; } - let target = QUBO::from_matrix(matrix).map_err(|message| { + let target = QUBO::from_rows(matrix).map_err(|message| { crate::rules::ReductionError::construction::, QUBO>( message, ) @@ -184,7 +179,7 @@ impl ReduceTo> for SpinGlass { fn reduce_to(&self) -> Result { let n = self.num_spins(); - let mut matrix = vec![vec![0_i64; n]; n]; + let mut matrix = vec![std::collections::BTreeMap::new(); n]; let overflow = |operation| { crate::rules::ReductionError::integer_overflow::, QUBO>( operation, @@ -195,16 +190,19 @@ impl ReduceTo> for SpinGlass { let interaction = coupling .checked_mul(4) .ok_or_else(|| overflow("scaling a spin-glass interaction"))?; - matrix[i][j] = matrix[i][j] + let coefficient = matrix[i].entry(j).or_insert(0i64); + *coefficient = coefficient .checked_add(interaction) .ok_or_else(|| overflow("summing QUBO interaction coefficients"))?; let diagonal = coupling .checked_mul(2) .ok_or_else(|| overflow("scaling a spin-glass diagonal contribution"))?; - matrix[i][i] = matrix[i][i] + let coefficient = matrix[i].entry(i).or_insert(0i64); + *coefficient = coefficient .checked_sub(diagonal) .ok_or_else(|| overflow("summing QUBO diagonal coefficients"))?; - matrix[j][j] = matrix[j][j] + let coefficient = matrix[j].entry(j).or_insert(0i64); + *coefficient = coefficient .checked_sub(diagonal) .ok_or_else(|| overflow("summing QUBO diagonal coefficients"))?; } @@ -213,14 +211,15 @@ impl ReduceTo> for SpinGlass { let diagonal = field .checked_mul(2) .ok_or_else(|| overflow("scaling a spin-glass field"))?; - matrix[i][i] = matrix[i][i] + let coefficient = matrix[i].entry(i).or_insert(0i64); + *coefficient = coefficient .checked_add(diagonal) .ok_or_else(|| overflow("summing QUBO diagonal coefficients"))?; } Ok(ReductionSGToQUBO { target: - QUBO::from_matrix(matrix).map_err( + QUBO::from_rows(matrix).map_err( crate::rules::ReductionError::construction::< SpinGlass, QUBO, diff --git a/src/rules/stackercrane_ilp.rs b/src/rules/stackercrane_ilp.rs index 5e9a0d7d9..91de9aae0 100644 --- a/src/rules/stackercrane_ilp.rs +++ b/src/rules/stackercrane_ilp.rs @@ -35,11 +35,9 @@ impl ReductionResult for ReductionSCToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ // Decode the permutation: for each position p, find the arc a with x_{a,p} = 1 - one_hot_decode(target_solution, self.num_arcs, self.num_arcs, 0)? + one_hot_decode(target_solution, self.num_arcs, self.num_arcs, 0) }) } } diff --git a/src/rules/steinertree_ilp.rs b/src/rules/steinertree_ilp.rs index e19b19e68..c4d54b303 100644 --- a/src/rules/steinertree_ilp.rs +++ b/src/rules/steinertree_ilp.rs @@ -30,14 +30,6 @@ impl ReductionResult for ReductionSteinerTreeToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - if crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)? - .value - .is_none() - { - return Err(crate::rules::ExtractionError::invalid( - "target ILP assignment is infeasible", - )); - } Ok(target_solution[..self.num_edges] .iter() .map(|&value| value == 1) @@ -61,7 +53,7 @@ impl ReduceTo> for SteinerTree { let n = self.num_vertices(); let m = self.num_edges(); let (num_vars, num_constraints) = tree_ilp_sizes(n, m, self.terminals().len())?; - // The source constructor requires at least two distinct terminals. + // The source constructor requires at least one terminal. let root = self.terminals()[0]; let edges = self.graph().edges(); let vertex_var = |v: usize| m + v; @@ -132,7 +124,7 @@ impl ReduceTo> for SteinerTree { } } -/// Bounds for all offsets and allocation sizes; n >= 2 is a source invariant. +/// Bounds for all offsets and allocation sizes; n >= 1 is a source invariant. fn tree_ilp_sizes( n: usize, m: usize, diff --git a/src/rules/steinertreeingraphs_ilp.rs b/src/rules/steinertreeingraphs_ilp.rs deleted file mode 100644 index af256448f..000000000 --- a/src/rules/steinertreeingraphs_ilp.rs +++ /dev/null @@ -1,167 +0,0 @@ -//! Reduction from SteinerTreeInGraphs to ILP (Integer Linear Programming). -//! -//! Uses the rooted multi-commodity flow formulation: -//! - Variables: binary edge selectors `y_e` plus binary directed flow variables -//! `f^t_(u,v)` for each non-root terminal `t` -//! - Constraints: flow conservation and capacity linking `f^t_(u,v) <= y_e` -//! - Objective: minimize total weight of selected edges - -use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; -use crate::models::graph::SteinerTreeInGraphs; -use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; -use crate::topology::{Graph, SimpleGraph}; -use crate::types::WeightElement; - -/// Result of reducing SteinerTreeInGraphs to ILP. -/// -/// Variable layout (all binary): -/// - `y_e` for each undirected source edge `e` (indices `0..m`) -/// - `f^t_(u,v)` and `f^t_(v,u)` for each non-root terminal `t` and each edge -/// (indices `m..m + 2m(k-1)`) -#[derive(Debug, Clone)] -pub struct ReductionSTIGToILP { - target: ILP, - num_edges: usize, -} - -impl ReductionResult for ReductionSTIGToILP { - type Source = SteinerTreeInGraphs; - type Target = ILP; - - fn target_problem(&self) -> &ILP { - &self.target - } - - fn extract_solution( - &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - - Ok(target_solution[..self.num_edges] - .iter() - .map(|&value| value == 1) - .collect()) - } -} - -#[reduction( - transform = exact { - num_vars = "num_edges + 2 * num_edges * (num_terminals - 1)", - num_constraints = "num_vertices * (num_terminals - 1) + 2 * num_edges * (num_terminals - 1)", - }, - unavailable = { - num_nonzeros = "the exact target parameter is not represented by this reduction's symbolic transform", - } -)] -impl ReduceTo> for SteinerTreeInGraphs { - type Result = ReductionSTIGToILP; - - fn reduce_to(&self) -> Result { - if self.weights().iter().any(|&weight| weight <= 0) { - return Err(crate::rules::ReductionError::invalid_target::< - SteinerTreeInGraphs, - ILP, - >( - "ILP construction requires strictly positive edge weights" - )); - } - - let n = self.num_vertices(); - let m = self.num_edges(); - let root = *self.terminals().first().ok_or_else(|| { - crate::rules::ReductionError::invalid_target::< - SteinerTreeInGraphs, - ILP, - >("source must contain at least one terminal") - })?; - let non_root_terminals = &self.terminals()[1..]; - let edges = self.graph().edges(); - let num_vars = m + 2 * m * non_root_terminals.len(); - let mut constraints = Vec::new(); - - let edge_var = |edge_idx: usize| edge_idx; - let flow_var = |terminal_pos: usize, edge_idx: usize, dir: usize| -> usize { - m + terminal_pos * 2 * m + 2 * edge_idx + dir - }; - - // Flow conservation for each non-root terminal commodity - for (terminal_pos, &terminal) in non_root_terminals.iter().enumerate() { - for vertex in 0..n { - let mut terms = Vec::new(); - for (edge_idx, &(u, v)) in edges.iter().enumerate() { - if v == vertex { - terms.push((flow_var(terminal_pos, edge_idx, 0), 1)); - terms.push((flow_var(terminal_pos, edge_idx, 1), -1)); - } - if u == vertex { - terms.push((flow_var(terminal_pos, edge_idx, 0), -1)); - terms.push((flow_var(terminal_pos, edge_idx, 1), 1)); - } - } - - let rhs = if vertex == root { - -1 - } else if vertex == terminal { - 1 - } else { - 0 - }; - constraints.push(LinearConstraint::eq(terms, rhs)); - } - } - - // Capacity linking: f^t_{e,dir} <= y_e - for terminal_pos in 0..non_root_terminals.len() { - for edge_idx in 0..m { - let selector = edge_var(edge_idx); - constraints.push(LinearConstraint::le( - vec![(flow_var(terminal_pos, edge_idx, 0), 1), (selector, -1)], - 0, - )); - constraints.push(LinearConstraint::le( - vec![(flow_var(terminal_pos, edge_idx, 1), 1), (selector, -1)], - 0, - )); - } - } - - // Objective: minimize total weight - let edge_weights = self.weights(); - let objective: Vec<(usize, i64)> = edge_weights - .iter() - .enumerate() - .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)?; - - Ok(ReductionSTIGToILP { - target, - num_edges: m, - }) - } -} - -#[cfg(feature = "example-db")] -pub(crate) fn canonical_rule_example_specs() -> Vec { - vec![crate::example_db::specs::RuleExampleSpec { - id: "steinertreeingraphs_to_ilp", - build: || { - // 4 vertices, 4 edges, 2 terminals - // ILP: 4 + 2*4*1 = 12 binary variables = 4096 configs - let source = SteinerTreeInGraphs::new( - SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3), (0, 3)]), - vec![0, 2], - vec![1, 1, 1, 3], - ); - crate::example_db::specs::rule_example_via_ilp::<_, bool>(source) - }, - }] -} - -#[cfg(test)] -#[path = "../unit_tests/rules/steinertreeingraphs_ilp.rs"] -mod tests; diff --git a/src/rules/stringtostringcorrection_ilp.rs b/src/rules/stringtostringcorrection_ilp.rs index 1f6cf33d2..88228982c 100644 --- a/src/rules/stringtostringcorrection_ilp.rs +++ b/src/rules/stringtostringcorrection_ilp.rs @@ -58,8 +58,6 @@ impl ReductionResult for ReductionSTSCToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ let n = self.n; let k = self.bound; @@ -88,19 +86,7 @@ impl ReductionResult for ReductionSTSCToILP { .filter(|&j| target_solution[idx_s(n, k, t, j)] == 1) .map(|j| current_len + j), ); - match selected.as_slice() { - [operation] => ops.push(*operation), - [] => { - return Err(crate::rules::ExtractionError::invalid(format!( - "edit step {t} has no selected operation" - ))) - } - _ => { - return Err(crate::rules::ExtractionError::invalid(format!( - "edit step {t} has multiple selected operations" - ))) - } - } + ops.push(selected.into_iter().sum()); } ops }) diff --git a/src/rules/strongconnectivityaugmentation_ilp.rs b/src/rules/strongconnectivityaugmentation_ilp.rs index 02bb8a4e3..ce2aad71d 100644 --- a/src/rules/strongconnectivityaugmentation_ilp.rs +++ b/src/rules/strongconnectivityaugmentation_ilp.rs @@ -27,8 +27,6 @@ impl ReductionResult for ReductionSCAToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution[..self.num_candidates] .iter() .map(|&value| value == 1) diff --git a/src/rules/subgraphisomorphism_ilp.rs b/src/rules/subgraphisomorphism_ilp.rs index d8c6c3f4b..958994f47 100644 --- a/src/rules/subgraphisomorphism_ilp.rs +++ b/src/rules/subgraphisomorphism_ilp.rs @@ -38,14 +38,12 @@ impl ReductionResult for ReductionSubIsoToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - - one_hot_decode_rows( + Ok(one_hot_decode_rows( target_solution, self.num_pattern_vertices, self.num_host_vertices, 0, - ) + )) } } diff --git a/src/rules/subsetsum_closestvectorproblem.rs b/src/rules/subsetsum_closestvectorproblem.rs index dd4762df5..630cb8def 100644 --- a/src/rules/subsetsum_closestvectorproblem.rs +++ b/src/rules/subsetsum_closestvectorproblem.rs @@ -3,16 +3,15 @@ use crate::models::algebraic::ClosestVectorProblem; use crate::models::misc::SubsetSum; use crate::reduction; -use crate::registry::ConstructionError; use crate::rules::traits::{ReduceTo, ReductionResult}; use crate::types::{Min, Or}; +use num_rational::BigRational; /// Result of reducing SubsetSum to ClosestVectorProblem. #[derive(Debug, Clone)] pub struct ReductionSubsetSumToClosestVectorProblem { target: ClosestVectorProblem, num_elements: usize, - target_distance: f64, } impl ReductionResult for ReductionSubsetSumToClosestVectorProblem { @@ -27,14 +26,6 @@ impl ReductionResult for ReductionSubsetSumToClosestVectorProblem { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - let value = - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - let certificate = crate::rules::AggregateReductionResult::extract_value(self, value); - if !certificate.0 { - return Err(crate::rules::ExtractionError::invalid( - "target lattice vector does not certify a subset sum", - )); - } Ok(target_solution[..self.num_elements] .iter() .map(|&value| value == 1) @@ -50,8 +41,8 @@ impl crate::rules::AggregateReductionResult for ReductionSubsetSumToClosestVecto &self.target } - fn extract_value(&self, target_value: Min) -> Or { - Or(target_value == Min(Some(self.target_distance))) + fn extract_value(&self, target_value: Min) -> Or { + Or(target_value == Min(Some(BigRational::from_integer(self.num_elements.into())))) } } @@ -112,9 +103,7 @@ impl ReduceTo> for SubsetSum { } basis.push(column); } - // Carry c_k occurs with +1 in bit k and -2 in bit k-1. Descending - // bit rows and carry columns preserve unit pivots in the formal rank - // checker, without changing its implementation or bypassing validation. + // Carry c_k occurs with +1 in bit k and -2 in bit k-1. for bit in (1..bits).rev() { let mut column = vec![0_i64; rows]; column[rows - 1 - bit] = 1; @@ -126,22 +115,11 @@ impl ReduceTo> for SubsetSum { for bit in 0..bits { target[rows - 1 - bit] = i64::from(self.target().bit(bit as u64)); } - // The checked dense byte count bounds n below 2^30 on 64-bit systems, - // so the integer threshold and its unit squared-distance gap are exact. - let count = >>::exact_i64( - n, - "representing the subset-sum distance threshold", - )?; - let target_distance = crate::types::i64_to_exact_f64(count) - .map_err(ConstructionError::from) - .map_err(>>::target_construction)? - .sqrt(); let target = ClosestVectorProblem::new(basis, target) .map_err(>>::target_construction)?; Ok(ReductionSubsetSumToClosestVectorProblem { target, num_elements: n, - target_distance, }) } } diff --git a/src/rules/subsetsum_integerexpressionmembership.rs b/src/rules/subsetsum_integerexpressionmembership.rs index c187a66e4..a0346bd1d 100644 --- a/src/rules/subsetsum_integerexpressionmembership.rs +++ b/src/rules/subsetsum_integerexpressionmembership.rs @@ -21,8 +21,6 @@ impl ReductionResult for ReductionSubsetSumToIntegerExpressionMembership { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ // Union choice 0 = left = Atom(1) = exclude, choice 1 = right = Atom(s_i+1) = include. // This maps directly to SubsetSum's 0/1 include/exclude encoding. diff --git a/src/rules/subsetsum_partition.rs b/src/rules/subsetsum_partition.rs index 8b1e2726f..43fb710c8 100644 --- a/src/rules/subsetsum_partition.rs +++ b/src/rules/subsetsum_partition.rs @@ -34,8 +34,6 @@ impl ReductionResult for ReductionSubsetSumToPartition { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ let source_bits = &target_solution[..self.source_len]; diff --git a/src/rules/sumofsquarespartition_ilp.rs b/src/rules/sumofsquarespartition_ilp.rs index 1d82db321..469d3bae6 100644 --- a/src/rules/sumofsquarespartition_ilp.rs +++ b/src/rules/sumofsquarespartition_ilp.rs @@ -61,14 +61,12 @@ impl ReductionResult for ReductionSSPToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - - crate::rules::ilp_helpers::one_hot_decode_rows( + Ok(crate::rules::ilp_helpers::one_hot_decode_rows( target_solution, self.num_elements, self.num_groups, 0, - ) + )) } } diff --git a/src/rules/test_helpers.rs b/src/rules/test_helpers.rs index 620f17233..f43dafa45 100644 --- a/src/rules/test_helpers.rs +++ b/src/rules/test_helpers.rs @@ -1,7 +1,7 @@ use crate::rules::{ReductionChain, ReductionResult}; use crate::solvers::BruteForce; +use crate::solvers::SolutionAggregate; use crate::traits::Problem; -use crate::types::SolutionAggregate; use std::collections::HashSet; fn verify_optimization_round_trip( @@ -227,6 +227,7 @@ where R: ReductionResult, R::Source: Problem + 'static, R::Target: Problem> + 'static, + ::Value: SolutionAggregate, ::Value: SolutionAggregate + std::fmt::Debug + PartialEq, { use crate::solvers::ILPSolver; @@ -288,8 +289,12 @@ mod tests { } impl crate::solvers::BruteForceProblem for ToyExtremumProblem { - fn dimensions(&self) -> Vec { - vec![2, 2] + fn num_variables(&self) -> Result { + Ok(2usize) + } + + fn dimension(&self, variable: usize) -> Result { + Ok([2, 2][variable]) } } @@ -322,8 +327,12 @@ mod tests { } impl crate::solvers::BruteForceProblem for ToyOrProblem { - fn dimensions(&self) -> Vec { - vec![2, 2] + fn num_variables(&self) -> Result { + Ok(2usize) + } + + fn dimension(&self, variable: usize) -> Result { + Ok([2, 2][variable]) } } @@ -380,8 +389,6 @@ mod tests { target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution.to_vec()) } } @@ -403,8 +410,6 @@ mod tests { target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution.to_vec()) } } @@ -426,8 +431,6 @@ mod tests { target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution.to_vec()) } } @@ -449,8 +452,6 @@ mod tests { target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution.to_vec()) } } diff --git a/src/rules/threedimensionalmatching_ilp.rs b/src/rules/threedimensionalmatching_ilp.rs index 57db1ddc7..a3dce750c 100644 --- a/src/rules/threedimensionalmatching_ilp.rs +++ b/src/rules/threedimensionalmatching_ilp.rs @@ -22,8 +22,6 @@ impl ReductionResult for ReductionThreeDimensionalMatchingToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution.iter().map(|&value| value == 1).collect()) } } diff --git a/src/rules/threedimensionalmatching_minimumweightdecoding.rs b/src/rules/threedimensionalmatching_minimumweightdecoding.rs index c8239dae0..9f39d4afa 100644 --- a/src/rules/threedimensionalmatching_minimumweightdecoding.rs +++ b/src/rules/threedimensionalmatching_minimumweightdecoding.rs @@ -51,15 +51,6 @@ impl ReductionResult for ReductionThreeDimensionalMatchingToMinimumWeightDecodin &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - if target_solution.len() != self.target.num_cols() { - return Err(crate::rules::ExtractionError::invalid(format!( - "expected {} target codeword bits, got {}", - self.target.num_cols(), - target_solution.len() - ))); - } - Ok(target_solution[..self.source_num_triples].to_vec()) } } diff --git a/src/rules/threepartition_resourceconstrainedscheduling.rs b/src/rules/threepartition_resourceconstrainedscheduling.rs index 817ace389..993b638a2 100644 --- a/src/rules/threepartition_resourceconstrainedscheduling.rs +++ b/src/rules/threepartition_resourceconstrainedscheduling.rs @@ -42,8 +42,6 @@ impl ReductionResult for ReductionThreePartitionToRCS { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok(target_solution.to_vec()) } } diff --git a/src/rules/threepartition_sequencingwithreleasetimesanddeadlines.rs b/src/rules/threepartition_sequencingwithreleasetimesanddeadlines.rs index d1b480304..871da9e18 100644 --- a/src/rules/threepartition_sequencingwithreleasetimesanddeadlines.rs +++ b/src/rules/threepartition_sequencingwithreleasetimesanddeadlines.rs @@ -51,32 +51,19 @@ impl ReductionResult for ReductionThreePartitionToSRTD { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ // Simulate the schedule to find start times let mut current_time: i64 = 0; let mut slot_assignment = vec![0usize; self.num_element_tasks]; - let slot_width = self.bound.checked_add(1).ok_or_else(|| { - crate::rules::ExtractionError::invalid("slot width overflows i64") - })?; // B + 1 (slot width including the filler gap) + let slot_width = self.bound + 1; for &task in target_solution { let start = current_time.max(self.target.release_times()[task]); - let finish = start - .checked_add(self.target.lengths()[task]) - .ok_or_else(|| { - crate::rules::ExtractionError::invalid("task finish time overflows i64") - })?; - current_time = finish; + current_time = start + self.target.lengths()[task]; // Only element tasks (indices 0..3m) contribute to the partition if task < self.num_element_tasks { - let slot = usize::try_from(start / slot_width).map_err(|_| { - crate::rules::ExtractionError::invalid( - "decoded task slot cannot be represented as usize", - ) - })?; + let slot = (start / slot_width) as usize; slot_assignment[task] = slot; } } diff --git a/src/rules/timetabledesign_ilp.rs b/src/rules/timetabledesign_ilp.rs index 9e771d48e..3b40bc01d 100644 --- a/src/rules/timetabledesign_ilp.rs +++ b/src/rules/timetabledesign_ilp.rs @@ -35,8 +35,6 @@ impl ReductionResult for ReductionTDToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok((0..self.num_craftsmen) .map(|craftsman| { (0..self.num_tasks) diff --git a/src/rules/traits.rs b/src/rules/traits.rs index c33c39754..7fd097e97 100644 --- a/src/rules/traits.rs +++ b/src/rules/traits.rs @@ -160,14 +160,6 @@ impl ExtractionError { pub type ExtractionResult = std::result::Result; -/// Validate a typed target solution and return its evaluated value for reuse. -pub(crate) fn validate_target_solution( - target: &P, - solution: &P::Solution, -) -> ExtractionResult { - Ok(target.evaluate(solution)?) -} - /// Result of reducing a source problem to a target problem. /// /// This trait encapsulates the target problem and provides methods @@ -184,7 +176,9 @@ pub trait ReductionResult { /// Extract a solution from target problem space to source problem space. /// /// # Arguments - /// * `target_solution` - A solution to the target problem + /// * `target_solution` - A target solution satisfying this reduction's + /// mathematical premises, including optimality when required. The solver + /// or external caller establishes these premises before extraction. /// /// # Returns /// The corresponding solution in the source problem space @@ -308,7 +302,6 @@ where } fn extract_solution(&self, target_solution: &T::Solution) -> ExtractionResult { - validate_target_solution(self.target_problem(), target_solution)?; Ok(target_solution.clone()) } } @@ -404,21 +397,13 @@ pub trait DynAggregateReductionResult { fn target_problem_any(&self) -> &dyn Any; /// Extract an aggregate value from target space to source space. fn extract_value_dyn(&self, target_value: serde_json::Value) -> serde_json::Value; - /// Map the value of a target solution without erasing the source value's type. - /// The caller must establish that the solution realizes the target aggregate - /// before interpreting the result as the source aggregate. - fn extract_value_from_solution_dyn( - &self, - target_solution: &dyn Any, - ) -> ExtractionResult>; } impl DynAggregateReductionResult for R where R::Target: 'static, - ::Solution: 'static, ::Value: Serialize + DeserializeOwned, - ::Value: Serialize + 'static, + ::Value: Serialize, { fn target_problem_any(&self) -> &dyn Any { self.target_problem() as &dyn Any @@ -431,22 +416,6 @@ where serde_json::to_value(source_value) .expect("DynAggregateReductionResult source value serialize failed") } - - fn extract_value_from_solution_dyn( - &self, - target_solution: &dyn Any, - ) -> ExtractionResult> { - let target_solution = target_solution - .downcast_ref::<::Solution>() - .ok_or_else(|| { - ExtractionError::invalid(format!( - "target solution type mismatch: expected {}", - std::any::type_name::<::Solution>() - )) - })?; - let target_value = self.target_problem().evaluate(target_solution)?; - Ok(Box::new(self.extract_value(target_value))) - } } #[cfg(test)] diff --git a/src/rules/travelingsalesman_ilp.rs b/src/rules/travelingsalesman_ilp.rs index ee89c2ec7..b232f15ef 100644 --- a/src/rules/travelingsalesman_ilp.rs +++ b/src/rules/travelingsalesman_ilp.rs @@ -36,28 +36,25 @@ impl ReductionResult for ReductionTSPToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ let n = self.num_vertices; - let tour = one_hot_decode(target_solution, n, n, 0)?; + let tour = one_hot_decode(target_solution, n, n, 0); // Map tour to edge selection let mut edge_selection = vec![false; self.source_edges.len()]; for k in 0..n { let u = tour[k]; let v = tour[(k + 1) % n]; - let edge = self + for (edge, _) in self .source_edges .iter() - .position(|&(a, b)| (a == u && b == v) || (a == v && b == u)) - .ok_or_else(|| { - crate::rules::ExtractionError::invalid(format!( - "target tour uses absent source edge ({u}, {v})" - )) - })?; - edge_selection[edge] = true; + .enumerate() + .filter(|&(_, &(a, b))| (a == u && b == v) || (a == v && b == u)) + .take(1) + { + edge_selection[edge] = true; + } } edge_selection diff --git a/src/rules/travelingsalesman_qubo.rs b/src/rules/travelingsalesman_qubo.rs index 42cbda1a9..31dbdbbbf 100644 --- a/src/rules/travelingsalesman_qubo.rs +++ b/src/rules/travelingsalesman_qubo.rs @@ -10,7 +10,7 @@ use crate::models::algebraic::QUBO; use crate::models::graph::TravelingSalesman; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; -use crate::topology::{Graph, SimpleGraph}; +use crate::topology::SimpleGraph; use std::collections::HashMap; /// Result of reducing TravelingSalesman to QUBO. @@ -20,6 +20,9 @@ pub struct ReductionTravelingSalesmanToQUBO { num_vertices: usize, num_edges: usize, edge_index: HashMap<(usize, usize), usize>, + objective_offset: i128, + feasible_energy_upper: i128, + small_optimum: Option<(Vec, i64)>, } impl ReductionResult for ReductionTravelingSalesmanToQUBO { @@ -30,52 +33,57 @@ impl ReductionResult for ReductionTravelingSalesmanToQUBO { &self.target } - /// Decode position encoding back to edge-based configuration. - /// - /// The QUBO solution uses n^2 binary variables x_{v,p} (vertex v at position p). - /// We extract the tour order, then map consecutive pairs to edge indices. + /// Decode an optimum whose value relation establishes source feasibility. + /// The energy gap guarantees a permutation using existing source edges. fn extract_solution( &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - - Ok({ - let n = self.num_vertices; - - let tour: Vec = (0..n) - .map(|position| { - let mut selected = - (0..n).filter(|&vertex| target_solution[vertex * n + position]); - match (selected.next(), selected.next()) { - (Some(vertex), None) => Ok(vertex), - _ => Err(crate::rules::ExtractionError::invalid(format!( - "tour position {position} does not select exactly one vertex" - ))), - } - }) - .collect::>()?; - - // Build edge-based config: for each consecutive pair in the tour, mark the edge - let mut config = vec![false; self.num_edges]; - for p in 0..n { - let u = tour[p]; - let v = tour[(p + 1) % n]; - let key = (u.min(v), u.max(v)); - let &edge = self.edge_index.get(&key).ok_or_else(|| { - crate::rules::ExtractionError::invalid(format!( - "target tour uses absent source edge ({u}, {v})" - )) - })?; - config[edge] = true; - } + if self.num_vertices < 3 { + return Ok(self.small_optimum.as_ref().unwrap().0.clone()); + } + let n = self.num_vertices; + let tour: Vec = (0..n) + .map(|position| { + (0..n) + .find(|&vertex| target_solution[vertex * n + position]) + .unwrap() + }) + .collect(); + let mut config = vec![false; self.num_edges]; + for p in 0..n { + let (u, v) = (tour[p], tour[(p + 1) % n]); + config[self.edge_index[&(u.min(v), u.max(v))]] = true; + } + Ok(config) + } +} - config - }) +impl crate::rules::AggregateReductionResult for ReductionTravelingSalesmanToQUBO { + type Source = TravelingSalesman; + type Target = QUBO; + fn target_problem(&self) -> &Self::Target { + &self.target + } + fn extract_value(&self, value: crate::types::Min) -> crate::types::Min { + if self.num_vertices < 3 { + return crate::types::Min( + value + .0 + .and(self.small_optimum.as_ref().map(|(_, cost)| *cost)), + ); + } + crate::types::Min( + value.0.filter(|&energy| i128::from(energy) < self.feasible_energy_upper) + // Construction bounds source tour costs by a representable sum of + // absolute edge weights; the offset is calculated in i128. + .map(|energy| i64::try_from(i128::from(energy) + self.objective_offset).unwrap()), + ) } } #[reduction( + aggregate = custom, transform = exact { num_vars = "num_vertices^2", } @@ -87,49 +95,106 @@ impl ReduceTo> for TravelingSalesman { let n = self.num_vertices(); let edges = self.edges(); - // Build edge weight map (both directions for undirected lookup) let overflow = |operation| { - crate::rules::ReductionError::integer_overflow::< - TravelingSalesman, - QUBO, - >(operation) + crate::rules::ReductionError::integer_overflow::>(operation) }; - let mut edge_weight_map: HashMap<(usize, usize), i64> = HashMap::new(); - let mut weight_sum = 0i64; - for &(u, v, w) in &edges { - edge_weight_map.insert((u, v), w); - edge_weight_map.insert((v, u), w); - let magnitude = w - .checked_abs() - .ok_or_else(|| overflow("taking the absolute value of a tour weight"))?; - weight_sum = weight_sum - .checked_add(magnitude) - .ok_or_else(|| overflow("summing absolute tour weights"))?; + let num_edges = edges.len(); + let dim = n + .checked_mul(n) + .ok_or_else(|| overflow("computing the number of QUBO variables"))?; + + // The source represents a connected degree-two edge set. With fewer + // than three vertices this means one loop or two parallel edges. + if n < 3 { + let mut candidates: Vec = edges + .iter() + .enumerate() + .filter(|&(_, &(u, v, _))| (n == 1 && u == v) || (n == 2 && u != v)) + .map(|(index, _)| index) + .collect(); + + let small_optimum = if n > 0 && candidates.len() >= n { + candidates.select_nth_unstable_by_key(n - 1, |&index| (edges[index].2, index)); + let mut solution = vec![false; num_edges]; + let mut cost = 0i64; + for &index in &candidates[..n] { + solution[index] = true; + cost = cost + .checked_add(edges[index].2) + .ok_or_else(|| overflow("summing a small tour cost"))?; + } + Some((solution, cost)) + } else { + None + }; + return Ok(ReductionTravelingSalesmanToQUBO { + target: QUBO::from_sparse(sprs::CsMat::zero((dim, dim))) + .map_err(>>::target_construction)?, + num_vertices: n, + num_edges, + edge_index: HashMap::new(), + objective_offset: 0, + feasible_energy_upper: 0, + small_optimum, + }); } - // Build edge index map: canonical (min, max) → edge index - let graph_edges = self.graph().edges(); - let num_edges = graph_edges.len(); + // A tour on at least three vertices uses no loops and at most one + // edge per endpoint pair. Retain the cheapest parallel edge. let mut edge_index: HashMap<(usize, usize), usize> = HashMap::new(); - for (idx, &(u, v)) in graph_edges.iter().enumerate() { - edge_index.insert((u.min(v), u.max(v)), idx); + for (index, &(u, v, weight)) in edges.iter().enumerate() { + if u == v { + continue; + } + let key = (u.min(v), u.max(v)); + edge_index + .entry(key) + .and_modify(|previous| { + if weight < edges[*previous].2 { + *previous = index; + } + }) + .or_insert(index); } - - // Penalty weight: must exceed any possible tour cost - let a = weight_sum + let shift = edge_index + .values() + .map(|&index| edges[index].2) + .fold(0, i64::min); + let mut shifted_sum = 0i64; + let mut absolute_sum = 0i64; + for &index in edge_index.values() { + let weight = edges[index].2; + absolute_sum = absolute_sum + .checked_add( + weight + .checked_abs() + .ok_or_else(|| overflow("taking the absolute value of a tour weight"))?, + ) + .ok_or_else(|| overflow("summing absolute tour weights"))?; + let shifted = weight + .checked_sub(shift) + .ok_or_else(|| overflow("shifting a tour weight"))?; + shifted_sum = shifted_sum + .checked_add(shifted) + .ok_or_else(|| overflow("summing shifted tour weights"))?; + } + // Every permutation tour uses n edges. Shifting each cost therefore + // adds a constant. All costs are now nonnegative even off-premise. + let a = shifted_sum .checked_add(1) .ok_or_else(|| overflow("computing the tour penalty"))?; + let omitted_constant = 2 * n as i128 * i128::from(a); + let objective_offset = omitted_constant + n as i128 * i128::from(shift); + let feasible_energy_upper = i128::from(a) - omitted_constant; // Build n^2 x n^2 upper-triangular QUBO matrix - let dim = n - .checked_mul(n) - .ok_or_else(|| overflow("computing the number of QUBO variables"))?; - let mut matrix = vec![vec![0i64; dim]; dim]; + let mut matrix = vec![std::collections::BTreeMap::new(); dim]; // Helper: add value to upper-triangular position let mut add_upper = |i: usize, j: usize, val: i64| { let (lo, hi) = if i <= j { (i, j) } else { (j, i) }; - matrix[lo][hi] = matrix[lo][hi] + let coefficient = matrix[lo].entry(hi).or_insert(0i64); + *coefficient = coefficient .checked_add(val) .ok_or_else(|| overflow("adding a tour QUBO coefficient"))?; Ok::<(), crate::rules::ReductionError>(()) @@ -189,7 +254,10 @@ impl ReduceTo> for TravelingSalesman { // For each pair (u, v), add cost for x_{u,p} * x_{v,p_next} and x_{v,p} * x_{u,p_next} for u in 0..n { for v in (u + 1)..n { - let cost = edge_weight_map.get(&(u, v)).copied().unwrap_or(a); + let cost = edge_index.get(&(u, v)).map_or(a, |&index| { + // The bound calculation already checked this subtraction. + edges[index].2 - shift + }); for p in 0..n { let p_next = (p + 1) % n; // x_{u,p} * x_{v,p_next} @@ -200,18 +268,17 @@ impl ReduceTo> for TravelingSalesman { } } - let target = QUBO::from_matrix(matrix).map_err(|message| { - crate::rules::ReductionError::construction::< - TravelingSalesman, - QUBO, - >(message) - })?; + let target = + QUBO::from_rows(matrix).map_err(>>::target_construction)?; Ok(ReductionTravelingSalesmanToQUBO { target, num_vertices: n, num_edges, edge_index, + objective_offset, + feasible_energy_upper, + small_optimum: None, }) } } diff --git a/src/rules/undirectedflowlowerbounds_ilp.rs b/src/rules/undirectedflowlowerbounds_ilp.rs index 88db49edf..ecae0d8de 100644 --- a/src/rules/undirectedflowlowerbounds_ilp.rs +++ b/src/rules/undirectedflowlowerbounds_ilp.rs @@ -58,8 +58,6 @@ impl ReductionResult for ReductionUFLBToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - Ok({ let e = self.num_edges; target_solution[2 * e..3 * e] diff --git a/src/rules/undirectedtwocommodityintegralflow_ilp.rs b/src/rules/undirectedtwocommodityintegralflow_ilp.rs index 821af6079..e61c3f14e 100644 --- a/src/rules/undirectedtwocommodityintegralflow_ilp.rs +++ b/src/rules/undirectedtwocommodityintegralflow_ilp.rs @@ -55,8 +55,6 @@ impl ReductionResult for ReductionU2CIFToILP { &self, target_solution: &::Solution, ) -> crate::rules::ExtractionResult<::Solution> { - crate::rules::traits::validate_target_solution(self.target_problem(), target_solution)?; - crate::rules::ilp_helpers::decode_usize_values(&target_solution[..4 * self.num_edges]) } } diff --git a/src/rules/unitdiskmapping/ksg/mapping.rs b/src/rules/unitdiskmapping/ksg/mapping.rs index 23e471a58..72dd341eb 100644 --- a/src/rules/unitdiskmapping/ksg/mapping.rs +++ b/src/rules/unitdiskmapping/ksg/mapping.rs @@ -55,7 +55,6 @@ pub struct MappingResult { /// Tape entries recording gadget applications (for unapply during solution extraction). pub tape: Vec, /// Doubled cells (where two copy lines overlap) for map_config_back. - #[serde(default)] pub doubled_cells: HashSet<(usize, usize)>, } @@ -230,16 +229,8 @@ impl MappingResult { &self, grid_config: &[usize], ) -> crate::rules::ExtractionResult> { - self.map_config_back_internal(grid_config) - .map_err(|error| crate::rules::ExtractionError::invalid(error.to_string())) - } - - fn map_config_back_internal( - &self, - grid_config: &[usize], - ) -> Result, ReductionError> { if grid_config.len() != self.positions.len() { - return Err(mapping_invalid( + return Err(crate::rules::ExtractionError::invalid( "grid configuration length must match the mapped vertex count", )); } @@ -248,12 +239,18 @@ impl MappingResult { let mut config_2d = vec![vec![0usize; cols]; rows]; for (idx, &(row, col)) in self.positions.iter().enumerate() { - let row = usize::try_from(row) - .map_err(|_| mapping_invalid("mapping result contains a negative grid row"))?; - let col = usize::try_from(col) - .map_err(|_| mapping_invalid("mapping result contains a negative grid column"))?; + let row = usize::try_from(row).map_err(|_| { + crate::rules::ExtractionError::invalid( + "mapping result contains a negative grid row", + ) + })?; + let col = usize::try_from(col).map_err(|_| { + crate::rules::ExtractionError::invalid( + "mapping result contains a negative grid column", + ) + })?; if row >= rows || col >= cols { - return Err(mapping_invalid( + return Err(crate::rules::ExtractionError::invalid( "mapping result contains a position outside its grid dimensions", )); } @@ -261,7 +258,8 @@ impl MappingResult { } // Step 2: Unapply gadgets in reverse order - unapply_gadgets(&self.tape, &mut config_2d)?; + unapply_gadgets(&self.tape, &mut config_2d) + .map_err(|error| crate::rules::ExtractionError::invalid(error.to_string()))?; // Step 3: Extract vertex configs from copylines map_config_copyback( @@ -271,6 +269,7 @@ impl MappingResult { &config_2d, &self.doubled_cells, ) + .map_err(|error| crate::rules::ExtractionError::invalid(error.to_string())) } } @@ -280,16 +279,8 @@ impl MappingResult { &self, grid_config: &[usize], ) -> crate::rules::ExtractionResult> { - self.map_config_back_internal(grid_config) - .map_err(|error| crate::rules::ExtractionError::invalid(error.to_string())) - } - - fn map_config_back_internal( - &self, - grid_config: &[usize], - ) -> Result, ReductionError> { if grid_config.len() != self.positions.len() { - return Err(mapping_invalid( + return Err(crate::rules::ExtractionError::invalid( "grid configuration length must match the mapped vertex count", )); } @@ -298,12 +289,18 @@ impl MappingResult { let mut config_2d = vec![vec![0usize; cols]; rows]; for (idx, &(row, col)) in self.positions.iter().enumerate() { - let row = usize::try_from(row) - .map_err(|_| mapping_invalid("mapping result contains a negative grid row"))?; - let col = usize::try_from(col) - .map_err(|_| mapping_invalid("mapping result contains a negative grid column"))?; + let row = usize::try_from(row).map_err(|_| { + crate::rules::ExtractionError::invalid( + "mapping result contains a negative grid row", + ) + })?; + let col = usize::try_from(col).map_err(|_| { + crate::rules::ExtractionError::invalid( + "mapping result contains a negative grid column", + ) + })?; if row >= rows || col >= cols { - return Err(mapping_invalid( + return Err(crate::rules::ExtractionError::invalid( "mapping result contains a position outside its grid dimensions", )); } @@ -311,7 +308,8 @@ impl MappingResult { } // Step 2: Unapply gadgets in reverse order - unapply_weighted_gadgets(&self.tape, &mut config_2d)?; + unapply_weighted_gadgets(&self.tape, &mut config_2d) + .map_err(|error| crate::rules::ExtractionError::invalid(error.to_string()))?; // Step 3: Extract vertex configs from copylines map_config_copyback( @@ -321,6 +319,7 @@ impl MappingResult { &config_2d, &self.doubled_cells, ) + .map_err(|error| crate::rules::ExtractionError::invalid(error.to_string())) } } diff --git a/src/rules/unitdiskmapping/triangular/mapping.rs b/src/rules/unitdiskmapping/triangular/mapping.rs index 7fa8ebe29..e662ef529 100644 --- a/src/rules/unitdiskmapping/triangular/mapping.rs +++ b/src/rules/unitdiskmapping/triangular/mapping.rs @@ -310,28 +310,22 @@ pub fn map_config_back( result: &MappingResult, grid_config: &[usize], ) -> crate::rules::ExtractionResult> { - map_config_back_internal(result, grid_config) - .map_err(|error| crate::rules::ExtractionError::invalid(error.to_string())) -} - -fn map_config_back_internal( - result: &MappingResult, - grid_config: &[usize], -) -> Result, ReductionError> { if grid_config.len() != result.positions.len() { - return Err(mapping_invalid( + return Err(crate::rules::ExtractionError::invalid( "grid configuration length must match the mapped vertex count", )); } - let positions = position_index(result)?; + let positions = position_index(result) + .map_err(|error| crate::rules::ExtractionError::invalid(error.to_string()))?; - super::super::weighted::trace_centers(result)? + super::super::weighted::trace_centers(result) + .map_err(|error| crate::rules::ExtractionError::invalid(error.to_string()))? .into_iter() .map(|center| { positions .get(¢er) .map(|&index| grid_config[index]) - .ok_or(mapping_invalid( + .ok_or(crate::rules::ExtractionError::invalid( "a traced center is missing from the mapped graph", )) }) diff --git a/src/solvers/brute_force.rs b/src/solvers/brute_force.rs index fd3add8fe..7ddcdc7e2 100644 --- a/src/solvers/brute_force.rs +++ b/src/solvers/brute_force.rs @@ -4,12 +4,56 @@ use std::any::Any; use crate::solvers::SolveError; use crate::traits::Problem; -use crate::types::{Aggregate, SolutionAggregate}; +use crate::types::{Aggregate, Extremum, Max, Min, Or}; +use serde::{de::DeserializeOwned, Serialize}; +use std::fmt; + +/// Brute-force capability for selecting witnesses from a completed aggregate. +/// +/// This is not required by model evaluation, reductions, or solvers that return +/// their solutions directly. +pub trait SolutionAggregate: Aggregate { + /// Whether a solution-level value contributes to the final aggregate value. + fn contributes_to_solution(value: &Self, total: &Self) -> bool; +} + +impl SolutionAggregate + for Max +{ + fn contributes_to_solution(value: &Self, total: &Self) -> bool { + matches!((value, total), (Max(Some(value)), Max(Some(best))) if value == best) + } +} + +impl SolutionAggregate + for Min +{ + fn contributes_to_solution(value: &Self, total: &Self) -> bool { + matches!((value, total), (Min(Some(value)), Min(Some(best))) if value == best) + } +} + +impl SolutionAggregate for Or { + fn contributes_to_solution(value: &Self, total: &Self) -> bool { + value.0 && total.0 + } +} + +impl SolutionAggregate + for Extremum +{ + fn contributes_to_solution(candidate: &Self, total: &Self) -> bool { + matches!( + (candidate.value.as_ref(), total.value.as_ref()), + (Some(value), Some(best)) if candidate.sense == total.sense && value == best + ) + } +} type CartesianWitness

= Option<(

::Solution,

::Value)>; #[doc(hidden)] -pub type BruteForceDimensionsFn = fn(&dyn Any) -> Vec; +pub type BruteForceDimensionsFn = fn(&dyn Any) -> Result, SolveError>; #[doc(hidden)] pub type BruteForceSolveFn = fn(&dyn Any) -> Result, SolveError>; @@ -34,38 +78,43 @@ inventory::collect!(BruteForceRegistration); /// A problem with a finite Cartesian coordinate space for reference solving. pub trait BruteForceProblem: Problem { - /// Cardinality of each coordinate in the brute-force search space. - fn dimensions(&self) -> Vec; + /// Number of coordinates needed to represent one candidate. + fn num_variables(&self) -> Result; + + /// Cardinality of a coordinate. `variable` must be less than `num_variables()`. + fn dimension(&self, variable: usize) -> Result; +} - /// Number of coordinates in the brute-force search space. - fn num_variables(&self) -> usize { - self.dimensions().len() +/// Materialize coordinate cardinalities for enumeration or inspection. +#[doc(hidden)] +pub fn cartesian_dimensions(problem: &P) -> Result, SolveError> { + let count = BruteForceProblem::num_variables(problem)?; + let mut dimensions = Vec::new(); + dimensions.try_reserve_exact(count)?; + for variable in 0..count { + dimensions.push(problem.dimension(variable)?); } + Ok(dimensions) } pub(crate) struct CartesianIndices { dimensions: Vec, current: Option>, - remaining: usize, } impl CartesianIndices { pub(crate) fn new(dimensions: Vec) -> Result { - let total = if dimensions.is_empty() { - 1 - } else if dimensions.contains(&0) { - 0 + let current = if dimensions.contains(&0) { + None } else { - dimensions.iter().try_fold(1usize, |total, &dimension| { - total - .checked_mul(dimension) - .ok_or_else(|| SolveError::SearchSpaceOverflow(dimensions.clone())) - })? + let mut current = Vec::new(); + current.try_reserve_exact(dimensions.len())?; + current.resize(dimensions.len(), 0); + Some(current) }; Ok(Self { - current: (total != 0).then(|| vec![0; dimensions.len()]), dimensions, - remaining: total, + current, }) } } @@ -79,24 +128,23 @@ impl Iterator for CartesianIndices { for index in (0..self.dimensions.len()).rev() { next[index] += 1; if next[index] < self.dimensions[index] { + self.current = Some(next); break; } next[index] = 0; } - self.remaining -= 1; - if self.remaining != 0 { - self.current = Some(next); - } Some(current) } fn size_hint(&self) -> (usize, Option) { - (self.remaining, Some(self.remaining)) + if self.current.is_some() { + (1, None) + } else { + (0, Some(0)) + } } } -impl ExactSizeIterator for CartesianIndices {} - /// Exact reference solver for variants with a registered finite enumeration. #[derive(Debug, Clone, Default)] pub struct BruteForce; @@ -186,7 +234,7 @@ impl BruteForce { F: Fn(Vec) -> P::Solution, { let mut total = P::Value::identity(); - for indices in CartesianIndices::new(problem.dimensions())? { + for indices in CartesianIndices::new(cartesian_dimensions(problem)?)? { total = total.combine(problem.evaluate(&decode(indices))?)?; if total.is_absorbing() { break; @@ -207,7 +255,7 @@ impl BruteForce { { let total = self.solve_cartesian(problem, &decode)?; let mut witnesses = Vec::new(); - for indices in CartesianIndices::new(problem.dimensions())? { + for indices in CartesianIndices::new(cartesian_dimensions(problem)?)? { let solution = decode(indices); let value = problem.evaluate(&solution)?; if P::Value::contributes_to_solution(&value, &total) { @@ -228,7 +276,7 @@ impl BruteForce { F: Fn(Vec) -> P::Solution, { let total = self.solve_cartesian(problem, &decode)?; - for indices in CartesianIndices::new(problem.dimensions())? { + for indices in CartesianIndices::new(cartesian_dimensions(problem)?)? { let solution = decode(indices); let value = problem.evaluate(&solution)?; if P::Value::contributes_to_solution(&value, &total) { diff --git a/src/solvers/customized/closest_vector_problem.rs b/src/solvers/customized/closest_vector_problem.rs index 7f901e8e9..e222cebd2 100644 --- a/src/solvers/customized/closest_vector_problem.rs +++ b/src/solvers/customized/closest_vector_problem.rs @@ -21,23 +21,15 @@ pub(crate) fn solve( .map(|column| { column .iter() - .map(|&entry| { - crate::types::i64_to_exact_f64(entry)?; - Ok(BigRational::from_integer(entry.into())) - }) - .collect::, SolveError>>() + .map(|&entry| BigRational::from_integer(entry.into())) + .collect() }) - .collect::, _>>()?; + .collect::>>(); let target = problem .target() .iter() - .map(|coordinate| { - let value = coordinate.to_f64().map_err(SolveError::Evaluation)?; - BigRational::from_float(value).ok_or_else(|| { - SolveError::NonFiniteResult("converting a CVP target to an exact rational".into()) - }) - }) - .collect::, _>>()?; + .map(ClosestVectorTarget::to_rational) + .collect::>(); let (mu, norms, alpha) = gram_schmidt(&basis, &target); let mut best_squared = (0..n).map(|i| &norms[i] * &alpha[i] * &alpha[i]).sum(); @@ -118,7 +110,6 @@ fn enumerate( center.round().to_integer().to_i64().ok_or_else(|| { SolveError::IntegerOverflow("rounding a CVP enumeration center".into()) })?; - crate::types::i64_to_exact_f64(candidate)?; let nearest = BigRational::from_integer(candidate.into()); let mut step = if center > nearest { 1_i64 } else { -1 }; @@ -127,7 +118,6 @@ fn enumerate( // branch uses the improved incumbent rather than a fixed initial interval. loop { coefficients[level] = candidate; - crate::types::i64_to_exact_f64(candidate)?; let delta = BigRational::from_integer(candidate.into()) - ¢er; let next_squared = &partial_squared + &norms[level] * &delta * δ if next_squared >= *best_squared { @@ -152,9 +142,15 @@ fn enumerate( break; } // Differences +1,-2,+3,... (or -1,+2,-3,...) alternate around the center. - // Exact f64 coefficient transport keeps these i64 updates below 2^55. - candidate += step; - step = -step - step.signum(); + candidate = candidate.checked_add(step).ok_or_else(|| { + SolveError::IntegerOverflow("advancing a CVP enumeration coefficient".into()) + })?; + step = step + .checked_neg() + .and_then(|value| value.checked_sub(step.signum())) + .ok_or_else(|| { + SolveError::IntegerOverflow("advancing a CVP enumeration step".into()) + })?; } Ok(()) } diff --git a/src/solvers/customized/minimum_decision_tree.rs b/src/solvers/customized/minimum_decision_tree.rs index 7422ab1dc..7302195bd 100644 --- a/src/solvers/customized/minimum_decision_tree.rs +++ b/src/solvers/customized/minimum_decision_tree.rs @@ -1,12 +1,23 @@ //! Exact minimum decision tree solver using dynamic programming over object subsets. use crate::models::misc::MinimumDecisionTree; +use crate::solvers::SolveError; -pub(crate) fn solve(problem: &MinimumDecisionTree) -> Option> { +pub(crate) fn solve(problem: &MinimumDecisionTree) -> Result, SolveError> { let n = problem.num_objects(); - let full = (1usize << n) - 1; - let mut costs = vec![usize::MAX; 1usize << n]; - let mut choices = vec![problem.num_tests(); 1usize << n]; + if n >= usize::BITS as usize { + return Err(SolveError::IntegerOverflow( + "indexing object subsets with a usize mask".into(), + )); + } + let states = 1usize << n; + let full = states - 1; + let mut costs = Vec::new(); + costs.try_reserve_exact(states)?; + costs.resize(states, usize::MAX); + let mut choices = Vec::new(); + choices.try_reserve_exact(states)?; + choices.resize(states, problem.num_tests()); for object in 0..n { costs[1 << object] = 0; } @@ -37,9 +48,11 @@ pub(crate) fn solve(problem: &MinimumDecisionTree) -> Option> { } let slots = (1usize << (n - 1)) - 1; - let mut solution = vec![problem.num_tests(); slots]; + let mut solution = Vec::new(); + solution.try_reserve_exact(slots)?; + solution.resize(slots, problem.num_tests()); write_tree(problem, full, 0, &choices, &mut solution); - Some(solution) + Ok(solution) } fn write_tree( diff --git a/src/solvers/customized/shortest_common_superstring.rs b/src/solvers/customized/shortest_common_superstring.rs index bcca60f3b..e3f5a2ea4 100644 --- a/src/solvers/customized/shortest_common_superstring.rs +++ b/src/solvers/customized/shortest_common_superstring.rs @@ -1,8 +1,9 @@ //! Exact shortest common superstring solver using subset dynamic programming. use crate::models::misc::ShortestCommonSuperstring; +use crate::solvers::SolveError; -pub(crate) fn solve(problem: &ShortestCommonSuperstring) -> Option>> { +pub(crate) fn solve(problem: &ShortestCommonSuperstring) -> Result>, SolveError> { let mut strings = problem.strings().to_vec(); strings.sort(); strings.dedup(); @@ -18,17 +19,31 @@ pub(crate) fn solve(problem: &ShortestCommonSuperstring) -> Option>; (1usize << n) * n]; + if n >= usize::BITS as usize { + return Err(SolveError::IntegerOverflow( + "indexing string subsets with a usize mask".into(), + )); + } + let states = 1usize << n; + let cells = states.checked_mul(n).ok_or_else(|| { + SolveError::IntegerOverflow("sizing the superstring dynamic-programming table".into()) + })?; + let mut dp = Vec::>>::new(); + dp.try_reserve_exact(cells)?; + dp.resize(cells, None); for (i, string) in strings.iter().enumerate() { dp[(1 << i) * n + i] = Some(string.clone()); } - for mask in 1usize..(1usize << n) { + for mask in 1usize..states { for last in 0..n { let Some(prefix) = dp[mask * n + last].clone() else { continue; @@ -51,14 +66,14 @@ pub(crate) fn solve(problem: &ShortestCommonSuperstring) -> Option>(); + solution.extend(shortest.into_iter().map(Some)); solution.resize(problem.max_length(), None); - Some(solution) + Ok(solution) } fn contains(haystack: &[usize], needle: &[usize]) -> bool { diff --git a/src/solvers/customized/solver.rs b/src/solvers/customized/solver.rs index 020fd82bc..87b9a5baf 100644 --- a/src/solvers/customized/solver.rs +++ b/src/solvers/customized/solver.rs @@ -70,12 +70,12 @@ register_customized_solver!( register_customized_solver!(GroupingBySwapping, "symbol-block-order", |problem| Ok( super::grouping_by_swapping::solve(problem) )); -register_customized_solver!(ShortestCommonSuperstring, "subset-dp", |problem| Ok( - super::shortest_common_superstring::solve(problem) -)); -register_customized_solver!(MinimumDecisionTree, "subset-dp", |problem| Ok( - super::minimum_decision_tree::solve(problem) -)); +register_customized_solver!(ShortestCommonSuperstring, "subset-dp", |problem| { + super::shortest_common_superstring::solve(problem).map(Some) +}); +register_customized_solver!(MinimumDecisionTree, "subset-dp", |problem| { + super::minimum_decision_tree::solve(problem).map(Some) +}); register_customized_solver!( MinimumCostCirculation, "negative-cycle-canceling", diff --git a/src/solvers/ilp/adapter.rs b/src/solvers/ilp/adapter.rs new file mode 100644 index 000000000..a766a69d1 --- /dev/null +++ b/src/solvers/ilp/adapter.rs @@ -0,0 +1,239 @@ +//! Numerical execution of a native ILP through HiGHS. +//! +//! This module knows only ILP data and backend settings. Registry lookup, +//! type-erased dispatch, and reduction-chain extraction belong to the caller. + +use crate::models::algebraic::{Comparison, ILPCoefficient, ObjectiveSense, VariableDomain, ILP}; +use crate::types::{i64_to_exact_f64, ExactI64ToF64Error, MAX_EXACT_F64_INTEGER}; +use highs::{HighsModelStatus, HighsSolutionStatus, RowProblem, Sense}; + +/// Internal errors are mapped to the existing public solver errors by orchestration. +#[derive(Debug, PartialEq, Eq, thiserror::Error)] +pub(crate) enum IlpBackendError { + #[error("the ILP is infeasible")] + Infeasible, + #[error("the ILP objective is unbounded")] + Unbounded, + #[error("the ILP solver reached its time limit before proving optimality")] + Timeout, + #[error("the ILP backend failed: {0}")] + BackendFailure(String), + #[error("the ILP backend returned an invalid rounded solution: {0}")] + InvalidSolution(String), + #[error(transparent)] + InexactTransport(#[from] ExactI64ToF64Error), +} + +/// Backend representation is an execution concern, not a model capability. +pub(crate) trait BackendCoefficient: ILPCoefficient { + fn to_backend_number(self) -> Result; +} +impl BackendCoefficient for i64 { + fn to_backend_number(self) -> Result { + Ok(i64_to_exact_f64(self)?) + } +} +impl BackendCoefficient for f64 { + fn to_backend_number(self) -> Result { + Ok(self) + } +} + +fn accept_backend_status(status: HighsModelStatus) -> Result<(), IlpBackendError> { + match status { + HighsModelStatus::Optimal => Ok(()), + HighsModelStatus::Infeasible => Err(IlpBackendError::Infeasible), + HighsModelStatus::Unbounded => Err(IlpBackendError::Unbounded), + HighsModelStatus::ReachedTimeLimit => Err(IlpBackendError::Timeout), + other => Err(IlpBackendError::BackendFailure(format!( + "HiGHS status: {other:?}" + ))), + } +} + +pub(crate) struct HighsAdapter { + time_limit: Option, +} + +impl HighsAdapter { + pub(crate) fn new(time_limit: Option) -> Self { + Self { time_limit } + } + pub(crate) fn solve(&self, problem: &ILP) -> Result, IlpBackendError> + where + V: VariableDomain, + C: BackendCoefficient, + { + if self + .time_limit + .is_some_and(|seconds| !seconds.is_finite() || seconds < 0.0) + { + return Err(IlpBackendError::BackendFailure( + "time limit must be finite and nonnegative".into(), + )); + } + self.solve_with_objective(problem, problem.objective()) + } + + fn solve_with_objective( + &self, + problem: &ILP, + objective_terms: &[(usize, C)], + ) -> Result, IlpBackendError> + where + V: VariableDomain, + C: BackendCoefficient, + { + let n = problem.num_vars(); + if n == 0 { + return if problem + .is_feasible(&[]) + .map_err(|error| IlpBackendError::InvalidSolution(error.to_string()))? + { + Ok(vec![]) + } else { + Err(IlpBackendError::Infeasible) + }; + } + + if n > i32::MAX as usize || problem.constraints().len() > i32::MAX as usize { + return Err(IlpBackendError::BackendFailure( + "ILP dimensions exceed the HiGHS index representation".into(), + )); + } + let mut backend = RowProblem::new(); + let mut costs = vec![0.0; n]; + for &(index, coefficient) in objective_terms { + costs[index] = coefficient.to_backend_number()?; + } + let columns = problem + .variables() + .iter() + .enumerate() + .map(|(index, bounds)| { + let lower = bounds + .lower_bound() + .map(i64_to_exact_f64) + .transpose()? + .unwrap_or(f64::NEG_INFINITY); + let upper = bounds + .upper_bound() + .map(i64_to_exact_f64) + .transpose()? + .unwrap_or(f64::INFINITY); + Ok(backend.add_integer_column(costs[index], lower..=upper)) + }) + .collect::, IlpBackendError>>()?; + for constraint in problem.constraints() { + let terms = constraint + .terms() + .iter() + .map(|&(index, coefficient)| Ok((columns[index], coefficient.to_backend_number()?))) + .collect::, IlpBackendError>>()?; + let rhs = constraint.rhs().to_backend_number()?; + let (lower, upper) = match constraint.comparison() { + Comparison::Le => (f64::NEG_INFINITY, rhs), + Comparison::Ge => (rhs, f64::INFINITY), + Comparison::Eq => (rhs, rhs), + }; + backend.add_row(lower..=upper, terms); + } + let sense = match problem.sense() { + ObjectiveSense::Minimize => Sense::Minimise, + ObjectiveSense::Maximize => Sense::Maximise, + }; + let mut model = backend.try_optimise(sense).map_err(|error| { + IlpBackendError::BackendFailure(format!("loading HiGHS model: {error:?}")) + })?; + model.make_quiet(); + for (option, value) in [("random_seed", 0), ("threads", 1)] { + model.try_set_option(option, value).map_err(|error| { + IlpBackendError::BackendFailure(format!("setting {option}: {error:?}")) + })?; + } + for option in ["mip_rel_gap", "mip_abs_gap"] { + model.try_set_option(option, 0.0).map_err(|error| { + IlpBackendError::BackendFailure(format!("setting {option}: {error:?}")) + })?; + } + model.try_set_option("parallel", "off").map_err(|error| { + IlpBackendError::BackendFailure(format!("setting parallel: {error:?}")) + })?; + if let Some(seconds) = self.time_limit { + model + .try_set_option("time_limit", seconds) + .map_err(|error| { + IlpBackendError::BackendFailure(format!("setting time_limit: {error:?}")) + })?; + } + let solved = model.try_solve().map_err(|error| { + IlpBackendError::BackendFailure(format!("running HiGHS: {error:?}")) + })?; + if solved.status() == HighsModelStatus::UnboundedOrInfeasible && !objective_terms.is_empty() + { + // A zero objective cannot be unbounded, so feasibility distinguishes these states. + self.solve_with_objective(problem, &[])?; + return Err(IlpBackendError::Unbounded); + } + accept_backend_status(solved.status())?; + let gap = solved.mip_gap(); + if gap.is_finite() && gap > 0.0 { + return Err(IlpBackendError::BackendFailure(format!( + "HiGHS returned a nonzero optimality gap: {gap}" + ))); + } + if solved.primal_solution_status() != HighsSolutionStatus::Feasible { + return Err(IlpBackendError::BackendFailure( + "HiGHS returned no feasible primal solution".into(), + )); + } + decode_and_validate(problem, solved.get_solution().columns().iter().copied()) + } +} + +fn decode_and_validate( + problem: &ILP, + values: impl IntoIterator, +) -> Result, IlpBackendError> { + let result = values + .into_iter() + .enumerate() + .map(|(index, value)| { + if !value.is_finite() { + return Err(IlpBackendError::InvalidSolution(format!( + "variable {index} is non-finite" + ))); + } + let rounded = value.round(); + if (value - rounded).abs() > 1e-6 { + return Err(IlpBackendError::InvalidSolution(format!( + "variable {index} has non-integral value {value}" + ))); + } + if rounded.abs() > MAX_EXACT_F64_INTEGER as f64 { + return Err(IlpBackendError::InvalidSolution(format!( + "variable {index} value {rounded} exceeds exact f64 integer transport" + ))); + } + Ok(rounded as i64) + }) + .collect::, _>>()?; + if !problem + .is_feasible(&result) + .map_err(|error| IlpBackendError::InvalidSolution(error.to_string()))? + { + return Err(IlpBackendError::InvalidSolution( + "the rounded assignment violates the ILP; this may be caused by numerical tolerances. \ + Consider tightening the backend's integer feasibility tolerance" + .into(), + )); + } + problem + .evaluate_objective(&result) + .map_err(|error| IlpBackendError::InvalidSolution(error.to_string()))?; + Ok(result) +} + +#[cfg(test)] +#[path = "../../unit_tests/solvers/ilp/adapter.rs"] +mod tests; diff --git a/src/solvers/ilp/mod.rs b/src/solvers/ilp/mod.rs index 55556679e..ecf31b7a0 100644 --- a/src/solvers/ilp/mod.rs +++ b/src/solvers/ilp/mod.rs @@ -1,8 +1,8 @@ //! ILP (Integer Linear Programming) solver module. //! -//! This module provides an ILP solver using the HiGHS solver via the `good_lp` crate. -//! It is only available when the `ilp` feature is enabled. +//! This module provides an ILP solver using the HiGHS solver through its native Rust bindings. +pub(super) mod adapter; mod solver; pub use solver::{ILPSolveError, ILPSolver}; diff --git a/src/solvers/ilp/solver.rs b/src/solvers/ilp/solver.rs index e5325a29f..db74766f2 100644 --- a/src/solvers/ilp/solver.rs +++ b/src/solvers/ilp/solver.rs @@ -1,27 +1,16 @@ //! ILP solver implementation using HiGHS. -use crate::models::algebraic::{Comparison, ObjectiveSense, VariableDomain, ILP}; +use super::adapter::{HighsAdapter, IlpBackendError}; use crate::solvers::registry::solver_capability_registry; use crate::solvers::ExactProblemKey; use crate::traits::Problem; -use crate::types::{i64_to_exact_f64, MAX_EXACT_F64_INTEGER}; -use good_lp::highs; -use good_lp::solvers::highs::HighsParallelType; -use good_lp::{ - variable, ProblemVariables, ResolutionError, Solution, SolutionStatus, SolverModel, Variable, -}; /// A failure to produce an ILP solution optimal within backend numerical tolerances. #[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)] pub enum ILPSolveError { - /// The constraints have no feasible assignment. - #[error("the ILP is infeasible")] + /// The completed solve establishes that the source has no feasible solution. + #[error("the problem is infeasible")] Infeasible, - /// A target witness did not establish the source decision threshold. - #[error( - "the ILP witness does not meet the decision threshold for {0}; the decision is unresolved" - )] - UnresolvedDecision(String), /// The objective is unbounded. #[error("the ILP objective is unbounded")] Unbounded, @@ -32,7 +21,7 @@ pub enum ILPSolveError { #[error("the ILP backend failed: {0}")] BackendFailure(String), /// Type-erased dispatch received a value other than a supported ILP variant. - #[error("the ILP backend requires bool/i64 variables and f64 coefficients")] + #[error("the ILP backend requires bool/i64 variables and i64/f64 coefficients")] UnsupportedProblemType, /// No ILP pipeline is registered for the exact problem variant. #[error("no ILP pipeline is registered for {0}")] @@ -43,9 +32,12 @@ pub enum ILPSolveError { /// A registered pipeline returned a solution for a different source type. #[error("registered ILP pipeline returned the wrong solution type for {0}")] PipelineTypeMismatch(String), - /// HiGHS reported an optimal solution that is invalid after integer rounding. - #[error("the ILP backend returned an invalid rounded solution: {0}")] + /// The backend or reduction pipeline returned an invalid witness. + #[error("the ILP solve returned an invalid solution: {0}")] InvalidSolution(String), + /// Evaluating the extracted source witness failed. + #[error(transparent)] + Evaluation(#[from] crate::traits::EvaluationError), /// An exact integer in the model cannot be transported through the f64 backend API. #[error("the ILP backend cannot represent an exact model integer: {0}")] InexactTransport(#[from] crate::types::ExactI64ToF64Error), @@ -57,19 +49,24 @@ pub enum ILPSolveError { Reduction(#[from] crate::rules::ReductionError), } -fn classify_backend_error(error: ResolutionError, time_limit: Option) -> ILPSolveError { - match error { - ResolutionError::Infeasible => ILPSolveError::Infeasible, - ResolutionError::Unbounded => ILPSolveError::Unbounded, - ResolutionError::Other("NoSolutionFound") if time_limit.is_some() => ILPSolveError::Timeout, - other => ILPSolveError::BackendFailure(other.to_string()), +// Keep adapter details out of the public error vocabulary. +impl From for ILPSolveError { + fn from(error: IlpBackendError) -> Self { + match error { + IlpBackendError::Infeasible => Self::Infeasible, + IlpBackendError::Unbounded => Self::Unbounded, + IlpBackendError::Timeout => Self::Timeout, + IlpBackendError::BackendFailure(message) => Self::BackendFailure(message), + IlpBackendError::InvalidSolution(message) => Self::InvalidSolution(message), + IlpBackendError::InexactTransport(error) => Self::InexactTransport(error), + } } } /// An ILP solver using the HiGHS backend. /// -/// Registered reductions map a source problem to an `ILP` terminal, -/// which this solver sends to HiGHS before extracting the source solution. +/// Registered reductions map a source problem to its native `ILP` terminal. +/// A shared adapter sends that ILP to HiGHS before source solution extraction. /// Optimality and infeasibility are assessed within HiGHS numerical tolerances. /// Zero MIP gaps do not make floating-point solving mathematically exact. /// @@ -127,172 +124,7 @@ impl ILPSolver { .lookup(&key) .ilp .ok_or_else(|| ILPSolveError::MissingPipeline(key.label()))?; - pipeline.solve_typed(problem, self) - } - - fn solve_backend(&self, problem: &ILP) -> Result, ILPSolveError> - where - V: VariableDomain, - { - self.solve_with_objective(problem, problem.objective()) - } - - fn solve_with_objective( - &self, - problem: &ILP, - objective_terms: &[(usize, f64)], - ) -> Result, ILPSolveError> - where - V: VariableDomain, - { - let n = problem.num_vars(); - if n == 0 { - return if problem - .is_feasible(&[]) - .map_err(|error| ILPSolveError::InvalidSolution(error.to_string()))? - { - Ok(vec![]) - } else { - Err(ILPSolveError::Infeasible) - }; - } - - let mut vars_builder = ProblemVariables::new(); - let vars: Vec = problem - .variables() - .iter() - .map(|variable_bounds| { - let mut definition = variable().integer(); - if let Some(lower) = variable_bounds.lower_bound() { - definition = definition.min(i64_to_exact_f64(lower)?); - } - if let Some(upper) = variable_bounds.upper_bound() { - definition = definition.max(i64_to_exact_f64(upper)?); - } - Ok(vars_builder.add(definition)) - }) - .collect::>()?; - - // Build objective expression - let objective: good_lp::Expression = objective_terms - .iter() - .map(|&(var_idx, coefficient)| coefficient * vars[var_idx]) - .sum(); - - // Build the model with objective - let unsolved = match problem.sense() { - ObjectiveSense::Maximize => vars_builder.maximise(&objective), - ObjectiveSense::Minimize => vars_builder.minimise(&objective), - }; - - // Create the solver model - let mut model = { - let mut model = unsolved - .using(highs) - .set_option("random_seed", 0i32) - .set_option("mip_rel_gap", 0.0) - .set_option("mip_abs_gap", 0.0) - .set_parallel(HighsParallelType::Off) - .set_threads(1); - if let Some(seconds) = self.time_limit { - model = model.set_time_limit(seconds); - } - model - }; - - // Add constraints - for constraint in problem.constraints() { - // Build left-hand side expression - let lhs: good_lp::Expression = constraint - .terms() - .iter() - .map(|&(var_idx, coefficient)| coefficient * vars[var_idx]) - .sum(); - - let rhs = constraint.rhs(); - - // Create the constraint based on comparison type - let good_lp_constraint = match constraint.comparison() { - Comparison::Le => lhs.leq(rhs), - Comparison::Ge => lhs.geq(rhs), - Comparison::Eq => lhs.eq(rhs), - }; - - model = model.with(good_lp_constraint); - } - - // Solve - let solution = match model.solve() { - Ok(solution) => solution, - Err(ResolutionError::Infeasible) - if !objective_terms.is_empty() - && problem.variables().iter().any(|variable| { - variable.lower_bound().is_none() || variable.upper_bound().is_none() - }) => - { - // A zero objective cannot be unbounded, so feasibility distinguishes the two states. - self.solve_with_objective(problem, &[])?; - return Err(ILPSolveError::Unbounded); - } - Err(error) => return Err(classify_backend_error(error, self.time_limit)), - }; - - match solution.status() { - SolutionStatus::Optimal => {} - SolutionStatus::TimeLimit => return Err(ILPSolveError::Timeout), - SolutionStatus::GapLimit => { - return Err(ILPSolveError::BackendFailure( - "the backend stopped at its gap limit before proving optimality".to_string(), - )); - } - } - - let result: Vec = vars - .iter() - .enumerate() - .map(|(index, v)| { - let value = solution.value(*v); - if !value.is_finite() { - return Err(ILPSolveError::InvalidSolution(format!( - "variable {index} is non-finite" - ))); - } - let rounded = value.round(); - if (value - rounded).abs() > 1e-6 { - return Err(ILPSolveError::InvalidSolution(format!( - "variable {index} has non-integral value {value}" - ))); - } - if rounded.abs() > MAX_EXACT_F64_INTEGER as f64 { - return Err(ILPSolveError::InvalidSolution(format!( - "variable {index} value {rounded} exceeds exact f64 integer transport" - ))); - } - Ok(rounded as i64) - }) - .collect::>()?; - - if !problem - .is_feasible(&result) - .map_err(|error| ILPSolveError::InvalidSolution(error.to_string()))? - { - return Err(ILPSolveError::InvalidSolution( - "the rounded assignment violates the ILP".into(), - )); - } - - Ok(result) - } - - /// Solve a type-erased supported ILP variant directly. - pub(crate) fn solve_dyn(&self, any: &dyn std::any::Any) -> Result, ILPSolveError> { - if let Some(ilp) = any.downcast_ref::>() { - return self.solve_backend(ilp); - } - if let Some(ilp) = any.downcast_ref::>() { - return self.solve_backend(ilp); - } - Err(ILPSolveError::UnsupportedProblemType) + pipeline.solve_typed(problem, &HighsAdapter::new(self.time_limit)) } } diff --git a/src/solvers/mod.rs b/src/solvers/mod.rs index a0e39a160..47351bb48 100644 --- a/src/solvers/mod.rs +++ b/src/solvers/mod.rs @@ -11,18 +11,22 @@ pub mod ilp; #[doc(hidden)] pub use brute_force::BruteForceRegistration; -pub use brute_force::{BruteForce, BruteForceProblem}; +pub use brute_force::{cartesian_dimensions, BruteForce, BruteForceProblem, SolutionAggregate}; pub use registry::{ brute_force_dimensions, solver_capabilities, CustomizedSolverCapability, ExactProblemKey, IlpSolverCapability, RegistryBuildError, SolverCapabilities, }; -pub use resolver::{solve, SolveOutcome, SolveResult, SolverExecution, SolverRequest}; +pub use resolver::{ + complete_reduction, solve, SolveOutcome, SolveResult, SolverExecution, SolverRequest, +}; pub use ilp::{ILPSolveError, ILPSolver}; /// Failure while solving a valid problem instance. #[derive(Debug, thiserror::Error)] pub enum SolveError { + #[error(transparent)] + Extraction(#[from] crate::rules::ExtractionError), #[error("configuration evaluation failed: {0}")] Evaluation(#[from] crate::traits::EvaluationError), #[error("aggregate combination failed: {0}")] @@ -31,8 +35,8 @@ pub enum SolveError { MissingRegistration(String), #[error("invalid reference-solver registration: {0}")] RegistrationTypeMismatch(String), - #[error("brute-force search space cardinality exceeds usize for dimensions {0:?}")] - SearchSpaceOverflow(Vec), + #[error("cannot allocate solver storage: {0}")] + Allocation(#[from] std::collections::TryReserveError), #[error("integer overflow while {0}")] IntegerOverflow(String), #[error("inexact integer-to-float conversion: {0}")] @@ -52,3 +56,9 @@ pub enum SolveError { source: ILPSolveError, }, } + +impl From for SolveError { + fn from(error: std::num::TryFromIntError) -> Self { + Self::IntegerOverflow(error.to_string()) + } +} diff --git a/src/solvers/pipelines.rs b/src/solvers/pipelines.rs index 15e5aa525..d9674cd78 100644 --- a/src/solvers/pipelines.rs +++ b/src/solvers/pipelines.rs @@ -23,12 +23,10 @@ macro_rules! register_ilp_pipeline { register_ilp_pipeline! { ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { @@ -42,118 +40,99 @@ register_ilp_pipeline! { register_ilp_pipeline! { ("AcyclicPartition", [("weight", "i64")]), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("BMF", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("BalancedCompleteBipartiteSubgraph", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("BicliqueCover", []), ("BMF", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("BiconnectivityAugmentation", [("graph", "SimpleGraph"), ("weight", "i64")]), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("BinPacking", [("weight", "i64")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("BottleneckTravelingSalesman", []), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("BoundedComponentSpanningForest", [("graph", "SimpleGraph"), ("weight", "i64")]), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("CapacityAssignment", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("CircuitSAT", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("ClosestString", []), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("ClosestSubstring", []), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("Clustering", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("ConsecutiveBlockMinimization", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("ConsecutiveOnesMatrixAugmentation", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("ConsecutiveOnesSubmatrix", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("ConsistencyOfDatabaseFrequencyTables", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("DecisionMinimumDominatingSet", [("graph", "SimpleGraph"), ("weight", "One")]), ("MinimumSumMulticenter", [("graph", "SimpleGraph"), ("weight", "i64")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("DecisionMinimumDominatingSet", [("graph", "SimpleGraph"), ("weight", "i64")]), ("MinimumDominatingSet", [("graph", "SimpleGraph"), ("weight", "i64")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { @@ -161,50 +140,42 @@ register_ilp_pipeline! { ("MinimumVertexCover", [("graph", "SimpleGraph"), ("weight", "i64")]), ("MinimumSetCovering", [("weight", "i64")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("DecisionOptimalLinearArrangement", [("graph", "SimpleGraph")]), ("OptimalLinearArrangement", [("graph", "SimpleGraph")]), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("DirectedHamiltonianPath", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("DirectedTwoCommodityIntegralFlow", []), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("DisjointConnectingPaths", [("graph", "SimpleGraph")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("EnsembleComputation", []), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("EulerianPath", []), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("ExactCoverBy3Sets", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { @@ -215,44 +186,37 @@ register_ilp_pipeline! { register_ilp_pipeline! { ("Factoring", []), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("FeasibleRegisterAssignment", []), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("FlowShopScheduling", []), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("GraphPartitioning", [("graph", "SimpleGraph")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("HamiltonianCircuit", [("graph", "SimpleGraph")]), ("LongestCircuit", [("graph", "SimpleGraph"), ("weight", "i64")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("HamiltonianPath", [("graph", "SimpleGraph")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("HighlyConnectedDeletion", [("graph", "SimpleGraph")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } // This exact variant also has a customized backend. Default dispatch selects the @@ -261,50 +225,42 @@ register_ilp_pipeline! { ("RootedTreeArrangement", [("graph", "SimpleGraph")]), ("RootedTreeStorageAssignment", []), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("IntegralFlowBundles", []), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("IntegralFlowHomologousArcs", []), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("IntegralFlowWithMultipliers", []), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("IsomorphicSpanningTree", [("graph", "SimpleGraph")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("KClique", [("graph", "SimpleGraph")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("KColoring", [("graph", "SimpleGraph"), ("k", "KN")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("KColoring", [("graph", "SimpleGraph"), ("k", "K3")]), ("Clustering", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { @@ -312,49 +268,41 @@ register_ilp_pipeline! { ("Satisfiability", []), ("NAESatisfiability", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("Knapsack", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("LengthBoundedDisjointPaths", [("graph", "SimpleGraph")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("LongestCircuit", [("graph", "SimpleGraph"), ("weight", "i64")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("LongestCommonSubsequence", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("LongestPath", [("graph", "SimpleGraph"), ("weight", "i64")]), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MaximalIS", [("graph", "SimpleGraph"), ("weight", "i64")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("Maximum2Satisfiability", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { @@ -363,43 +311,36 @@ register_ilp_pipeline! { ("MaximumIndependentSet", [("graph", "SimpleGraph"), ("weight", "i64")]), ("MaximumSetPacking", [("weight", "i64")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MaximumClique", [("graph", "SimpleGraph"), ("weight", "i64")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MaximumCoKPlex", [("graph", "SimpleGraph"), ("k", "KN"), ("weight", "One")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MaximumCoKPlex", [("graph", "SimpleGraph"), ("k", "KN"), ("weight", "i64")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MaximumCommonEdgeSubgraph", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MaximumContactMapOverlap", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MaximumDomaticNumber", [("graph", "SimpleGraph")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { @@ -410,7 +351,6 @@ register_ilp_pipeline! { register_ilp_pipeline! { ("MaximumEdgeWeightedKClique", [("weight", "i64")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { @@ -418,7 +358,6 @@ register_ilp_pipeline! { ("MaximumIndependentSet", [("graph", "SimpleGraph"), ("weight", "i64")]), ("MaximumSetPacking", [("weight", "i64")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { @@ -428,14 +367,12 @@ register_ilp_pipeline! { ("MaximumIndependentSet", [("graph", "SimpleGraph"), ("weight", "i64")]), ("MaximumClique", [("graph", "SimpleGraph"), ("weight", "i64")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MaximumIndependentSet", [("graph", "SimpleGraph"), ("weight", "i64")]), ("MaximumSetPacking", [("weight", "i64")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { @@ -444,7 +381,6 @@ register_ilp_pipeline! { ("MaximumIndependentSet", [("graph", "SimpleGraph"), ("weight", "i64")]), ("MaximumClique", [("graph", "SimpleGraph"), ("weight", "i64")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { @@ -453,7 +389,6 @@ register_ilp_pipeline! { ("MaximumIndependentSet", [("graph", "SimpleGraph"), ("weight", "i64")]), ("MaximumClique", [("graph", "SimpleGraph"), ("weight", "i64")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { @@ -462,7 +397,6 @@ register_ilp_pipeline! { ("MaximumIndependentSet", [("graph", "SimpleGraph"), ("weight", "i64")]), ("MaximumClique", [("graph", "SimpleGraph"), ("weight", "i64")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { @@ -470,32 +404,27 @@ register_ilp_pipeline! { ("MaximumIndependentSet", [("graph", "SimpleGraph"), ("weight", "i64")]), ("MaximumClique", [("graph", "SimpleGraph"), ("weight", "i64")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MaximumLeafSpanningTree", [("graph", "SimpleGraph")]), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MaximumLikelihoodRanking", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MaximumMatching", [("graph", "SimpleGraph"), ("weight", "i64")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MaximumSetPacking", [("weight", "One")]), ("MaximumSetPacking", [("weight", "i64")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { @@ -507,31 +436,26 @@ register_ilp_pipeline! { register_ilp_pipeline! { ("MaximumSetPacking", [("weight", "i64")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MinMaxMulticenter", [("graph", "SimpleGraph"), ("weight", "i64")]), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MinimumCapacitatedSpanningTree", [("graph", "SimpleGraph"), ("weight", "i64")]), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MinimumCoveringByCliques", [("graph", "SimpleGraph")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MinimumCutIntoBoundedSets", [("graph", "SimpleGraph"), ("weight", "i64")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { @@ -543,245 +467,205 @@ register_ilp_pipeline! { register_ilp_pipeline! { ("MinimumDominatingSet", [("graph", "SimpleGraph"), ("weight", "i64")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MinimumEdgeCostFlow", []), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MinimumExternalMacroDataCompression", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MinimumFaultDetectionTestSet", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MinimumFeedbackArcSet", [("weight", "i64")]), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MinimumFeedbackVertexSet", [("weight", "i64")]), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MinimumGraphBandwidth", [("graph", "SimpleGraph")]), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MinimumHittingSet", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MinimumInternalMacroDataCompression", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MinimumMatrixCover", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MinimumMaximalMatching", [("graph", "SimpleGraph")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MinimumMetricDimension", [("graph", "SimpleGraph")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MinimumMultiwayCut", [("graph", "SimpleGraph"), ("weight", "i64")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MinimumSetCovering", [("weight", "i64")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MinimumSumMulticenter", [("graph", "SimpleGraph"), ("weight", "i64")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MinimumTardinessSequencing", [("weight", "One")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MinimumTardinessSequencing", [("weight", "i64")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MinimumVertexCover", [("graph", "SimpleGraph"), ("weight", "One")]), ("MinimumHittingSet", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MinimumVertexCover", [("graph", "SimpleGraph"), ("weight", "i64")]), ("MinimumSetCovering", [("weight", "i64")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MinimumWeightDecoding", []), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MixedChinesePostman", [("weight", "i64")]), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MonochromaticTriangle", [("graph", "SimpleGraph")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MultipleCopyFileAllocation", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MultipleChoiceBranching", [("weight", "i64")]), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("MultiprocessorScheduling", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("NAESatisfiability", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("Numerical3DimensionalMatching", []), ("NumericalMatchingWithTargetSums", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("NumericalMatchingWithTargetSums", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("OpenShopScheduling", []), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("OptimalLinearArrangement", [("graph", "SimpleGraph")]), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("OptimumCommunicationSpanningTree", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("PaintShop", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("PartiallyOrderedKnapsack", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("Partition", []), ("MultiprocessorScheduling", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("PartitionIntoCliques", [("graph", "SimpleGraph")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("PartitionIntoPathsOfLength2", [("graph", "SimpleGraph")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("PartitionIntoTriangles", [("graph", "SimpleGraph")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("PathConstrainedNetworkFlow", []), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("PrecedenceConstrainedScheduling", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("PreemptiveScheduling", []), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { @@ -792,122 +676,102 @@ register_ilp_pipeline! { register_ilp_pipeline! { ("QUBO", [("weight", "i64")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("QuadraticAssignment", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("RectilinearPictureCompression", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("RegisterSufficiency", []), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("ResourceConstrainedScheduling", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("RootedTreeStorageAssignment", []), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("RuralPostman", [("graph", "SimpleGraph"), ("weight", "i64")]), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("Satisfiability", []), ("NAESatisfiability", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("SchedulingToMinimizeWeightedCompletionTime", []), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("SchedulingWithIndividualDeadlines", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("SequencingToMinimizeMaximumCumulativeCost", []), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("SequencingToMinimizeTardyTaskWeight", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("SequencingToMinimizeWeightedTardiness", []), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("SequencingWithDeadlinesAndSetUpTimes", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("SequencingWithReleaseTimesAndDeadlines", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("SequencingWithinIntervals", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("SetSplitting", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("ShortestCommonSupersequence", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("ShortestWeightConstrainedPath", [("graph", "SimpleGraph"), ("weight", "i64")]), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("SparseMatrixCompression", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { @@ -920,66 +784,55 @@ register_ilp_pipeline! { ("SpinGlass", [("graph", "SimpleGraph"), ("weight", "i64")]), ("QUBO", [("weight", "i64")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("StackerCrane", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("StringToStringCorrection", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("StrongConnectivityAugmentation", [("weight", "i64")]), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("SubgraphIsomorphism", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("SumOfSquaresPartition", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("ThreeDimensionalMatching", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("ThreePartition", []), ("ResourceConstrainedScheduling", []), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("TravelingSalesman", [("graph", "SimpleGraph"), ("weight", "i64")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), - ("ILP", [("variable", "bool"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("UndirectedFlowLowerBounds", []), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } register_ilp_pipeline! { ("UndirectedTwoCommodityIntegralFlow", []), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), - ("ILP", [("variable", "i64"), ("coefficient", "f64")]), } diff --git a/src/solvers/registry.rs b/src/solvers/registry.rs index c044e3d36..2619ba8c5 100644 --- a/src/solvers/registry.rs +++ b/src/solvers/registry.rs @@ -1,13 +1,31 @@ //! Deterministic solver capabilities for exact problem variants. +use super::ilp::adapter::HighsAdapter; +use crate::models::algebraic::ILP; use crate::registry::VariantEntry; -use crate::rules::registry::{reduction_entries, AggregateReduceFn, ReduceFn, ReductionEntry}; +use crate::rules::registry::{reduction_entries, ReduceFn, ReductionEntry}; use crate::rules::DynReductionResult; use serde::Serialize; use std::any::Any; use std::collections::{BTreeMap, BTreeSet}; use std::sync::OnceLock; +/// Type erasure is resolved at the registry boundary, never inside the adapter. +fn solve_ilp_terminal( + source: &dyn Any, + adapter: &HighsAdapter, +) -> Result, super::ILPSolveError> { + macro_rules! dispatch { + ($($v:ty, $c:ty);* $(;)?) => { $( + if let Some(ilp) = source.downcast_ref::>() { + return adapter.solve(ilp).map_err(Into::into); + } + )* }; + } + dispatch! { bool, i64; i64, i64; bool, f64; i64, f64; } + Err(super::ILPSolveError::UnsupportedProblemType) +} + /// Canonical identity of one concrete problem variant. #[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize)] pub struct ExactProblemKey { @@ -53,7 +71,10 @@ impl ExactProblemKey { self.variant.get("variable").map(String::as_str), Some("bool" | "i64") ) - && self.variant.get("coefficient").map(String::as_str) == Some("f64") + && matches!( + self.variant.get("coefficient").map(String::as_str), + Some("i64" | "f64") + ) } } @@ -103,7 +124,7 @@ inventory::collect!(CustomizedSolverRegistration); #[derive(Debug)] pub(crate) struct CompiledIlpPipeline { path: Vec, - reducers: Vec<(ReduceFn, Option)>, + reducers: Vec, } impl CompiledIlpPipeline { @@ -118,59 +139,29 @@ impl CompiledIlpPipeline { fn solve_with( &self, source: &dyn Any, - solver: &super::ILPSolver, + adapter: &HighsAdapter, finish: impl FnOnce( Box, Option<&dyn DynReductionResult>, ) -> Result, ) -> Result { if self.reducers.is_empty() { - return finish(Box::new(solver.solve_dyn(source)?), None); + return finish(Box::new(solve_ilp_terminal(source, adapter)?), None); } - let mut reductions: Vec> = Vec::new(); - for (reducer, _) in &self.reducers { - let input = reductions - .last() - .map(|step| step.target_problem_any()) - .unwrap_or(source); - reductions.push(reducer(input)?); - } - - let target = reductions - .last() - .expect("non-empty fixed pipeline must produce a target") - .target_problem_any(); - let solution = solver.solve_dyn(target)?; - let mut source_solution: Box = Box::new(solution); - for (index, step) in reductions.iter().enumerate().rev() { - if let Some(reduce) = self.reducers[index].1 { - let input = if index == 0 { - source - } else { - reductions[index - 1].target_problem_any() - }; - let aggregate = reduce(input)?; - // A numerical target optimum can establish YES through a source witness, - // but a missed threshold alone cannot establish NO. - let value = aggregate.extract_value_from_solution_dyn(source_solution.as_ref())?; - if value.downcast_ref::() == Some(&crate::types::Or(false)) { - return Err(super::ILPSolveError::UnresolvedDecision( - self.path[index].label(), - )); - } - } - source_solution = step.extract_solution_dyn(source_solution.as_ref())?; - } - finish(source_solution, Some(reductions[0].as_ref())) + let chain = crate::rules::ReductionChain::execute(source, &self.reducers)?; + let target_solution = solve_ilp_terminal(chain.target_problem_any(), adapter)?; + let source_solution = super::resolver::complete_chain(&chain, &target_solution)? + .ok_or(super::ILPSolveError::Infeasible)?; + finish(source_solution, Some(chain.steps[0].witness.as_ref())) } pub(crate) fn solve( &self, source: &dyn Any, - solver: &super::ILPSolver, + adapter: &HighsAdapter, ) -> Result { - self.solve_with(source, solver, |solution, first_reduction| { + self.solve_with(source, adapter, |solution, first_reduction| { if let Some(reduction) = first_reduction { return reduction .source_solution_json(solution.as_ref()) @@ -185,23 +176,20 @@ impl CompiledIlpPipeline { }) } - pub(crate) fn solve_typed( + pub(crate) fn solve_typed

( &self, - source: &dyn Any, - solver: &super::ILPSolver, - ) -> Result { - self.solve_with(source, solver, |solution, _| { - solution - .downcast::() - .map(|solution| *solution) - .map_err(|_| { - super::ILPSolveError::PipelineTypeMismatch( - self.path - .first() - .expect("compiled pipeline has a source") - .label(), - ) - }) + source: &P, + adapter: &HighsAdapter, + ) -> Result + where + P: crate::traits::Problem + 'static, + P::Solution: 'static, + { + self.solve_with(source, adapter, |solution, _| { + let solution = solution + .downcast::() + .map_err(|_| super::ILPSolveError::PipelineTypeMismatch(self.path[0].label()))?; + Ok(*solution) }) } } @@ -287,7 +275,7 @@ pub enum RegistryBuildError { MissingSolverCapability(String), #[error("ILP pipeline must contain at least one node")] EmptyPipeline, - #[error("ILP pipeline for {0} does not end at an f64-coefficient ILP")] + #[error("ILP pipeline for {0} does not end at a supported native ILP")] UnsupportedTarget(String), #[error("ILP pipeline for {0} continues after reaching a supported ILP node")] ContinuesAfterIlp(String), @@ -411,12 +399,11 @@ fn build_registry( matches: matches.len(), }); } - reducers.push(( + reducers.push( matches[0] .reduce_fn .expect("indexed only entries with reduce_fn"), - matches[0].reduce_aggregate_fn, - )); + ); } if registry @@ -485,10 +472,12 @@ pub(crate) fn brute_force_registration( #[doc(hidden)] pub fn brute_force_dimensions( problem: &crate::registry::LoadedDynProblem, -) -> Result>, &'static RegistryBuildError> { +) -> Result>, crate::solvers::SolveError> { let key = ExactProblemKey::new(problem.problem_name(), problem.variant_map()); - Ok(brute_force_registration(&key)? - .map(|registration| (registration.dimensions_fn)(problem.as_any()))) + brute_force_registration(&key) + .map_err(crate::solvers::SolveError::InvalidRegistry)? + .map(|registration| (registration.dimensions_fn)(problem.as_any())) + .transpose() } #[cfg(test)] diff --git a/src/solvers/resolver.rs b/src/solvers/resolver.rs index 6444d1254..eadfd5e75 100644 --- a/src/solvers/resolver.rs +++ b/src/solvers/resolver.rs @@ -45,6 +45,51 @@ pub enum SolveOutcome { Infeasible, } +/// Interpret aggregate outcomes before mapping each accepted target optimum. +pub(crate) fn complete_chain( + chain: &crate::rules::ReductionChain, + target_solution: &dyn std::any::Any, +) -> crate::rules::ExtractionResult>> { + let mut solution: Option> = None; + for step in chain.steps.iter().rev() { + let input = solution.as_deref().unwrap_or(target_solution); + if let Some(interpret) = &step.interpret_optimum { + if !interpret(input)? { + return Ok(None); + } + } + solution = Some(step.witness.extract_solution_dyn(input)?); + } + Ok(Some(solution.expect("reduction chain has no steps"))) +} + +/// Map a completed target solve through an executed reduction chain. +/// +/// The target outcome must come from a completed solve, not merely a feasible +/// assignment: only an accepted optimum can establish a source decision's NO. +pub fn complete_reduction( + source: &dyn crate::registry::DynProblem, + chain: &crate::rules::ReductionChain, + target: &SolveOutcome, +) -> Result { + let SolveOutcome::Optimal { solution, .. } = target else { + return Ok(SolveOutcome::Infeasible); + }; + let last = chain.steps.last().expect("reduction chain has no steps"); + let target_solution = last.witness.target_solution_from_json(solution.clone())?; + let Some(solution) = complete_chain(chain, target_solution.as_ref())? else { + return Ok(SolveOutcome::Infeasible); + }; + let solution = chain.steps[0] + .witness + .source_solution_json(solution.as_ref())?; + let (evaluation, _) = source.evaluate_dyn(&solution)?; + Ok(SolveOutcome::Optimal { + solution, + evaluation, + }) +} + fn problem_key(problem: &LoadedDynProblem) -> ExactProblemKey { ExactProblemKey::new(problem.problem_name(), problem.variant_map()) } @@ -54,10 +99,13 @@ fn solve_customized( registration: &'static CustomizedSolverRegistration, ) -> Result { let outcome = match (registration.solve_fn)(problem.as_any())? { - Some(solution) => SolveOutcome::Optimal { - evaluation: problem.evaluate_dyn(&solution)?, - solution, - }, + Some(solution) => { + let (evaluation, _) = problem.evaluate_dyn(&solution)?; + SolveOutcome::Optimal { + evaluation, + solution, + } + } None => SolveOutcome::Infeasible, }; Ok(SolveResult { @@ -72,11 +120,17 @@ fn solve_ilp( problem: &LoadedDynProblem, pipeline: &CompiledIlpPipeline, ) -> Result { - let outcome = match pipeline.solve(problem.as_any(), &super::ILPSolver::new()) { - Ok(solution) => SolveOutcome::Optimal { - evaluation: problem.evaluate_dyn(&solution)?, - solution, - }, + let outcome = match pipeline.solve( + problem.as_any(), + &super::ilp::adapter::HighsAdapter::new(None), + ) { + Ok(solution) => { + let (evaluation, _) = problem.evaluate_dyn(&solution)?; + SolveOutcome::Optimal { + evaluation, + solution, + } + } Err(super::ILPSolveError::Infeasible) => SolveOutcome::Infeasible, Err(source) => { return Err(super::SolveError::IlpSolve { diff --git a/src/truth_table.rs b/src/truth_table.rs index 479b02983..a829ba264 100644 --- a/src/truth_table.rs +++ b/src/truth_table.rs @@ -3,6 +3,7 @@ //! This module provides a `TruthTable` type for representing boolean functions //! and their truth tables, useful for constructing logic gadgets in reductions. +use crate::registry::ConstructionError; use bitvec::prelude::*; use serde::{Deserialize, Serialize}; @@ -45,10 +46,8 @@ impl<'de> Deserialize<'de> for TruthTable { D: serde::Deserializer<'de>, { let serde_repr = TruthTableSerde::deserialize(deserializer)?; - Ok(TruthTable { - num_inputs: serde_repr.num_inputs, - outputs: serde_repr.outputs.into_iter().collect(), - }) + Self::from_outputs(serde_repr.num_inputs, serde_repr.outputs) + .map_err(serde::de::Error::custom) } } @@ -57,42 +56,64 @@ impl TruthTable { /// /// The outputs vector must have exactly 2^num_inputs elements. /// Index i corresponds to the input where the j-th bit represents variable j. - pub fn from_outputs(num_inputs: usize, outputs: Vec) -> Self { - let expected_len = 1 << num_inputs; - assert_eq!( - outputs.len(), - expected_len, - "outputs length must be 2^num_inputs = {}, got {}", - expected_len, - outputs.len() - ); - - let bits: BitVec = outputs.into_iter().collect(); - Self { + pub fn from_outputs(num_inputs: usize, outputs: Vec) -> Result { + let expected_len = Self::row_count(num_inputs)?; + if outputs.len() != expected_len { + return Err(ConstructionError::InvalidInput(format!( + "outputs length must be 2^num_inputs = {expected_len}, got {}", + outputs.len() + ))); + } + let mut bits = Self::allocate_outputs(expected_len)?; + for (mut bit, output) in bits.iter_mut().zip(outputs) { + *bit = output; + } + Ok(Self { num_inputs, outputs: bits, - } + }) } - /// Create a truth table from a function. - /// - /// The function takes a slice of booleans (the input) and returns the output. - pub fn from_function(num_inputs: usize, f: F) -> Self + /// Create a truth table by evaluating a function for each input combination. + pub fn from_function(num_inputs: usize, f: F) -> Result where F: Fn(&[bool]) -> bool, { - let num_rows = 1 << num_inputs; - let mut outputs = BitVec::with_capacity(num_rows); - + let num_rows = Self::row_count(num_inputs)?; + let mut outputs = Self::allocate_outputs(num_rows)?; + let mut input = vec![false; num_inputs]; for i in 0..num_rows { - let input: Vec = (0..num_inputs).map(|j| (i >> j) & 1 == 1).collect(); - outputs.push(f(&input)); + for (j, bit) in input.iter_mut().enumerate() { + *bit = (i >> j) & 1 == 1; + } + outputs.set(i, f(&input)); } - - Self { + Ok(Self { num_inputs, outputs, - } + }) + } + + fn row_count(num_inputs: usize) -> Result { + u32::try_from(num_inputs) + .ok() + .and_then(|shift| 1usize.checked_shl(shift)) + .filter(|&rows| rows <= BitSlice::::MAX_BITS) + .ok_or_else(|| { + ConstructionError::IntegerOverflow("representing truth-table rows".into()) + }) + } + + fn allocate_outputs(num_rows: usize) -> Result { + let words = num_rows.div_ceil(usize::BITS as usize); + let mut storage = Vec::::new(); + storage.try_reserve_exact(words).map_err(|error| { + ConstructionError::Conversion(format!("allocating truth-table storage: {error}")) + })?; + storage.resize(words, 0); + let mut outputs = BitVec::from_vec(storage); + outputs.truncate(num_rows); + Ok(outputs) } /// Get the number of input variables. @@ -102,7 +123,7 @@ impl TruthTable { /// Get the number of rows (2^num_inputs). pub fn num_rows(&self) -> usize { - 1 << self.num_inputs + self.outputs.len() } /// Evaluate the truth table for a given input. @@ -183,39 +204,39 @@ impl TruthTable { } /// Create an AND gate truth table. - pub fn and(num_inputs: usize) -> Self { + pub fn and(num_inputs: usize) -> Result { Self::from_function(num_inputs, |input| input.iter().all(|&b| b)) } /// Create an OR gate truth table. - pub fn or(num_inputs: usize) -> Self { + pub fn or(num_inputs: usize) -> Result { Self::from_function(num_inputs, |input| input.iter().any(|&b| b)) } /// Create a NOT gate truth table (1 input). pub fn not() -> Self { - Self::from_outputs(1, vec![true, false]) + Self::from_outputs(1, vec![true, false]).expect("NOT has two rows") } /// Create an XOR gate truth table. - pub fn xor(num_inputs: usize) -> Self { + pub fn xor(num_inputs: usize) -> Result { Self::from_function(num_inputs, |input| { input.iter().filter(|&&b| b).count() % 2 == 1 }) } /// Create a NAND gate truth table. - pub fn nand(num_inputs: usize) -> Self { + pub fn nand(num_inputs: usize) -> Result { Self::from_function(num_inputs, |input| !input.iter().all(|&b| b)) } /// Create a NOR gate truth table. - pub fn nor(num_inputs: usize) -> Self { + pub fn nor(num_inputs: usize) -> Result { Self::from_function(num_inputs, |input| !input.iter().any(|&b| b)) } /// Create an XNOR gate truth table. - pub fn xnor(num_inputs: usize) -> Self { + pub fn xnor(num_inputs: usize) -> Result { Self::from_function(num_inputs, |input| { input.iter().filter(|&&b| b).count().is_multiple_of(2) }) @@ -225,7 +246,7 @@ impl TruthTable { /// Input 0 is 'a', input 1 is 'b'. pub fn implies() -> Self { // Index 0: [F,F] -> T, Index 1: [T,F] -> F, Index 2: [F,T] -> T, Index 3: [T,T] -> T - Self::from_outputs(2, vec![true, false, true, true]) + Self::from_outputs(2, vec![true, false, true, true]).expect("implication has four rows") } /// Combine two truth tables using AND. diff --git a/src/types.rs b/src/types.rs index 03146feba..9de4d611d 100644 --- a/src/types.rs +++ b/src/types.rs @@ -4,13 +4,13 @@ use serde::de::{self, DeserializeOwned, Visitor}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; use std::fmt; -/// Largest integer magnitude represented exactly by an IEEE 754 `f64`. +/// Maximum integer magnitude accepted by the exact `i64` to `f64` conversion. pub const MAX_EXACT_F64_INTEGER: i64 = (1_i64 << 53) - 1; -/// An `i64` cannot cross an exact-integer `f64` boundary without precision loss. +/// An `i64` is outside the supported exact-integer `f64` conversion range. #[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)] #[error( - "integer {value} is outside the exactly representable f64 range [{min}, {max}]", + "integer {value} is outside the supported exact-integer f64 conversion range [{min}, {max}]", min = -MAX_EXACT_F64_INTEGER, max = MAX_EXACT_F64_INTEGER )] @@ -29,7 +29,8 @@ pub enum NumericArithmeticError { NonFiniteResult, } -/// Convert an `i64` to `f64` only when the integer value remains exact. +/// Convert an `i64` to `f64` within the supported range ±(2^53 − 1). +/// Values outside this range are rejected even if individually representable. pub fn i64_to_exact_f64(value: i64) -> Result { if (-MAX_EXACT_F64_INTEGER..=MAX_EXACT_F64_INTEGER).contains(&value) { Ok(value as f64) @@ -38,7 +39,8 @@ pub fn i64_to_exact_f64(value: i64) -> Result { } } -/// Bound for objective value types (i64, f64, etc.) +/// Bound for objective value types (i64, f64, etc.). +/// Integers reject overflow; floats allow rounding and reject non-finite results. pub trait NumericSize: Clone + Default @@ -49,9 +51,9 @@ pub trait NumericSize: + std::ops::AddAssign + 'static { - /// Add two values when the exact result remains representable and finite. + /// Checked addition. fn checked_add_value(self, other: Self) -> Result; - /// Multiply two values when the exact result remains representable and finite. + /// Checked multiplication. fn checked_mul_value(self, other: Self) -> Result; } @@ -304,12 +306,6 @@ pub trait Aggregate: Clone + fmt::Debug + Serialize + DeserializeOwned { } } -/// Aggregate value whose optimum identifies contributing solutions. -pub trait SolutionAggregate: Aggregate { - /// Whether a solution-level value contributes to the final aggregate value. - fn contributes_to_solution(value: &Self, total: &Self) -> bool; -} - /// Maximum aggregate over feasible values. #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] pub struct Max(pub Option); @@ -338,14 +334,6 @@ impl Aggregat } } -impl SolutionAggregate - for Max -{ - fn contributes_to_solution(value: &Self, total: &Self) -> bool { - matches!((value, total), (Max(Some(value)), Max(Some(best))) if value == best) - } -} - impl fmt::Display for Max { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match &self.0 { @@ -397,14 +385,6 @@ impl Aggregat } } -impl SolutionAggregate - for Min -{ - fn contributes_to_solution(value: &Self, total: &Self) -> bool { - matches!((value, total), (Min(Some(value)), Min(Some(best))) if value == best) - } -} - impl fmt::Display for Min { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match &self.0 { @@ -508,12 +488,6 @@ impl Aggregate for Or { } } -impl SolutionAggregate for Or { - fn contributes_to_solution(value: &Self, total: &Self) -> bool { - value.0 && total.0 - } -} - impl fmt::Display for Or { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "Or({})", self.0) @@ -648,17 +622,6 @@ impl Aggregat } } -impl SolutionAggregate - for Extremum -{ - fn contributes_to_solution(candidate: &Self, total: &Self) -> bool { - matches!( - (candidate.value.as_ref(), total.value.as_ref()), - (Some(value), Some(best)) if candidate.sense == total.sense && value == best - ) - } -} - impl fmt::Display for Extremum { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match (&self.sense, &self.value) { diff --git a/src/unit_tests/example_db.rs b/src/unit_tests/example_db.rs index 73d85007f..37f0a3f81 100644 --- a/src/unit_tests/example_db.rs +++ b/src/unit_tests/example_db.rs @@ -712,29 +712,15 @@ fn rule_specs_solution_pairs_are_consistent() { .unwrap_or_else(|error| { panic!("Rule {label}: source configuration evaluation failed: {error}") }); - assert_ne!( - source_eval, "Max(None)", - "Rule {label}: source_config evaluates to Max(None)" + assert!( + source_eval.1, + "Rule {label}: infeasible source configuration: {}", + source_eval.0 ); - assert_ne!( - source_eval, "Min(None)", - "Rule {label}: source_config evaluates to Min(None)" - ); - assert_ne!( - source_eval, "Or(false)", - "Rule {label}: source_config evaluates to Or(false)" - ); - assert_ne!( - target_eval, "Max(None)", - "Rule {label}: target_config evaluates to Max(None)" - ); - assert_ne!( - target_eval, "Min(None)", - "Rule {label}: target_config evaluates to Min(None)" - ); - assert_ne!( - target_eval, "Or(false)", - "Rule {label}: target_config evaluates to Or(false)" + assert!( + target_eval.1, + "Rule {label}: infeasible target configuration: {}", + target_eval.0 ); // Round-trip: extract_solution(target_config) must produce a valid // source config with the same evaluation value (witness paths only) diff --git a/src/unit_tests/graph_models.rs b/src/unit_tests/graph_models.rs index 675a9c487..97ef035a1 100644 --- a/src/unit_tests/graph_models.rs +++ b/src/unit_tests/graph_models.rs @@ -29,7 +29,7 @@ mod maximum_independent_set { ); assert_eq!(problem.graph().num_vertices(), 4); assert_eq!(problem.graph().num_edges(), 3); - assert_eq!(problem.num_variables(), 4); + assert_eq!(problem.num_variables().unwrap(), 4); } #[test] @@ -268,7 +268,7 @@ mod minimum_vertex_cover { ); assert_eq!(problem.graph().num_vertices(), 4); assert_eq!(problem.graph().num_edges(), 3); - assert_eq!(problem.num_variables(), 4); + assert_eq!(problem.num_variables().unwrap(), 4); } #[test] @@ -502,7 +502,10 @@ mod integral_flow_homologous_arcs { ); assert_eq!(problem.num_vertices(), 6); assert_eq!(problem.num_arcs(), 8); - assert_eq!(problem.dimensions(), vec![2; 8]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 8] + ); } } @@ -519,7 +522,7 @@ mod kcoloring { assert_eq!(problem.graph().num_vertices(), 4); assert_eq!(problem.graph().num_edges(), 3); assert_eq!(problem.num_colors(), 3); - assert_eq!(problem.num_variables(), 4); + assert_eq!(problem.num_variables().unwrap(), 4); } #[test] diff --git a/src/unit_tests/models/algebraic/algebraic_equations_over_gf2.rs b/src/unit_tests/models/algebraic/algebraic_equations_over_gf2.rs index 87df06fc5..804ad6c59 100644 --- a/src/unit_tests/models/algebraic/algebraic_equations_over_gf2.rs +++ b/src/unit_tests/models/algebraic/algebraic_equations_over_gf2.rs @@ -1,6 +1,5 @@ use crate::models::algebraic::AlgebraicEquationsOverGF2; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::traits::Problem; use crate::types::Or; @@ -39,7 +38,10 @@ fn test_algebraic_equations_over_gf2_creation_and_accessors() { assert_eq!(p.num_variables(), 3); assert_eq!(p.num_equations(), 3); assert_eq!(p.equations().len(), 3); - assert_eq!(p.dimensions(), vec![2, 2, 2]); + assert_eq!( + crate::solvers::cartesian_dimensions(&p).unwrap(), + vec![2, 2, 2] + ); assert_eq!(p.num_variables(), 3); assert_eq!( ::NAME, @@ -71,7 +73,10 @@ fn test_algebraic_equations_over_gf2_evaluate_satisfiable() { #[test] fn test_algebraic_equations_over_gf2_evaluate_unsatisfiable() { let p = unsatisfiable_problem(); - assert_eq!(p.dimensions(), vec![2, 2]); + assert_eq!( + crate::solvers::cartesian_dimensions(&p).unwrap(), + vec![2, 2] + ); // All 4 assignments should fail assert_eq!(p.evaluate(&vec![false, false]).unwrap(), Or(false)); // eq0: 0+0=0 ✓, eq1: 0+0+1=1 ✗ assert_eq!(p.evaluate(&vec![false, true]).unwrap(), Or(false)); // eq0: 0+1=1 ✗ diff --git a/src/unit_tests/models/algebraic/bmf.rs b/src/unit_tests/models/algebraic/bmf.rs index a32c2a23c..3e4425a4a 100644 --- a/src/unit_tests/models/algebraic/bmf.rs +++ b/src/unit_tests/models/algebraic/bmf.rs @@ -11,7 +11,7 @@ fn test_bmf_creation() { assert_eq!(problem.rows(), 2); assert_eq!(problem.cols(), 2); assert_eq!(problem.rank(), 2); - assert_eq!(problem.num_variables(), 8); // 2*2 + 2*2 + assert_eq!(problem.num_variables().unwrap(), 8); // 2*2 + 2*2 } #[test] @@ -168,7 +168,7 @@ fn test_matrix_hamming_distance_function() { fn test_empty_matrix() { let matrix: Vec> = vec![]; let problem = BMF::new(matrix, 1); - assert_eq!(problem.num_variables(), 0); + assert_eq!(problem.num_variables().unwrap(), 0); // Empty matrix factors exactly with zero factor size. assert_eq!( Problem::evaluate(&problem, &(vec![], vec![vec![]])).unwrap(), @@ -179,7 +179,10 @@ fn test_empty_matrix() { #[test] fn test_rank_zero_exactness() { let nonzero = BMF::new(vec![vec![true, false]], 0); - assert_eq!(nonzero.dimensions(), Vec::::new()); + assert_eq!( + crate::solvers::cartesian_dimensions(&nonzero).unwrap(), + Vec::::new() + ); let empty_factors = (vec![vec![]], vec![]); assert_eq!(nonzero.hamming_distance(&empty_factors).unwrap(), 1); assert!(!nonzero.is_exact(&empty_factors).unwrap()); @@ -218,7 +221,10 @@ fn test_bmf_problem() { let problem = BMF::new(matrix, 2); // dims: B(2*2) + C(2*2) = 8 binary variables - assert_eq!(problem.dimensions(), vec![2; 8]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 8] + ); // Exact factorization: B = I, C = I — total factor size = 4 assert_eq!( @@ -246,7 +252,10 @@ fn test_bmf_problem() { // 1x1 matrix let matrix = vec![vec![true]]; let problem = BMF::new(matrix, 1); - assert_eq!(problem.dimensions(), vec![2; 2]); // B(1*1) + C(1*1) + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 2] + ); // B(1*1) + C(1*1) assert_eq!( Problem::evaluate(&problem, &(vec![vec![true]], vec![vec![true]])).unwrap(), Min(Some(2)) diff --git a/src/unit_tests/models/algebraic/closest_vector_problem.rs b/src/unit_tests/models/algebraic/closest_vector_problem.rs index c78712962..5d7ad48fb 100644 --- a/src/unit_tests/models/algebraic/closest_vector_problem.rs +++ b/src/unit_tests/models/algebraic/closest_vector_problem.rs @@ -29,7 +29,7 @@ fn test_cvp_evaluates_without_coefficient_bounds() { ClosestVectorProblem::new(vec![vec![2, 0, 0], vec![1, 2, 0]], vec![3_i64, 3, 1]).unwrap(); assert_eq!( problem.evaluate(&vec![1, 1]).unwrap(), - Min(Some(2.0_f64.sqrt())) + Min(Some(BigRational::from_integer(2.into()))) ); assert!(problem.evaluate(&vec![11, -12]).unwrap().0.is_some()); assert!(matches!( @@ -48,11 +48,15 @@ fn test_cvp_rejects_invalid_basis() { } #[test] -fn test_cvp_reports_rank_arithmetic_overflow() { - let error = +fn test_cvp_rank_uses_exact_integer_elimination() { + let problem = ClosestVectorProblem::new(vec![vec![i64::MAX, 1], vec![1, i64::MAX]], vec![0_i64, 0]) - .unwrap_err(); - assert!(matches!(error, ConstructionError::IntegerOverflow(_))); + .unwrap(); + assert_eq!(problem.independent_rows(), vec![0, 1]); + // Swapped pivots and a redundant ambient row preserve column rank. + let rectangular = + ClosestVectorProblem::new(vec![vec![0, 0, 1], vec![0, 1, 0]], vec![0_i64; 3]).unwrap(); + assert_eq!(rectangular.independent_rows(), vec![2, 1]); } #[test] @@ -68,16 +72,61 @@ fn test_cvp_rejects_non_finite_real_target() { } #[test] -fn test_cvp_reports_exact_to_float_boundary() { - let problem = ClosestVectorProblem::new( - vec![vec![crate::types::MAX_EXACT_F64_INTEGER + 1]], - vec![0_i64], +fn test_cvp_integer_coordinates_preserve_zero_and_unit_distance() { + let target = (1_i64 << 53) + 1; + let problem = ClosestVectorProblem::new(vec![vec![1]], vec![target]).unwrap(); + assert_eq!( + crate::solvers::customized::closest_vector_problem::solve(&problem).unwrap(), + vec![target] + ); + assert_eq!( + problem.squared_distance(&[target]).unwrap(), + BigRational::zero() + ); + assert_eq!( + problem.squared_distance(&[target - 1]).unwrap(), + BigRational::from_integer(1.into()) + ); + let cancellation = ClosestVectorProblem::new( + vec![vec![i64::MAX, 1], vec![i64::MAX - 1, 1]], + vec![1_i64, 0], ) .unwrap(); - assert!(matches!( - problem.evaluate(&vec![1]), - Err(crate::traits::EvaluationError::InexactFloatConversion(_)) - )); + assert_eq!( + cancellation.squared_distance(&[1, -1]).unwrap(), + BigRational::zero() + ); +} + +#[test] +fn test_cvp_real_target_preserves_its_stored_rational_value() { + let problem = ClosestVectorProblem::new(vec![vec![1]], vec![0.25]).unwrap(); + assert_eq!( + problem.squared_distance(&[1]).unwrap(), + BigRational::new(9.into(), 16.into()) + ); + let value = problem.evaluate(&vec![1]).unwrap(); + let serialized = + crate::registry::DynProblem::evaluate_json(&problem, &serde_json::json!([1])).unwrap(); + assert_eq!( + serde_json::from_value::>(serialized).unwrap(), + value + ); + assert_eq!( + crate::registry::DynProblem::evaluate_dyn(&problem, &serde_json::json!([1])).unwrap(), + ("Min(9/16)".into(), true) + ); + let loaded = crate::registry::LoadedDynProblem::new(Box::new(problem)); + let outcome = crate::solvers::solve(&loaded, crate::solvers::SolverRequest::Default) + .unwrap() + .outcome; + assert_eq!( + outcome, + crate::solvers::SolveOutcome::Optimal { + solution: serde_json::json!([0]), + evaluation: "Min(1/16)".into(), + } + ); } #[test] @@ -132,5 +181,8 @@ fn test_cvp_registers_both_target_variants() { #[test] fn test_cvp_empty_basis_is_valid() { let problem = ClosestVectorProblem::new(Vec::new(), vec![3_i64, 4]).unwrap(); - assert_eq!(problem.evaluate(&Vec::new()).unwrap(), Min(Some(5.0))); + assert_eq!( + problem.evaluate(&Vec::new()).unwrap(), + Min(Some(BigRational::from_integer(25.into()))) + ); } diff --git a/src/unit_tests/models/algebraic/consecutive_block_minimization.rs b/src/unit_tests/models/algebraic/consecutive_block_minimization.rs index 92ae4a385..40bf02f8b 100644 --- a/src/unit_tests/models/algebraic/consecutive_block_minimization.rs +++ b/src/unit_tests/models/algebraic/consecutive_block_minimization.rs @@ -26,8 +26,11 @@ fn test_consecutive_block_minimization_basic() { assert_eq!(problem.num_rows(), 2); assert_eq!(problem.num_cols(), 3); assert_eq!(problem.bound(), 2); - assert_eq!(problem.num_variables(), 3); - assert_eq!(problem.dimensions(), vec![3; 3]); + assert_eq!(problem.num_variables().unwrap(), 3); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![3; 3] + ); } #[test] diff --git a/src/unit_tests/models/algebraic/consecutive_ones_matrix_augmentation.rs b/src/unit_tests/models/algebraic/consecutive_ones_matrix_augmentation.rs index b3cce7057..dc1f27d42 100644 --- a/src/unit_tests/models/algebraic/consecutive_ones_matrix_augmentation.rs +++ b/src/unit_tests/models/algebraic/consecutive_ones_matrix_augmentation.rs @@ -43,8 +43,11 @@ fn test_consecutive_ones_matrix_augmentation_basic() { assert_eq!(problem.num_rows(), 4); assert_eq!(problem.num_cols(), 5); assert_eq!(problem.bound(), 2); - assert_eq!(problem.num_variables(), 5); - assert_eq!(problem.dimensions(), vec![5; 5]); + assert_eq!(problem.num_variables().unwrap(), 5); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![5; 5] + ); assert_eq!( ::NAME, "ConsecutiveOnesMatrixAugmentation" diff --git a/src/unit_tests/models/algebraic/consecutive_ones_submatrix.rs b/src/unit_tests/models/algebraic/consecutive_ones_submatrix.rs index bc20d02df..0e88e23d0 100644 --- a/src/unit_tests/models/algebraic/consecutive_ones_submatrix.rs +++ b/src/unit_tests/models/algebraic/consecutive_ones_submatrix.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::traits::Problem; /// Tucker matrix (3×4) — the classic C1P obstruction. @@ -18,7 +17,10 @@ fn test_consecutive_ones_submatrix_basic() { assert_eq!(problem.num_rows(), 3); assert_eq!(problem.num_cols(), 4); assert_eq!(problem.bound(), 3); - assert_eq!(problem.dimensions(), vec![2; 4]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 4] + ); assert_eq!( ::NAME, "ConsecutiveOnesSubmatrix" @@ -205,7 +207,10 @@ fn test_consecutive_ones_submatrix_empty_matrix_vacuous_case() { assert!(problem.matrix().is_empty()); assert_eq!(problem.num_rows(), 0); assert_eq!(problem.num_cols(), 0); - assert_eq!(problem.dimensions(), Vec::::new()); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + Vec::::new() + ); assert!(problem.evaluate(&vec![]).unwrap()); } diff --git a/src/unit_tests/models/algebraic/equilibrium_point.rs b/src/unit_tests/models/algebraic/equilibrium_point.rs index dd30d1d37..2ab175c18 100644 --- a/src/unit_tests/models/algebraic/equilibrium_point.rs +++ b/src/unit_tests/models/algebraic/equilibrium_point.rs @@ -51,8 +51,11 @@ fn test_equilibrium_point_creation_and_accessors() { assert_eq!(p.range_sets()[0], vec![0, 1]); assert_eq!(p.range_sets()[1], vec![0, 1]); assert_eq!(p.range_sets()[2], vec![0, 1]); - assert_eq!(p.dimensions(), vec![2, 2, 2]); - assert_eq!(p.num_variables(), 3); + assert_eq!( + crate::solvers::cartesian_dimensions(&p).unwrap(), + vec![2, 2, 2] + ); + assert_eq!(p.num_variables().unwrap(), 3); assert_eq!(::NAME, "EquilibriumPoint"); assert_eq!(::variant(), vec![]); } diff --git a/src/unit_tests/models/algebraic/feasible_basis_extension.rs b/src/unit_tests/models/algebraic/feasible_basis_extension.rs index 489d91788..d9c3453e2 100644 --- a/src/unit_tests/models/algebraic/feasible_basis_extension.rs +++ b/src/unit_tests/models/algebraic/feasible_basis_extension.rs @@ -1,5 +1,4 @@ use super::*; -use crate::solvers::BruteForceProblem as _; #[test] fn create_spec_validates_matrix_shape() { @@ -41,7 +40,10 @@ fn test_feasible_basis_extension_creation() { assert_eq!(problem.num_rows(), 3); assert_eq!(problem.num_columns(), 6); assert_eq!(problem.num_required(), 2); - assert_eq!(problem.dimensions(), vec![2; 4]); // 6 - 2 = 4 free columns + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 4] + ); // 6 - 2 = 4 free columns assert_eq!( ::NAME, "FeasibleBasisExtension" diff --git a/src/unit_tests/models/algebraic/ilp.rs b/src/unit_tests/models/algebraic/ilp.rs index 64bdc8143..01e00e9de 100644 --- a/src/unit_tests/models/algebraic/ilp.rs +++ b/src/unit_tests/models/algebraic/ilp.rs @@ -54,7 +54,10 @@ fn float_constraints_use_float_arithmetic() { ) .unwrap(); - assert!(ilp.is_feasible(&[1, 1]).unwrap()); + assert!(!ilp.is_feasible(&[1, 1]).unwrap()); + assert!(LinearConstraint::eq(vec![(0, 0.1), (1, 0.2)], 0.1 + 0.2) + .is_satisfied(&[1, 1]) + .unwrap()); assert_eq!(ilp.evaluate_objective(&[1, 0]).unwrap(), 0.5); } diff --git a/src/unit_tests/models/algebraic/minimum_matrix_cover.rs b/src/unit_tests/models/algebraic/minimum_matrix_cover.rs index 89eb9ad5f..41a312085 100644 --- a/src/unit_tests/models/algebraic/minimum_matrix_cover.rs +++ b/src/unit_tests/models/algebraic/minimum_matrix_cover.rs @@ -12,11 +12,14 @@ fn test_minimum_matrix_cover_creation() { vec![1, 0, 0, 4], vec![0, 2, 4, 0], ]; - let problem = MinimumMatrixCover::new(matrix.clone()); + let problem = MinimumMatrixCover::new(matrix.clone()).unwrap(); assert_eq!(problem.num_rows(), 4); assert_eq!(problem.matrix(), &matrix); - assert_eq!(problem.dimensions(), vec![2; 4]); - assert_eq!(problem.num_variables(), 4); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 4] + ); + assert_eq!(problem.num_variables().unwrap(), 4); } #[test] @@ -29,7 +32,7 @@ fn test_minimum_matrix_cover_evaluate_all_minus() { vec![1, 0, 0, 4], vec![0, 2, 4, 0], ]; - let problem = MinimumMatrixCover::new(matrix); + let problem = MinimumMatrixCover::new(matrix).unwrap(); let value = problem.evaluate(&vec![false, false, false, false]).unwrap(); // Sum of all entries = 0+3+1+0 + 3+0+0+2 + 1+0+0+4 + 0+2+4+0 = 20 assert_eq!(value, Min(Some(20))); @@ -43,7 +46,7 @@ fn test_minimum_matrix_cover_evaluate_mixed() { vec![1, 0, 0, 4], vec![0, 2, 4, 0], ]; - let problem = MinimumMatrixCover::new(matrix); + let problem = MinimumMatrixCover::new(matrix).unwrap(); // Config [0,1,1,0] → f=(-1,+1,+1,-1) // Compute: Σ a_ij * f(i) * f(j) @@ -64,7 +67,7 @@ fn test_minimum_matrix_cover_evaluate_mixed() { #[test] fn test_minimum_matrix_cover_evaluate_invalid() { - let problem = MinimumMatrixCover::new(vec![vec![0, 1], vec![1, 0]]); + let problem = MinimumMatrixCover::new(vec![vec![0, 1], vec![1, 0]]).unwrap(); // Wrong length assert!(matches!( @@ -86,7 +89,7 @@ fn test_minimum_matrix_cover_solver() { vec![1, 0, 0, 4], vec![0, 2, 4, 0], ]; - let problem = MinimumMatrixCover::new(matrix); + let problem = MinimumMatrixCover::new(matrix).unwrap(); let solver = BruteForce::new(); let value_solution = solver.solve(&problem).unwrap().unwrap(); @@ -103,7 +106,7 @@ fn test_minimum_matrix_cover_solver() { #[test] fn test_minimum_matrix_cover_serialization() { let matrix = vec![vec![0, 1], vec![1, 0]]; - let problem = MinimumMatrixCover::new(matrix); + let problem = MinimumMatrixCover::new(matrix).unwrap(); let json = serde_json::to_string(&problem).unwrap(); let deserialized: MinimumMatrixCover = serde_json::from_str(&json).unwrap(); assert_eq!(deserialized.num_rows(), 2); @@ -114,7 +117,7 @@ fn test_minimum_matrix_cover_serialization() { fn test_minimum_matrix_cover_1x1() { // 1×1 matrix: only one variable, f(1) = ±1 // value = a_11 * f(1)^2 = a_11 regardless of sign - let problem = MinimumMatrixCover::new(vec![vec![5]]); + let problem = MinimumMatrixCover::new(vec![vec![5]]).unwrap(); assert_eq!(problem.evaluate(&vec![false]).unwrap(), Min(Some(5))); assert_eq!(problem.evaluate(&vec![true]).unwrap(), Min(Some(5))); @@ -136,7 +139,7 @@ fn test_minimum_matrix_cover_paper_example() { vec![1, 0, 0, 4], vec![0, 2, 4, 0], ]; - let problem = MinimumMatrixCover::new(matrix); + let problem = MinimumMatrixCover::new(matrix).unwrap(); let solver = BruteForce::new(); // Verify the claimed optimal from the issue @@ -167,3 +170,17 @@ fn test_minimum_matrix_cover_canonical_example_spec() { serde_json::json!([false, true, true, false]) ); } + +#[test] +fn construction_and_deserialization_enforce_nonnegative_square_matrices() { + for matrix in [vec![vec![-1]], vec![vec![0, 1]], vec![vec![0, 1], vec![1]]] { + assert!(matches!( + MinimumMatrixCover::new(matrix.clone()), + Err(ConstructionError::InvalidInput(_)) + )); + assert!(serde_json::from_value::( + serde_json::json!({"matrix": matrix}) + ) + .is_err()); + } +} diff --git a/src/unit_tests/models/algebraic/minimum_matrix_domination.rs b/src/unit_tests/models/algebraic/minimum_matrix_domination.rs index 7e31fa47e..f2e107fd8 100644 --- a/src/unit_tests/models/algebraic/minimum_matrix_domination.rs +++ b/src/unit_tests/models/algebraic/minimum_matrix_domination.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::traits::Problem; use crate::types::Min; @@ -22,7 +21,10 @@ fn test_minimum_matrix_domination_creation() { assert_eq!(problem.num_rows(), 6); assert_eq!(problem.num_cols(), 6); assert_eq!(problem.num_ones(), 10); - assert_eq!(problem.dimensions(), vec![2; 10]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 10] + ); assert_eq!( ::NAME, "MinimumMatrixDomination" @@ -152,7 +154,10 @@ fn test_minimum_matrix_domination_single_row() { fn test_minimum_matrix_domination_empty_matrix() { let problem = MinimumMatrixDomination::new(vec![]); assert_eq!(problem.num_ones(), 0); - assert_eq!(problem.dimensions(), Vec::::new()); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + Vec::::new() + ); // Empty config: vacuously valid with 0 selected assert_eq!(problem.evaluate(&vec![]).unwrap(), Min(Some(0))); } diff --git a/src/unit_tests/models/algebraic/minimum_weight_decoding.rs b/src/unit_tests/models/algebraic/minimum_weight_decoding.rs index 352d1b2f8..5915ab151 100644 --- a/src/unit_tests/models/algebraic/minimum_weight_decoding.rs +++ b/src/unit_tests/models/algebraic/minimum_weight_decoding.rs @@ -1,5 +1,4 @@ use super::*; -use crate::solvers::BruteForceProblem as _; #[test] fn create_spec_maps_rhs_to_target() { @@ -30,7 +29,10 @@ fn test_minimum_weight_decoding_creation() { let problem = example_instance(); assert_eq!(problem.num_rows(), 3); assert_eq!(problem.num_cols(), 4); - assert_eq!(problem.dimensions(), vec![2; 4]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 4] + ); assert_eq!( ::NAME, "MinimumWeightDecoding" diff --git a/src/unit_tests/models/algebraic/minimum_weight_solution_to_linear_equations.rs b/src/unit_tests/models/algebraic/minimum_weight_solution_to_linear_equations.rs index b4603a584..668e02a4f 100644 --- a/src/unit_tests/models/algebraic/minimum_weight_solution_to_linear_equations.rs +++ b/src/unit_tests/models/algebraic/minimum_weight_solution_to_linear_equations.rs @@ -1,5 +1,4 @@ use super::*; -use crate::solvers::BruteForceProblem as _; #[test] fn create_spec_rejects_rhs_length_mismatch() { @@ -27,7 +26,10 @@ fn test_minimum_weight_solution_creation() { let problem = example_instance(); assert_eq!(problem.num_equations(), 2); assert_eq!(problem.num_variables(), 4); - assert_eq!(problem.dimensions(), vec![2; 4]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 4] + ); assert_eq!( ::NAME, "MinimumWeightSolutionToLinearEquations" diff --git a/src/unit_tests/models/algebraic/quadratic_assignment.rs b/src/unit_tests/models/algebraic/quadratic_assignment.rs index 66f2de170..038eb17ff 100644 --- a/src/unit_tests/models/algebraic/quadratic_assignment.rs +++ b/src/unit_tests/models/algebraic/quadratic_assignment.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::traits::Problem; use crate::types::Min; @@ -40,7 +39,10 @@ fn test_quadratic_assignment_creation() { let qap = make_test_instance(); assert_eq!(qap.num_facilities(), 4); assert_eq!(qap.num_locations(), 4); - assert_eq!(qap.dimensions(), vec![4, 4, 4, 4]); + assert_eq!( + crate::solvers::cartesian_dimensions(&qap).unwrap(), + vec![4, 4, 4, 4] + ); assert_eq!(qap.cost_matrix().len(), 4); assert_eq!(qap.distance_matrix().len(), 4); } @@ -121,7 +123,10 @@ fn test_quadratic_assignment_rectangular() { let qap = QuadraticAssignment::new(cost_matrix, distance_matrix); assert_eq!(qap.num_facilities(), 2); assert_eq!(qap.num_locations(), 3); - assert_eq!(qap.dimensions(), vec![3, 3]); + assert_eq!( + crate::solvers::cartesian_dimensions(&qap).unwrap(), + vec![3, 3] + ); // Assignment f=(0,1): cost = C[0][1]*D[0][1] + C[1][0]*D[1][0] = 3*1 + 3*1 = 6 assert_eq!(Problem::evaluate(&qap, &vec![0, 1]).unwrap(), Min(Some(6))); // Assignment f=(0,2): cost = 3*D[0][2] + 3*D[2][0] = 3*4 + 3*4 = 24 diff --git a/src/unit_tests/models/algebraic/quadratic_congruences.rs b/src/unit_tests/models/algebraic/quadratic_congruences.rs index 41a805acc..fada632db 100644 --- a/src/unit_tests/models/algebraic/quadratic_congruences.rs +++ b/src/unit_tests/models/algebraic/quadratic_congruences.rs @@ -33,8 +33,11 @@ fn test_quadratic_congruences_creation_and_accessors() { assert_eq!(p.bit_length_b(), 4); assert_eq!(p.bit_length_c(), 4); // x is encoded as 4 binary digits because c - 1 = 9 has 4 bits. - assert_eq!(p.dimensions(), vec![2, 2, 2, 2]); - assert_eq!(p.num_variables(), 4); + assert_eq!( + crate::solvers::cartesian_dimensions(&p).unwrap(), + vec![2, 2, 2, 2] + ); + assert_eq!(p.num_variables().unwrap(), 4); assert_eq!( ::NAME, "QuadraticCongruences" @@ -56,7 +59,10 @@ fn test_quadratic_congruences_evaluate_yes() { fn test_quadratic_congruences_evaluate_no() { let p = no_problem(); // c - 1 = 6 has 3 bits. - assert_eq!(p.dimensions(), vec![2, 2, 2]); + assert_eq!( + crate::solvers::cartesian_dimensions(&p).unwrap(), + vec![2, 2, 2] + ); for x in 1..7 { // quadratic residues mod 7 are {0,1,2,4}; 3 is not one assert_eq!(p.evaluate(&config_for_x(&p, x)).unwrap(), Or(false)); @@ -74,7 +80,10 @@ fn test_quadratic_congruences_evaluate_invalid_config() { fn test_quadratic_congruences_c_le_1() { // c=1: search space {1..0} is empty let p = QuadraticCongruences::new(0, 5, 1); - assert_eq!(p.dimensions(), Vec::::new()); + assert_eq!( + crate::solvers::cartesian_dimensions(&p).unwrap(), + Vec::::new() + ); assert_eq!(p.evaluate(&BigUint::default()).unwrap(), Or(false)); assert_eq!(p.evaluate(&bu(1)).unwrap(), Or(false)); } @@ -86,7 +95,10 @@ fn test_quadratic_congruences_bigint_witness_encoding_round_trip() { let x = (BigUint::from(1u32) << 100usize) + BigUint::from(1u32); let config = p.encode_witness(&x).expect("x should be encodable"); - assert_eq!(config.len(), p.dimensions().len()); + assert_eq!( + config.len(), + crate::solvers::cartesian_dimensions(&p).unwrap().len() + ); assert_eq!(p.decode_witness(&config), Some(x)); } diff --git a/src/unit_tests/models/algebraic/quadratic_diophantine_equations.rs b/src/unit_tests/models/algebraic/quadratic_diophantine_equations.rs index 09d0c968b..e6d0b59ec 100644 --- a/src/unit_tests/models/algebraic/quadratic_diophantine_equations.rs +++ b/src/unit_tests/models/algebraic/quadratic_diophantine_equations.rs @@ -33,8 +33,11 @@ fn test_quadratic_diophantine_equations_creation_and_accessors() { assert_eq!(problem.bit_length_b(), 3); assert_eq!(problem.bit_length_c(), 6); // max_x = floor(sqrt(53 / 3)) = 4, encoded in 3 binary digits. - assert_eq!(problem.dimensions(), vec![2, 2, 2]); - assert_eq!(problem.num_variables(), 3); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2, 2, 2] + ); + assert_eq!(problem.num_variables().unwrap(), 3); assert_eq!( ::NAME, "QuadraticDiophantineEquations" @@ -69,7 +72,10 @@ fn test_quadratic_diophantine_equations_evaluate_yes() { #[test] fn test_quadratic_diophantine_equations_evaluate_no() { let problem = no_problem(); - assert_eq!(problem.dimensions(), vec![2]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2] + ); assert_eq!( problem.evaluate(&config_for_x(&problem, 1)).unwrap(), Or(false) @@ -86,7 +92,10 @@ fn test_quadratic_diophantine_equations_evaluate_invalid_config() { #[test] fn test_quadratic_diophantine_equations_c_le_a() { let problem = QuadraticDiophantineEquations::new(10, 1, 5); - assert_eq!(problem.dimensions(), Vec::::new()); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + Vec::::new() + ); assert_eq!(problem.evaluate(&BigUint::default()).unwrap(), Or(false)); } diff --git a/src/unit_tests/models/algebraic/qubo.rs b/src/unit_tests/models/algebraic/qubo.rs index 448fef75b..97eb6ed27 100644 --- a/src/unit_tests/models/algebraic/qubo.rs +++ b/src/unit_tests/models/algebraic/qubo.rs @@ -1,31 +1,31 @@ use super::*; +include!("../../jl_helpers.rs"); use crate::solvers::BruteForce; use crate::solvers::BruteForceProblem as _; use crate::traits::Problem; use crate::types::Min; -include!("../../jl_helpers.rs"); #[test] fn test_qubo_from_matrix() { let problem = QUBO::from_matrix(vec![vec![1, 2], vec![0, 3]]).unwrap(); assert_eq!(problem.num_vars(), 2); - assert_eq!(problem.get(0, 0), Some(&1)); - assert_eq!(problem.get(0, 1), Some(&2)); - assert_eq!(problem.get(1, 1), Some(&3)); + assert_eq!(problem.get(0, 0), Some(1)); + assert_eq!(problem.get(0, 1), Some(2)); + assert_eq!(problem.get(1, 1), Some(3)); } #[test] fn test_qubo_new() { let problem = QUBO::new(vec![1.0, 2.0], vec![((0, 1), 3.0)]).unwrap(); - assert_eq!(problem.get(0, 0), Some(&1.0)); - assert_eq!(problem.get(1, 1), Some(&2.0)); - assert_eq!(problem.get(0, 1), Some(&3.0)); + assert_eq!(problem.get(0, 0), Some(1.0)); + assert_eq!(problem.get(1, 1), Some(2.0)); + assert_eq!(problem.get(0, 1), Some(3.0)); } #[test] fn test_num_variables() { let problem = QUBO::::from_matrix(vec![vec![0.0; 5]; 5]).unwrap(); - assert_eq!(problem.num_variables(), 5); + assert_eq!(problem.num_variables().unwrap(), 5); } #[test] @@ -37,8 +37,8 @@ fn test_matrix_access() { ]) .unwrap(); let matrix = problem.matrix(); - assert_eq!(matrix.len(), 3); - assert_eq!(matrix[0], vec![1.0, 2.0, 3.0]); + assert_eq!(matrix.rows(), 3); + assert_eq!(matrix.outer_view(0).unwrap().data(), &[1.0, 2.0, 3.0]); } #[test] @@ -70,7 +70,7 @@ fn test_qubo_rejects_invalid_configurations() { fn test_qubo_new_reverse_indices() { // Test the case where (j, i) is provided with i < j let problem = QUBO::new(vec![1.0, 2.0], vec![((1, 0), 3.0)]).unwrap(); // j > i - assert_eq!(problem.get(0, 1), Some(&3.0)); // Should be stored at (0, 1) + assert_eq!(problem.get(0, 1), Some(3.0)); // Should be stored at (0, 1) } #[test] @@ -157,8 +157,8 @@ fn test_qubo_f64_create_spec() { }) .unwrap(); - assert_eq!(problem.get(0, 0), Some(&0.5)); - assert_eq!(problem.get(0, 1), Some(&-1.25)); + assert_eq!(problem.get(0, 0), Some(0.5)); + assert_eq!(problem.get(0, 1), Some(-1.25)); } #[test] @@ -172,10 +172,10 @@ fn test_qubo_rejects_non_square_matrix() { #[test] fn test_qubo_rejects_non_finite_coefficients() { - let error = QUBO::from_matrix(vec![vec![f64::NAN]]).unwrap_err(); + let error = QUBO::from_matrix(vec![vec![0.0, f64::NAN], vec![0.0, 0.0]]).unwrap_err(); assert!(matches!( error, - crate::registry::ConstructionError::NonFiniteFloat(_) + crate::registry::ConstructionError::NonFiniteFloat(message) if message.contains("(0, 1)") )); let error = QUBO::new(vec![f64::INFINITY], vec![]).unwrap_err(); assert!(matches!( @@ -202,3 +202,81 @@ fn test_integer_qubo_reports_objective_overflow() { Err(crate::traits::EvaluationError::IntegerOverflow(_)) )); } + +#[test] +fn sparse_storage_preserves_every_assignment_and_sum_order() { + let integer = vec![ + vec![3, -5, 0, 2], + vec![99, 0, 7, -4], + vec![0, 0, -6, 0], + vec![0, 0, 0, 1], + ]; + let floating = vec![ + vec![1e16, 1.0, -1e16, 0.0], + vec![99.0, 0.5, 0.0, -0.25], + vec![0.0, 0.0, -2.0, 0.0], + vec![0.0, 0.0, 0.0, 1.0], + ]; + let int_problem = QUBO::from_matrix(integer.clone()).unwrap(); + let float_problem = QUBO::from_matrix(floating.clone()).unwrap(); + for mask in 0..16 { + let solution: Vec = (0..4).map(|i| mask & (1 << i) != 0).collect(); + let mut int_value = 0i64; + let mut float_value = 0.0f64; + for i in 0..4 { + for j in i..4 { + if solution[i] && solution[j] { + int_value = int_value.checked_add(integer[i][j]).unwrap(); + float_value += floating[i][j]; + } + } + } + assert_eq!( + int_problem.evaluate(&solution).unwrap(), + Min(Some(int_value)) + ); + assert_eq!( + float_problem + .evaluate(&solution) + .unwrap() + .unwrap() + .to_bits(), + float_value.to_bits() + ); + } +} + +#[test] +fn sparse_qubo_keeps_unused_variables_and_last_assignment() { + let problem = QUBO::new( + vec![0i64; 10_000], + vec![((2, 7), i64::MAX), ((7, 2), 5), ((9, 9), 3), ((9, 9), 0)], + ) + .unwrap(); + assert_eq!(problem.num_vars(), 10_000); + assert_eq!(problem.matrix().nnz(), 1); + assert_eq!(problem.get(9, 9), Some(0)); + assert_eq!(problem.get(2, 7), Some(5)); + let json = serde_json::to_string(&problem).unwrap(); + assert!(json.len() < 100_000); + let restored: QUBO = serde_json::from_str(&json).unwrap(); + assert_eq!(restored.matrix(), problem.matrix()); + let mut solution = vec![false; 10_000]; + solution[2] = true; + solution[7] = true; + assert_eq!(restored.evaluate(&solution).unwrap(), Min(Some(5))); +} + +#[test] +fn sparse_qubo_validates_shape_values_and_serialized_structure() { + assert!(QUBO::from_sparse(CsMat::::zero((2, 3))).is_err()); + let invalid = CsMat::new((1, 1), vec![0, 1], vec![0], vec![f64::INFINITY]); + assert!(QUBO::from_sparse(invalid).is_err()); + let column_matrix = CsMat::new_csc((2, 2), vec![0, 1, 2], vec![0, 0], vec![2i64, 3]); + let problem = QUBO::from_sparse(column_matrix).unwrap(); + assert!(problem.matrix().is_csr()); + assert_eq!(problem.evaluate(&vec![true, true]).unwrap(), Min(Some(5))); + let mut json = serde_json::to_value(&problem).unwrap(); + json["matrix"]["indptr"] = serde_json::json!([0, 3, 2]); + assert!(serde_json::from_value::>(json).is_err()); +} diff --git a/src/unit_tests/models/algebraic/simultaneous_incongruences.rs b/src/unit_tests/models/algebraic/simultaneous_incongruences.rs index 0506d3829..d7a3edff6 100644 --- a/src/unit_tests/models/algebraic/simultaneous_incongruences.rs +++ b/src/unit_tests/models/algebraic/simultaneous_incongruences.rs @@ -25,9 +25,9 @@ fn test_simultaneous_incongruences_creation_and_accessors() { assert_eq!(p.num_pairs(), 4); assert_eq!(p.pairs(), &[(2, 2), (1, 3), (2, 5), (3, 7)]); // lcm(2,3,5,7) = 210 - assert_eq!(p.lcm_moduli(), 210); - assert_eq!(p.dimensions(), vec![210]); - assert_eq!(p.num_variables(), 1); + assert_eq!(p.lcm_moduli().unwrap(), 210); + assert_eq!(crate::solvers::cartesian_dimensions(&p).unwrap(), vec![210]); + assert_eq!(p.num_variables().unwrap(), 1); assert_eq!( ::NAME, "SimultaneousIncongruences" @@ -49,7 +49,7 @@ fn test_simultaneous_incongruences_evaluate_no() { let p = covering_system(); // pairs (2,2) and (1,2): together require x≡0 (mod 2) AND x≡1 (mod 2), // which is impossible. - let lcm = p.lcm_moduli(); + let lcm = p.lcm_moduli().unwrap(); assert_eq!(lcm, 2); // All x in {0,1} should fail for x in 0..lcm { @@ -72,8 +72,8 @@ fn test_simultaneous_incongruences_evaluate_invalid_config() { fn test_simultaneous_incongruences_empty_pairs() { let p = SimultaneousIncongruences::new(vec![]).unwrap(); assert_eq!(p.num_pairs(), 0); - assert_eq!(p.lcm_moduli(), 1); - assert_eq!(p.dimensions(), vec![1]); + assert_eq!(p.lcm_moduli().unwrap(), 1); + assert_eq!(crate::solvers::cartesian_dimensions(&p).unwrap(), vec![1]); // Any x (here x=0) satisfies vacuously assert_eq!(p.evaluate(&0).unwrap(), Or(true)); } @@ -131,3 +131,19 @@ fn test_simultaneous_incongruences_paper_example() { let witness = solver.solve(&p).unwrap().unwrap(); assert_eq!(p.evaluate(&witness).unwrap(), Or(true)); } + +#[test] +fn period_overflow_does_not_restrict_model_evaluation() { + let problem = SimultaneousIncongruences::new(vec![(1, i64::MAX), (1, i64::MAX - 1)]).unwrap(); + let restored: SimultaneousIncongruences = + serde_json::from_value(serde_json::to_value(&problem).unwrap()).unwrap(); + assert_eq!(restored.evaluate(&0).unwrap(), Or(true)); + assert_eq!(restored.evaluate(&-1).unwrap(), Or(false)); + assert_eq!(restored.parameters(), problem.parameters()); + assert!(matches!( + crate::solvers::cartesian_dimensions(&restored), + Err(crate::solvers::SolveError::Evaluation( + crate::traits::EvaluationError::IntegerOverflow(_) + )) + )); +} diff --git a/src/unit_tests/models/algebraic/sparse_matrix_compression.rs b/src/unit_tests/models/algebraic/sparse_matrix_compression.rs index 7ee9784c7..843481a86 100644 --- a/src/unit_tests/models/algebraic/sparse_matrix_compression.rs +++ b/src/unit_tests/models/algebraic/sparse_matrix_compression.rs @@ -1,5 +1,4 @@ use super::*; -use crate::solvers::BruteForceProblem as _; #[test] fn create_spec_rejects_zero_bound() { @@ -32,7 +31,10 @@ fn test_sparse_matrix_compression_basic() { assert_eq!(problem.num_cols(), 4); assert_eq!(problem.bound_k(), 2); assert_eq!(problem.storage_len(), 6); - assert_eq!(problem.dimensions(), vec![2; 4]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 4] + ); assert_eq!( ::NAME, "SparseMatrixCompression" diff --git a/src/unit_tests/models/decision.rs b/src/unit_tests/models/decision.rs index 9bce00a5b..bffba4846 100644 --- a/src/unit_tests/models/decision.rs +++ b/src/unit_tests/models/decision.rs @@ -1,7 +1,6 @@ use crate::models::decision::Decision; use crate::models::graph::{MaximumIndependentSet, MinimumDominatingSet, MinimumVertexCover}; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::topology::SimpleGraph; use crate::traits::Problem; use crate::types::{One, Or}; @@ -84,7 +83,10 @@ fn test_decision_max_evaluate() { #[test] fn test_decision_dims() { let decision = Decision::new(triangle_mvc(), 2); - assert_eq!(decision.dimensions(), vec![2, 2, 2]); + assert_eq!( + crate::solvers::cartesian_dimensions(&decision).unwrap(), + vec![2, 2, 2] + ); } #[test] @@ -319,8 +321,13 @@ fn test_decision_mis_unit_dynamic_identity_edges() { assert_eq!((edge.parameter_declarations_fn)().fields.len(), 2); let witness = vec![true, false, true]; let reduced = (edge.reduce_fn.unwrap())(&decision).unwrap(); + assert!(std::ptr::eq( + reduced.witness.target_problem_any(), + reduced.aggregate.as_ref().unwrap().target_problem_any(), + )); assert_eq!( *reduced + .witness .extract_solution_dyn(&witness) .unwrap() .downcast::>() @@ -333,12 +340,8 @@ fn test_decision_mis_unit_dynamic_identity_edges() { )); let aggregate = (edge.reduce_aggregate_fn.unwrap())(&decision).unwrap(); assert_eq!( - *aggregate - .extract_value_from_solution_dyn(&witness) - .unwrap() - .downcast::() - .unwrap(), - Or(true) + aggregate.extract_value_dyn(serde_json::json!(2)), + serde_json::json!(true) ); assert!(matches!( (edge.reduce_aggregate_fn.unwrap())(decision.inner()), @@ -356,3 +359,47 @@ fn test_decision_mis_unit_dynamic_identity_edges() { assert!(reverse.reduce_fn.is_none()); assert_eq!((reverse.parameter_declarations_fn)().fields.len(), 2); } + +#[test] +fn unit_vertex_cover_uses_registered_construction_and_solver() { + use crate::models::decision::DecisionCreateSpec; + type Unit = MinimumVertexCover; + let spec: DecisionCreateSpec = serde_json::from_value(serde_json::json!({ + "graph": {"num_vertices": 3, "edges": [[0, 1], [1, 2]]}, "bound": 1 + })) + .unwrap(); + let problem: Decision = spec.into(); + let solution = BruteForce::new().solve(&problem).unwrap().unwrap(); + assert_eq!(solution, vec![false, true, false]); + assert_eq!(problem.evaluate(&solution), Ok(Or(true))); + let restored: Decision = + serde_json::from_value(serde_json::to_value(&problem).unwrap()).unwrap(); + assert_eq!(restored.evaluate(&solution), Ok(Or(true))); + assert_eq!(problem.num_vertices(), 3); + assert_eq!(problem.num_edges(), 2); +} + +#[test] +fn decision_executed_result_maps_witness_and_bound_together() { + use crate::rules::{AggregateReductionResult, ReduceTo, ReductionResult}; + use crate::types::Min; + + let witness = vec![true, true, false]; + for bound in [1, 2] { + let decision = Decision::new(triangle_mvc(), bound); + let result = + as ReduceTo>>::reduce_to(&decision) + .unwrap(); + let target = ReductionResult::target_problem(&result); + assert!(std::ptr::eq( + target, + AggregateReductionResult::target_problem(&result), + )); + let value = target.evaluate(&witness).unwrap(); + assert_eq!(result.extract_value(value), Or(bound == 2)); + assert_eq!(result.extract_value(Min(None)), Or(false)); + if bound == 2 { + assert_eq!(result.extract_solution(&witness).unwrap(), witness); + } + } +} diff --git a/src/unit_tests/models/formula/circuit.rs b/src/unit_tests/models/formula/circuit.rs index 8e692d141..8616db9b5 100644 --- a/src/unit_tests/models/formula/circuit.rs +++ b/src/unit_tests/models/formula/circuit.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::traits::Problem; #[test] @@ -124,7 +123,10 @@ fn test_circuit_sat_creation() { )]); let problem = CircuitSAT::new(circuit); assert_eq!(problem.num_variables(), 3); // c, x, y - assert_eq!(problem.dimensions(), vec![2, 2, 2]); // binary variables + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2, 2, 2] + ); // binary variables } #[test] @@ -227,7 +229,10 @@ fn test_circuit_sat_problem() { let p = CircuitSAT::new(circuit); // Variables sorted: c, x, y - assert_eq!(p.dimensions(), vec![2, 2, 2]); + assert_eq!( + crate::solvers::cartesian_dimensions(&p).unwrap(), + vec![2, 2, 2] + ); // c=1, x=1, y=1: c = 1 AND 1 = 1 => satisfied assert!(p.evaluate(&vec![true, true, true]).unwrap()); diff --git a/src/unit_tests/models/formula/ksat.rs b/src/unit_tests/models/formula/ksat.rs index 4ec78e2a4..d2a58fbd3 100644 --- a/src/unit_tests/models/formula/ksat.rs +++ b/src/unit_tests/models/formula/ksat.rs @@ -1,9 +1,8 @@ use super::*; +include!("../../jl_helpers.rs"); use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::traits::Problem; use crate::variant::{K2, K3, KN}; -include!("../../jl_helpers.rs"); #[test] fn test_3sat_creation() { @@ -129,7 +128,10 @@ fn test_ksat_problem_v2() { ], ); - assert_eq!(p.dimensions(), vec![2, 2, 2]); + assert_eq!( + crate::solvers::cartesian_dimensions(&p).unwrap(), + vec![2, 2, 2] + ); assert!(p.evaluate(&vec![true, false, false]).unwrap()); assert!(!p.evaluate(&vec![true, true, true]).unwrap()); assert!(!p.evaluate(&vec![false, false, false]).unwrap()); @@ -146,7 +148,10 @@ fn test_ksat_problem_v2_2sat() { vec![CNFClause::new(vec![1, 2]), CNFClause::new(vec![-1, -2])], ); - assert_eq!(p.dimensions(), vec![2, 2]); + assert_eq!( + crate::solvers::cartesian_dimensions(&p).unwrap(), + vec![2, 2] + ); assert!(p.evaluate(&vec![true, false]).unwrap()); assert!(p.evaluate(&vec![false, true]).unwrap()); assert!(!p.evaluate(&vec![true, true]).unwrap()); diff --git a/src/unit_tests/models/formula/maximum_2_satisfiability.rs b/src/unit_tests/models/formula/maximum_2_satisfiability.rs index fec3102ca..a062b2932 100644 --- a/src/unit_tests/models/formula/maximum_2_satisfiability.rs +++ b/src/unit_tests/models/formula/maximum_2_satisfiability.rs @@ -1,7 +1,6 @@ use super::*; use crate::models::formula::CNFClause; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::traits::Problem; use crate::types::Max; @@ -25,7 +24,10 @@ fn test_maximum_2_satisfiability_creation() { let problem = issue_instance(); assert_eq!(problem.num_vars(), 4); assert_eq!(problem.num_clauses(), 7); - assert_eq!(problem.dimensions(), vec![2; 4]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 4] + ); } #[test] diff --git a/src/unit_tests/models/formula/nae_satisfiability.rs b/src/unit_tests/models/formula/nae_satisfiability.rs index 1e164b6ee..31f52e7b0 100644 --- a/src/unit_tests/models/formula/nae_satisfiability.rs +++ b/src/unit_tests/models/formula/nae_satisfiability.rs @@ -24,7 +24,7 @@ fn test_nae_satisfiability_creation() { assert_eq!(problem.num_vars(), 5); assert_eq!(problem.num_clauses(), 5); assert_eq!(problem.num_literals(), 15); - assert_eq!(problem.num_variables(), 5); + assert_eq!(problem.num_variables().unwrap(), 5); } #[test] diff --git a/src/unit_tests/models/formula/non_tautology.rs b/src/unit_tests/models/formula/non_tautology.rs index f3c41161a..d15c2d574 100644 --- a/src/unit_tests/models/formula/non_tautology.rs +++ b/src/unit_tests/models/formula/non_tautology.rs @@ -8,8 +8,11 @@ fn test_non_tautology_creation() { let problem = NonTautology::new(3, vec![vec![1, 2, 3], vec![-1, -2, -3]]).unwrap(); assert_eq!(problem.num_vars(), 3); assert_eq!(problem.num_disjuncts(), 2); - assert_eq!(problem.num_variables(), 3); - assert_eq!(problem.dimensions(), vec![2, 2, 2]); + assert_eq!(problem.num_variables().unwrap(), 3); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2, 2, 2] + ); } #[test] diff --git a/src/unit_tests/models/formula/one_in_three_satisfiability.rs b/src/unit_tests/models/formula/one_in_three_satisfiability.rs index 601ee47bc..fdcbcd8f3 100644 --- a/src/unit_tests/models/formula/one_in_three_satisfiability.rs +++ b/src/unit_tests/models/formula/one_in_three_satisfiability.rs @@ -15,8 +15,11 @@ fn test_one_in_three_satisfiability_creation() { ); assert_eq!(problem.num_vars(), 4); assert_eq!(problem.num_clauses(), 3); - assert_eq!(problem.num_variables(), 4); - assert_eq!(problem.dimensions(), vec![2, 2, 2, 2]); + assert_eq!(problem.num_variables().unwrap(), 4); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2, 2, 2, 2] + ); } #[test] diff --git a/src/unit_tests/models/formula/planar_3_satisfiability.rs b/src/unit_tests/models/formula/planar_3_satisfiability.rs index 7ecbe2c52..eac779e21 100644 --- a/src/unit_tests/models/formula/planar_3_satisfiability.rs +++ b/src/unit_tests/models/formula/planar_3_satisfiability.rs @@ -16,8 +16,11 @@ fn test_planar_3_satisfiability_creation() { ); assert_eq!(problem.num_vars(), 4); assert_eq!(problem.num_clauses(), 4); - assert_eq!(problem.num_variables(), 4); - assert_eq!(problem.dimensions(), vec![2, 2, 2, 2]); + assert_eq!(problem.num_variables().unwrap(), 4); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2, 2, 2, 2] + ); } #[test] diff --git a/src/unit_tests/models/formula/qbf.rs b/src/unit_tests/models/formula/qbf.rs index 32bee8687..cb4a35295 100644 --- a/src/unit_tests/models/formula/qbf.rs +++ b/src/unit_tests/models/formula/qbf.rs @@ -21,7 +21,7 @@ fn test_qbf_creation() { ); assert_eq!(problem.num_vars(), 3); assert_eq!(problem.num_clauses(), 2); - assert_eq!(problem.num_variables(), 0); + assert_eq!(problem.num_variables().unwrap(), 0); assert_eq!(problem.quantifiers().len(), 3); assert_eq!(problem.clauses().len(), 2); } @@ -47,7 +47,10 @@ fn test_qbf_evaluate_true() { ); // dims() is empty; evaluate([]) runs the game-tree search - assert_eq!(problem.dimensions(), Vec::::new()); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + Vec::::new() + ); assert!(problem.evaluate(&()).unwrap()); assert!(problem.is_true()); } @@ -131,7 +134,10 @@ fn test_qbf_zero_vars() { let problem = QuantifiedBooleanFormulas::new(0, vec![], vec![]); assert!(problem.evaluate(&()).unwrap()); assert!(problem.is_true()); - assert_eq!(problem.dimensions(), Vec::::new()); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + Vec::::new() + ); } #[test] @@ -204,7 +210,10 @@ fn test_qbf_serialization() { assert_eq!(deserialized.num_vars(), problem.num_vars()); assert_eq!(deserialized.num_clauses(), problem.num_clauses()); assert_eq!(deserialized.quantifiers(), problem.quantifiers()); - assert_eq!(deserialized.dimensions(), problem.dimensions()); + assert_eq!( + crate::solvers::cartesian_dimensions(&deserialized).unwrap(), + crate::solvers::cartesian_dimensions(&problem).unwrap() + ); } #[test] @@ -238,7 +247,10 @@ fn test_qbf_dims() { vec![CNFClause::new(vec![1, 2, 3, 4])], ); // dims() is always empty — QBF has no external config variables - assert_eq!(problem.dimensions(), Vec::::new()); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + Vec::::new() + ); } #[test] diff --git a/src/unit_tests/models/formula/sat.rs b/src/unit_tests/models/formula/sat.rs index 41621a4ca..006995b4b 100644 --- a/src/unit_tests/models/formula/sat.rs +++ b/src/unit_tests/models/formula/sat.rs @@ -1,8 +1,8 @@ use super::*; +include!("../../jl_helpers.rs"); use crate::solvers::BruteForce; use crate::solvers::BruteForceProblem as _; use crate::traits::Problem; -include!("../../jl_helpers.rs"); #[test] fn test_cnf_clause_creation() { @@ -40,7 +40,7 @@ fn test_sat_creation() { ); assert_eq!(problem.num_vars(), 3); assert_eq!(problem.num_clauses(), 2); - assert_eq!(problem.num_variables(), 3); + assert_eq!(problem.num_variables().unwrap(), 3); } #[test] @@ -149,7 +149,7 @@ fn test_is_satisfying_assignment_defaults() { #[test] fn test_num_variables() { let problem = Satisfiability::new(5, vec![CNFClause::new(vec![1])]); - assert_eq!(problem.num_variables(), 5); + assert_eq!(problem.num_variables().unwrap(), 5); } #[test] diff --git a/src/unit_tests/models/graph/acyclic_partition.rs b/src/unit_tests/models/graph/acyclic_partition.rs index 75ab99132..72548dd95 100644 --- a/src/unit_tests/models/graph/acyclic_partition.rs +++ b/src/unit_tests/models/graph/acyclic_partition.rs @@ -81,7 +81,10 @@ fn test_acyclic_partition_creation_and_accessors() { assert_eq!(problem.num_vertices(), 6); assert_eq!(problem.num_arcs(), 8); - assert_eq!(problem.dimensions(), vec![6; 6]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![6; 6] + ); assert_eq!(problem.graph().arcs().len(), 8); assert_eq!(problem.vertex_weights(), &[2, 3, 2, 1, 3, 1]); assert_eq!(problem.arc_costs(), &[1, 1, 1, 1, 1, 1, 1, 1]); @@ -211,7 +214,7 @@ fn test_acyclic_partition_serialization() { #[test] fn test_acyclic_partition_num_variables() { let problem = yes_instance(); - assert_eq!(problem.num_variables(), 6); + assert_eq!(problem.num_variables().unwrap(), 6); } #[test] diff --git a/src/unit_tests/models/graph/balanced_complete_bipartite_subgraph.rs b/src/unit_tests/models/graph/balanced_complete_bipartite_subgraph.rs index af225e573..c9b2ea52d 100644 --- a/src/unit_tests/models/graph/balanced_complete_bipartite_subgraph.rs +++ b/src/unit_tests/models/graph/balanced_complete_bipartite_subgraph.rs @@ -1,5 +1,4 @@ use super::*; -use crate::solvers::BruteForceProblem as _; #[test] fn create_spec_builds_bipartite_graph_and_rejects_invalid_edges() { @@ -80,7 +79,10 @@ fn test_balanced_complete_bipartite_subgraph_creation() { assert_eq!(problem.num_vertices(), 8); assert_eq!(problem.num_edges(), 10); assert_eq!(problem.k(), 2); - assert_eq!(problem.dimensions(), vec![2; 8]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 8] + ); } #[test] diff --git a/src/unit_tests/models/graph/biclique_cover.rs b/src/unit_tests/models/graph/biclique_cover.rs index ffcfb1f4a..c1d9e11b6 100644 --- a/src/unit_tests/models/graph/biclique_cover.rs +++ b/src/unit_tests/models/graph/biclique_cover.rs @@ -85,7 +85,7 @@ fn test_biclique_cover_creation() { assert_eq!(problem.num_vertices(), 4); assert_eq!(problem.num_edges(), 3); assert_eq!(problem.k(), 2); - assert_eq!(problem.num_variables(), 8); // 4 vertices * 2 bicliques + assert_eq!(problem.num_variables().unwrap(), 8); // 4 vertices * 2 bicliques } #[test] @@ -243,7 +243,10 @@ fn test_biclique_problem() { let problem = BicliqueCover::new(graph, 1); // dims: 4 vertices * 1 biclique = 4 binary variables - assert_eq!(problem.dimensions(), vec![2, 2, 2, 2]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2, 2, 2, 2] + ); // Valid cover: vertex 0 and vertex 2 in biclique 0 // Config: [v0_b0=1, v1_b0=0, v2_b0=1, v3_b0=0] @@ -311,7 +314,12 @@ fn test_complexity_includes_number_of_bicliques() { .find(|entry| entry.name == "BicliqueCover") .expect("BicliqueCover variant should be registered"); - assert_eq!(problem.dimensions().len(), 8); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem) + .unwrap() + .len(), + 8 + ); assert_eq!( (entry.complexity_eval_fn)(&problem as &dyn std::any::Any), 256.0 diff --git a/src/unit_tests/models/graph/biconnectivity_augmentation.rs b/src/unit_tests/models/graph/biconnectivity_augmentation.rs index eae27d5eb..0d9bf5f6d 100644 --- a/src/unit_tests/models/graph/biconnectivity_augmentation.rs +++ b/src/unit_tests/models/graph/biconnectivity_augmentation.rs @@ -28,8 +28,11 @@ fn test_biconnectivity_augmentation_creation() { assert_eq!(problem.num_vertices(), 4); assert_eq!(problem.num_edges(), 3); assert_eq!(problem.num_potential_edges(), 2); - assert_eq!(problem.dimensions(), vec![2, 2]); - assert_eq!(problem.num_variables(), 2); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2, 2] + ); + assert_eq!(problem.num_variables().unwrap(), 2); assert!(problem.is_weighted()); assert_eq!( as Problem>::NAME, diff --git a/src/unit_tests/models/graph/bottleneck_traveling_salesman.rs b/src/unit_tests/models/graph/bottleneck_traveling_salesman.rs index 9aa70a24d..1f55bf05e 100644 --- a/src/unit_tests/models/graph/bottleneck_traveling_salesman.rs +++ b/src/unit_tests/models/graph/bottleneck_traveling_salesman.rs @@ -34,8 +34,11 @@ fn test_bottleneck_traveling_salesman_creation_and_parameter_getters() { assert_eq!(problem.num_vertices(), 5); assert_eq!(problem.graph().num_edges(), 10); assert_eq!(problem.num_edges(), 10); - assert_eq!(problem.dimensions(), vec![2; 10]); - assert_eq!(problem.num_variables(), 10); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 10] + ); + assert_eq!(problem.num_variables().unwrap(), 10); assert_eq!(problem.weights(), vec![5, 4, 4, 5, 4, 1, 2, 1, 5, 4]); assert_eq!( problem.edges(), diff --git a/src/unit_tests/models/graph/bounded_component_spanning_forest.rs b/src/unit_tests/models/graph/bounded_component_spanning_forest.rs index 020d1bfbb..ae5a5e49d 100644 --- a/src/unit_tests/models/graph/bounded_component_spanning_forest.rs +++ b/src/unit_tests/models/graph/bounded_component_spanning_forest.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::topology::SimpleGraph; use crate::traits::Problem; use std::alloc::{GlobalAlloc, Layout, System}; @@ -104,7 +103,10 @@ fn test_bounded_component_spanning_forest_creation() { assert_eq!(problem.max_weight(), &6); assert_eq!(problem.num_vertices(), 8); assert_eq!(problem.num_edges(), 10); - assert_eq!(problem.dimensions(), vec![3; 8]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![3; 8] + ); assert!(problem.is_weighted()); } diff --git a/src/unit_tests/models/graph/bounded_diameter_spanning_tree.rs b/src/unit_tests/models/graph/bounded_diameter_spanning_tree.rs index 77c13c30f..d720743f7 100644 --- a/src/unit_tests/models/graph/bounded_diameter_spanning_tree.rs +++ b/src/unit_tests/models/graph/bounded_diameter_spanning_tree.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::topology::SimpleGraph; use crate::traits::Problem; @@ -26,7 +25,10 @@ fn test_bounded_diameter_spanning_tree_creation() { assert_eq!(problem.num_edges(), 7); assert_eq!(problem.weight_bound(), &5); assert_eq!(problem.diameter_bound(), 3); - assert_eq!(problem.dimensions(), vec![2; 7]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 7] + ); assert_eq!(problem.graph().num_vertices(), 5); assert_eq!(problem.edge_list().len(), 7); assert_eq!(problem.edge_weights().len(), 7); diff --git a/src/unit_tests/models/graph/degree_constrained_spanning_tree.rs b/src/unit_tests/models/graph/degree_constrained_spanning_tree.rs index 847436d94..bceb8e40f 100644 --- a/src/unit_tests/models/graph/degree_constrained_spanning_tree.rs +++ b/src/unit_tests/models/graph/degree_constrained_spanning_tree.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::topology::SimpleGraph; use crate::traits::Problem; @@ -21,7 +20,10 @@ fn test_degree_constrained_spanning_tree_creation() { assert_eq!(problem.num_vertices(), 5); assert_eq!(problem.num_edges(), 7); assert_eq!(problem.max_degree(), 2); - assert_eq!(problem.dimensions(), vec![2; 7]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 7] + ); assert_eq!(problem.graph().num_vertices(), 5); assert_eq!(problem.edge_list().len(), 7); } diff --git a/src/unit_tests/models/graph/directed_hamiltonian_path.rs b/src/unit_tests/models/graph/directed_hamiltonian_path.rs index d00fa1c5f..48fd4c763 100644 --- a/src/unit_tests/models/graph/directed_hamiltonian_path.rs +++ b/src/unit_tests/models/graph/directed_hamiltonian_path.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::topology::DirectedGraph; use crate::traits::Problem; @@ -12,7 +11,10 @@ fn test_directed_hamiltonian_path_creation() { assert_eq!(problem.num_vertices(), 4); assert_eq!(problem.num_arcs(), 3); // Lehmer dims: [4, 3, 2, 1] - assert_eq!(problem.dimensions(), vec![4, 3, 2, 1]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![4, 3, 2, 1] + ); } #[test] @@ -131,7 +133,10 @@ fn test_parameter_getters() { assert_eq!(problem.num_vertices(), 5); assert_eq!(problem.num_arcs(), 4); // Lehmer dims: [5, 4, 3, 2, 1] - assert_eq!(problem.dimensions(), vec![5, 4, 3, 2, 1]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![5, 4, 3, 2, 1] + ); } #[test] diff --git a/src/unit_tests/models/graph/directed_two_commodity_integral_flow.rs b/src/unit_tests/models/graph/directed_two_commodity_integral_flow.rs index 444a2e69f..c9850c139 100644 --- a/src/unit_tests/models/graph/directed_two_commodity_integral_flow.rs +++ b/src/unit_tests/models/graph/directed_two_commodity_integral_flow.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::topology::DirectedGraph; use crate::traits::Problem; @@ -36,8 +35,16 @@ fn test_directed_two_commodity_integral_flow_creation() { let problem = yes_instance(); assert_eq!(problem.num_vertices(), 6); assert_eq!(problem.num_arcs(), 8); - assert_eq!(problem.dimensions().len(), 16); // 2 * 8 - assert!(problem.dimensions().iter().all(|&d| d == 2)); // capacity 1 -> domain {0,1} + assert_eq!( + crate::solvers::cartesian_dimensions(&problem) + .unwrap() + .len(), + 16 + ); // 2 * 8 + assert!(crate::solvers::cartesian_dimensions(&problem) + .unwrap() + .iter() + .all(|&d| d == 2)); // capacity 1 -> domain {0,1} assert_eq!(problem.source_1(), 0); assert_eq!(problem.sink_1(), 4); assert_eq!(problem.source_2(), 1); @@ -207,7 +214,10 @@ fn test_directed_two_commodity_integral_flow_higher_capacity() { 1, 1, ); - assert_eq!(problem.dimensions(), vec![3, 3, 3, 3]); // each variable in {0,1,2} + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![3, 3, 3, 3] + ); // each variable in {0,1,2} // Both commodities can share: f1=1, f2=1 on both arcs let config = vec![1, 1, 1, 1]; diff --git a/src/unit_tests/models/graph/disjoint_connecting_paths.rs b/src/unit_tests/models/graph/disjoint_connecting_paths.rs index c1e93c582..bad4d1a8c 100644 --- a/src/unit_tests/models/graph/disjoint_connecting_paths.rs +++ b/src/unit_tests/models/graph/disjoint_connecting_paths.rs @@ -1,5 +1,4 @@ use super::*; -use crate::solvers::BruteForceProblem as _; #[test] fn create_spec_rejects_reused_terminal() { assert!( @@ -43,7 +42,10 @@ fn test_disjoint_connecting_paths_creation() { assert_eq!(problem.num_edges(), 7); assert_eq!(problem.num_pairs(), 2); assert_eq!(problem.terminal_pairs(), &[(0, 3), (2, 5)]); - assert_eq!(problem.dimensions(), vec![2; 7]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 7] + ); assert_eq!( problem.ordered_edges(), vec![(0, 1), (0, 2), (1, 3), (1, 4), (2, 4), (3, 5), (4, 5)] diff --git a/src/unit_tests/models/graph/eulerian_path.rs b/src/unit_tests/models/graph/eulerian_path.rs index 2b6724a77..ef7930041 100644 --- a/src/unit_tests/models/graph/eulerian_path.rs +++ b/src/unit_tests/models/graph/eulerian_path.rs @@ -21,8 +21,11 @@ fn test_eulerian_path_creation() { assert_eq!(problem.num_vertices(), 3); assert_eq!(problem.num_arcs(), 4); // m = 4 position variables, each with domain {0..3}. - assert_eq!(problem.dimensions(), vec![4, 4, 4, 4]); - assert_eq!(problem.num_variables(), 4); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![4, 4, 4, 4] + ); + assert_eq!(problem.num_variables().unwrap(), 4); } #[test] @@ -103,8 +106,11 @@ fn test_eulerian_path_empty_arcs_instance() { // m = 0 (only isolated vertices): dims = [] and the empty witness is valid. let graph = DirectedGraph::new(3, vec![]); let problem = EulerianPath::new(graph); - assert_eq!(problem.dimensions(), Vec::::new()); - assert_eq!(problem.num_variables(), 0); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + Vec::::new() + ); + assert_eq!(problem.num_variables().unwrap(), 0); assert_eq!(problem.evaluate(&vec![]).unwrap(), Or(true)); let solver = BruteForce::new(); diff --git a/src/unit_tests/models/graph/generalized_hex.rs b/src/unit_tests/models/graph/generalized_hex.rs index dad6efcb1..fb558b646 100644 --- a/src/unit_tests/models/graph/generalized_hex.rs +++ b/src/unit_tests/models/graph/generalized_hex.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::topology::SimpleGraph; use crate::traits::Problem; @@ -57,7 +56,10 @@ fn test_generalized_hex_creation_and_getters() { assert_eq!(problem.num_vertices(), 6); assert_eq!(problem.num_edges(), 7); assert_eq!(problem.num_playable_vertices(), 4); - assert_eq!(problem.dimensions(), Vec::::new()); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + Vec::::new() + ); assert_eq!(problem.graph().num_vertices(), 6); } diff --git a/src/unit_tests/models/graph/graph_partitioning.rs b/src/unit_tests/models/graph/graph_partitioning.rs index e29077c1a..a8d572c74 100644 --- a/src/unit_tests/models/graph/graph_partitioning.rs +++ b/src/unit_tests/models/graph/graph_partitioning.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::topology::SimpleGraph; use crate::traits::Problem; @@ -29,7 +28,10 @@ fn test_graphpartitioning_basic() { let problem = issue_example(); // Check dims: 6 binary variables - assert_eq!(problem.dimensions(), vec![2, 2, 2, 2, 2, 2]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2, 2, 2, 2, 2, 2] + ); // Evaluate a valid balanced partition: A={0,1,2}, B={3,4,5} // config: [0, 0, 0, 1, 1, 1] diff --git a/src/unit_tests/models/graph/hamiltonian_circuit.rs b/src/unit_tests/models/graph/hamiltonian_circuit.rs index fc2d684cb..2ec72a576 100644 --- a/src/unit_tests/models/graph/hamiltonian_circuit.rs +++ b/src/unit_tests/models/graph/hamiltonian_circuit.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::topology::SimpleGraph; use crate::traits::Problem; @@ -26,7 +25,10 @@ fn test_hamiltonian_circuit_basic() { assert_eq!(problem.num_vertices(), 6); assert_eq!(problem.num_edges(), 9); - assert_eq!(problem.dimensions(), vec![6; 6]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![6; 6] + ); // Valid Hamiltonian circuit: 0->1->2->5->4->3->0 // Edges used: (0,1), (1,2), (2,5), (5,4), (4,3), (3,0) -- all present @@ -129,7 +131,10 @@ fn test_hamiltonian_circuit_serialization() { let json = serde_json::to_string(&problem).unwrap(); let restored: HamiltonianCircuit = serde_json::from_str(&json).unwrap(); - assert_eq!(problem.dimensions(), restored.dimensions()); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + crate::solvers::cartesian_dimensions(&restored).unwrap() + ); // Valid circuit gives the same result on both instances assert_eq!( diff --git a/src/unit_tests/models/graph/hamiltonian_path.rs b/src/unit_tests/models/graph/hamiltonian_path.rs index c86819a08..4e55d9cfa 100644 --- a/src/unit_tests/models/graph/hamiltonian_path.rs +++ b/src/unit_tests/models/graph/hamiltonian_path.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::topology::SimpleGraph; #[test] @@ -11,7 +10,10 @@ fn test_hamiltonian_path_basic() { let problem = HamiltonianPath::new(SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)])); assert_eq!(problem.num_vertices(), 4); assert_eq!(problem.num_edges(), 3); - assert_eq!(problem.dimensions(), vec![4, 4, 4, 4]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![4, 4, 4, 4] + ); // Valid path: 0->1->2->3 assert!(problem.evaluate(&vec![0, 1, 2, 3]).unwrap()); diff --git a/src/unit_tests/models/graph/hamiltonian_path_between_two_vertices.rs b/src/unit_tests/models/graph/hamiltonian_path_between_two_vertices.rs index e2e22856f..87450667c 100644 --- a/src/unit_tests/models/graph/hamiltonian_path_between_two_vertices.rs +++ b/src/unit_tests/models/graph/hamiltonian_path_between_two_vertices.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::topology::SimpleGraph; #[test] @@ -17,7 +16,10 @@ fn test_hamiltonian_path_between_two_vertices_basic() { assert_eq!(problem.num_edges(), 3); assert_eq!(problem.source_vertex(), 0); assert_eq!(problem.target_vertex(), 3); - assert_eq!(problem.dimensions(), vec![4, 4, 4, 4]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![4, 4, 4, 4] + ); // Valid path: 0->1->2->3 assert!(problem.evaluate(&vec![0, 1, 2, 3]).unwrap()); diff --git a/src/unit_tests/models/graph/highly_connected_deletion.rs b/src/unit_tests/models/graph/highly_connected_deletion.rs index f0bffa6cb..290089933 100644 --- a/src/unit_tests/models/graph/highly_connected_deletion.rs +++ b/src/unit_tests/models/graph/highly_connected_deletion.rs @@ -28,8 +28,11 @@ fn test_highly_connected_deletion_creation() { assert_eq!(problem.graph().num_edges(), 4); assert_eq!(problem.num_vertices(), 4); assert_eq!(problem.num_edges(), 4); - assert_eq!(problem.dimensions(), vec![2, 2, 2, 2]); - assert_eq!(problem.num_variables(), 4); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2, 2, 2, 2] + ); + assert_eq!(problem.num_variables().unwrap(), 4); } #[test] diff --git a/src/unit_tests/models/graph/integral_flow_bundles.rs b/src/unit_tests/models/graph/integral_flow_bundles.rs index d8c7b3f79..31c7a1b2f 100644 --- a/src/unit_tests/models/graph/integral_flow_bundles.rs +++ b/src/unit_tests/models/graph/integral_flow_bundles.rs @@ -1,5 +1,4 @@ use super::*; -use crate::solvers::BruteForceProblem as _; #[test] fn create_spec_requires_bundle_coverage() { assert!( @@ -61,7 +60,10 @@ fn test_integral_flow_bundles_creation_and_getters() { #[test] fn test_integral_flow_bundles_dims_use_tight_arc_bounds() { let problem = yes_instance(); - assert_eq!(problem.dimensions(), vec![2, 2, 2, 2, 2, 2]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2, 2, 2, 2, 2, 2] + ); } #[test] diff --git a/src/unit_tests/models/graph/integral_flow_homologous_arcs.rs b/src/unit_tests/models/graph/integral_flow_homologous_arcs.rs index e15116232..7e2367016 100644 --- a/src/unit_tests/models/graph/integral_flow_homologous_arcs.rs +++ b/src/unit_tests/models/graph/integral_flow_homologous_arcs.rs @@ -1,5 +1,4 @@ use super::*; -use crate::solvers::BruteForceProblem as _; #[test] fn create_spec_defaults_capacities() { let problem = IntegralFlowHomologousArcs::try_from(IntegralFlowHomologousArcsCreateSpec { @@ -50,7 +49,10 @@ fn test_integral_flow_homologous_arcs_creation() { assert_eq!(problem.requirement(), 2); assert_eq!(problem.max_capacity(), 1); assert_eq!(problem.homologous_pairs(), &[(2, 5), (4, 3)]); - assert_eq!(problem.dimensions(), vec![2; 8]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 8] + ); } #[test] @@ -141,7 +143,10 @@ fn test_integral_flow_homologous_arcs_non_unit_capacity() { // equal flow. R=2 is satisfiable: f=[2,2]. let graph = DirectedGraph::new(3, vec![(0, 1), (1, 2)]); let problem = IntegralFlowHomologousArcs::new(graph, vec![3, 3], 0, 2, 2, vec![(0, 1)]); - assert_eq!(problem.dimensions(), vec![4, 4]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![4, 4] + ); assert_eq!(problem.max_capacity(), 3); assert!(problem.evaluate(&vec![2, 2]).unwrap()); assert!(problem.evaluate(&vec![3, 3]).unwrap()); 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 394d59128..54e2f3e1d 100644 --- a/src/unit_tests/models/graph/integral_flow_with_multipliers.rs +++ b/src/unit_tests/models/graph/integral_flow_with_multipliers.rs @@ -1,5 +1,4 @@ use super::*; -use crate::solvers::BruteForceProblem as _; #[test] fn create_spec_rejects_zero_internal_multiplier() { assert!( @@ -69,7 +68,7 @@ fn test_integral_flow_with_multipliers_creation_accessors_and_dimensions() { assert_eq!(problem.multipliers(), &[1, 2, 3, 4, 5, 6, 4, 1]); assert_eq!(problem.capacities(), &[1, 1, 1, 1, 1, 1, 2, 3, 4, 5, 6, 4]); assert_eq!( - problem.dimensions(), + crate::solvers::cartesian_dimensions(&problem).unwrap(), vec![2, 2, 2, 2, 2, 2, 3, 4, 5, 6, 7, 5] ); } diff --git a/src/unit_tests/models/graph/isomorphic_spanning_tree.rs b/src/unit_tests/models/graph/isomorphic_spanning_tree.rs index 3f7f244c5..91d5064ee 100644 --- a/src/unit_tests/models/graph/isomorphic_spanning_tree.rs +++ b/src/unit_tests/models/graph/isomorphic_spanning_tree.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::topology::{Graph, SimpleGraph}; use crate::traits::Problem; @@ -12,7 +11,10 @@ fn test_isomorphicspanningtree_basic() { let problem: IsomorphicSpanningTree = IsomorphicSpanningTree::new(graph.clone(), tree.clone()); - assert_eq!(problem.dimensions(), vec![3, 3, 3]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![3, 3, 3] + ); assert_eq!(problem.graph(), &graph); assert_eq!(problem.tree(), &tree); assert_eq!(problem.num_vertices(), 3); diff --git a/src/unit_tests/models/graph/kclique.rs b/src/unit_tests/models/graph/kclique.rs index 910275599..488ddfebe 100644 --- a/src/unit_tests/models/graph/kclique.rs +++ b/src/unit_tests/models/graph/kclique.rs @@ -1,5 +1,4 @@ use super::*; -use crate::solvers::BruteForceProblem as _; #[test] fn create_spec_rejects_k_above_vertex_count() { assert!(KClique::try_from(KCliqueCreateSpec { @@ -30,7 +29,10 @@ fn test_kclique_creation() { assert_eq!(problem.k(), 3); assert_eq!(problem.num_vertices(), 5); assert_eq!(problem.num_edges(), 6); - assert_eq!(problem.dimensions(), vec![2; 5]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 5] + ); } #[test] diff --git a/src/unit_tests/models/graph/kcoloring.rs b/src/unit_tests/models/graph/kcoloring.rs index 588c5721e..9c377c0fc 100644 --- a/src/unit_tests/models/graph/kcoloring.rs +++ b/src/unit_tests/models/graph/kcoloring.rs @@ -1,5 +1,4 @@ use super::*; -use crate::solvers::BruteForceProblem as _; #[test] fn create_specs_separate_runtime_and_fixed_color_counts() { @@ -32,10 +31,10 @@ fn fixed_and_runtime_variants_report_num_colors_parameter() { as Problem>::parameter_names() ); } +include!("../../jl_helpers.rs"); use crate::solvers::BruteForce; use crate::topology::SimpleGraph; use crate::variant::{K1, K2, K3, K4}; -include!("../../jl_helpers.rs"); #[test] fn test_kcoloring_creation() { @@ -43,7 +42,10 @@ fn test_kcoloring_creation() { assert_eq!(problem.graph().num_vertices(), 4); assert_eq!(problem.graph().num_edges(), 3); assert_eq!(problem.num_colors(), 3); - assert_eq!(problem.dimensions(), vec![3, 3, 3, 3]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![3, 3, 3, 3] + ); } #[test] @@ -176,7 +178,10 @@ fn test_kcoloring_problem() { // Triangle graph with 3 colors let p = KColoring::::new(SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)])); - assert_eq!(p.dimensions(), vec![3, 3, 3]); + assert_eq!( + crate::solvers::cartesian_dimensions(&p).unwrap(), + vec![3, 3, 3] + ); // Valid: each vertex different color assert!(p.evaluate(&vec![0, 1, 2]).unwrap()); // Invalid: vertices 0 and 1 same color diff --git a/src/unit_tests/models/graph/kernel.rs b/src/unit_tests/models/graph/kernel.rs index 3e4ed36f7..4efaeaa50 100644 --- a/src/unit_tests/models/graph/kernel.rs +++ b/src/unit_tests/models/graph/kernel.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::topology::DirectedGraph; use crate::traits::Problem; @@ -13,7 +12,10 @@ fn test_kernel_creation() { let problem = Kernel::new(graph); assert_eq!(problem.num_vertices(), 5); assert_eq!(problem.num_arcs(), 7); - assert_eq!(problem.dimensions(), vec![2, 2, 2, 2, 2]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2, 2, 2, 2, 2] + ); } #[test] diff --git a/src/unit_tests/models/graph/kth_best_spanning_tree.rs b/src/unit_tests/models/graph/kth_best_spanning_tree.rs index bb266893d..ed5380b86 100644 --- a/src/unit_tests/models/graph/kth_best_spanning_tree.rs +++ b/src/unit_tests/models/graph/kth_best_spanning_tree.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::topology::SimpleGraph; use crate::traits::Problem; @@ -37,7 +36,10 @@ fn yes_witness_config() -> Vec> { fn test_kthbestspanningtree_creation() { let problem = yes_instance(); - assert_eq!(problem.dimensions(), vec![2; 12]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 12] + ); assert_eq!(problem.graph().num_vertices(), 4); assert_eq!(problem.graph().num_edges(), 6); assert_eq!(problem.num_vertices(), 4); diff --git a/src/unit_tests/models/graph/length_bounded_disjoint_paths.rs b/src/unit_tests/models/graph/length_bounded_disjoint_paths.rs index 619d0d892..9e01688e4 100644 --- a/src/unit_tests/models/graph/length_bounded_disjoint_paths.rs +++ b/src/unit_tests/models/graph/length_bounded_disjoint_paths.rs @@ -35,7 +35,10 @@ fn test_length_bounded_disjoint_paths_creation() { assert_eq!(problem.max_paths(), 3); assert_eq!(problem.max_length(), 3); // 3 slots * 6 edges = 18 binary variables - assert_eq!(problem.dimensions(), vec![2; 18]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 18] + ); } #[test] @@ -196,7 +199,7 @@ fn test_length_bounded_disjoint_paths_graph_getter() { #[test] fn test_length_bounded_disjoint_paths_num_variables() { let problem = sample_problem(); - assert_eq!(problem.num_variables(), 18); + assert_eq!(problem.num_variables().unwrap(), 18); } #[test] diff --git a/src/unit_tests/models/graph/longest_circuit.rs b/src/unit_tests/models/graph/longest_circuit.rs index 1789804b2..5f8d3119b 100644 --- a/src/unit_tests/models/graph/longest_circuit.rs +++ b/src/unit_tests/models/graph/longest_circuit.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::topology::SimpleGraph; use crate::traits::Problem; use crate::types::Max; @@ -32,7 +31,10 @@ fn test_longest_circuit_creation() { assert_eq!(problem.num_vertices(), 6); assert_eq!(problem.num_edges(), 10); assert_eq!(problem.edge_lengths(), &[3, 2, 4, 1, 5, 2, 3, 2, 1, 2]); - assert_eq!(problem.dimensions(), vec![2; 10]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 10] + ); assert!(problem.is_weighted()); } diff --git a/src/unit_tests/models/graph/longest_path.rs b/src/unit_tests/models/graph/longest_path.rs index f3d81c1c8..dbbc4f69a 100644 --- a/src/unit_tests/models/graph/longest_path.rs +++ b/src/unit_tests/models/graph/longest_path.rs @@ -1,5 +1,4 @@ use super::*; -use crate::solvers::BruteForceProblem as _; #[test] fn create_spec_rejects_nonpositive_lengths() { assert!(LongestPath::try_from(LongestPathI64CreateSpec { @@ -61,7 +60,10 @@ fn test_longest_path_creation() { assert_eq!(problem.num_edges(), 10); assert_eq!(problem.source_vertex(), 0); assert_eq!(problem.target_vertex(), 6); - assert_eq!(problem.dimensions(), vec![2; 10]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 10] + ); assert_eq!(problem.edge_lengths(), &[3, 2, 4, 1, 5, 2, 3, 2, 4, 1]); assert!(problem.is_weighted()); diff --git a/src/unit_tests/models/graph/max_cut.rs b/src/unit_tests/models/graph/max_cut.rs index cb7497e77..863cf42cc 100644 --- a/src/unit_tests/models/graph/max_cut.rs +++ b/src/unit_tests/models/graph/max_cut.rs @@ -1,8 +1,7 @@ use super::*; +include!("../../jl_helpers.rs"); use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::topology::SimpleGraph; -include!("../../jl_helpers.rs"); #[test] fn test_maxcut_creation() { @@ -12,7 +11,10 @@ fn test_maxcut_creation() { ); assert_eq!(problem.graph().num_vertices(), 4); assert_eq!(problem.graph().num_edges(), 3); - assert_eq!(problem.dimensions(), vec![2, 2, 2, 2]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2, 2, 2, 2] + ); } #[test] diff --git a/src/unit_tests/models/graph/maximal_is.rs b/src/unit_tests/models/graph/maximal_is.rs index f68ef7c18..7f51f0fb8 100644 --- a/src/unit_tests/models/graph/maximal_is.rs +++ b/src/unit_tests/models/graph/maximal_is.rs @@ -10,9 +10,9 @@ fn create_spec_rejects_weight_count_mismatch() { }); assert!(result.is_err()); } +include!("../../jl_helpers.rs"); use crate::solvers::BruteForce; use crate::topology::SimpleGraph; -include!("../../jl_helpers.rs"); #[test] fn test_maximal_is_creation() { @@ -22,8 +22,11 @@ fn test_maximal_is_creation() { ); assert_eq!(problem.graph().num_vertices(), 4); assert_eq!(problem.graph().num_edges(), 3); - assert_eq!(problem.num_variables(), 4); - assert_eq!(problem.dimensions(), vec![2; 4]); + assert_eq!(problem.num_variables().unwrap(), 4); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 4] + ); } #[test] diff --git a/src/unit_tests/models/graph/maximum_achromatic_number.rs b/src/unit_tests/models/graph/maximum_achromatic_number.rs index 9bafd0ea9..8a19aaee5 100644 --- a/src/unit_tests/models/graph/maximum_achromatic_number.rs +++ b/src/unit_tests/models/graph/maximum_achromatic_number.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::topology::SimpleGraph; use crate::traits::Problem; use crate::types::Max; @@ -12,7 +11,10 @@ fn test_maximum_achromatic_number_c6() { let problem = MaximumAchromaticNumber::new(graph); assert_eq!(problem.num_vertices(), 6); assert_eq!(problem.num_edges(), 6); - assert_eq!(problem.dimensions(), vec![6; 6]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![6; 6] + ); // [0,1,2,0,1,2] is a valid complete proper 3-coloring let config = vec![0, 1, 2, 0, 1, 2]; diff --git a/src/unit_tests/models/graph/maximum_clique.rs b/src/unit_tests/models/graph/maximum_clique.rs index f83c6107d..76e182aac 100644 --- a/src/unit_tests/models/graph/maximum_clique.rs +++ b/src/unit_tests/models/graph/maximum_clique.rs @@ -1,5 +1,4 @@ use super::*; -use crate::solvers::BruteForceProblem as _; #[test] fn create_spec_rejects_weight_count_mismatch() { @@ -22,7 +21,10 @@ fn test_clique_creation() { ); assert_eq!(problem.graph().num_vertices(), 4); assert_eq!(problem.graph().num_edges(), 3); - assert_eq!(problem.dimensions(), vec![2, 2, 2, 2]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2, 2, 2, 2] + ); } #[test] @@ -291,7 +293,10 @@ fn test_clique_problem() { SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]), vec![1i64; 3], ); - assert_eq!(p.dimensions(), vec![2, 2, 2]); + assert_eq!( + crate::solvers::cartesian_dimensions(&p).unwrap(), + vec![2, 2, 2] + ); // Valid clique: select all 3 vertices (triangle is a clique) assert_eq!(p.evaluate(&vec![true, true, true]).unwrap(), Max(Some(3))); // Valid clique: select just vertex 0 diff --git a/src/unit_tests/models/graph/maximum_co_k_plex.rs b/src/unit_tests/models/graph/maximum_co_k_plex.rs index f92e1eaf2..711c1a05b 100644 --- a/src/unit_tests/models/graph/maximum_co_k_plex.rs +++ b/src/unit_tests/models/graph/maximum_co_k_plex.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::topology::SimpleGraph; use crate::traits::Problem; use crate::types::{Max, One}; @@ -34,7 +33,10 @@ fn test_maximum_co_k_plex_creation() { assert_eq!(problem.graph().num_edges(), 5); assert_eq!(problem.weights(), &[5, 1, 4, 1, 3]); assert_eq!(problem.bound_k(), 2); - assert_eq!(problem.dimensions(), vec![2; 5]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 5] + ); assert_eq!(problem.num_vertices(), 5); assert_eq!(problem.num_edges(), 5); assert!(problem.is_weighted()); diff --git a/src/unit_tests/models/graph/maximum_common_edge_subgraph.rs b/src/unit_tests/models/graph/maximum_common_edge_subgraph.rs index 00118979c..924d142e7 100644 --- a/src/unit_tests/models/graph/maximum_common_edge_subgraph.rs +++ b/src/unit_tests/models/graph/maximum_common_edge_subgraph.rs @@ -41,8 +41,11 @@ fn test_maximum_common_edge_subgraph_creation() { assert_eq!(problem.num_arcs_2(), 6); assert_eq!(problem.bottom_index(), 4); // dims must be [|V2| + 1; |V1|] = [5; 5]. - assert_eq!(problem.dimensions(), vec![5; 5]); - assert_eq!(problem.num_variables(), 5); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![5; 5] + ); + assert_eq!(problem.num_variables().unwrap(), 5); } #[test] diff --git a/src/unit_tests/models/graph/maximum_contact_map_overlap.rs b/src/unit_tests/models/graph/maximum_contact_map_overlap.rs index 03cb0e25d..2976a7617 100644 --- a/src/unit_tests/models/graph/maximum_contact_map_overlap.rs +++ b/src/unit_tests/models/graph/maximum_contact_map_overlap.rs @@ -20,8 +20,11 @@ fn test_maximum_contact_map_overlap_creation() { assert_eq!(problem.num_contacts_1(), 2); assert_eq!(problem.num_contacts_2(), 3); // dims must be [|V_2| + 1; |V_1|] = [6; 4]. - assert_eq!(problem.dimensions(), vec![6; 4]); - assert_eq!(problem.num_variables(), 4); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![6; 4] + ); + assert_eq!(problem.num_variables().unwrap(), 4); // Contacts get normalized so the smaller endpoint comes first. let contacts_2 = problem.contacts_2(); assert!(contacts_2.contains(&(0, 2))); diff --git a/src/unit_tests/models/graph/maximum_domatic_number.rs b/src/unit_tests/models/graph/maximum_domatic_number.rs index f922d8cea..46d09bb50 100644 --- a/src/unit_tests/models/graph/maximum_domatic_number.rs +++ b/src/unit_tests/models/graph/maximum_domatic_number.rs @@ -11,8 +11,11 @@ fn test_maximum_domatic_number_creation() { let problem = MaximumDomaticNumber::new(graph); assert_eq!(problem.graph().num_vertices(), 4); assert_eq!(problem.graph().num_edges(), 3); - assert_eq!(problem.num_variables(), 4); - assert_eq!(problem.dimensions(), vec![4; 4]); + assert_eq!(problem.num_variables().unwrap(), 4); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![4; 4] + ); } #[test] diff --git a/src/unit_tests/models/graph/maximum_edge_weighted_k_clique.rs b/src/unit_tests/models/graph/maximum_edge_weighted_k_clique.rs index a00cd688c..dd84986cb 100644 --- a/src/unit_tests/models/graph/maximum_edge_weighted_k_clique.rs +++ b/src/unit_tests/models/graph/maximum_edge_weighted_k_clique.rs @@ -35,8 +35,11 @@ fn test_maximum_edge_weighted_k_clique_creation() { assert_eq!(problem.num_edges(), 5); assert_eq!(problem.k(), 3); assert_eq!(problem.edge_weights(), &[5, 4, -1, 1, 0]); - assert_eq!(problem.dimensions(), vec![2; 4]); - assert_eq!(problem.num_variables(), 4); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 4] + ); + assert_eq!(problem.num_variables().unwrap(), 4); assert!(problem.graph().has_edge(0, 1)); assert!(!problem.graph().has_edge(2, 3)); } diff --git a/src/unit_tests/models/graph/maximum_independent_set.rs b/src/unit_tests/models/graph/maximum_independent_set.rs index e5fb725ca..abdb3b7a4 100644 --- a/src/unit_tests/models/graph/maximum_independent_set.rs +++ b/src/unit_tests/models/graph/maximum_independent_set.rs @@ -1,5 +1,4 @@ use super::*; -use crate::solvers::BruteForceProblem as _; #[test] fn create_spec_defaults_simple_weights() { let problem = MaximumIndependentSet::try_from(MaximumIndependentSetSimpleI64CreateSpec { @@ -10,10 +9,10 @@ fn create_spec_defaults_simple_weights() { .unwrap(); assert_eq!(problem.weights(), &[1, 1, 1]); } +include!("../../jl_helpers.rs"); use crate::solvers::BruteForce; use crate::topology::SimpleGraph; use crate::traits::Problem; -include!("../../jl_helpers.rs"); #[test] fn test_independent_set_creation() { @@ -23,7 +22,12 @@ fn test_independent_set_creation() { ); assert_eq!(problem.graph().num_vertices(), 4); assert_eq!(problem.graph().num_edges(), 3); - assert_eq!(problem.dimensions().len(), 4); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem) + .unwrap() + .len(), + 4 + ); } #[test] diff --git a/src/unit_tests/models/graph/maximum_leaf_spanning_tree.rs b/src/unit_tests/models/graph/maximum_leaf_spanning_tree.rs index 4698599ce..d73026687 100644 --- a/src/unit_tests/models/graph/maximum_leaf_spanning_tree.rs +++ b/src/unit_tests/models/graph/maximum_leaf_spanning_tree.rs @@ -1,5 +1,4 @@ use super::*; -use crate::solvers::BruteForceProblem as _; use crate::{solvers::BruteForce, topology::SimpleGraph, traits::Problem}; /// Issue #897 example: 6 vertices, 9 edges. @@ -29,8 +28,16 @@ fn test_maximum_leaf_spanning_tree_creation() { assert_eq!(problem.graph().num_edges(), 9); assert_eq!(problem.num_vertices(), 6); assert_eq!(problem.num_edges(), 9); - assert_eq!(problem.dimensions().len(), 9); - assert!(problem.dimensions().iter().all(|&d| d == 2)); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem) + .unwrap() + .len(), + 9 + ); + assert!(crate::solvers::cartesian_dimensions(&problem) + .unwrap() + .iter() + .all(|&d| d == 2)); } #[test] @@ -128,7 +135,10 @@ fn test_maximum_leaf_spanning_tree_small_path() { // Path graph P3: 0-1-2, only spanning tree is the path itself -> 2 leaves let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); let problem = MaximumLeafSpanningTree::new(graph); - assert_eq!(problem.dimensions(), vec![2, 2]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2, 2] + ); let config = vec![true, true]; assert_eq!(problem.evaluate(&config).unwrap(), Max(Some(2))); } diff --git a/src/unit_tests/models/graph/maximum_matching.rs b/src/unit_tests/models/graph/maximum_matching.rs index d39e31e4c..c06d8a30a 100644 --- a/src/unit_tests/models/graph/maximum_matching.rs +++ b/src/unit_tests/models/graph/maximum_matching.rs @@ -1,10 +1,10 @@ use super::*; +include!("../../jl_helpers.rs"); use crate::solvers::BruteForce; use crate::solvers::BruteForceProblem as _; use crate::topology::SimpleGraph; use crate::traits::Problem; use crate::types::Max; -include!("../../jl_helpers.rs"); #[test] fn test_matching_creation() { @@ -14,7 +14,7 @@ fn test_matching_creation() { ); assert_eq!(problem.graph().num_vertices(), 4); assert_eq!(problem.graph().num_edges(), 3); - assert_eq!(problem.num_variables(), 3); + assert_eq!(problem.num_variables().unwrap(), 3); } #[test] diff --git a/src/unit_tests/models/graph/min_max_multicenter.rs b/src/unit_tests/models/graph/min_max_multicenter.rs index 8db1f88fa..a98630e6f 100644 --- a/src/unit_tests/models/graph/min_max_multicenter.rs +++ b/src/unit_tests/models/graph/min_max_multicenter.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::topology::SimpleGraph; use crate::traits::Problem; use crate::types::Min; @@ -24,7 +23,10 @@ fn test_minmaxmulticenter_basic() { assert_eq!(problem.k(), 2); assert_eq!(problem.vertex_weights(), &[1, 1, 1, 1, 1, 1]); assert_eq!(problem.edge_lengths(), &[1, 1, 1, 1, 1, 1, 1]); - assert_eq!(problem.dimensions(), vec![2; 6]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 6] + ); assert_eq!(problem.num_vertices(), 6); assert_eq!(problem.num_edges(), 7); assert_eq!(problem.num_centers(), 2); diff --git a/src/unit_tests/models/graph/minimum_capacitated_spanning_tree.rs b/src/unit_tests/models/graph/minimum_capacitated_spanning_tree.rs index 3576d79b5..a11302c6e 100644 --- a/src/unit_tests/models/graph/minimum_capacitated_spanning_tree.rs +++ b/src/unit_tests/models/graph/minimum_capacitated_spanning_tree.rs @@ -1,5 +1,4 @@ use super::*; -use crate::solvers::BruteForceProblem as _; #[test] fn create_spec_defaults_edge_weights() { @@ -55,7 +54,12 @@ fn test_creation() { assert_eq!(problem.root(), 0); assert_eq!(problem.requirements(), &[0, 1, 1, 1, 1]); assert_eq!(*problem.capacity(), 3); - assert_eq!(problem.dimensions().len(), 8); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem) + .unwrap() + .len(), + 8 + ); assert!(problem.is_weighted()); } diff --git a/src/unit_tests/models/graph/minimum_cost_circulation.rs b/src/unit_tests/models/graph/minimum_cost_circulation.rs index c22563730..97dedae29 100644 --- a/src/unit_tests/models/graph/minimum_cost_circulation.rs +++ b/src/unit_tests/models/graph/minimum_cost_circulation.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::topology::DirectedGraph; use crate::traits::Problem; use crate::types::Min; @@ -32,7 +31,10 @@ fn test_minimum_cost_circulation_creation() { assert_eq!(problem.num_arcs(), 4); assert_eq!(problem.capacities(), &[2, 2, 1, 1]); assert_eq!(problem.costs(), &[2, -3, 1, -4]); - assert_eq!(problem.dimensions(), vec![3, 3, 2, 2]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![3, 3, 2, 2] + ); assert_eq!( ::NAME, "MinimumCostCirculation" diff --git a/src/unit_tests/models/graph/minimum_cost_maximum_flow.rs b/src/unit_tests/models/graph/minimum_cost_maximum_flow.rs index 5c56ee644..26802d61c 100644 --- a/src/unit_tests/models/graph/minimum_cost_maximum_flow.rs +++ b/src/unit_tests/models/graph/minimum_cost_maximum_flow.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::topology::DirectedGraph; use crate::traits::Problem; use crate::types::Min; @@ -58,7 +57,10 @@ fn test_minimum_cost_maximum_flow_creation() { assert_eq!(problem.sink(), 3); assert_eq!(problem.capacities(), &[2, 1, 1, 1, 2]); assert_eq!(problem.costs(), &[1, 0, 0, 1, 2]); - assert_eq!(problem.dimensions(), vec![3, 2, 2, 2, 3]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![3, 2, 2, 2, 3] + ); assert_eq!( ::NAME, "MinimumCostMaximumFlow" diff --git a/src/unit_tests/models/graph/minimum_covering_by_cliques.rs b/src/unit_tests/models/graph/minimum_covering_by_cliques.rs index e2916b241..436bf83b9 100644 --- a/src/unit_tests/models/graph/minimum_covering_by_cliques.rs +++ b/src/unit_tests/models/graph/minimum_covering_by_cliques.rs @@ -11,9 +11,12 @@ fn test_minimum_covering_by_cliques_creation() { let problem = MinimumCoveringByCliques::new(graph); assert_eq!(problem.num_vertices(), 4); assert_eq!(problem.num_edges(), 3); - assert_eq!(problem.num_variables(), 3); + assert_eq!(problem.num_variables().unwrap(), 3); // Each edge can be assigned to one of 3 groups - assert_eq!(problem.dimensions(), vec![3; 3]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![3; 3] + ); } #[test] diff --git a/src/unit_tests/models/graph/minimum_cut_into_bounded_sets.rs b/src/unit_tests/models/graph/minimum_cut_into_bounded_sets.rs index 84f6d6aab..632c0e8f0 100644 --- a/src/unit_tests/models/graph/minimum_cut_into_bounded_sets.rs +++ b/src/unit_tests/models/graph/minimum_cut_into_bounded_sets.rs @@ -1,5 +1,4 @@ use super::*; -use crate::solvers::BruteForceProblem as _; #[test] fn create_spec_defaults_edge_weights() { @@ -16,7 +15,7 @@ fn create_spec_defaults_edge_weights() { use crate::solvers::BruteForce; use crate::topology::SimpleGraph; use crate::traits::Problem; -use crate::types::{Min, SolutionAggregate}; +use crate::types::Min; /// Build the example instance from issue #228: /// 8 vertices, 12 edges, s=0, t=7, B=5 @@ -50,7 +49,10 @@ fn test_minimumcutintoboundedsets_basic() { assert_eq!(problem.source(), 0); assert_eq!(problem.sink(), 7); assert_eq!(problem.size_bound(), 5); - assert_eq!(problem.dimensions(), vec![2; 8]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 8] + ); } #[test] @@ -192,9 +194,3 @@ fn test_minimumcutintoboundedsets_variant() { assert!(variant.iter().any(|(k, _)| *k == "graph")); assert!(variant.iter().any(|(k, _)| *k == "weight")); } - -#[test] -fn test_minimumcutintoboundedsets_selects_optimal_solutions() { - type Value = as Problem>::Value; - assert!(Value::contributes_to_solution(&Min(Some(3)), &Min(Some(3)))); -} diff --git a/src/unit_tests/models/graph/minimum_dominating_set.rs b/src/unit_tests/models/graph/minimum_dominating_set.rs index 5edd12d7b..5e1e167e1 100644 --- a/src/unit_tests/models/graph/minimum_dominating_set.rs +++ b/src/unit_tests/models/graph/minimum_dominating_set.rs @@ -13,10 +13,10 @@ fn create_spec_rejects_weight_count_mismatch() { }); assert!(result.is_err()); } +include!("../../jl_helpers.rs"); use crate::solvers::BruteForce; use crate::topology::SimpleGraph; use crate::traits::Problem; -include!("../../jl_helpers.rs"); #[test] fn test_dominating_set_creation() { @@ -26,8 +26,11 @@ fn test_dominating_set_creation() { ); assert_eq!(problem.graph().num_vertices(), 4); assert_eq!(problem.graph().num_edges(), 3); - assert_eq!(problem.num_variables(), 4); - assert_eq!(problem.dimensions(), vec![2; 4]); + assert_eq!(problem.num_variables().unwrap(), 4); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 4] + ); } #[test] diff --git a/src/unit_tests/models/graph/minimum_dummy_activities_pert.rs b/src/unit_tests/models/graph/minimum_dummy_activities_pert.rs index fed0eb787..88af45b6d 100644 --- a/src/unit_tests/models/graph/minimum_dummy_activities_pert.rs +++ b/src/unit_tests/models/graph/minimum_dummy_activities_pert.rs @@ -1,5 +1,4 @@ use super::*; -use crate::solvers::BruteForceProblem as _; #[test] fn create_spec_rejects_cycle() { @@ -43,7 +42,10 @@ fn test_minimum_dummy_activities_pert_creation() { let problem = issue_problem(); assert_eq!(problem.num_vertices(), 6); assert_eq!(problem.num_arcs(), 5); - assert_eq!(problem.dimensions(), vec![2; 5]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 5] + ); } #[test] diff --git a/src/unit_tests/models/graph/minimum_edge_cost_flow.rs b/src/unit_tests/models/graph/minimum_edge_cost_flow.rs index 6253a1c80..1821d31d6 100644 --- a/src/unit_tests/models/graph/minimum_edge_cost_flow.rs +++ b/src/unit_tests/models/graph/minimum_edge_cost_flow.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::topology::DirectedGraph; use crate::traits::Problem; use crate::types::Min; @@ -44,7 +43,10 @@ fn test_minimum_edge_cost_flow_creation() { assert_eq!(problem.max_capacity(), 2); assert_eq!(problem.prices(), &[3, 1, 2, 0, 0, 0]); assert_eq!(problem.capacities(), &[2, 2, 2, 2, 2, 2]); - assert_eq!(problem.dimensions(), vec![3, 3, 3, 3, 3, 3]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![3, 3, 3, 3, 3, 3] + ); assert_eq!( ::NAME, "MinimumEdgeCostFlow" diff --git a/src/unit_tests/models/graph/minimum_feedback_arc_set.rs b/src/unit_tests/models/graph/minimum_feedback_arc_set.rs index 649abd44b..a7e710644 100644 --- a/src/unit_tests/models/graph/minimum_feedback_arc_set.rs +++ b/src/unit_tests/models/graph/minimum_feedback_arc_set.rs @@ -1,5 +1,4 @@ use super::*; -use crate::solvers::BruteForceProblem as _; #[test] fn create_spec_defaults_arc_weights() { @@ -34,8 +33,16 @@ fn test_minimum_feedback_arc_set_creation() { let problem = MinimumFeedbackArcSet::new(graph, vec![1i64; 9]); assert_eq!(problem.num_vertices(), 6); assert_eq!(problem.num_arcs(), 9); - assert_eq!(problem.dimensions().len(), 9); - assert!(problem.dimensions().iter().all(|&d| d == 2)); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem) + .unwrap() + .len(), + 9 + ); + assert!(crate::solvers::cartesian_dimensions(&problem) + .unwrap() + .iter() + .all(|&d| d == 2)); } #[test] diff --git a/src/unit_tests/models/graph/minimum_feedback_vertex_set.rs b/src/unit_tests/models/graph/minimum_feedback_vertex_set.rs index cc1e4005a..e0d640a4b 100644 --- a/src/unit_tests/models/graph/minimum_feedback_vertex_set.rs +++ b/src/unit_tests/models/graph/minimum_feedback_vertex_set.rs @@ -1,5 +1,4 @@ use super::*; -use crate::solvers::BruteForceProblem as _; #[test] fn create_spec_defaults_vertex_weights() { @@ -52,7 +51,10 @@ fn test_minimum_feedback_vertex_set_basic() { let problem = MinimumFeedbackVertexSet::new(graph, vec![1i64; 9]); // dims should be [2; 9] - assert_eq!(problem.dimensions(), vec![2usize; 9]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2usize; 9] + ); // Valid FVS: {0, 3, 8} → config = [1,0,0,1,0,0,0,0,1] let config_valid = vec![true, false, false, true, false, false, false, false, true]; diff --git a/src/unit_tests/models/graph/minimum_geometric_connected_dominating_set.rs b/src/unit_tests/models/graph/minimum_geometric_connected_dominating_set.rs index a5a1c6535..6236476ae 100644 --- a/src/unit_tests/models/graph/minimum_geometric_connected_dominating_set.rs +++ b/src/unit_tests/models/graph/minimum_geometric_connected_dominating_set.rs @@ -10,8 +10,11 @@ fn test_creation_and_getters() { assert_eq!(problem.num_points(), 3); assert!((problem.radius() - 1.5).abs() < f64::EPSILON); assert_eq!(problem.points().len(), 3); - assert_eq!(problem.num_variables(), 3); - assert_eq!(problem.dimensions(), vec![2; 3]); + assert_eq!(problem.num_variables().unwrap(), 3); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 3] + ); } #[test] diff --git a/src/unit_tests/models/graph/minimum_graph_bandwidth.rs b/src/unit_tests/models/graph/minimum_graph_bandwidth.rs index bdfa58ec7..6b3f5e841 100644 --- a/src/unit_tests/models/graph/minimum_graph_bandwidth.rs +++ b/src/unit_tests/models/graph/minimum_graph_bandwidth.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::topology::SimpleGraph; use crate::traits::Problem; use crate::types::Min; @@ -22,7 +21,10 @@ fn test_minimumgraphbandwidth_creation() { let problem = star_example(); assert_eq!(problem.num_vertices(), 4); assert_eq!(problem.num_edges(), 3); - assert_eq!(problem.dimensions(), vec![4, 4, 4, 4]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![4, 4, 4, 4] + ); } #[test] @@ -106,7 +108,10 @@ fn test_minimumgraphbandwidth_serialization() { fn test_minimumgraphbandwidth_single_vertex() { let graph = SimpleGraph::new(1, vec![]); let problem = MinimumGraphBandwidth::new(graph); - assert_eq!(problem.dimensions(), vec![1]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![1] + ); assert_eq!(problem.evaluate(&vec![0]).unwrap(), Min(Some(0))); assert_eq!(problem.bandwidth(&[0]).unwrap(), Some(0)); } diff --git a/src/unit_tests/models/graph/minimum_intersection_graph_basis.rs b/src/unit_tests/models/graph/minimum_intersection_graph_basis.rs index 17fd4e160..0bab5b0b2 100644 --- a/src/unit_tests/models/graph/minimum_intersection_graph_basis.rs +++ b/src/unit_tests/models/graph/minimum_intersection_graph_basis.rs @@ -12,8 +12,11 @@ fn test_minimum_intersection_graph_basis_creation() { assert_eq!(problem.num_vertices(), 3); assert_eq!(problem.num_edges(), 2); // 3 vertices * 2 edges = 6 binary variables - assert_eq!(problem.num_variables(), 6); - assert_eq!(problem.dimensions(), vec![2; 6]); + assert_eq!(problem.num_variables().unwrap(), 6); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 6] + ); } #[test] @@ -66,7 +69,10 @@ fn test_minimum_intersection_graph_basis_triangle() { let problem = MinimumIntersectionGraphBasis::new(graph); // 3 vertices * 3 edges = 9 binary variables - assert_eq!(problem.dimensions(), vec![2; 9]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 9] + ); // Valid: S[0]={0}, S[1]={0}, S[2]={0} // config: v0: [1,0,0], v1: [1,0,0], v2: [1,0,0] diff --git a/src/unit_tests/models/graph/minimum_maximal_matching.rs b/src/unit_tests/models/graph/minimum_maximal_matching.rs index 4a15078d7..b557c2fd5 100644 --- a/src/unit_tests/models/graph/minimum_maximal_matching.rs +++ b/src/unit_tests/models/graph/minimum_maximal_matching.rs @@ -11,7 +11,7 @@ fn test_minimum_maximal_matching_creation() { let problem = MinimumMaximalMatching::new(graph); assert_eq!(problem.num_vertices(), 4); assert_eq!(problem.num_edges(), 3); - assert_eq!(problem.num_variables(), 3); + assert_eq!(problem.num_variables().unwrap(), 3); } #[test] diff --git a/src/unit_tests/models/graph/minimum_metric_dimension.rs b/src/unit_tests/models/graph/minimum_metric_dimension.rs index 6e4764d1f..2fbe2ce97 100644 --- a/src/unit_tests/models/graph/minimum_metric_dimension.rs +++ b/src/unit_tests/models/graph/minimum_metric_dimension.rs @@ -11,8 +11,11 @@ fn test_minimum_metric_dimension_creation() { let problem = MinimumMetricDimension::new(graph); assert_eq!(problem.graph().num_vertices(), 5); assert_eq!(problem.graph().num_edges(), 6); - assert_eq!(problem.num_variables(), 5); - assert_eq!(problem.dimensions(), vec![2; 5]); + assert_eq!(problem.num_variables().unwrap(), 5); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 5] + ); } #[test] diff --git a/src/unit_tests/models/graph/minimum_multiway_cut.rs b/src/unit_tests/models/graph/minimum_multiway_cut.rs index c9831cc6e..4c5c0b099 100644 --- a/src/unit_tests/models/graph/minimum_multiway_cut.rs +++ b/src/unit_tests/models/graph/minimum_multiway_cut.rs @@ -1,5 +1,4 @@ use super::*; -use crate::solvers::BruteForceProblem as _; #[test] fn create_spec_rejects_invalid_terminals() { @@ -20,7 +19,12 @@ use crate::types::Min; fn test_minimummultiwaycut_creation() { let graph = SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4), (0, 4), (1, 3)]); let problem = MinimumMultiwayCut::new(graph, vec![0, 2, 4], vec![2, 3, 1, 2, 4, 5]); - assert_eq!(problem.dimensions().len(), 6); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem) + .unwrap() + .len(), + 6 + ); assert_eq!(problem.num_vertices(), 5); assert_eq!(problem.num_edges(), 6); assert_eq!(problem.num_terminals(), 3); diff --git a/src/unit_tests/models/graph/minimum_sum_multicenter.rs b/src/unit_tests/models/graph/minimum_sum_multicenter.rs index e636dee31..855082d05 100644 --- a/src/unit_tests/models/graph/minimum_sum_multicenter.rs +++ b/src/unit_tests/models/graph/minimum_sum_multicenter.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::topology::SimpleGraph; use crate::traits::Problem; @@ -271,7 +270,10 @@ fn test_min_sum_multicenter_paper_example() { fn test_min_sum_multicenter_dims() { let graph = SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4)]); let problem = MinimumSumMulticenter::new(graph, vec![1i64; 5], vec![1i64; 4], 2); - assert_eq!(problem.dimensions(), vec![2; 5]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 5] + ); } #[test] diff --git a/src/unit_tests/models/graph/minimum_vertex_cover.rs b/src/unit_tests/models/graph/minimum_vertex_cover.rs index 809475eef..200394243 100644 --- a/src/unit_tests/models/graph/minimum_vertex_cover.rs +++ b/src/unit_tests/models/graph/minimum_vertex_cover.rs @@ -13,10 +13,10 @@ fn create_spec_rejects_weight_count_mismatch() { }); assert!(result.is_err()); } +include!("../../jl_helpers.rs"); use crate::solvers::BruteForce; use crate::topology::SimpleGraph; use crate::traits::Problem; -include!("../../jl_helpers.rs"); #[test] fn test_vertex_cover_creation() { @@ -26,7 +26,7 @@ fn test_vertex_cover_creation() { ); assert_eq!(problem.graph().num_vertices(), 4); assert_eq!(problem.graph().num_edges(), 3); - assert_eq!(problem.num_variables(), 4); + assert_eq!(problem.num_variables().unwrap(), 4); } #[test] diff --git a/src/unit_tests/models/graph/mixed_chinese_postman.rs b/src/unit_tests/models/graph/mixed_chinese_postman.rs index 5c8b3cbc8..e5c1528f3 100644 --- a/src/unit_tests/models/graph/mixed_chinese_postman.rs +++ b/src/unit_tests/models/graph/mixed_chinese_postman.rs @@ -1,5 +1,4 @@ use super::*; -use crate::solvers::BruteForceProblem as _; #[test] fn create_spec_infers_graph_and_default_weights() { @@ -51,7 +50,10 @@ fn test_mixed_chinese_postman_creation_and_accessors() { assert_eq!(problem.num_vertices(), 5); assert_eq!(problem.num_arcs(), 4); assert_eq!(problem.num_edges(), 4); - assert_eq!(problem.dimensions(), vec![2, 2, 2, 2]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2, 2, 2, 2] + ); assert_eq!(problem.arc_weights(), &[2, 3, 1, 4]); assert_eq!(problem.edge_weights(), &[2, 3, 1, 2]); } diff --git a/src/unit_tests/models/graph/monochromatic_triangle.rs b/src/unit_tests/models/graph/monochromatic_triangle.rs index 54f7a56f8..6dcb462b7 100644 --- a/src/unit_tests/models/graph/monochromatic_triangle.rs +++ b/src/unit_tests/models/graph/monochromatic_triangle.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::topology::SimpleGraph; use crate::traits::Problem; @@ -20,7 +19,10 @@ fn test_monochromatic_triangle_creation() { // K4 has 4 triangles assert_eq!(problem.triangles().len(), 4); // One binary variable per edge - assert_eq!(problem.dimensions(), vec![2; 6]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 6] + ); assert_eq!(problem.graph().num_vertices(), 4); } diff --git a/src/unit_tests/models/graph/multiple_choice_branching.rs b/src/unit_tests/models/graph/multiple_choice_branching.rs index c4054593d..1ee93e1f9 100644 --- a/src/unit_tests/models/graph/multiple_choice_branching.rs +++ b/src/unit_tests/models/graph/multiple_choice_branching.rs @@ -58,7 +58,10 @@ fn test_multiple_choice_branching_creation_and_accessors() { assert_eq!(problem.num_vertices(), 6); assert_eq!(problem.num_arcs(), 8); assert_eq!(problem.num_partition_groups(), 4); - assert_eq!(problem.dimensions(), vec![2; 8]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 8] + ); assert_eq!(problem.graph().arcs().len(), 8); assert_eq!(problem.weights(), &[3, 2, 4, 1, 2, 3, 1, 3]); assert_eq!( @@ -277,5 +280,5 @@ fn test_multiple_choice_branching_set_weights_rejects_wrong_length() { #[test] fn test_multiple_choice_branching_num_variables() { let problem = yes_instance(); - assert_eq!(problem.num_variables(), 8); + assert_eq!(problem.num_variables().unwrap(), 8); } diff --git a/src/unit_tests/models/graph/multiple_copy_file_allocation.rs b/src/unit_tests/models/graph/multiple_copy_file_allocation.rs index f8b910cf7..9f8fe991d 100644 --- a/src/unit_tests/models/graph/multiple_copy_file_allocation.rs +++ b/src/unit_tests/models/graph/multiple_copy_file_allocation.rs @@ -1,5 +1,4 @@ use super::*; -use crate::solvers::BruteForceProblem as _; #[test] fn create_spec_preserves_isolated_vertices() { @@ -31,7 +30,10 @@ fn test_multiple_copy_file_allocation_creation() { assert_eq!(problem.num_edges(), 6); assert_eq!(problem.usage(), &[10; 6]); assert_eq!(problem.storage(), &[1; 6]); - assert_eq!(problem.dimensions(), vec![2; 6]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 6] + ); assert!(MultipleCopyFileAllocation::variant().is_empty()); } diff --git a/src/unit_tests/models/graph/optimal_linear_arrangement.rs b/src/unit_tests/models/graph/optimal_linear_arrangement.rs index cd433e63d..82dbd9054 100644 --- a/src/unit_tests/models/graph/optimal_linear_arrangement.rs +++ b/src/unit_tests/models/graph/optimal_linear_arrangement.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::topology::SimpleGraph; use crate::traits::Problem; use crate::types::Min; @@ -25,7 +24,10 @@ fn test_optimallineararrangement_basic() { let problem = issue_example(); // Check dims: 6 variables, each with domain size 6 - assert_eq!(problem.dimensions(), vec![6, 6, 6, 6, 6, 6]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![6, 6, 6, 6, 6, 6] + ); // Identity arrangement: f(i) = i // Cost: |0-1| + |1-2| + |2-3| + |3-4| + |4-5| + |0-3| + |2-5| = 1+1+1+1+1+3+3 = 11 @@ -143,7 +145,10 @@ fn test_optimallineararrangement_single_vertex() { let graph = SimpleGraph::new(1, vec![]); let problem = OptimalLinearArrangement::new(graph); - assert_eq!(problem.dimensions(), vec![1]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![1] + ); assert_eq!(problem.evaluate(&vec![0]).unwrap(), Min(Some(0))); assert_eq!(problem.total_edge_length(&[0]).unwrap(), Some(0)); } diff --git a/src/unit_tests/models/graph/partial_feedback_edge_set.rs b/src/unit_tests/models/graph/partial_feedback_edge_set.rs index c39b3895a..bd56b4278 100644 --- a/src/unit_tests/models/graph/partial_feedback_edge_set.rs +++ b/src/unit_tests/models/graph/partial_feedback_edge_set.rs @@ -66,8 +66,11 @@ fn test_partial_feedback_edge_set_creation() { assert_eq!(problem.max_cycle_length(), 4); assert_eq!(problem.num_vertices(), 6); assert_eq!(problem.num_edges(), 9); - assert_eq!(problem.num_variables(), 9); - assert_eq!(problem.dimensions(), vec![2; 9]); + assert_eq!(problem.num_variables().unwrap(), 9); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 9] + ); } #[test] diff --git a/src/unit_tests/models/graph/partition_into_cliques.rs b/src/unit_tests/models/graph/partition_into_cliques.rs index 2ee0b8fe3..b1d602f57 100644 --- a/src/unit_tests/models/graph/partition_into_cliques.rs +++ b/src/unit_tests/models/graph/partition_into_cliques.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::topology::SimpleGraph; use crate::traits::Problem; @@ -31,7 +30,10 @@ fn test_partition_into_cliques_creation() { assert_eq!(problem.num_vertices(), 6); assert_eq!(problem.num_edges(), 9); assert_eq!(problem.num_cliques(), 3); - assert_eq!(problem.dimensions(), vec![3; 6]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![3; 6] + ); assert_eq!(problem.graph().num_vertices(), 6); } diff --git a/src/unit_tests/models/graph/partition_into_forests.rs b/src/unit_tests/models/graph/partition_into_forests.rs index de56c11ba..c174713fa 100644 --- a/src/unit_tests/models/graph/partition_into_forests.rs +++ b/src/unit_tests/models/graph/partition_into_forests.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::topology::SimpleGraph; use crate::traits::Problem; @@ -22,7 +21,10 @@ fn test_partition_into_forests_creation() { assert_eq!(problem.num_vertices(), 6); assert_eq!(problem.num_edges(), 7); assert_eq!(problem.num_forests(), 2); - assert_eq!(problem.dimensions(), vec![2; 6]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 6] + ); assert_eq!(problem.graph().num_vertices(), 6); } diff --git a/src/unit_tests/models/graph/partition_into_paths_of_length_2.rs b/src/unit_tests/models/graph/partition_into_paths_of_length_2.rs index a3421104a..46f0984af 100644 --- a/src/unit_tests/models/graph/partition_into_paths_of_length_2.rs +++ b/src/unit_tests/models/graph/partition_into_paths_of_length_2.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::topology::SimpleGraph; use crate::traits::Problem; @@ -27,7 +26,10 @@ fn test_partition_into_paths_basic() { assert_eq!(problem.num_vertices(), 9); assert_eq!(problem.num_edges(), 10); assert_eq!(problem.num_groups(), 3); - assert_eq!(problem.dimensions(), vec![3; 9]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![3; 9] + ); // Valid partition: {0,1,2}, {3,4,5}, {6,7,8} // Config: vertex i -> group i/3 diff --git a/src/unit_tests/models/graph/partition_into_perfect_matchings.rs b/src/unit_tests/models/graph/partition_into_perfect_matchings.rs index 8ea2f64a2..492d4c2c5 100644 --- a/src/unit_tests/models/graph/partition_into_perfect_matchings.rs +++ b/src/unit_tests/models/graph/partition_into_perfect_matchings.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::topology::SimpleGraph; use crate::traits::Problem; @@ -15,7 +14,10 @@ fn test_partition_into_perfect_matchings_creation() { assert_eq!(problem.num_vertices(), 4); assert_eq!(problem.num_edges(), 4); assert_eq!(problem.num_matchings(), 2); - assert_eq!(problem.dimensions(), vec![2; 4]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 4] + ); assert_eq!(problem.graph().num_vertices(), 4); } diff --git a/src/unit_tests/models/graph/partition_into_triangles.rs b/src/unit_tests/models/graph/partition_into_triangles.rs index 42fb8c9c3..7a38781a8 100644 --- a/src/unit_tests/models/graph/partition_into_triangles.rs +++ b/src/unit_tests/models/graph/partition_into_triangles.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::topology::SimpleGraph; #[test] @@ -25,7 +24,10 @@ fn test_partitionintotriangles_basic() { assert_eq!(problem.num_vertices(), 9); assert_eq!(problem.num_edges(), 9); - assert_eq!(problem.dimensions(), vec![3; 9]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![3; 9] + ); // Valid partition: vertices 0,1,2 in group 0; 3,4,5 in group 1; 6,7,8 in group 2 assert!(problem.evaluate(&vec![0, 0, 0, 1, 1, 1, 2, 2, 2]).unwrap()); @@ -44,7 +46,10 @@ fn test_partitionintotriangles_no_solution() { let problem = PartitionIntoTriangles::new(graph); assert_eq!(problem.num_vertices(), 6); - assert_eq!(problem.dimensions(), vec![2; 6]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 6] + ); // No valid partition exists since there are no triangles let solver = BruteForce::new(); 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 af1cf3617..4f78768d6 100644 --- a/src/unit_tests/models/graph/path_constrained_network_flow.rs +++ b/src/unit_tests/models/graph/path_constrained_network_flow.rs @@ -1,5 +1,4 @@ use super::*; -use crate::solvers::BruteForceProblem as _; #[test] fn create_spec_defaults_capacities_and_validates_paths() { @@ -76,7 +75,10 @@ fn test_path_constrained_network_flow_creation() { #[test] fn test_path_constrained_network_flow_dims_use_path_bottlenecks() { let problem = yes_instance(); - assert_eq!(problem.dimensions(), vec![2, 2, 2, 2, 2]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2, 2, 2, 2, 2] + ); } #[test] diff --git a/src/unit_tests/models/graph/prize_collecting_steiner_forest.rs b/src/unit_tests/models/graph/prize_collecting_steiner_forest.rs index e5a801e5a..3181533ee 100644 --- a/src/unit_tests/models/graph/prize_collecting_steiner_forest.rs +++ b/src/unit_tests/models/graph/prize_collecting_steiner_forest.rs @@ -29,8 +29,11 @@ fn test_prize_collecting_steiner_forest_creation() { assert_eq!(*problem.beta(), 1); assert_eq!(*problem.omega(), 2); // n + m = 3 + 2 = 5 binary variables. - assert_eq!(problem.dimensions(), vec![2; 5]); - assert_eq!(problem.num_variables(), 5); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 5] + ); + assert_eq!(problem.num_variables().unwrap(), 5); assert!(problem.graph().has_edge(0, 1)); } @@ -246,3 +249,25 @@ fn create_specs_default_prizes_and_costs_to_one() { assert!(!PrizeCollectingSteinerForestI64CreateSpec::inputs()[2].required); assert!(!PrizeCollectingSteinerForestI64CreateSpec::inputs()[3].required); } + +#[test] +fn nonnegative_domain_is_shared_by_construction_and_serde() { + for (prizes, costs, beta, omega) in [ + (vec![-1, 0], vec![0], 1, 1), + (vec![0, 0], vec![-1], 1, 1), + (vec![0, 0], vec![0], -1, 1), + (vec![0, 0], vec![0], 1, -1), + ] { + let graph = SimpleGraph::new(2, vec![(0, 1)]); + let data = serde_json::json!({"graph": graph, "vertex_prizes": prizes, + "edge_costs": costs, "beta": beta, "omega": omega}); + assert!( + serde_json::from_value::>(data.clone()) + .is_err() + ); + assert!( + serde_json::from_value::>(data).is_err() + ); + assert!(PrizeCollectingSteinerForest::new(graph, prizes, costs, beta, omega).is_err()); + } +} diff --git a/src/unit_tests/models/graph/rooted_tree_arrangement.rs b/src/unit_tests/models/graph/rooted_tree_arrangement.rs index a34c88abf..c8404e01f 100644 --- a/src/unit_tests/models/graph/rooted_tree_arrangement.rs +++ b/src/unit_tests/models/graph/rooted_tree_arrangement.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::topology::SimpleGraph; use crate::traits::Problem; @@ -21,7 +20,10 @@ fn test_rootedtreearrangement_basic_yes_example() { assert_eq!(problem.num_vertices(), 5); assert_eq!(problem.num_edges(), 5); assert_eq!(problem.bound(), 7); - assert_eq!(problem.dimensions(), vec![5; 10]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![5; 10] + ); assert!(problem.evaluate(&config).unwrap()); assert_eq!(problem.total_edge_stretch(&config).unwrap(), Some(6)); } diff --git a/src/unit_tests/models/graph/rural_postman.rs b/src/unit_tests/models/graph/rural_postman.rs index aec2d3a5d..1a5ab5aaf 100644 --- a/src/unit_tests/models/graph/rural_postman.rs +++ b/src/unit_tests/models/graph/rural_postman.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::topology::SimpleGraph; use crate::traits::Problem; use crate::types::Min; @@ -42,8 +41,16 @@ fn test_rural_postman_creation() { assert_eq!(problem.num_vertices(), 6); assert_eq!(problem.num_edges(), 8); assert_eq!(problem.num_required_edges(), 3); - assert_eq!(problem.dimensions().len(), 8); - assert!(problem.dimensions().iter().all(|&d| d == 3)); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem) + .unwrap() + .len(), + 8 + ); + assert!(crate::solvers::cartesian_dimensions(&problem) + .unwrap() + .iter() + .all(|&d| d == 3)); } #[test] diff --git a/src/unit_tests/models/graph/shortest_weight_constrained_path.rs b/src/unit_tests/models/graph/shortest_weight_constrained_path.rs index 5135bd671..70de8b939 100644 --- a/src/unit_tests/models/graph/shortest_weight_constrained_path.rs +++ b/src/unit_tests/models/graph/shortest_weight_constrained_path.rs @@ -1,5 +1,4 @@ use super::*; -use crate::solvers::BruteForceProblem as _; #[test] fn create_spec_rejects_nonpositive_edge_values() { @@ -53,7 +52,10 @@ fn test_shortest_weight_constrained_path_creation() { assert_eq!(problem.source_vertex(), 0); assert_eq!(problem.target_vertex(), 5); assert_eq!(*problem.weight_bound(), 8); - assert_eq!(problem.dimensions(), vec![2; 8]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 8] + ); assert!(problem.is_weighted()); } diff --git a/src/unit_tests/models/graph/spin_glass.rs b/src/unit_tests/models/graph/spin_glass.rs index 27cc19d84..3ccb73366 100644 --- a/src/unit_tests/models/graph/spin_glass.rs +++ b/src/unit_tests/models/graph/spin_glass.rs @@ -13,9 +13,9 @@ fn create_spec_defaults_couplings_and_fields() { assert_eq!(problem.couplings(), &[1]); assert_eq!(problem.fields(), &[0, 0, 0]); } +include!("../../jl_helpers.rs"); use crate::solvers::BruteForce; use crate::traits::Problem; -include!("../../jl_helpers.rs"); #[test] fn test_spin_glass_creation() { @@ -94,7 +94,7 @@ fn test_compute_energy_rejects_invalid_spin_configuration() { #[test] fn test_num_variables() { let problem = SpinGlass::::without_fields(5, vec![]).unwrap(); - assert_eq!(problem.num_variables(), 5); + assert_eq!(problem.num_variables().unwrap(), 5); } #[test] diff --git a/src/unit_tests/models/graph/steiner_tree.rs b/src/unit_tests/models/graph/steiner_tree.rs index 92d4d7a29..2eb9cc12c 100644 --- a/src/unit_tests/models/graph/steiner_tree.rs +++ b/src/unit_tests/models/graph/steiner_tree.rs @@ -1,5 +1,4 @@ use super::*; -use crate::solvers::BruteForceProblem as _; #[test] fn create_spec_rejects_duplicate_terminals() { @@ -31,7 +30,12 @@ fn test_steiner_tree_creation() { assert_eq!(problem.graph().num_vertices(), 5); assert_eq!(problem.graph().num_edges(), 7); assert_eq!(problem.terminals(), &[0, 2, 4]); - assert_eq!(problem.dimensions().len(), 7); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem) + .unwrap() + .len(), + 7 + ); } #[test] @@ -176,10 +180,10 @@ fn test_steiner_tree_edge_weights_and_set_weights() { } #[test] -#[should_panic(expected = "at least 2 terminals required")] -fn test_steiner_tree_rejects_single_terminal() { +#[should_panic(expected = "at least one terminal required")] +fn test_steiner_tree_rejects_empty_terminals() { let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); - let _ = SteinerTree::new(graph, vec![1, 1], vec![0]); + let _ = SteinerTree::new(graph, vec![1, 1], vec![]); } #[test] @@ -198,12 +202,12 @@ fn test_steiner_tree_rejects_wrong_weight_count() { #[test] fn test_steiner_tree_deserialization_rejects_invalid_invariants() { - let one_terminal = serde_json::json!({ + let no_terminals = serde_json::json!({ "graph": {"num_vertices": 2, "edges": [[0, 1]]}, "edge_weights": [1], - "terminals": [0] + "terminals": [] }); - assert!(serde_json::from_value::>(one_terminal).is_err()); + assert!(serde_json::from_value::>(no_terminals).is_err()); let wrong_weights = serde_json::json!({ "graph": {"num_vertices": 2, "edges": [[0, 1]]}, @@ -212,3 +216,24 @@ fn test_steiner_tree_deserialization_rejects_invalid_invariants() { }); assert!(serde_json::from_value::>(wrong_weights).is_err()); } + +#[test] +fn test_steiner_tree_single_terminal_semantics() { + let problem = SteinerTree::try_from(SteinerTreeCreateSpec { + graph: SimpleGraph::new(4, vec![(0, 1), (1, 2), (0, 2), (2, 3)]), + edge_weights: vec![-5, 1, 1, -10], + terminals: vec![0], + }) + .unwrap(); + let restored: SteinerTree = + serde_json::from_value(serde_json::to_value(&problem).unwrap()).unwrap(); + for (edges, value) in [ + (vec![false, false, false, false], Min(Some(0))), + (vec![true, false, false, false], Min(Some(-5))), + (vec![false, false, false, true], Min(None)), + (vec![true, false, false, true], Min(None)), + (vec![true, true, true, false], Min(None)), + ] { + assert_eq!(restored.evaluate(&edges).unwrap(), value); + } +} diff --git a/src/unit_tests/models/graph/steiner_tree_in_graphs.rs b/src/unit_tests/models/graph/steiner_tree_in_graphs.rs deleted file mode 100644 index fd845b214..000000000 --- a/src/unit_tests/models/graph/steiner_tree_in_graphs.rs +++ /dev/null @@ -1,232 +0,0 @@ -use super::*; -use crate::solvers::BruteForceProblem as _; - -#[test] -fn create_spec_defaults_edge_weights() { - let p = SteinerTreeInGraphs::try_from(SteinerTreeInGraphsCreateSpec:: { - graph: SimpleGraph::new(2, vec![(0, 1)]), - terminals: vec![0, 1], - edge_weights: None, - }) - .unwrap(); - assert_eq!(p.weights(), &[1]); -} -use crate::solvers::BruteForce; -use crate::topology::SimpleGraph; -use crate::traits::Problem; - -#[test] -fn test_steiner_tree_creation() { - // Path graph: 0-1-2-3 - let graph = SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]); - let problem = SteinerTreeInGraphs::new(graph, vec![0, 3], vec![1i64, 2, 3]); - assert_eq!(problem.graph().num_vertices(), 4); - assert_eq!(problem.graph().num_edges(), 3); - assert_eq!(problem.terminals(), &[0, 3]); - assert_eq!(problem.dimensions().len(), 3); - assert_eq!(problem.num_vertices(), 4); - assert_eq!(problem.num_edges(), 3); - assert_eq!(problem.num_terminals(), 2); -} - -#[test] -fn test_steiner_tree_evaluation() { - // Triangle graph: 0-1, 1-2, 0-2, with terminal {0, 2} - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]); - let problem = SteinerTreeInGraphs::new(graph, vec![0, 2], vec![3i64, 4, 1]); - - // Select edge 0-2 (weight 1): valid, connects terminals directly - let config_direct = vec![false, false, true]; - let result = problem.evaluate(&config_direct).unwrap(); - assert!(result.is_valid()); - assert_eq!(result.unwrap(), 1); - - // Select edges 0-1 and 1-2 (weights 3+4=7): valid, connects via vertex 1 - let config_via = vec![true, true, false]; - let result = problem.evaluate(&config_via).unwrap(); - assert!(result.is_valid()); - assert_eq!(result.unwrap(), 7); - - // Select only edge 0-1: invalid (terminal 2 not reached) - let config_invalid = vec![true, false, false]; - let result = problem.evaluate(&config_invalid).unwrap(); - assert!(!result.is_valid()); - - // Select no edges: invalid - let config_empty = vec![false, false, false]; - let result = problem.evaluate(&config_empty).unwrap(); - assert!(!result.is_valid()); -} - -#[test] -fn test_steiner_tree_solver() { - // Diamond graph: - // 1 - // / \ - // 0 3 - // \ / - // 2 - // Edges: 0-1(w=2), 0-2(w=1), 1-3(w=2), 2-3(w=1) - // Terminals: {0, 3} - // Optimal path: 0-2-3 with weight 1+1=2 - let graph = SimpleGraph::new(4, vec![(0, 1), (0, 2), (1, 3), (2, 3)]); - let problem = SteinerTreeInGraphs::new(graph, vec![0, 3], vec![2, 1, 2, 1]); - - let solver = BruteForce::new(); - let solution = solver.solve(&problem).unwrap().unwrap(); - let value = problem.evaluate(&solution).unwrap(); - assert!(value.is_valid()); - assert_eq!(value.unwrap(), 2); - // Should select edges 0-2 and 2-3 - assert_eq!(solution, vec![false, true, false, true]); -} - -#[test] -fn test_steiner_tree_with_steiner_vertices() { - // Star graph: center vertex 1 connected to 0, 2, 3 - // Edges: 0-1(w=1), 1-2(w=1), 1-3(w=1) - // Terminals: {0, 2, 3} - // Optimal: use vertex 1 as Steiner vertex, select all 3 edges, weight = 3 - let graph = SimpleGraph::new(4, vec![(0, 1), (1, 2), (1, 3)]); - let problem = SteinerTreeInGraphs::new(graph, vec![0, 2, 3], vec![1i64; 3]); - - let solver = BruteForce::new(); - let solution = solver.solve(&problem).unwrap().unwrap(); - let value = problem.evaluate(&solution).unwrap(); - assert!(value.is_valid()); - assert_eq!(value.unwrap(), 3); - assert_eq!(solution, vec![true, true, true]); -} - -#[test] -fn test_steiner_tree_is_valid_solution() { - // Path graph: 0-1-2 - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); - let problem = SteinerTreeInGraphs::new(graph, vec![0, 2], vec![1i64; 2]); - - // Valid: both edges selected - assert!(problem.is_valid_solution(&[1, 1])); - // Invalid: only first edge - assert!(!problem.is_valid_solution(&[1, 0])); - // Invalid: only second edge - assert!(!problem.is_valid_solution(&[0, 1])); - // Invalid: no edges - assert!(!problem.is_valid_solution(&[0, 0])); -} - -#[test] -fn test_steiner_tree_parameter_getters() { - let graph = SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4)]); - let problem = SteinerTreeInGraphs::new(graph, vec![0, 2, 4], vec![1i64; 4]); - assert_eq!(problem.num_vertices(), 5); - assert_eq!(problem.num_edges(), 4); - assert_eq!(problem.num_terminals(), 3); -} - -#[test] -fn test_steiner_tree_problem_name() { - assert_eq!( - as Problem>::NAME, - "SteinerTreeInGraphs" - ); -} - -#[test] -fn test_steiner_tree_serialization() { - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); - let problem = SteinerTreeInGraphs::new(graph, vec![0, 2], vec![1i64; 2]); - let json = serde_json::to_string(&problem).unwrap(); - let deserialized: SteinerTreeInGraphs = serde_json::from_str(&json).unwrap(); - assert_eq!(deserialized.graph().num_vertices(), 3); - assert_eq!(deserialized.terminals(), &[0, 2]); - assert_eq!(deserialized.num_edges(), 2); -} - -#[test] -fn test_steiner_tree_single_terminal() { - // Single terminal: any config (including empty) is valid - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); - let problem = SteinerTreeInGraphs::new(graph, vec![1], vec![1i64; 2]); - - // No edges needed for a single terminal - let result = problem.evaluate(&vec![false, false]).unwrap(); - assert!(result.is_valid()); - assert_eq!(result.unwrap(), 0); -} - -#[test] -fn test_steiner_tree_all_vertices_terminal() { - // When all vertices are terminals, it degenerates to spanning tree - // Path: 0-1-2 - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); - let problem = SteinerTreeInGraphs::new(graph, vec![0, 1, 2], vec![1i64; 2]); - - let solver = BruteForce::new(); - let solution = solver.solve(&problem).unwrap().unwrap(); - let value = problem.evaluate(&solution).unwrap(); - assert!(value.is_valid()); - assert_eq!(value.unwrap(), 2); -} - -#[test] -fn test_steiner_tree_edges_accessor() { - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); - let problem = SteinerTreeInGraphs::new(graph, vec![0, 2], vec![5i64, 10]); - let edges = problem.edges(); - assert_eq!(edges.len(), 2); - assert_eq!(edges[0].2, 5); - assert_eq!(edges[1].2, 10); -} - -#[test] -fn test_steiner_tree_weights_management() { - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); - let mut problem = SteinerTreeInGraphs::new(graph, vec![0, 2], vec![1i64; 2]); - assert!(problem.is_weighted()); - assert_eq!(problem.weights(), vec![1, 1]); - - problem.set_weights(vec![5, 10]); - assert_eq!(problem.weights(), vec![5, 10]); -} - -#[test] -fn test_steiner_tree_example_from_issue() { - // Example from issue #255: - // Graph with 8 vertices {0,1,2,3,4,5,6,7} and 12 edges - // Terminals R = {0, 3, 5, 7} - let graph = SimpleGraph::new( - 8, - vec![ - (0, 1), // w=2, idx=0 - (0, 2), // w=3, idx=1 - (1, 2), // w=1, idx=2 - (1, 3), // w=4, idx=3 - (2, 4), // w=2, idx=4 - (3, 4), // w=3, idx=5 - (3, 5), // w=5, idx=6 - (4, 5), // w=1, idx=7 - (4, 6), // w=2, idx=8 - (5, 6), // w=3, idx=9 - (5, 7), // w=4, idx=10 - (6, 7), // w=1, idx=11 - ], - ); - let weights = vec![2, 3, 1, 4, 2, 3, 5, 1, 2, 3, 4, 1]; - let problem = SteinerTreeInGraphs::new(graph, vec![0, 3, 5, 7], weights); - - // Brute-force verification: independently confirm optimal weight is 12 - let solver = BruteForce::new(); - let solution = solver.solve(&problem).unwrap().unwrap(); - let value = problem.evaluate(&solution).unwrap(); - assert!(value.is_valid()); - assert_eq!(value.unwrap(), 12); - - // Verify the claimed optimal solution from the issue: - // Edges: {0,1}(2) + {1,2}(1) + {2,4}(2) + {3,4}(3) + {4,5}(1) + {4,6}(2) + {6,7}(1) = 12 - let config = vec![ - true, false, true, false, true, true, false, true, true, false, false, true, - ]; - let result = problem.evaluate(&config).unwrap(); - assert!(result.is_valid()); - assert_eq!(result.unwrap(), 12); -} diff --git a/src/unit_tests/models/graph/strong_connectivity_augmentation.rs b/src/unit_tests/models/graph/strong_connectivity_augmentation.rs index 107ca5e19..2134075ed 100644 --- a/src/unit_tests/models/graph/strong_connectivity_augmentation.rs +++ b/src/unit_tests/models/graph/strong_connectivity_augmentation.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::topology::DirectedGraph; use crate::traits::Problem; @@ -71,7 +70,10 @@ fn test_strong_connectivity_augmentation_creation() { assert_eq!(problem.num_potential_arcs(), 18); assert_eq!(problem.candidate_arcs().len(), 18); assert_eq!(problem.bound(), &1); - assert_eq!(problem.dimensions(), vec![2; 18]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 18] + ); assert!(problem.is_weighted()); } @@ -103,7 +105,10 @@ fn test_strong_connectivity_augmentation_wrong_length() { #[test] fn test_strong_connectivity_augmentation_already_strongly_connected() { let problem = issue_example_already_strongly_connected(); - assert_eq!(problem.dimensions(), vec![2]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2] + ); assert!(problem.evaluate(&vec![false]).unwrap()); assert!(!problem.evaluate(&vec![true]).unwrap()); } diff --git a/src/unit_tests/models/graph/subgraph_isomorphism.rs b/src/unit_tests/models/graph/subgraph_isomorphism.rs index bde097812..a53850034 100644 --- a/src/unit_tests/models/graph/subgraph_isomorphism.rs +++ b/src/unit_tests/models/graph/subgraph_isomorphism.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::topology::SimpleGraph; use crate::traits::Problem; @@ -14,7 +13,10 @@ fn test_subgraph_isomorphism_creation() { assert_eq!(problem.num_pattern_vertices(), 2); assert_eq!(problem.num_pattern_edges(), 1); // dims: 2 pattern vertices, each can map to 4 host vertices - assert_eq!(problem.dimensions(), vec![4, 4]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![4, 4] + ); } #[test] diff --git a/src/unit_tests/models/graph/traveling_salesman.rs b/src/unit_tests/models/graph/traveling_salesman.rs index ef89561a5..3b62235b7 100644 --- a/src/unit_tests/models/graph/traveling_salesman.rs +++ b/src/unit_tests/models/graph/traveling_salesman.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::topology::SimpleGraph; use crate::traits::Problem; use crate::types::Min; @@ -18,7 +17,12 @@ fn test_traveling_salesman_creation() { let problem = k4_tsp(); assert_eq!(problem.graph().num_vertices(), 4); assert_eq!(problem.graph().num_edges(), 6); - assert_eq!(problem.dimensions().len(), 6); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem) + .unwrap() + .len(), + 6 + ); } #[test] 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 c67868b1c..c46460a96 100644 --- a/src/unit_tests/models/graph/undirected_flow_lower_bounds.rs +++ b/src/unit_tests/models/graph/undirected_flow_lower_bounds.rs @@ -1,5 +1,4 @@ use super::*; -use crate::solvers::BruteForceProblem as _; #[test] fn create_spec_rejects_lower_bound_above_capacity() { @@ -64,7 +63,10 @@ fn test_undirected_flow_lower_bounds_creation() { assert_eq!(problem.requirement(), 3); assert_eq!(problem.num_vertices(), 6); assert_eq!(problem.num_edges(), 7); - assert_eq!(problem.dimensions(), vec![2; 7]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 7] + ); } #[test] diff --git a/src/unit_tests/models/graph/undirected_two_commodity_integral_flow.rs b/src/unit_tests/models/graph/undirected_two_commodity_integral_flow.rs index d09689d25..3381ea720 100644 --- a/src/unit_tests/models/graph/undirected_two_commodity_integral_flow.rs +++ b/src/unit_tests/models/graph/undirected_two_commodity_integral_flow.rs @@ -1,5 +1,4 @@ use super::*; -use crate::solvers::BruteForceProblem as _; #[test] fn create_spec_validates_capacity_shape() { @@ -72,7 +71,7 @@ fn test_undirected_two_commodity_integral_flow_creation() { assert_eq!(problem.num_vertices(), 4); assert_eq!(problem.num_edges(), 3); assert_eq!( - problem.dimensions(), + crate::solvers::cartesian_dimensions(&problem).unwrap(), vec![2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3] ); } diff --git a/src/unit_tests/models/misc/additional_key.rs b/src/unit_tests/models/misc/additional_key.rs index 8df337615..0a853e4e0 100644 --- a/src/unit_tests/models/misc/additional_key.rs +++ b/src/unit_tests/models/misc/additional_key.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::traits::Problem; /// Instance 1: 6 attributes, cyclic FDs, 3 known keys. @@ -31,7 +30,10 @@ fn test_additional_key_creation() { assert_eq!(problem.num_dependencies(), 5); assert_eq!(problem.num_relation_attrs(), 6); assert_eq!(problem.num_known_keys(), 3); - assert_eq!(problem.dimensions(), vec![2, 2, 2, 2, 2, 2]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2, 2, 2, 2, 2, 2] + ); assert_eq!(::NAME, "AdditionalKey"); assert_eq!(::variant(), vec![]); // Data getters diff --git a/src/unit_tests/models/misc/betweenness.rs b/src/unit_tests/models/misc/betweenness.rs index ffd895343..cff1d9ba8 100644 --- a/src/unit_tests/models/misc/betweenness.rs +++ b/src/unit_tests/models/misc/betweenness.rs @@ -17,8 +17,11 @@ fn test_betweenness_basic() { problem.triples(), &[(0, 1, 2), (2, 3, 4), (0, 2, 4), (1, 3, 4)] ); - assert_eq!(problem.dimensions(), vec![5; 5]); - assert_eq!(problem.num_variables(), 5); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![5; 5] + ); + assert_eq!(problem.num_variables().unwrap(), 5); assert_eq!(::NAME, "Betweenness"); assert_eq!(::variant(), vec![]); } diff --git a/src/unit_tests/models/misc/bin_packing.rs b/src/unit_tests/models/misc/bin_packing.rs index b0a8e77eb..02d10d8c4 100644 --- a/src/unit_tests/models/misc/bin_packing.rs +++ b/src/unit_tests/models/misc/bin_packing.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::traits::Problem; #[test] @@ -9,9 +8,17 @@ fn test_bin_packing_creation() { assert_eq!(problem.num_items(), 6); assert_eq!(problem.sizes(), &[6, 6, 5, 5, 4, 4]); assert_eq!(*problem.capacity(), 10); - assert_eq!(problem.dimensions().len(), 6); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem) + .unwrap() + .len(), + 6 + ); // Each variable has domain {0, ..., 5} - assert!(problem.dimensions().iter().all(|&d| d == 6)); + assert!(crate::solvers::cartesian_dimensions(&problem) + .unwrap() + .iter() + .all(|&d| d == 6)); } #[test] @@ -92,7 +99,10 @@ fn test_bin_packing_brute_force_small() { fn test_bin_packing_empty_items() { let problem = BinPacking::new(Vec::::new(), 10).unwrap(); assert_eq!(problem.num_items(), 0); - assert_eq!(problem.dimensions(), Vec::::new()); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + Vec::::new() + ); let result = problem.evaluate(&vec![]).unwrap(); assert!(result.is_valid()); assert_eq!(result.unwrap(), 0); diff --git a/src/unit_tests/models/misc/boyce_codd_normal_form_violation.rs b/src/unit_tests/models/misc/boyce_codd_normal_form_violation.rs index a15ea8bd2..42bc0ceb3 100644 --- a/src/unit_tests/models/misc/boyce_codd_normal_form_violation.rs +++ b/src/unit_tests/models/misc/boyce_codd_normal_form_violation.rs @@ -38,8 +38,11 @@ fn test_bcnf_creation() { assert_eq!(problem.num_attributes(), 6); assert_eq!(problem.num_functional_deps(), 3); assert_eq!(problem.num_target_attributes(), 6); - assert_eq!(problem.num_variables(), 6); - assert_eq!(problem.dimensions(), vec![2; 6]); + assert_eq!(problem.num_variables().unwrap(), 6); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 6] + ); assert_eq!(problem.target_subset(), &[0, 1, 2, 3, 4, 5]); assert_eq!(problem.functional_deps().len(), 3); } diff --git a/src/unit_tests/models/misc/capacity_assignment.rs b/src/unit_tests/models/misc/capacity_assignment.rs index 09894567c..01f79f1fa 100644 --- a/src/unit_tests/models/misc/capacity_assignment.rs +++ b/src/unit_tests/models/misc/capacity_assignment.rs @@ -1,6 +1,5 @@ use super::CapacityAssignmentCreateSpec; use crate::models::misc::CapacityAssignment; -use crate::solvers::BruteForceProblem as _; #[test] fn create_spec_validates_monotonicity() { @@ -32,7 +31,10 @@ fn test_capacity_assignment_basic_properties() { assert_eq!(problem.num_capacities(), 3); assert_eq!(problem.capacities(), &[1, 2, 3]); assert_eq!(problem.delay_budget(), 12); - assert_eq!(problem.dimensions(), vec![3, 3, 3]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![3, 3, 3] + ); assert_eq!(::NAME, "CapacityAssignment"); assert_eq!(::variant(), Vec::new()); } diff --git a/src/unit_tests/models/misc/closest_string.rs b/src/unit_tests/models/misc/closest_string.rs index fba543793..d0ef506b4 100644 --- a/src/unit_tests/models/misc/closest_string.rs +++ b/src/unit_tests/models/misc/closest_string.rs @@ -18,8 +18,11 @@ fn test_closest_string_creation() { assert_eq!(problem.num_strings(), 4); assert_eq!(problem.string_length(), 3); assert_eq!(problem.total_length(), 12); - assert_eq!(problem.dimensions(), vec![2, 2, 2]); - assert_eq!(problem.num_variables(), 3); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2, 2, 2] + ); + assert_eq!(problem.num_variables().unwrap(), 3); assert_eq!(::NAME, "ClosestString"); assert_eq!(::variant(), vec![]); } @@ -101,7 +104,10 @@ fn test_closest_string_larger_alphabet_smoke() { // must have radius at least 2; e.g., c = 00 achieves d(00,01)=1, // d(00,12)=2, d(00,20)=1, giving a max of 2. let problem = ClosestString::new(3, vec![vec![0, 1], vec![1, 2], vec![2, 0]]); - assert_eq!(problem.dimensions(), vec![3, 3]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![3, 3] + ); assert_eq!(problem.num_strings(), 3); assert_eq!(problem.string_length(), 2); let solver = BruteForce::new(); @@ -120,7 +126,10 @@ fn test_closest_string_serialization() { let restored: ClosestString = serde_json::from_value(json).unwrap(); assert_eq!(restored.alphabet_size(), problem.alphabet_size()); assert_eq!(restored.strings(), problem.strings()); - assert_eq!(restored.dimensions(), problem.dimensions()); + assert_eq!( + crate::solvers::cartesian_dimensions(&restored).unwrap(), + crate::solvers::cartesian_dimensions(&problem).unwrap() + ); assert_eq!( restored.evaluate(&vec![0, 0, 0]).unwrap(), problem.evaluate(&vec![0, 0, 0]).unwrap() diff --git a/src/unit_tests/models/misc/closest_substring.rs b/src/unit_tests/models/misc/closest_substring.rs index 1a98cfef5..47ff0ee22 100644 --- a/src/unit_tests/models/misc/closest_substring.rs +++ b/src/unit_tests/models/misc/closest_substring.rs @@ -4,8 +4,8 @@ use crate::solvers::BruteForceProblem as _; use crate::traits::Problem; use crate::types::Min; -fn issue_instance() -> ClosestSubstring { - // The #1033 canonical example: q = 2, ell = 3, three length-5 binary strings. +fn canonical_instance() -> ClosestSubstring { + // Canonical example: q = 2, ell = 3, three length-5 binary strings. ClosestSubstring::new( 2, vec![ @@ -20,24 +20,26 @@ fn issue_instance() -> ClosestSubstring { #[test] fn test_closest_substring_creation() { - let problem = issue_instance(); + let problem = canonical_instance(); assert_eq!(problem.alphabet_size(), 2); assert_eq!(problem.num_strings(), 3); assert_eq!(problem.substring_length(), 3); assert_eq!(problem.total_length(), 15); assert_eq!(problem.total_num_windows(), 9); - assert_eq!(problem.num_window_choice_product(), 27); // dims: 3 center slots (each of size 2) + one window-position slot per // string (each of size W_i = 5 - 3 + 1 = 3). - assert_eq!(problem.dimensions(), vec![2, 2, 2, 3, 3, 3]); - assert_eq!(problem.num_variables(), 6); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2, 2, 2, 3, 3, 3] + ); + assert_eq!(problem.num_variables().unwrap(), 6); assert_eq!(::NAME, "ClosestSubstring"); assert_eq!(::variant(), vec![]); } #[test] fn test_closest_substring_evaluate_at_optimum() { - let problem = issue_instance(); + let problem = canonical_instance(); // Center [0,1,0] with window picks (0, 1, 0): // s_1[0..3] = [0,0,0], d_H([0,1,0], [0,0,0]) = 1 // s_2[1..4] = [0,1,0], d_H = 0 @@ -51,7 +53,7 @@ fn test_closest_substring_evaluate_at_optimum() { #[test] fn test_closest_substring_evaluate_all_zero_windows() { - let problem = issue_instance(); + let problem = canonical_instance(); // c = [0,0,0], windows (0, 0, 0): // s_1[0..3] = [0,0,0] d = 0 // s_2[0..3] = [1,0,1] d = 2 @@ -65,7 +67,7 @@ fn test_closest_substring_evaluate_all_zero_windows() { #[test] fn test_closest_substring_evaluate_at_111_center() { - let problem = issue_instance(); + let problem = canonical_instance(); // Any center [1,1,1] has Hamming distance >= 1 to every length-3 binary // string that contains at least one 0. All windows of s_1, s_2, s_3 // contain at least one zero, so the radius is at least 1. @@ -79,7 +81,7 @@ fn test_closest_substring_evaluate_at_111_center() { #[test] fn test_closest_substring_evaluate_invalid_length() { - let problem = issue_instance(); + let problem = canonical_instance(); assert!(matches!( problem.evaluate(&vec![0, 0, 0]), Err(crate::traits::EvaluationError::InvalidConfiguration(_)) @@ -92,7 +94,7 @@ fn test_closest_substring_evaluate_invalid_length() { #[test] fn test_closest_substring_bruteforce_finds_optimum() { - let problem = issue_instance(); + let problem = canonical_instance(); let solver = BruteForce::new(); // 8 centers * 27 window combinations = 216 configurations; optimum is 1. assert_eq!( @@ -112,7 +114,7 @@ fn test_closest_substring_bruteforce_finds_optimum() { fn test_closest_substring_specializes_to_closest_string() { // When substring_length == string_length, each input string has exactly // one window (W_i = 1) and the problem reduces to ClosestString on the - // same instance. Use the #1032 canonical (4 binary strings of length 3), + // same instance. Use four binary strings of length 3, // whose optimum radius is 2. let problem = ClosestSubstring::new( 2, @@ -120,8 +122,10 @@ fn test_closest_substring_specializes_to_closest_string() { 3, ) .unwrap(); - assert_eq!(problem.num_window_choice_product(), 1); - assert_eq!(problem.dimensions(), vec![2, 2, 2, 1, 1, 1, 1]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2, 2, 2, 1, 1, 1, 1] + ); let solver = BruteForce::new(); assert_eq!( problem @@ -161,13 +165,16 @@ fn test_closest_substring_rejects_out_of_alphabet_symbol() { #[test] fn test_closest_substring_serialization() { - let problem = issue_instance(); + let problem = canonical_instance(); let json = serde_json::to_value(&problem).unwrap(); let restored: ClosestSubstring = serde_json::from_value(json).unwrap(); assert_eq!(restored.alphabet_size(), problem.alphabet_size()); assert_eq!(restored.strings(), problem.strings()); assert_eq!(restored.substring_length(), problem.substring_length()); - assert_eq!(restored.dimensions(), problem.dimensions()); + assert_eq!( + crate::solvers::cartesian_dimensions(&restored).unwrap(), + crate::solvers::cartesian_dimensions(&problem).unwrap() + ); assert_eq!( restored.evaluate(&vec![0, 1, 0, 0, 1, 0]).unwrap(), problem.evaluate(&vec![0, 1, 0, 0, 1, 0]).unwrap() diff --git a/src/unit_tests/models/misc/clustering.rs b/src/unit_tests/models/misc/clustering.rs index 6fcaccfec..7991863aa 100644 --- a/src/unit_tests/models/misc/clustering.rs +++ b/src/unit_tests/models/misc/clustering.rs @@ -1,6 +1,5 @@ use crate::models::misc::Clustering; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::traits::Problem; /// Helper: build the 6-element two-group instance from the issue. @@ -23,7 +22,10 @@ fn test_clustering_creation() { assert_eq!(problem.num_clusters(), 2); assert_eq!(problem.diameter_bound(), 1); assert_eq!(problem.distances().len(), 6); - assert_eq!(problem.dimensions(), vec![2; 6]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 6] + ); } #[test] diff --git a/src/unit_tests/models/misc/conjunctive_boolean_query.rs b/src/unit_tests/models/misc/conjunctive_boolean_query.rs index 165eb95a0..7b5d9fa00 100644 --- a/src/unit_tests/models/misc/conjunctive_boolean_query.rs +++ b/src/unit_tests/models/misc/conjunctive_boolean_query.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::traits::Problem; /// Helper to build the issue example instance. @@ -37,7 +36,10 @@ fn test_conjunctivebooleanquery_basic() { assert_eq!(problem.num_relations(), 2); assert_eq!(problem.num_variables(), 2); assert_eq!(problem.num_conjuncts(), 3); - assert_eq!(problem.dimensions(), vec![6, 6]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![6, 6] + ); assert_eq!( ::NAME, "ConjunctiveBooleanQuery" diff --git a/src/unit_tests/models/misc/conjunctive_query_foldability.rs b/src/unit_tests/models/misc/conjunctive_query_foldability.rs index 29fc69f39..ad91e74a6 100644 --- a/src/unit_tests/models/misc/conjunctive_query_foldability.rs +++ b/src/unit_tests/models/misc/conjunctive_query_foldability.rs @@ -59,8 +59,11 @@ fn test_conjunctive_query_foldability_creation() { let problem = yes_instance(); // dims = [domain_size + num_distinguished + num_undistinguished; num_undistinguished] // = [0 + 1 + 3; 3] = [4, 4, 4] - assert_eq!(problem.dimensions(), vec![4, 4, 4]); - assert_eq!(problem.num_variables(), 3); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![4, 4, 4] + ); + assert_eq!(problem.num_variables().unwrap(), 3); assert_eq!( ::NAME, "ConjunctiveQueryFoldability" @@ -117,7 +120,10 @@ fn test_conjunctive_query_foldability_serialization() { let problem = yes_instance(); let json = serde_json::to_value(&problem).unwrap(); let restored: ConjunctiveQueryFoldability = serde_json::from_value(json).unwrap(); - assert_eq!(restored.dimensions(), problem.dimensions()); + assert_eq!( + crate::solvers::cartesian_dimensions(&restored).unwrap(), + crate::solvers::cartesian_dimensions(&problem).unwrap() + ); assert_eq!(restored.domain_size(), problem.domain_size()); assert_eq!(restored.num_distinguished(), problem.num_distinguished()); assert_eq!( @@ -174,7 +180,10 @@ fn test_conjunctive_query_foldability_with_constants() { ], ); // dims = [1+1+1; 1] = [3] - assert_eq!(problem.dimensions(), vec![3]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![3] + ); // σ(u→x): index for X(0) = domain_size + 0 = 1 assert!(problem.evaluate(&vec![1]).unwrap()); // σ(u→c0): index for C(0) = 0 → R(c0, c0) ∧ R(c0, x) ≠ Q2 @@ -286,7 +295,10 @@ fn test_conjunctive_query_foldability_no_undistinguished() { vec![(0, vec![X(0), X(0)])], vec![(0, vec![X(0), X(0)])], ); - assert_eq!(problem.dimensions(), Vec::::new()); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + Vec::::new() + ); assert!(problem.evaluate(&vec![]).unwrap()); } diff --git a/src/unit_tests/models/misc/consistency_of_database_frequency_tables.rs b/src/unit_tests/models/misc/consistency_of_database_frequency_tables.rs index 1aadc7b88..b6af910da 100644 --- a/src/unit_tests/models/misc/consistency_of_database_frequency_tables.rs +++ b/src/unit_tests/models/misc/consistency_of_database_frequency_tables.rs @@ -1,5 +1,4 @@ use super::*; -use crate::solvers::BruteForceProblem as _; #[test] fn create_spec_defaults_known_values() { @@ -60,7 +59,7 @@ fn test_cdft_creation_and_getters() { let problem = issue_yes_instance(); assert_eq!(problem.num_objects(), 6); assert_eq!(problem.num_attributes(), 3); - assert_eq!(problem.domain_size_product(), 12); + assert_eq!(problem.max_domain_size(), 3); assert_eq!(problem.num_assignment_variables(), 18); assert_eq!(problem.attribute_domains(), &[2, 3, 2]); assert_eq!(problem.frequency_tables().len(), 2); @@ -79,7 +78,7 @@ fn test_cdft_creation_and_getters() { fn test_cdft_dims_repeat_attribute_domains_for_each_object() { let problem = issue_yes_instance(); assert_eq!( - problem.dimensions(), + crate::solvers::cartesian_dimensions(&problem).unwrap(), vec![2, 3, 2, 2, 3, 2, 2, 3, 2, 2, 3, 2, 2, 3, 2, 2, 3, 2] ); } diff --git a/src/unit_tests/models/misc/cosine_product_integration.rs b/src/unit_tests/models/misc/cosine_product_integration.rs index 5787b6f66..ecd36f60f 100644 --- a/src/unit_tests/models/misc/cosine_product_integration.rs +++ b/src/unit_tests/models/misc/cosine_product_integration.rs @@ -1,6 +1,5 @@ use crate::models::misc::CosineProductIntegration; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::traits::Problem; #[test] @@ -13,7 +12,10 @@ fn test_cosine_product_integration_creation() { #[test] fn test_cosine_product_integration_dims() { let p = CosineProductIntegration::new(vec![1, 2, 3]); - assert_eq!(p.dimensions(), vec![2, 2, 2]); + assert_eq!( + crate::solvers::cartesian_dimensions(&p).unwrap(), + vec![2, 2, 2] + ); } #[test] diff --git a/src/unit_tests/models/misc/cyclic_ordering.rs b/src/unit_tests/models/misc/cyclic_ordering.rs index 5f8bb0b76..20eaaaee4 100644 --- a/src/unit_tests/models/misc/cyclic_ordering.rs +++ b/src/unit_tests/models/misc/cyclic_ordering.rs @@ -14,8 +14,11 @@ fn test_cyclic_ordering_basic() { assert_eq!(problem.num_elements(), 5); assert_eq!(problem.num_triples(), 3); assert_eq!(problem.triples(), &[(0, 1, 2), (2, 3, 0), (1, 3, 4)]); - assert_eq!(problem.dimensions(), vec![5; 5]); - assert_eq!(problem.num_variables(), 5); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![5; 5] + ); + assert_eq!(problem.num_variables().unwrap(), 5); assert_eq!(::NAME, "CyclicOrdering"); assert_eq!(::variant(), vec![]); } diff --git a/src/unit_tests/models/misc/dynamic_storage_allocation.rs b/src/unit_tests/models/misc/dynamic_storage_allocation.rs index 42fd55c8c..79f463dc7 100644 --- a/src/unit_tests/models/misc/dynamic_storage_allocation.rs +++ b/src/unit_tests/models/misc/dynamic_storage_allocation.rs @@ -20,8 +20,11 @@ fn test_dynamic_storage_allocation_basic() { assert_eq!(problem.items().len(), 5); // dims: D - s(a) + 1 for each item // sizes are 2, 3, 1, 3, 2 => dims are 5, 4, 6, 4, 5 - assert_eq!(problem.dimensions(), vec![5, 4, 6, 4, 5]); - assert_eq!(problem.num_variables(), 5); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![5, 4, 6, 4, 5] + ); + assert_eq!(problem.num_variables().unwrap(), 5); assert_eq!( ::NAME, "DynamicStorageAllocation" diff --git a/src/unit_tests/models/misc/ensemble_computation.rs b/src/unit_tests/models/misc/ensemble_computation.rs index 890207764..89e53816c 100644 --- a/src/unit_tests/models/misc/ensemble_computation.rs +++ b/src/unit_tests/models/misc/ensemble_computation.rs @@ -15,8 +15,11 @@ fn test_ensemble_computation_creation() { assert_eq!(problem.universe_size(), 4); assert_eq!(problem.num_subsets(), 2); assert_eq!(problem.budget(), 4); - assert_eq!(problem.num_variables(), 8); - assert_eq!(problem.dimensions(), vec![8; 8]); + assert_eq!(problem.num_variables().unwrap(), 8); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![8; 8] + ); assert_eq!( ::NAME, "EnsembleComputation" diff --git a/src/unit_tests/models/misc/expected_retrieval_cost.rs b/src/unit_tests/models/misc/expected_retrieval_cost.rs index 9cd84b62c..f2172769a 100644 --- a/src/unit_tests/models/misc/expected_retrieval_cost.rs +++ b/src/unit_tests/models/misc/expected_retrieval_cost.rs @@ -16,8 +16,11 @@ fn test_expected_retrieval_cost_basic_accessors() { assert_eq!(problem.num_records(), 6); assert_eq!(problem.num_sectors(), 3); assert_eq!(problem.probabilities(), &[0.2, 0.15, 0.15, 0.2, 0.1, 0.2]); - assert_eq!(problem.dimensions(), vec![3; 6]); - assert_eq!(problem.num_variables(), 6); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![3; 6] + ); + assert_eq!(problem.num_variables().unwrap(), 6); } #[test] diff --git a/src/unit_tests/models/misc/factoring.rs b/src/unit_tests/models/misc/factoring.rs index 61bf2f201..3091fa794 100644 --- a/src/unit_tests/models/misc/factoring.rs +++ b/src/unit_tests/models/misc/factoring.rs @@ -1,9 +1,9 @@ use super::*; +include!("../../jl_helpers.rs"); use crate::solvers::BruteForce; use crate::solvers::BruteForceProblem as _; use crate::traits::Problem; use num_bigint::BigUint; -include!("../../jl_helpers.rs"); #[test] fn test_factoring_creation() { @@ -11,7 +11,7 @@ fn test_factoring_creation() { assert_eq!(problem.m(), 3); assert_eq!(problem.n(), 3); assert_eq!(problem.target(), &BigUint::from(15u32)); - assert_eq!(problem.num_variables(), 6); + assert_eq!(problem.num_variables().unwrap(), 6); } #[test] @@ -136,7 +136,7 @@ fn test_parameter_getters() { fn test_factoring_paper_example() { // Paper: N=15, m=2 bits, n=3 bits, p=3, q=5 let problem = Factoring::with_factor_bits(15, 2, 3); - assert_eq!(problem.num_variables(), 5); + assert_eq!(problem.num_variables().unwrap(), 5); // p=3 -> bits [1,1], q=5 -> bits [1,0,1] let config = (BigUint::from(3u32), BigUint::from(5u32)); diff --git a/src/unit_tests/models/misc/feasible_register_assignment.rs b/src/unit_tests/models/misc/feasible_register_assignment.rs index 08b10831f..3c84f3948 100644 --- a/src/unit_tests/models/misc/feasible_register_assignment.rs +++ b/src/unit_tests/models/misc/feasible_register_assignment.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::traits::Problem; #[test] @@ -13,7 +12,10 @@ fn test_feasible_register_assignment_basic() { assert_eq!(problem.num_same_register_pairs(), 3); assert_eq!(problem.arcs(), &[(0, 1), (0, 2), (1, 3)]); assert_eq!(problem.assignment(), &[0, 1, 0, 0]); - assert_eq!(problem.dimensions(), vec![4; 4]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![4; 4] + ); assert_eq!( ::NAME, "FeasibleRegisterAssignment" @@ -143,7 +145,10 @@ fn test_feasible_register_assignment_serialization() { fn test_feasible_register_assignment_empty() { let problem = FeasibleRegisterAssignment::new(0, vec![], 0, vec![]); assert_eq!(problem.num_vertices(), 0); - assert_eq!(problem.dimensions(), Vec::::new()); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + Vec::::new() + ); assert!(problem.evaluate(&vec![]).unwrap()); } diff --git a/src/unit_tests/models/misc/flow_shop_scheduling.rs b/src/unit_tests/models/misc/flow_shop_scheduling.rs index a3b327622..87d2f0788 100644 --- a/src/unit_tests/models/misc/flow_shop_scheduling.rs +++ b/src/unit_tests/models/misc/flow_shop_scheduling.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::traits::Problem; #[test] @@ -19,9 +18,17 @@ fn test_flow_shop_scheduling_creation() { assert_eq!(problem.num_jobs(), 5); assert_eq!(problem.num_processors(), 3); assert_eq!(problem.deadline(), 25); - assert_eq!(problem.dimensions().len(), 5); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem) + .unwrap() + .len(), + 5 + ); // Lehmer code encoding: dims = [5, 4, 3, 2, 1] - assert_eq!(problem.dimensions(), vec![5, 4, 3, 2, 1]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![5, 4, 3, 2, 1] + ); } #[test] @@ -149,7 +156,10 @@ fn test_flow_shop_scheduling_brute_force_unsatisfiable() { fn test_flow_shop_scheduling_empty() { let problem = FlowShopScheduling::new(3, vec![], 0); assert_eq!(problem.num_jobs(), 0); - assert_eq!(problem.dimensions(), Vec::::new()); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + Vec::::new() + ); // Empty config should be satisfying (no jobs to schedule) assert!(problem.evaluate(&vec![]).unwrap()); } diff --git a/src/unit_tests/models/misc/grouping_by_swapping.rs b/src/unit_tests/models/misc/grouping_by_swapping.rs index 7b56a31d8..91c70ffb5 100644 --- a/src/unit_tests/models/misc/grouping_by_swapping.rs +++ b/src/unit_tests/models/misc/grouping_by_swapping.rs @@ -22,8 +22,11 @@ fn test_grouping_by_swapping_basic() { assert_eq!(problem.string(), &[0, 1, 2, 0, 1, 2]); assert_eq!(problem.budget(), 5); assert_eq!(problem.string_len(), 6); - assert_eq!(problem.num_variables(), 5); - assert_eq!(problem.dimensions(), vec![6; 5]); + assert_eq!(problem.num_variables().unwrap(), 5); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![6; 5] + ); assert_eq!(::NAME, "GroupingBySwapping"); assert_eq!(::variant(), vec![]); diff --git a/src/unit_tests/models/misc/integer_expression_membership.rs b/src/unit_tests/models/misc/integer_expression_membership.rs index 781cdab77..ab54ac0e0 100644 --- a/src/unit_tests/models/misc/integer_expression_membership.rs +++ b/src/unit_tests/models/misc/integer_expression_membership.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::traits::Problem; /// Helper: build expression (1 ∪ 4) + (3 ∪ 6) + (2 ∪ 5) @@ -32,7 +31,10 @@ fn test_integer_expression_membership_creation() { assert_eq!(problem.num_atoms(), 6); assert_eq!(problem.expression_size(), 11); // 6 atoms + 3 unions + 2 sums assert_eq!(problem.expression_depth(), 3); - assert_eq!(problem.dimensions(), vec![2, 2, 2]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2, 2, 2] + ); assert_eq!( ::NAME, "IntegerExpressionMembership" @@ -116,7 +118,10 @@ fn test_integer_expression_membership_single_atom() { let expr = IntExpr::Atom(42); let problem = IntegerExpressionMembership::new(expr, 42); assert_eq!(problem.num_union_nodes(), 0); - assert_eq!(problem.dimensions(), Vec::::new()); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + Vec::::new() + ); assert!(problem.evaluate(&vec![]).unwrap()); // empty config, atom == target } @@ -133,7 +138,10 @@ fn test_integer_expression_membership_simple_union() { let expr = IntExpr::Union(Box::new(IntExpr::Atom(3)), Box::new(IntExpr::Atom(7))); let problem = IntegerExpressionMembership::new(expr, 7); assert_eq!(problem.num_union_nodes(), 1); - assert_eq!(problem.dimensions(), vec![2]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2] + ); assert!(!problem.evaluate(&vec![false]).unwrap()); // 3 ≠ 7 assert!(problem.evaluate(&vec![true]).unwrap()); // 7 == 7 } diff --git a/src/unit_tests/models/misc/job_shop_scheduling.rs b/src/unit_tests/models/misc/job_shop_scheduling.rs index e190677ca..77be4e447 100644 --- a/src/unit_tests/models/misc/job_shop_scheduling.rs +++ b/src/unit_tests/models/misc/job_shop_scheduling.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::traits::Problem; use crate::types::Min; @@ -28,7 +27,7 @@ fn test_job_shop_scheduling_creation_and_dims() { assert_eq!(problem.num_jobs(), 5); assert_eq!(problem.num_tasks(), 12); assert_eq!( - problem.dimensions(), + crate::solvers::cartesian_dimensions(&problem).unwrap(), vec![6, 5, 4, 3, 2, 1, 6, 5, 4, 3, 2, 1] ); } diff --git a/src/unit_tests/models/misc/knapsack.rs b/src/unit_tests/models/misc/knapsack.rs index b58568b2f..898cf519a 100644 --- a/src/unit_tests/models/misc/knapsack.rs +++ b/src/unit_tests/models/misc/knapsack.rs @@ -1,5 +1,4 @@ use super::*; -use crate::solvers::BruteForceProblem as _; #[test] fn create_spec_defaults_item_weights() { @@ -21,7 +20,10 @@ fn test_knapsack_basic() { assert_eq!(problem.weights(), &[2, 3, 4, 5]); assert_eq!(problem.values(), &[3, 4, 5, 7]); assert_eq!(problem.capacity(), 7); - assert_eq!(problem.dimensions(), vec![2; 4]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 4] + ); assert_eq!(::NAME, "Knapsack"); assert_eq!(::variant(), vec![]); } @@ -97,7 +99,10 @@ fn test_knapsack_evaluate_invalid_variable_value() { fn test_knapsack_empty_instance() { let problem = Knapsack::new(vec![], vec![], 10); assert_eq!(problem.num_items(), 0); - assert_eq!(problem.dimensions(), Vec::::new()); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + Vec::::new() + ); assert_eq!(problem.evaluate(&vec![]).unwrap(), Max(Some(0))); } diff --git a/src/unit_tests/models/misc/kth_largest_m_tuple.rs b/src/unit_tests/models/misc/kth_largest_m_tuple.rs index e7e905157..5e2906eb4 100644 --- a/src/unit_tests/models/misc/kth_largest_m_tuple.rs +++ b/src/unit_tests/models/misc/kth_largest_m_tuple.rs @@ -31,9 +31,12 @@ fn test_kth_largest_m_tuple_creation() { assert_eq!(p.k(), 14); assert_eq!(p.bound(), 12); assert_eq!(p.num_sets(), 3); - assert_eq!(p.total_tuples(), 18); - assert_eq!(p.dimensions(), Vec::::new()); - assert_eq!(p.num_variables(), 0); + assert_eq!(p.num_elements(), 8); + assert_eq!( + crate::solvers::cartesian_dimensions(&p).unwrap(), + Vec::::new() + ); + assert_eq!(p.num_variables().unwrap(), 0); assert_eq!(::NAME, "KthLargestMTuple"); assert_eq!(::variant(), vec![]); } @@ -136,7 +139,7 @@ fn test_kth_largest_m_tuple_all_qualify() { p.evaluate(&solver.solve(&p).unwrap().unwrap()).unwrap(), Or(true) ); - assert_eq!(p.total_tuples(), 1); + assert_eq!(p.num_elements(), 2); } #[test] @@ -169,8 +172,11 @@ fn test_kth_largest_m_tuple_many_singleton_sets_do_not_use_call_stack() { } #[test] -#[should_panic(expected = "total tuple count exceeds usize")] -fn test_kth_largest_m_tuple_total_tuples_overflow_panics() { - let p = KthLargestMTuple::new(vec![vec![1, 2]; usize::BITS as usize], 1, 1); - p.total_tuples(); +fn tuple_product_does_not_restrict_parameters_or_evaluation() { + let p = KthLargestMTuple::new(vec![vec![1, 2]; 64], 1, 1); + assert_eq!(p.num_elements(), 128); + assert_eq!(p.evaluate(&()).unwrap(), Or(true)); + let restored: KthLargestMTuple = + serde_json::from_value(serde_json::to_value(&p).unwrap()).unwrap(); + assert_eq!(p.parameters(), restored.parameters()); } diff --git a/src/unit_tests/models/misc/longest_common_subsequence.rs b/src/unit_tests/models/misc/longest_common_subsequence.rs index 913a300db..76c5be2f7 100644 --- a/src/unit_tests/models/misc/longest_common_subsequence.rs +++ b/src/unit_tests/models/misc/longest_common_subsequence.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::traits::Problem; use crate::types::Max; @@ -35,7 +34,10 @@ fn test_lcs_basic() { assert_eq!(problem.sum_squared_lengths(), 216); assert_eq!(problem.sum_triangular_lengths(), 126); assert_eq!(problem.num_transitions(), 5); - assert_eq!(problem.dimensions(), vec![3; 6]); // alphabet_size + 1 = 3, max_length = 6 + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![3; 6] + ); // alphabet_size + 1 = 3, max_length = 6 assert_eq!( ::NAME, "LongestCommonSubsequence" @@ -156,8 +158,11 @@ fn test_lcs_empty_string_max_length_zero() { // When all strings are empty or any string is empty, max_length = 0 let problem = LongestCommonSubsequence::new(2, vec![vec![], vec![0, 1]]); assert_eq!(problem.max_length(), 0); - assert_eq!(problem.dimensions(), Vec::::new()); // empty config space - // Empty config is the only valid config; LCS length is 0 + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + Vec::::new() + ); // empty config space + // Empty config is the only valid config; LCS length is 0 assert_eq!(problem.evaluate(&vec![]).unwrap(), Max(Some(0))); } diff --git a/src/unit_tests/models/misc/maximum_likelihood_ranking.rs b/src/unit_tests/models/misc/maximum_likelihood_ranking.rs index 8d06afd26..8e0693a20 100644 --- a/src/unit_tests/models/misc/maximum_likelihood_ranking.rs +++ b/src/unit_tests/models/misc/maximum_likelihood_ranking.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::traits::Problem; #[test] @@ -15,7 +14,10 @@ fn test_maximum_likelihood_ranking_creation() { assert_eq!(problem.num_items(), 4); assert_eq!(problem.matrix(), &matrix); assert_eq!(problem.comparison_count(), 5); - assert_eq!(problem.dimensions(), vec![4; 4]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![4; 4] + ); assert_eq!( ::NAME, "MaximumLikelihoodRanking" @@ -148,7 +150,10 @@ fn test_maximum_likelihood_ranking_single_item() { let problem = MaximumLikelihoodRanking::new(vec![vec![0]]); assert_eq!(problem.num_items(), 1); assert_eq!(problem.comparison_count(), 0); - assert_eq!(problem.dimensions(), vec![1]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![1] + ); assert_eq!(problem.evaluate(&vec![0]).unwrap(), Min(Some(0))); } diff --git a/src/unit_tests/models/misc/minimum_axiom_set.rs b/src/unit_tests/models/misc/minimum_axiom_set.rs index f3f746808..d3b5e3bab 100644 --- a/src/unit_tests/models/misc/minimum_axiom_set.rs +++ b/src/unit_tests/models/misc/minimum_axiom_set.rs @@ -28,8 +28,11 @@ fn test_minimum_axiom_set_creation() { assert_eq!(problem.num_true_sentences(), 8); assert_eq!(problem.num_implications(), 8); assert_eq!(problem.true_sentences(), &[0, 1, 2, 3, 4, 5, 6, 7]); - assert_eq!(problem.dimensions(), vec![2; 8]); - assert_eq!(problem.num_variables(), 8); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 8] + ); + assert_eq!(problem.num_variables().unwrap(), 8); } #[test] @@ -125,7 +128,10 @@ fn test_minimum_axiom_set_partial_true_sentences() { let problem = MinimumAxiomSet::new(5, vec![0, 1, 2], vec![(vec![0], 1), (vec![1], 2)]); assert_eq!(problem.num_sentences(), 5); assert_eq!(problem.num_true_sentences(), 3); - assert_eq!(problem.dimensions(), vec![2; 3]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 3] + ); // Select sentence 0 only let result = problem.evaluate(&vec![true, false, false]).unwrap(); diff --git a/src/unit_tests/models/misc/minimum_code_generation_one_register.rs b/src/unit_tests/models/misc/minimum_code_generation_one_register.rs index 78337e23a..b2f959de9 100644 --- a/src/unit_tests/models/misc/minimum_code_generation_one_register.rs +++ b/src/unit_tests/models/misc/minimum_code_generation_one_register.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::traits::Problem; use crate::types::Min; @@ -24,7 +23,10 @@ fn test_minimum_code_generation_one_register_creation() { assert_eq!(problem.num_edges(), 8); assert_eq!(problem.num_leaves(), 3); assert_eq!(problem.num_internal(), 4); - assert_eq!(problem.dimensions(), vec![4; 4]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![4; 4] + ); assert_eq!( ::NAME, "MinimumCodeGenerationOneRegister" diff --git a/src/unit_tests/models/misc/minimum_code_generation_parallel_assignments.rs b/src/unit_tests/models/misc/minimum_code_generation_parallel_assignments.rs index 541ed2e59..e6f60edf0 100644 --- a/src/unit_tests/models/misc/minimum_code_generation_parallel_assignments.rs +++ b/src/unit_tests/models/misc/minimum_code_generation_parallel_assignments.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::traits::Problem; #[test] @@ -10,7 +9,10 @@ fn test_minimum_code_generation_parallel_assignments_creation() { assert_eq!(problem.num_variables(), 4); assert_eq!(problem.num_assignments(), 4); assert_eq!(problem.assignments(), &assignments); - assert_eq!(problem.dimensions(), vec![4; 4]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![4; 4] + ); assert_eq!( ::NAME, "MinimumCodeGenerationParallelAssignments" diff --git a/src/unit_tests/models/misc/minimum_code_generation_unlimited_registers.rs b/src/unit_tests/models/misc/minimum_code_generation_unlimited_registers.rs index c852009b8..2c9900bb2 100644 --- a/src/unit_tests/models/misc/minimum_code_generation_unlimited_registers.rs +++ b/src/unit_tests/models/misc/minimum_code_generation_unlimited_registers.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::traits::Problem; use crate::types::Min; @@ -16,7 +15,10 @@ fn test_minimum_code_generation_unlimited_registers_creation() { assert_eq!(problem.num_internal(), 3); assert_eq!(problem.left_arcs(), &[(1, 3), (2, 3), (0, 1)]); assert_eq!(problem.right_arcs(), &[(1, 4), (2, 4), (0, 2)]); - assert_eq!(problem.dimensions(), vec![3; 3]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![3; 3] + ); assert_eq!( ::NAME, "MinimumCodeGenerationUnlimitedRegisters" diff --git a/src/unit_tests/models/misc/minimum_decision_tree.rs b/src/unit_tests/models/misc/minimum_decision_tree.rs index 561066d05..31f2e22b2 100644 --- a/src/unit_tests/models/misc/minimum_decision_tree.rs +++ b/src/unit_tests/models/misc/minimum_decision_tree.rs @@ -1,5 +1,4 @@ use super::*; -use crate::solvers::BruteForceProblem as _; #[test] fn create_spec_rejects_indistinguishable_objects() { @@ -33,8 +32,16 @@ fn test_minimum_decision_tree_creation() { let problem = issue_instance(); assert_eq!(problem.num_objects(), 4); assert_eq!(problem.num_tests(), 3); - assert_eq!(problem.dimensions().len(), 7); // 2^(4-1) - 1 = 7 - assert_eq!(problem.dimensions(), vec![4; 7]); // 3 tests + 1 sentinel = 4 choices + assert_eq!( + crate::solvers::cartesian_dimensions(&problem) + .unwrap() + .len(), + 7 + ); // 2^(4-1) - 1 = 7 + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![4; 7] + ); // 3 tests + 1 sentinel = 4 choices } #[test] @@ -111,8 +118,13 @@ fn test_minimum_decision_tree_two_objects() { 2, 1, ); - assert_eq!(problem.dimensions().len(), 1); // 2^(2-1) - 1 = 1 slot - // Test at root, both objects go to leaves at depth 1 + assert_eq!( + crate::solvers::cartesian_dimensions(&problem) + .unwrap() + .len(), + 1 + ); // 2^(2-1) - 1 = 1 slot + // Test at root, both objects go to leaves at depth 1 assert_eq!(problem.evaluate(&vec![0]).unwrap(), Min(Some(2))); // depth 1 + depth 1 assert_eq!(problem.evaluate(&vec![1]).unwrap(), Min(None)); // sentinel=1 is leaf at root, both objects at same leaf } diff --git a/src/unit_tests/models/misc/minimum_discrete_planar_inverse_kinematics.rs b/src/unit_tests/models/misc/minimum_discrete_planar_inverse_kinematics.rs index 6c82a88b4..b583a0665 100644 --- a/src/unit_tests/models/misc/minimum_discrete_planar_inverse_kinematics.rs +++ b/src/unit_tests/models/misc/minimum_discrete_planar_inverse_kinematics.rs @@ -25,8 +25,11 @@ fn test_minimum_discrete_planar_inverse_kinematics_creation() { assert_eq!(problem.target_point(), (2.0, 1.0)); assert_eq!(problem.orientation_samples().len(), 2); assert_eq!(problem.allowed_pairs().len(), 1); - assert_eq!(problem.dimensions(), vec![2, 2]); - assert_eq!(problem.num_variables(), 2); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2, 2] + ); + assert_eq!(problem.num_variables().unwrap(), 2); assert_eq!(problem.num_orientation_samples(), 4); } @@ -116,7 +119,10 @@ fn test_minimum_discrete_planar_inverse_kinematics_serialization() { problem.orientation_samples() ); assert_eq!(restored.allowed_pairs(), problem.allowed_pairs()); - assert_eq!(restored.dimensions(), problem.dimensions()); + assert_eq!( + crate::solvers::cartesian_dimensions(&restored).unwrap(), + crate::solvers::cartesian_dimensions(&problem).unwrap() + ); } #[test] diff --git a/src/unit_tests/models/misc/minimum_disjunctive_normal_form.rs b/src/unit_tests/models/misc/minimum_disjunctive_normal_form.rs index 75c7388ee..4daf64f6e 100644 --- a/src/unit_tests/models/misc/minimum_disjunctive_normal_form.rs +++ b/src/unit_tests/models/misc/minimum_disjunctive_normal_form.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::traits::Problem; use crate::types::Min; @@ -15,7 +14,10 @@ fn test_minimum_dnf_creation() { assert_eq!(problem.num_variables(), 3); assert_eq!(problem.minterms().len(), 6); assert_eq!(problem.num_prime_implicants(), 6); - assert_eq!(problem.dimensions(), vec![2; 6]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 6] + ); } #[test] diff --git a/src/unit_tests/models/misc/minimum_external_macro_data_compression.rs b/src/unit_tests/models/misc/minimum_external_macro_data_compression.rs index 13b50275b..dbb6a86b5 100644 --- a/src/unit_tests/models/misc/minimum_external_macro_data_compression.rs +++ b/src/unit_tests/models/misc/minimum_external_macro_data_compression.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::traits::Problem; use crate::types::Min; @@ -20,7 +19,7 @@ fn test_minimum_external_macro_data_compression_creation() { vec![] ); // dims: 6 D-slots (domain 4) + 6 C-slots (domain 4 + 6*7/2 = 25) - let dims = problem.dimensions(); + let dims = crate::solvers::cartesian_dimensions(&problem).unwrap(); assert_eq!(dims.len(), 12); assert_eq!(dims[0], 4); // alphabet_size + 1 assert_eq!(dims[6], 25); // alphabet_size + 1 + 6*7/2 @@ -96,7 +95,10 @@ fn test_minimum_external_macro_data_compression_evaluate_pointer_out_of_range() #[test] fn test_minimum_external_macro_data_compression_empty_string() { let problem = MinimumExternalMacroDataCompression::new(2, vec![], 2); - assert_eq!(problem.dimensions(), Vec::::new()); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + Vec::::new() + ); assert_eq!(problem.evaluate(&vec![]).unwrap(), Min(Some(0))); } diff --git a/src/unit_tests/models/misc/minimum_fault_detection_test_set.rs b/src/unit_tests/models/misc/minimum_fault_detection_test_set.rs index 50b725e11..532e40ff6 100644 --- a/src/unit_tests/models/misc/minimum_fault_detection_test_set.rs +++ b/src/unit_tests/models/misc/minimum_fault_detection_test_set.rs @@ -35,8 +35,11 @@ fn test_minimum_fault_detection_test_set_creation() { assert_eq!(problem.num_inputs(), 2); assert_eq!(problem.num_outputs(), 2); // 2 inputs * 2 outputs = 4 pairs - assert_eq!(problem.num_variables(), 4); - assert_eq!(problem.dimensions(), vec![2; 4]); + assert_eq!(problem.num_variables().unwrap(), 4); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 4] + ); assert_eq!( ::NAME, "MinimumFaultDetectionTestSet" diff --git a/src/unit_tests/models/misc/minimum_internal_macro_data_compression.rs b/src/unit_tests/models/misc/minimum_internal_macro_data_compression.rs index 82be1e6e6..25c03c2c5 100644 --- a/src/unit_tests/models/misc/minimum_internal_macro_data_compression.rs +++ b/src/unit_tests/models/misc/minimum_internal_macro_data_compression.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::traits::Problem; use crate::types::Min; @@ -20,7 +19,7 @@ fn test_minimum_internal_macro_data_compression_creation() { vec![] ); // dims: 9 slots, domain = 3 + 9 + 1 = 13 - let dims = problem.dimensions(); + let dims = crate::solvers::cartesian_dimensions(&problem).unwrap(); assert_eq!(dims.len(), 9); assert!(dims.iter().all(|&d| d == 13)); } @@ -88,7 +87,10 @@ fn test_minimum_internal_macro_data_compression_evaluate_pointer_forward_ref() { #[test] fn test_minimum_internal_macro_data_compression_empty_string() { let problem = MinimumInternalMacroDataCompression::new(2, vec![], 2); - assert_eq!(problem.dimensions(), Vec::::new()); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + Vec::::new() + ); assert_eq!(problem.evaluate(&vec![]).unwrap(), Min(Some(0))); } diff --git a/src/unit_tests/models/misc/minimum_register_sufficiency_for_loops.rs b/src/unit_tests/models/misc/minimum_register_sufficiency_for_loops.rs index 1c07a636d..051e79db3 100644 --- a/src/unit_tests/models/misc/minimum_register_sufficiency_for_loops.rs +++ b/src/unit_tests/models/misc/minimum_register_sufficiency_for_loops.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::traits::Problem; use crate::types::Min; @@ -10,7 +9,10 @@ fn test_creation() { assert_eq!(problem.loop_length(), 6); assert_eq!(problem.num_variables(), 3); assert_eq!(problem.variables(), &[(0, 3), (2, 3), (4, 3)]); - assert_eq!(problem.dimensions(), vec![3, 3, 3]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![3, 3, 3] + ); } #[test] diff --git a/src/unit_tests/models/misc/minimum_tardiness_sequencing.rs b/src/unit_tests/models/misc/minimum_tardiness_sequencing.rs index 635e66453..c9cab81d3 100644 --- a/src/unit_tests/models/misc/minimum_tardiness_sequencing.rs +++ b/src/unit_tests/models/misc/minimum_tardiness_sequencing.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::traits::Problem; use crate::types::One; @@ -17,7 +16,10 @@ fn test_minimum_tardiness_sequencing_basic() { assert_eq!(problem.deadlines(), &[5, 5, 5, 3, 3]); assert_eq!(problem.precedences(), &[(0, 3), (1, 3), (1, 4), (2, 4)]); assert_eq!(problem.num_precedences(), 4); - assert_eq!(problem.dimensions(), vec![5, 4, 3, 2, 1]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![5, 4, 3, 2, 1] + ); assert_eq!( as Problem>::NAME, "MinimumTardinessSequencing" @@ -126,14 +128,20 @@ fn test_minimum_tardiness_sequencing_serialization() { fn test_minimum_tardiness_sequencing_empty() { let problem = MinimumTardinessSequencing::::new(0, vec![], vec![]); assert_eq!(problem.num_tasks(), 0); - assert_eq!(problem.dimensions(), Vec::::new()); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + Vec::::new() + ); assert_eq!(problem.evaluate(&vec![]).unwrap(), Min(Some(0))); } #[test] fn test_minimum_tardiness_sequencing_single_task() { let problem = MinimumTardinessSequencing::::new(1, vec![1], vec![]); - assert_eq!(problem.dimensions(), vec![1]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![1] + ); assert_eq!(problem.evaluate(&vec![0]).unwrap(), Min(Some(0))); let problem_tardy = MinimumTardinessSequencing::::new(1, vec![0], vec![]); diff --git a/src/unit_tests/models/misc/minimum_weight_and_or_graph.rs b/src/unit_tests/models/misc/minimum_weight_and_or_graph.rs index 5cbe85f53..1142adcdf 100644 --- a/src/unit_tests/models/misc/minimum_weight_and_or_graph.rs +++ b/src/unit_tests/models/misc/minimum_weight_and_or_graph.rs @@ -38,8 +38,11 @@ fn test_minimum_weight_and_or_graph_creation() { assert_eq!(problem.source(), 0); assert_eq!(problem.gate_types().len(), 7); assert_eq!(problem.arc_weights().len(), 6); - assert_eq!(problem.num_variables(), 6); - assert_eq!(problem.dimensions(), vec![2; 6]); + assert_eq!(problem.num_variables().unwrap(), 6); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 6] + ); assert_eq!( ::NAME, "MinimumWeightAndOrGraph" diff --git a/src/unit_tests/models/misc/multiprocessor_scheduling.rs b/src/unit_tests/models/misc/multiprocessor_scheduling.rs index a8581588c..622523157 100644 --- a/src/unit_tests/models/misc/multiprocessor_scheduling.rs +++ b/src/unit_tests/models/misc/multiprocessor_scheduling.rs @@ -1,5 +1,4 @@ use super::*; -use crate::solvers::BruteForceProblem as _; #[test] fn create_spec_rejects_zero_processors() { @@ -28,7 +27,10 @@ fn test_multiprocessor_scheduling_basic() { assert_eq!(problem.num_processors(), 2); assert_eq!(problem.deadline(), 10); assert_eq!(problem.total_length(), 20); - assert_eq!(problem.dimensions(), vec![2; 5]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 5] + ); assert_eq!( ::NAME, "MultiprocessorScheduling" @@ -84,7 +86,10 @@ fn test_multiprocessor_scheduling_invalid_processor_index() { fn test_multiprocessor_scheduling_empty_instance() { let problem = MultiprocessorScheduling::new(vec![], 2, 10); assert_eq!(problem.num_tasks(), 0); - assert_eq!(problem.dimensions(), Vec::::new()); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + Vec::::new() + ); // Empty assignment is always feasible assert!(problem.evaluate(&vec![]).unwrap()); } @@ -106,7 +111,10 @@ fn test_multiprocessor_scheduling_single_task_exceeds_deadline() { #[test] fn test_multiprocessor_scheduling_three_processors() { let problem = MultiprocessorScheduling::new(vec![3, 3, 3], 3, 3); - assert_eq!(problem.dimensions(), vec![3; 3]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![3; 3] + ); // One task per processor assert!(problem.evaluate(&vec![0, 1, 2]).unwrap()); // Two tasks on one processor exceeds deadline diff --git a/src/unit_tests/models/misc/non_liveness_free_petri_net.rs b/src/unit_tests/models/misc/non_liveness_free_petri_net.rs index 987c583b8..66ba4a44b 100644 --- a/src/unit_tests/models/misc/non_liveness_free_petri_net.rs +++ b/src/unit_tests/models/misc/non_liveness_free_petri_net.rs @@ -29,8 +29,11 @@ fn test_non_liveness_free_petri_net_basic() { assert_eq!(problem.num_transitions(), 3); assert_eq!(problem.num_arcs(), 6); assert_eq!(problem.initial_token_sum(), 1); - assert_eq!(problem.dimensions(), vec![2; 3]); - assert_eq!(problem.num_variables(), 3); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 3] + ); + assert_eq!(problem.num_variables().unwrap(), 3); assert_eq!( ::NAME, "NonLivenessFreePetriNet" diff --git a/src/unit_tests/models/misc/numerical_3_dimensional_matching.rs b/src/unit_tests/models/misc/numerical_3_dimensional_matching.rs index aa0f97684..cfbe42bb8 100644 --- a/src/unit_tests/models/misc/numerical_3_dimensional_matching.rs +++ b/src/unit_tests/models/misc/numerical_3_dimensional_matching.rs @@ -18,8 +18,11 @@ fn test_numerical_3dm_creation() { assert_eq!(problem.sizes_y(), &[5, 7]); assert_eq!(problem.bound(), 15); assert_eq!(problem.num_groups(), 2); - assert_eq!(problem.dimensions(), vec![2; 4]); - assert_eq!(problem.num_variables(), 4); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 4] + ); + assert_eq!(problem.num_variables().unwrap(), 4); assert_eq!( ::NAME, "Numerical3DimensionalMatching" diff --git a/src/unit_tests/models/misc/numerical_matching_with_target_sums.rs b/src/unit_tests/models/misc/numerical_matching_with_target_sums.rs index c2eb0221e..417969a15 100644 --- a/src/unit_tests/models/misc/numerical_matching_with_target_sums.rs +++ b/src/unit_tests/models/misc/numerical_matching_with_target_sums.rs @@ -17,8 +17,11 @@ fn test_nmts_creation() { assert_eq!(problem.sizes_y(), &[2, 5, 3]); assert_eq!(problem.targets(), &[3, 7, 12]); assert_eq!(problem.num_pairs(), 3); - assert_eq!(problem.dimensions(), vec![3; 3]); - assert_eq!(problem.num_variables(), 3); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![3; 3] + ); + assert_eq!(problem.num_variables().unwrap(), 3); assert_eq!( ::NAME, "NumericalMatchingWithTargetSums" diff --git a/src/unit_tests/models/misc/open_shop_scheduling.rs b/src/unit_tests/models/misc/open_shop_scheduling.rs index a80a35455..6abacfbe6 100644 --- a/src/unit_tests/models/misc/open_shop_scheduling.rs +++ b/src/unit_tests/models/misc/open_shop_scheduling.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::traits::Problem; use crate::types::Min; @@ -60,10 +59,16 @@ fn test_open_shop_scheduling_creation() { #[test] fn test_open_shop_scheduling_dims() { let p = issue_example(); - assert_eq!(p.dimensions(), vec![24usize; 12]); + assert_eq!( + crate::solvers::cartesian_dimensions(&p).unwrap(), + vec![24usize; 12] + ); let p2 = two_by_two(); - assert_eq!(p2.dimensions(), vec![7usize; 4]); + assert_eq!( + crate::solvers::cartesian_dimensions(&p2).unwrap(), + vec![7usize; 4] + ); } // ─── evaluate ──────────────────────────────────────────────────────────────── @@ -119,7 +124,10 @@ fn test_open_shop_scheduling_evaluate_wrong_length() { #[test] fn test_open_shop_scheduling_evaluate_empty() { let p = OpenShopScheduling::new(3, vec![]); - assert_eq!(p.dimensions(), Vec::::new()); + assert_eq!( + crate::solvers::cartesian_dimensions(&p).unwrap(), + Vec::::new() + ); assert_eq!(p.evaluate(&vec![]).unwrap(), Min(Some(0))); } diff --git a/src/unit_tests/models/misc/optimum_communication_spanning_tree.rs b/src/unit_tests/models/misc/optimum_communication_spanning_tree.rs index ef3555fd7..f321e47ad 100644 --- a/src/unit_tests/models/misc/optimum_communication_spanning_tree.rs +++ b/src/unit_tests/models/misc/optimum_communication_spanning_tree.rs @@ -1,5 +1,4 @@ use super::*; -use crate::solvers::BruteForceProblem as _; #[test] fn create_spec_defaults_edge_weights() { @@ -36,7 +35,10 @@ fn test_ocst_creation() { let problem = k4_problem(); assert_eq!(problem.num_vertices(), 4); assert_eq!(problem.num_edges(), 6); - assert_eq!(problem.dimensions(), vec![2; 6]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 6] + ); assert_eq!( ::NAME, "OptimumCommunicationSpanningTree" diff --git a/src/unit_tests/models/misc/paintshop.rs b/src/unit_tests/models/misc/paintshop.rs index 93fce4912..db68a3b4d 100644 --- a/src/unit_tests/models/misc/paintshop.rs +++ b/src/unit_tests/models/misc/paintshop.rs @@ -1,15 +1,15 @@ use super::*; +include!("../../jl_helpers.rs"); use crate::solvers::BruteForce; use crate::solvers::BruteForceProblem as _; use crate::traits::Problem; -include!("../../jl_helpers.rs"); #[test] fn test_paintshop_creation() { let problem = PaintShop::new(vec!["a", "b", "a", "b"]); assert_eq!(problem.num_cars(), 2); assert_eq!(problem.sequence_len(), 4); - assert_eq!(problem.num_variables(), 2); + assert_eq!(problem.num_variables().unwrap(), 2); } #[test] diff --git a/src/unit_tests/models/misc/partially_ordered_knapsack.rs b/src/unit_tests/models/misc/partially_ordered_knapsack.rs index 58794aeaf..26311ed1a 100644 --- a/src/unit_tests/models/misc/partially_ordered_knapsack.rs +++ b/src/unit_tests/models/misc/partially_ordered_knapsack.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::traits::Problem; /// Helper: create the example instance from the issue. @@ -29,7 +28,10 @@ fn test_partially_ordered_knapsack_basic() { &[(0, 2), (0, 3), (1, 4), (3, 5), (4, 5)] ); assert_eq!(problem.capacity(), 11); - assert_eq!(problem.dimensions(), vec![2; 6]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 6] + ); assert_eq!( ::NAME, "PartiallyOrderedKnapsack" @@ -169,7 +171,10 @@ fn test_partially_ordered_knapsack_empty_instance() { let problem = PartiallyOrderedKnapsack::new(vec![], vec![], vec![], 10); assert_eq!(problem.num_items(), 0); assert_eq!(problem.num_precedences(), 0); - assert_eq!(problem.dimensions(), Vec::::new()); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + Vec::::new() + ); assert_eq!(problem.evaluate(&vec![]).unwrap(), Max(Some(0))); } diff --git a/src/unit_tests/models/misc/partition.rs b/src/unit_tests/models/misc/partition.rs index f6a969235..94a032b38 100644 --- a/src/unit_tests/models/misc/partition.rs +++ b/src/unit_tests/models/misc/partition.rs @@ -1,6 +1,5 @@ use crate::models::misc::Partition; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::traits::Problem; #[test] @@ -9,7 +8,10 @@ fn test_partition_basic() { assert_eq!(problem.num_elements(), 6); assert_eq!(problem.sizes(), &[3, 1, 1, 2, 2, 1]); assert_eq!(problem.total_sum(), 10); - assert_eq!(problem.dimensions(), vec![2; 6]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 6] + ); } #[test] diff --git a/src/unit_tests/models/misc/precedence_constrained_scheduling.rs b/src/unit_tests/models/misc/precedence_constrained_scheduling.rs index 47cf34313..445331039 100644 --- a/src/unit_tests/models/misc/precedence_constrained_scheduling.rs +++ b/src/unit_tests/models/misc/precedence_constrained_scheduling.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::traits::Problem; #[test] @@ -10,7 +9,10 @@ fn test_precedence_constrained_scheduling_basic() { assert_eq!(problem.num_processors(), 2); assert_eq!(problem.deadline(), 3); assert_eq!(problem.precedences(), &[(0, 2), (1, 3)]); - assert_eq!(problem.dimensions(), vec![3; 4]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![3; 4] + ); assert_eq!( ::NAME, "PrecedenceConstrainedScheduling" @@ -129,7 +131,10 @@ fn test_precedence_constrained_scheduling_serialization() { fn test_precedence_constrained_scheduling_empty() { let problem = PrecedenceConstrainedScheduling::new(0, 1, 1, vec![]); assert_eq!(problem.num_tasks(), 0); - assert_eq!(problem.dimensions(), Vec::::new()); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + Vec::::new() + ); assert!(problem.evaluate(&vec![]).unwrap()); } diff --git a/src/unit_tests/models/misc/preemptive_scheduling.rs b/src/unit_tests/models/misc/preemptive_scheduling.rs index 299bb177c..c300f06fe 100644 --- a/src/unit_tests/models/misc/preemptive_scheduling.rs +++ b/src/unit_tests/models/misc/preemptive_scheduling.rs @@ -1,5 +1,4 @@ use super::*; -use crate::solvers::BruteForceProblem as _; use crate::traits::Problem; use crate::types::Min; @@ -29,7 +28,10 @@ fn test_preemptive_scheduling_creation() { assert_eq!(p.lengths(), &[2, 1, 3]); assert_eq!(p.precedences(), &[(0, 2)]); assert_eq!(p.d_max(), 6); - assert_eq!(p.dimensions(), vec![2; 3 * 6]); + assert_eq!( + crate::solvers::cartesian_dimensions(&p).unwrap(), + vec![2; 3 * 6] + ); assert_eq!( ::NAME, "PreemptiveScheduling" @@ -42,7 +44,10 @@ fn test_preemptive_scheduling_empty_tasks() { let p = PreemptiveScheduling::new(vec![], 1, vec![]).unwrap(); assert_eq!(p.num_tasks(), 0); assert_eq!(p.d_max(), 0); - assert_eq!(p.dimensions(), Vec::::new()); + assert_eq!( + crate::solvers::cartesian_dimensions(&p).unwrap(), + Vec::::new() + ); assert_eq!(p.evaluate(&vec![]).unwrap(), Min(Some(0))); } diff --git a/src/unit_tests/models/misc/production_planning.rs b/src/unit_tests/models/misc/production_planning.rs index 1141a3d07..6615a4fb7 100644 --- a/src/unit_tests/models/misc/production_planning.rs +++ b/src/unit_tests/models/misc/production_planning.rs @@ -1,5 +1,4 @@ use super::*; -use crate::solvers::BruteForceProblem as _; #[test] fn create_spec_rejects_period_vector_mismatch() { @@ -54,7 +53,10 @@ fn test_production_planning_creation() { assert_eq!(problem.inventory_costs(), &[1, 1, 1, 1, 1, 1]); assert_eq!(problem.cost_bound(), 80); assert_eq!(problem.max_capacity(), 12); - assert_eq!(problem.dimensions(), vec![13; 6]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![13; 6] + ); assert_eq!(::NAME, "ProductionPlanning"); assert_eq!(::variant(), vec![]); } diff --git a/src/unit_tests/models/misc/rectilinear_picture_compression.rs b/src/unit_tests/models/misc/rectilinear_picture_compression.rs index 00acdec63..e1f2c2486 100644 --- a/src/unit_tests/models/misc/rectilinear_picture_compression.rs +++ b/src/unit_tests/models/misc/rectilinear_picture_compression.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::traits::Problem; fn two_block_matrix() -> Vec> { @@ -52,7 +51,10 @@ fn test_rectilinear_picture_compression_maximal_rectangles_two_blocks() { fn test_rectilinear_picture_compression_dims() { let problem = RectilinearPictureCompression::new(two_block_matrix(), 2); // 2 maximal rectangles -> 2 binary variables - assert_eq!(problem.dimensions(), vec![2, 2]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2, 2] + ); } #[test] @@ -173,7 +175,10 @@ fn test_rectilinear_picture_compression_single_cell() { let problem = RectilinearPictureCompression::new(matrix, 1); let rects = problem.maximal_rectangles(); assert_eq!(rects, vec![(0, 0, 0, 0)]); - assert_eq!(problem.dimensions(), vec![2]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2] + ); assert!(problem.evaluate(&vec![true]).unwrap()); assert!(!problem.evaluate(&vec![false]).unwrap()); } @@ -185,7 +190,10 @@ fn test_rectilinear_picture_compression_all_zeros() { let problem = RectilinearPictureCompression::new(matrix, 0); let rects = problem.maximal_rectangles(); assert!(rects.is_empty()); - assert_eq!(problem.dimensions(), Vec::::new()); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + Vec::::new() + ); // Empty config satisfies (no 1-entries to cover) assert!(problem.evaluate(&vec![]).unwrap()); } diff --git a/src/unit_tests/models/misc/register_sufficiency.rs b/src/unit_tests/models/misc/register_sufficiency.rs index a112f8bc8..1de17b70f 100644 --- a/src/unit_tests/models/misc/register_sufficiency.rs +++ b/src/unit_tests/models/misc/register_sufficiency.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::traits::Problem; #[test] @@ -35,7 +34,10 @@ fn test_register_sufficiency_basic() { (6, 5) ] ); - assert_eq!(problem.dimensions(), vec![7; 7]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![7; 7] + ); assert_eq!( ::NAME, "RegisterSufficiency" @@ -183,7 +185,10 @@ fn test_register_sufficiency_serialization() { fn test_register_sufficiency_empty() { let problem = RegisterSufficiency::new(0, vec![], 0); assert_eq!(problem.num_vertices(), 0); - assert_eq!(problem.dimensions(), Vec::::new()); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + Vec::::new() + ); assert!(problem.evaluate(&vec![]).unwrap()); } diff --git a/src/unit_tests/models/misc/resource_constrained_scheduling.rs b/src/unit_tests/models/misc/resource_constrained_scheduling.rs index 70415a3b0..45c71ee8f 100644 --- a/src/unit_tests/models/misc/resource_constrained_scheduling.rs +++ b/src/unit_tests/models/misc/resource_constrained_scheduling.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::traits::Problem; #[test] @@ -17,9 +16,17 @@ fn test_resource_constrained_scheduling_creation() { assert_eq!(problem.resource_bounds(), &[20]); assert_eq!(problem.deadline(), 2); assert_eq!(problem.num_resources(), 1); - assert_eq!(problem.dimensions().len(), 6); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem) + .unwrap() + .len(), + 6 + ); // Each variable has domain {0, 1} (deadline = 2) - assert!(problem.dimensions().iter().all(|&d| d == 2)); + assert!(crate::solvers::cartesian_dimensions(&problem) + .unwrap() + .iter() + .all(|&d| d == 2)); } #[test] @@ -115,7 +122,10 @@ fn test_resource_constrained_scheduling_empty_tasks() { let problem = ResourceConstrainedScheduling::new(2, vec![10], Vec::>::new(), 3).unwrap(); assert_eq!(problem.num_tasks(), 0); - assert_eq!(problem.dimensions(), Vec::::new()); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + Vec::::new() + ); assert!(problem.evaluate(&vec![]).unwrap()); } diff --git a/src/unit_tests/models/misc/scheduling_to_minimize_weighted_completion_time.rs b/src/unit_tests/models/misc/scheduling_to_minimize_weighted_completion_time.rs index 54fd389aa..9ad8c1347 100644 --- a/src/unit_tests/models/misc/scheduling_to_minimize_weighted_completion_time.rs +++ b/src/unit_tests/models/misc/scheduling_to_minimize_weighted_completion_time.rs @@ -1,5 +1,4 @@ use super::*; -use crate::solvers::BruteForceProblem as _; #[test] fn create_spec_defaults_task_weights() { @@ -28,7 +27,10 @@ fn test_scheduling_min_wct_creation() { assert_eq!(problem.num_processors(), 2); assert_eq!(problem.lengths(), &[1, 2, 3, 4, 5]); assert_eq!(problem.weights(), &[6, 4, 3, 2, 1]); - assert_eq!(problem.dimensions(), vec![2; 5]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 5] + ); assert_eq!( ::NAME, "SchedulingToMinimizeWeightedCompletionTime" @@ -195,7 +197,10 @@ fn test_scheduling_min_wct_single_processor() { #[test] fn test_scheduling_min_wct_three_processors() { let problem = SchedulingToMinimizeWeightedCompletionTime::new(vec![3, 3, 3], vec![1, 1, 1], 3); - assert_eq!(problem.dimensions(), vec![3; 3]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![3; 3] + ); // One task per processor: each completes at 3, WCT = 3*1 + 3*1 + 3*1 = 9 assert_eq!(problem.evaluate(&vec![0, 1, 2]).unwrap(), Min(Some(9))); // All on one processor: C(t0)=3, C(t1)=6, C(t2)=9, WCT = 3+6+9 = 18 diff --git a/src/unit_tests/models/misc/scheduling_with_individual_deadlines.rs b/src/unit_tests/models/misc/scheduling_with_individual_deadlines.rs index 3a017d7a4..8268ad0ae 100644 --- a/src/unit_tests/models/misc/scheduling_with_individual_deadlines.rs +++ b/src/unit_tests/models/misc/scheduling_with_individual_deadlines.rs @@ -1,5 +1,4 @@ use super::*; -use crate::solvers::BruteForceProblem as _; #[test] fn create_spec_rejects_deadline_count_mismatch() { @@ -42,7 +41,10 @@ fn test_scheduling_with_individual_deadlines_basic() { ); assert_eq!(problem.num_precedences(), 5); assert_eq!(problem.max_deadline(), 3); - assert_eq!(problem.dimensions(), vec![2, 1, 2, 2, 3, 3, 2]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2, 1, 2, 2, 3, 3, 2] + ); assert_eq!( ::NAME, "SchedulingWithIndividualDeadlines" diff --git a/src/unit_tests/models/misc/sequencing_to_minimize_maximum_cumulative_cost.rs b/src/unit_tests/models/misc/sequencing_to_minimize_maximum_cumulative_cost.rs index de3ae8efe..49cb187fc 100644 --- a/src/unit_tests/models/misc/sequencing_to_minimize_maximum_cumulative_cost.rs +++ b/src/unit_tests/models/misc/sequencing_to_minimize_maximum_cumulative_cost.rs @@ -1,5 +1,4 @@ use super::*; -use crate::solvers::BruteForceProblem as _; #[test] fn create_spec_defaults_precedences() { @@ -32,7 +31,10 @@ fn test_sequencing_to_minimize_maximum_cumulative_cost_creation() { ); assert_eq!(problem.num_tasks(), 6); assert_eq!(problem.num_precedences(), 6); - assert_eq!(problem.dimensions(), vec![6, 5, 4, 3, 2, 1]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![6, 5, 4, 3, 2, 1] + ); assert_eq!( ::NAME, "SequencingToMinimizeMaximumCumulativeCost" @@ -128,7 +130,10 @@ fn test_sequencing_to_minimize_maximum_cumulative_cost_solver_aggregate() { fn test_sequencing_to_minimize_maximum_cumulative_cost_empty_instance() { let problem = SequencingToMinimizeMaximumCumulativeCost::new(vec![], vec![]); assert_eq!(problem.num_tasks(), 0); - assert_eq!(problem.dimensions(), Vec::::new()); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + Vec::::new() + ); // Empty schedule: no tasks, max cumulative cost is 0. let val = problem.evaluate(&vec![]).unwrap(); assert_eq!(val, Min(Some(0))); diff --git a/src/unit_tests/models/misc/sequencing_to_minimize_tardy_task_weight.rs b/src/unit_tests/models/misc/sequencing_to_minimize_tardy_task_weight.rs index feae2b123..739b1530c 100644 --- a/src/unit_tests/models/misc/sequencing_to_minimize_tardy_task_weight.rs +++ b/src/unit_tests/models/misc/sequencing_to_minimize_tardy_task_weight.rs @@ -1,5 +1,4 @@ use super::*; -use crate::solvers::BruteForceProblem as _; #[test] fn create_spec_defaults_task_weights() { @@ -29,7 +28,10 @@ fn test_sequencing_to_minimize_tardy_task_weight_basic() { assert_eq!(problem.lengths(), &[3, 2, 4, 1, 2]); assert_eq!(problem.weights(), &[5, 3, 7, 2, 4]); assert_eq!(problem.deadlines(), &[6, 4, 10, 2, 8]); - assert_eq!(problem.dimensions(), vec![5, 5, 5, 5, 5]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![5, 5, 5, 5, 5] + ); assert_eq!( ::NAME, "SequencingToMinimizeTardyTaskWeight" @@ -187,7 +189,10 @@ fn test_sequencing_to_minimize_tardy_task_weight_deserialization_rejects_zero_we #[test] fn test_sequencing_to_minimize_tardy_task_weight_single_task() { let problem = SequencingToMinimizeTardyTaskWeight::new(vec![3], vec![2], vec![5]); - assert_eq!(problem.dimensions(), vec![1]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![1] + ); // completes at 3, deadline 5, on time assert_eq!(problem.evaluate(&vec![0]).unwrap(), Min(Some(0))); } @@ -203,7 +208,10 @@ fn test_sequencing_to_minimize_tardy_task_weight_single_task_tardy() { fn test_sequencing_to_minimize_tardy_task_weight_empty() { let problem = SequencingToMinimizeTardyTaskWeight::new(vec![], vec![], vec![]); assert_eq!(problem.num_tasks(), 0); - assert_eq!(problem.dimensions(), Vec::::new()); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + Vec::::new() + ); assert_eq!(problem.evaluate(&vec![]).unwrap(), Min(Some(0))); } diff --git a/src/unit_tests/models/misc/sequencing_to_minimize_weighted_completion_time.rs b/src/unit_tests/models/misc/sequencing_to_minimize_weighted_completion_time.rs index 97c31b24d..688705c4c 100644 --- a/src/unit_tests/models/misc/sequencing_to_minimize_weighted_completion_time.rs +++ b/src/unit_tests/models/misc/sequencing_to_minimize_weighted_completion_time.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::traits::Problem; use crate::types::Min; @@ -17,7 +16,10 @@ fn test_sequencing_to_minimize_weighted_completion_time_basic() { assert_eq!(problem.weights(), &[3, 5, 1, 4, 2]); assert_eq!(problem.precedences(), &[(0, 2), (1, 4)]); assert_eq!(problem.num_precedences(), 2); - assert_eq!(problem.dimensions(), vec![5, 4, 3, 2, 1]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![5, 4, 3, 2, 1] + ); assert_eq!( ::NAME, "SequencingToMinimizeWeightedCompletionTime" @@ -129,7 +131,10 @@ fn test_sequencing_to_minimize_weighted_completion_time_empty() { let problem = SequencingToMinimizeWeightedCompletionTime::new(vec![], vec![], vec![]); assert_eq!(problem.num_tasks(), 0); - assert_eq!(problem.dimensions(), Vec::::new()); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + Vec::::new() + ); assert_eq!(problem.evaluate(&vec![]).unwrap(), Min(Some(0))); } @@ -137,7 +142,10 @@ fn test_sequencing_to_minimize_weighted_completion_time_empty() { fn test_sequencing_to_minimize_weighted_completion_time_single_task() { let problem = SequencingToMinimizeWeightedCompletionTime::new(vec![3], vec![2], vec![]); - assert_eq!(problem.dimensions(), vec![1]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![1] + ); assert_eq!(problem.evaluate(&vec![0]).unwrap(), Min(Some(6))); } diff --git a/src/unit_tests/models/misc/sequencing_to_minimize_weighted_tardiness.rs b/src/unit_tests/models/misc/sequencing_to_minimize_weighted_tardiness.rs index 3214d9c90..c3b28a9c2 100644 --- a/src/unit_tests/models/misc/sequencing_to_minimize_weighted_tardiness.rs +++ b/src/unit_tests/models/misc/sequencing_to_minimize_weighted_tardiness.rs @@ -1,5 +1,4 @@ use super::*; -use crate::solvers::BruteForceProblem as _; #[test] fn create_spec_rejects_vector_length_mismatch() { @@ -47,7 +46,10 @@ fn test_sequencing_to_minimize_weighted_tardiness_basic() { assert_eq!(problem.deadlines(), &[5, 8, 4, 15, 10]); assert_eq!(problem.bound(), 13); assert_eq!(problem.num_tasks(), 5); - assert_eq!(problem.dimensions(), vec![5, 4, 3, 2, 1]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![5, 4, 3, 2, 1] + ); assert_eq!( ::NAME, "SequencingToMinimizeWeightedTardiness" diff --git a/src/unit_tests/models/misc/sequencing_with_deadlines_and_set_up_times.rs b/src/unit_tests/models/misc/sequencing_with_deadlines_and_set_up_times.rs index 26f9f9f0a..7d406a2e1 100644 --- a/src/unit_tests/models/misc/sequencing_with_deadlines_and_set_up_times.rs +++ b/src/unit_tests/models/misc/sequencing_with_deadlines_and_set_up_times.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::traits::Problem; use crate::types::Or; @@ -19,7 +18,10 @@ fn test_sequencing_with_deadlines_and_set_up_times_creation() { assert_eq!(problem.deadlines(), &[4, 11, 3, 16, 7]); assert_eq!(problem.compilers(), &[0, 1, 0, 1, 0]); assert_eq!(problem.setup_times(), &[1, 2]); - assert_eq!(problem.dimensions(), vec![5, 5, 5, 5, 5]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![5, 5, 5, 5, 5] + ); assert_eq!( ::NAME, "SequencingWithDeadlinesAndSetUpTimes" diff --git a/src/unit_tests/models/misc/sequencing_with_release_times_and_deadlines.rs b/src/unit_tests/models/misc/sequencing_with_release_times_and_deadlines.rs index 1c86ed2d8..64fbb012b 100644 --- a/src/unit_tests/models/misc/sequencing_with_release_times_and_deadlines.rs +++ b/src/unit_tests/models/misc/sequencing_with_release_times_and_deadlines.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::traits::Problem; #[test] @@ -16,7 +15,10 @@ fn test_sequencing_rtd_basic() { assert_eq!(problem.deadlines(), &[5, 6, 10, 3, 12]); assert_eq!(problem.time_horizon(), 12); // Lehmer code dims: [5, 4, 3, 2, 1] - assert_eq!(problem.dimensions(), vec![5, 4, 3, 2, 1]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![5, 4, 3, 2, 1] + ); assert_eq!( ::NAME, "SequencingWithReleaseTimesAndDeadlines" @@ -73,14 +75,20 @@ fn test_sequencing_rtd_empty_instance() { let problem = SequencingWithReleaseTimesAndDeadlines::new(vec![], vec![], vec![]); assert_eq!(problem.num_tasks(), 0); assert_eq!(problem.time_horizon(), 0); - assert_eq!(problem.dimensions(), Vec::::new()); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + Vec::::new() + ); assert!(problem.evaluate(&vec![]).unwrap()); } #[test] fn test_sequencing_rtd_single_task() { let problem = SequencingWithReleaseTimesAndDeadlines::new(vec![2], vec![1], vec![5]); - assert_eq!(problem.dimensions(), vec![1]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![1] + ); // Only one permutation: task 0 starts at max(1,0)=1, finish=3 <= 5 assert!(problem.evaluate(&vec![0]).unwrap()); } diff --git a/src/unit_tests/models/misc/sequencing_within_intervals.rs b/src/unit_tests/models/misc/sequencing_within_intervals.rs index d040834e4..2bd472838 100644 --- a/src/unit_tests/models/misc/sequencing_within_intervals.rs +++ b/src/unit_tests/models/misc/sequencing_within_intervals.rs @@ -1,5 +1,4 @@ use super::*; -use crate::solvers::BruteForceProblem as _; #[test] fn create_spec_accepts_empty_window() { @@ -37,7 +36,10 @@ fn test_sequencing_within_intervals_creation() { // Task 2: 9 - 3 - 2 + 1 = 5 // Task 3: 12 - 6 - 3 + 1 = 4 // Task 4: 12 - 0 - 2 + 1 = 11 - assert_eq!(problem.dimensions(), vec![4, 6, 5, 4, 11]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![4, 6, 5, 4, 11] + ); } #[test] @@ -152,7 +154,10 @@ fn test_sequencing_within_intervals_empty() { let problem = SequencingWithinIntervals::new(vec![], vec![], vec![]).unwrap(); assert_eq!(problem.num_tasks(), 0); assert_eq!(problem.num_start_slots(), 0); - assert_eq!(problem.dimensions(), Vec::::new()); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + Vec::::new() + ); assert!(problem.evaluate(&vec![]).unwrap()); } @@ -174,7 +179,10 @@ fn test_sequencing_within_intervals_variant() { fn test_sequencing_within_intervals_single_task() { let problem = SequencingWithinIntervals::new(vec![0], vec![5], vec![3]).unwrap(); // dims = 5 - 0 - 3 + 1 = 3 - assert_eq!(problem.dimensions(), vec![3]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![3] + ); // Any valid config should be feasible (only one task, no overlaps possible) assert!(problem.evaluate(&vec![0]).unwrap()); assert!(problem.evaluate(&vec![1]).unwrap()); @@ -217,7 +225,10 @@ fn test_sequencing_within_intervals_empty_start_domain() { .unwrap(); let restored: SequencingWithinIntervals = serde_json::from_value(serde_json::to_value(&problem).unwrap()).unwrap(); - assert_eq!(restored.dimensions(), vec![2, 0]); + assert_eq!( + crate::solvers::cartesian_dimensions(&restored).unwrap(), + vec![2, 0] + ); assert_eq!(restored.num_start_slots(), 2); let (value, witnesses) = BruteForce::new().solve_with_witnesses(&restored).unwrap(); assert_eq!(value, crate::types::Or(false)); diff --git a/src/unit_tests/models/misc/shortest_common_supersequence.rs b/src/unit_tests/models/misc/shortest_common_supersequence.rs index 65dfccfcb..1ba7fbdbf 100644 --- a/src/unit_tests/models/misc/shortest_common_supersequence.rs +++ b/src/unit_tests/models/misc/shortest_common_supersequence.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::traits::Problem; use crate::types::Min; @@ -70,7 +69,10 @@ fn test_shortestcommonsupersequence_basic() { assert_eq!(problem.num_strings(), 3); assert_eq!(problem.max_length(), 12); // 4+4+4 assert_eq!(problem.total_length(), 12); - assert_eq!(problem.dimensions(), vec![4; 12]); // alphabet_size+1 = 4, max_length = 12 + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![4; 12] + ); // alphabet_size+1 = 4, max_length = 12 assert_eq!( ::NAME, "ShortestCommonSupersequence" diff --git a/src/unit_tests/models/misc/shortest_common_superstring.rs b/src/unit_tests/models/misc/shortest_common_superstring.rs index c2462e6c3..a32e6918d 100644 --- a/src/unit_tests/models/misc/shortest_common_superstring.rs +++ b/src/unit_tests/models/misc/shortest_common_superstring.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::traits::Problem; use crate::types::Min; @@ -26,7 +25,10 @@ fn test_shortestcommonsuperstring_basic() { assert_eq!(problem.num_strings(), 3); assert_eq!(problem.max_length(), 9); // 3+3+3 assert_eq!(problem.total_length(), 9); - assert_eq!(problem.dimensions(), vec![4; 9]); // alphabet_size + 1 = 4 across max_length = 9 positions + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![4; 9] + ); // alphabet_size + 1 = 4 across max_length = 9 positions assert_eq!( ::NAME, "ShortestCommonSuperstring" diff --git a/src/unit_tests/models/misc/square_tiling.rs b/src/unit_tests/models/misc/square_tiling.rs index 720a2128a..b26990e1d 100644 --- a/src/unit_tests/models/misc/square_tiling.rs +++ b/src/unit_tests/models/misc/square_tiling.rs @@ -21,8 +21,11 @@ fn test_square_tiling_basic() { assert_eq!(problem.num_tiles(), 4); assert_eq!(problem.grid_size(), 2); assert_eq!(problem.tiles().len(), 4); - assert_eq!(problem.dimensions(), vec![4; 4]); - assert_eq!(problem.num_variables(), 4); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![4; 4] + ); + assert_eq!(problem.num_variables().unwrap(), 4); assert_eq!(::NAME, "SquareTiling"); assert_eq!(::variant(), vec![]); } diff --git a/src/unit_tests/models/misc/stacker_crane.rs b/src/unit_tests/models/misc/stacker_crane.rs index 071b9df8b..2522293d5 100644 --- a/src/unit_tests/models/misc/stacker_crane.rs +++ b/src/unit_tests/models/misc/stacker_crane.rs @@ -1,5 +1,4 @@ use super::*; -use crate::solvers::BruteForceProblem as _; #[test] fn create_spec_defaults_lengths_and_checks_inferred_vertex_counts() { @@ -40,7 +39,10 @@ fn test_stacker_crane_creation_and_metadata() { assert_eq!(problem.num_vertices(), 6); assert_eq!(problem.num_arcs(), 5); assert_eq!(problem.num_edges(), 7); - assert_eq!(problem.dimensions(), vec![5; 5]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![5; 5] + ); assert_eq!(::NAME, "StackerCrane"); assert!(::variant().is_empty()); } diff --git a/src/unit_tests/models/misc/staff_scheduling.rs b/src/unit_tests/models/misc/staff_scheduling.rs index ac58c7aba..25c823aad 100644 --- a/src/unit_tests/models/misc/staff_scheduling.rs +++ b/src/unit_tests/models/misc/staff_scheduling.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::traits::Problem; #[test] @@ -39,7 +38,10 @@ fn test_staff_scheduling_creation() { assert_eq!(problem.num_schedules(), 5); assert_eq!(problem.requirements(), &[2, 2, 2, 3, 3, 2, 1]); assert_eq!(problem.num_workers(), 4); - assert_eq!(problem.dimensions(), vec![5; 5]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![5; 5] + ); } #[test] diff --git a/src/unit_tests/models/misc/string_to_string_correction.rs b/src/unit_tests/models/misc/string_to_string_correction.rs index 4530eab36..f09e06f6b 100644 --- a/src/unit_tests/models/misc/string_to_string_correction.rs +++ b/src/unit_tests/models/misc/string_to_string_correction.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::traits::Problem; #[test] @@ -13,7 +12,10 @@ fn test_string_to_string_correction_creation() { assert_eq!(problem.source_length(), 6); assert_eq!(problem.target_length(), 5); // domain = 2*6+1 = 13, bound = 2 - assert_eq!(problem.dimensions(), vec![13; 2]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![13; 2] + ); assert_eq!( ::NAME, "StringToStringCorrection" @@ -111,7 +113,10 @@ fn test_string_to_string_correction_paper_example() { fn test_string_to_string_correction_unsatisfiable() { // bound=0, source != target → impossible let problem = StringToStringCorrection::new(2, vec![0, 1], vec![1, 0], 0); - assert_eq!(problem.dimensions(), Vec::::new()); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + Vec::::new() + ); assert!(!problem.evaluate(&vec![]).unwrap()); let solver = BruteForce::new(); diff --git a/src/unit_tests/models/misc/subset_product.rs b/src/unit_tests/models/misc/subset_product.rs index 25c767a06..f0344829f 100644 --- a/src/unit_tests/models/misc/subset_product.rs +++ b/src/unit_tests/models/misc/subset_product.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::traits::Problem; use num_bigint::BigUint; @@ -18,7 +17,10 @@ fn test_subsetproduct_basic() { assert_eq!(problem.num_elements(), 6); assert_eq!(problem.sizes(), buv(&[2, 3, 5, 7, 6, 10]).as_slice()); assert_eq!(problem.target(), &bu(210)); - assert_eq!(problem.dimensions(), vec![2; 6]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 6] + ); assert_eq!(::NAME, "SubsetProduct"); assert_eq!(::variant(), vec![]); } @@ -80,7 +82,10 @@ fn test_subsetproduct_empty_instance() { // Empty set, target 1: empty subset product = 1 satisfies let problem = SubsetProduct::new_unchecked(vec![], bu(1)); assert_eq!(problem.num_elements(), 0); - assert_eq!(problem.dimensions(), Vec::::new()); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + Vec::::new() + ); assert!(problem.evaluate(&vec![]).unwrap()); } diff --git a/src/unit_tests/models/misc/subset_sum.rs b/src/unit_tests/models/misc/subset_sum.rs index 80807cefe..bb9102811 100644 --- a/src/unit_tests/models/misc/subset_sum.rs +++ b/src/unit_tests/models/misc/subset_sum.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::traits::Problem; use num_bigint::BigUint; @@ -18,7 +17,10 @@ fn test_subsetsum_basic() { assert_eq!(problem.num_elements(), 6); assert_eq!(problem.sizes(), buv(&[3, 7, 1, 8, 2, 4]).as_slice()); assert_eq!(problem.target(), &bu(11)); - assert_eq!(problem.dimensions(), vec![2; 6]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 6] + ); assert_eq!(::NAME, "SubsetSum"); assert_eq!(::variant(), vec![]); } @@ -80,7 +82,10 @@ fn test_subsetsum_empty_instance() { // Empty set, target 0: empty subset satisfies let problem = SubsetSum::new_unchecked(vec![], bu(0)); assert_eq!(problem.num_elements(), 0); - assert_eq!(problem.dimensions(), Vec::::new()); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + Vec::::new() + ); assert!(problem.evaluate(&vec![]).unwrap()); } diff --git a/src/unit_tests/models/misc/sum_of_squares_partition.rs b/src/unit_tests/models/misc/sum_of_squares_partition.rs index 5afb1a20e..4201e33d3 100644 --- a/src/unit_tests/models/misc/sum_of_squares_partition.rs +++ b/src/unit_tests/models/misc/sum_of_squares_partition.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::traits::Problem; use crate::types::Min; @@ -10,7 +9,10 @@ fn test_sum_of_squares_partition_basic() { assert_eq!(problem.num_elements(), 6); assert_eq!(problem.num_groups(), 3); assert_eq!(problem.sizes(), &[5, 3, 8, 2, 7, 1]); - assert_eq!(problem.dimensions(), vec![3; 6]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![3; 6] + ); assert_eq!( ::NAME, "SumOfSquaresPartition" diff --git a/src/unit_tests/models/misc/three_partition.rs b/src/unit_tests/models/misc/three_partition.rs index 44c354c02..857e61430 100644 --- a/src/unit_tests/models/misc/three_partition.rs +++ b/src/unit_tests/models/misc/three_partition.rs @@ -16,8 +16,11 @@ fn test_three_partition_basic() { assert_eq!(problem.num_elements(), 6); assert_eq!(problem.num_groups(), 2); assert_eq!(problem.total_sum(), 30); - assert_eq!(problem.dimensions(), vec![2; 6]); - assert_eq!(problem.num_variables(), 6); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 6] + ); + assert_eq!(problem.num_variables().unwrap(), 6); assert_eq!(::NAME, "ThreePartition"); assert_eq!(::variant(), vec![]); } diff --git a/src/unit_tests/models/misc/timetable_design.rs b/src/unit_tests/models/misc/timetable_design.rs index ee8aaad02..a2c1322d9 100644 --- a/src/unit_tests/models/misc/timetable_design.rs +++ b/src/unit_tests/models/misc/timetable_design.rs @@ -1,5 +1,4 @@ use super::*; -use crate::solvers::BruteForceProblem as _; #[test] fn create_spec_rejects_matrix_shape_mismatch() { @@ -49,7 +48,10 @@ fn test_timetable_design_creation_and_dims() { ); assert_eq!(problem.task_avail(), &[vec![true, true], vec![false, true]]); assert_eq!(problem.requirements(), &[vec![1, 0], vec![0, 1]]); - assert_eq!(problem.dimensions(), vec![2; 8]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 8] + ); } #[test] diff --git a/src/unit_tests/models/set/comparative_containment.rs b/src/unit_tests/models/set/comparative_containment.rs index 8d0ecd984..2f58e7085 100644 --- a/src/unit_tests/models/set/comparative_containment.rs +++ b/src/unit_tests/models/set/comparative_containment.rs @@ -55,8 +55,11 @@ fn test_comparative_containment_creation() { assert_eq!(problem.universe_size(), 4); assert_eq!(problem.num_r_sets(), 2); assert_eq!(problem.num_s_sets(), 2); - assert_eq!(problem.num_variables(), 4); - assert_eq!(problem.dimensions(), vec![2, 2, 2, 2]); + assert_eq!(problem.num_variables().unwrap(), 4); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2, 2, 2, 2] + ); } #[test] diff --git a/src/unit_tests/models/set/consecutive_sets.rs b/src/unit_tests/models/set/consecutive_sets.rs index 4c94c0afa..76db9b531 100644 --- a/src/unit_tests/models/set/consecutive_sets.rs +++ b/src/unit_tests/models/set/consecutive_sets.rs @@ -13,8 +13,11 @@ fn test_consecutive_sets_creation() { assert_eq!(problem.alphabet_size(), 6); assert_eq!(problem.num_subsets(), 5); assert_eq!(problem.bound_k(), 6); - assert_eq!(problem.num_variables(), 6); - assert_eq!(problem.dimensions(), vec![7; 6]); // alphabet_size + 1 = 7 + assert_eq!(problem.num_variables().unwrap(), 6); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![7; 6] + ); // alphabet_size + 1 = 7 } #[test] diff --git a/src/unit_tests/models/set/exact_cover_by_3_sets.rs b/src/unit_tests/models/set/exact_cover_by_3_sets.rs index a7cdf05cc..9de0ed2e8 100644 --- a/src/unit_tests/models/set/exact_cover_by_3_sets.rs +++ b/src/unit_tests/models/set/exact_cover_by_3_sets.rs @@ -18,8 +18,11 @@ fn test_exact_cover_by_3_sets_creation() { assert_eq!(problem.universe_size(), 6); assert_eq!(problem.num_subsets(), 3); assert_eq!(problem.num_sets(), 3); - assert_eq!(problem.num_variables(), 3); - assert_eq!(problem.dimensions(), vec![2, 2, 2]); + assert_eq!(problem.num_variables().unwrap(), 3); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2, 2, 2] + ); } #[test] diff --git a/src/unit_tests/models/set/integer_knapsack.rs b/src/unit_tests/models/set/integer_knapsack.rs index b03fc1744..e767866ae 100644 --- a/src/unit_tests/models/set/integer_knapsack.rs +++ b/src/unit_tests/models/set/integer_knapsack.rs @@ -10,7 +10,10 @@ fn test_integer_knapsack_basic() { assert_eq!(problem.values(), &[4, 5, 7, 3, 9]); assert_eq!(problem.capacity(), 15); // dims: floor(15/3)+1=6, floor(15/4)+1=4, floor(15/5)+1=4, floor(15/2)+1=8, floor(15/7)+1=3 - assert_eq!(problem.dimensions(), vec![6, 4, 4, 8, 3]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![6, 4, 4, 8, 3] + ); assert_eq!(::NAME, "IntegerKnapsack"); assert_eq!(::variant(), vec![]); } @@ -65,20 +68,19 @@ fn test_integer_knapsack_evaluate_wrong_config_length() { } #[test] -fn test_integer_knapsack_evaluate_out_of_domain() { +fn test_integer_knapsack_evaluate_single_item_overweight() { let problem = IntegerKnapsack::new(vec![3, 4], vec![4, 5], 10).unwrap(); - // dims = [4, 3], so config [4, 0] is out of domain for item 0 - assert!(matches!( - problem.evaluate(&vec![4, 0]), - Err(crate::traits::EvaluationError::InvalidConfiguration(_)) - )); + assert_eq!(problem.evaluate(&vec![4, 0]).unwrap(), Max(None)); } #[test] fn test_integer_knapsack_empty_instance() { let problem = IntegerKnapsack::new(vec![], vec![], 10).unwrap(); assert_eq!(problem.num_items(), 0); - assert_eq!(problem.dimensions(), Vec::::new()); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + Vec::::new() + ); assert_eq!(problem.evaluate(&vec![]).unwrap(), Max(Some(0))); } @@ -107,7 +109,10 @@ fn test_integer_knapsack_serialization() { #[test] fn test_integer_knapsack_zero_capacity() { let problem = IntegerKnapsack::new(vec![1, 2], vec![10, 20], 0).unwrap(); - assert_eq!(problem.dimensions(), vec![1, 1]); // floor(0/1)+1=1, floor(0/2)+1=1 + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![1, 1] + ); // floor(0/1)+1=1, floor(0/2)+1=1 assert_eq!(problem.evaluate(&vec![0, 0]).unwrap(), Max(Some(0))); let solver = BruteForce::new(); let solution = solver.solve(&problem).unwrap().unwrap(); @@ -118,7 +123,10 @@ fn test_integer_knapsack_zero_capacity() { #[test] fn test_integer_knapsack_dimension_uses_structural_range() { let problem = IntegerKnapsack::new(vec![1], vec![1], i64::MAX).unwrap(); - assert_eq!(problem.dimensions(), vec![1_usize << 63]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![1_usize << 63] + ); } #[test] @@ -126,7 +134,10 @@ fn test_integer_knapsack_single_item() { // Single item size=3, value=5, capacity=7 // Max multiplicity: floor(7/3)=2, dims=[3] let problem = IntegerKnapsack::new(vec![3], vec![5], 7).unwrap(); - assert_eq!(problem.dimensions(), vec![3]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![3] + ); assert_eq!(problem.evaluate(&vec![0]).unwrap(), Max(Some(0))); assert_eq!(problem.evaluate(&vec![1]).unwrap(), Max(Some(5))); assert_eq!(problem.evaluate(&vec![2]).unwrap(), Max(Some(10))); @@ -231,7 +242,7 @@ fn test_integer_knapsack_deserialization_rejects_invalid_fields() { #[test] fn test_integer_knapsack_paper_example() { - // From issue #532: 5 items, sizes=[3,4,5,2,7], values=[4,5,7,3,9], B=15 + // 5 items, sizes=[3,4,5,2,7], values=[4,5,7,3,9], B=15 // Optimal=22 with c=(0,0,1,5,0) or c=(1,0,0,6,0) let problem = IntegerKnapsack::new(vec![3, 4, 5, 2, 7], vec![4, 5, 7, 3, 9], 15).unwrap(); diff --git a/src/unit_tests/models/set/maximum_set_packing.rs b/src/unit_tests/models/set/maximum_set_packing.rs index 9993ca2ef..53024f41b 100644 --- a/src/unit_tests/models/set/maximum_set_packing.rs +++ b/src/unit_tests/models/set/maximum_set_packing.rs @@ -1,9 +1,9 @@ use super::*; +include!("../../jl_helpers.rs"); use crate::solvers::BruteForce; use crate::solvers::BruteForceProblem as _; use crate::traits::Problem; use crate::types::Max; -include!("../../jl_helpers.rs"); #[test] fn test_maximum_set_packing_create_spec_uses_subsets_input() { @@ -24,7 +24,7 @@ fn test_maximum_set_packing_create_spec_uses_subsets_input() { fn test_set_packing_creation() { let problem = MaximumSetPacking::::new(vec![vec![0, 1], vec![1, 2], vec![3, 4]]); assert_eq!(problem.num_sets(), 3); - assert_eq!(problem.num_variables(), 3); + assert_eq!(problem.num_variables().unwrap(), 3); } #[test] diff --git a/src/unit_tests/models/set/minimum_cardinality_key.rs b/src/unit_tests/models/set/minimum_cardinality_key.rs index d73369de6..ef8c29c12 100644 --- a/src/unit_tests/models/set/minimum_cardinality_key.rs +++ b/src/unit_tests/models/set/minimum_cardinality_key.rs @@ -29,8 +29,11 @@ fn test_minimum_cardinality_key_creation() { let problem = instance1(); assert_eq!(problem.num_attributes(), 6); assert_eq!(problem.num_dependencies(), 4); - assert_eq!(problem.num_variables(), 6); - assert_eq!(problem.dimensions(), vec![2; 6]); + assert_eq!(problem.num_variables().unwrap(), 6); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 6] + ); } #[test] diff --git a/src/unit_tests/models/set/minimum_hitting_set.rs b/src/unit_tests/models/set/minimum_hitting_set.rs index 2b7a59b5f..dd14c99f1 100644 --- a/src/unit_tests/models/set/minimum_hitting_set.rs +++ b/src/unit_tests/models/set/minimum_hitting_set.rs @@ -41,8 +41,11 @@ fn test_minimum_hitting_set_creation_accessors_and_dimensions() { assert_eq!(problem.universe_size(), 4); assert_eq!(problem.num_sets(), 2); - assert_eq!(problem.num_variables(), 4); - assert_eq!(problem.dimensions(), vec![2; 4]); + assert_eq!(problem.num_variables().unwrap(), 4); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 4] + ); assert_eq!(problem.sets(), &[vec![1, 2], vec![3]]); assert_eq!(problem.get_set(0), Some(&vec![1, 2])); assert_eq!(problem.get_set(1), Some(&vec![3])); diff --git a/src/unit_tests/models/set/minimum_set_covering.rs b/src/unit_tests/models/set/minimum_set_covering.rs index 299647f7c..ccc1dc652 100644 --- a/src/unit_tests/models/set/minimum_set_covering.rs +++ b/src/unit_tests/models/set/minimum_set_covering.rs @@ -1,9 +1,9 @@ use super::*; +include!("../../jl_helpers.rs"); use crate::solvers::BruteForce; use crate::solvers::BruteForceProblem as _; use crate::traits::Problem; use crate::types::Min; -include!("../../jl_helpers.rs"); #[test] fn test_minimum_set_covering_create_spec_uses_subsets_input() { @@ -23,7 +23,7 @@ fn test_set_covering_creation() { let problem = MinimumSetCovering::::new(4, vec![vec![0, 1], vec![1, 2], vec![2, 3]]); assert_eq!(problem.universe_size(), 4); assert_eq!(problem.num_sets(), 3); - assert_eq!(problem.num_variables(), 3); + assert_eq!(problem.num_variables().unwrap(), 3); } #[test] diff --git a/src/unit_tests/models/set/prime_attribute_name.rs b/src/unit_tests/models/set/prime_attribute_name.rs index e9550959a..ca34b79d5 100644 --- a/src/unit_tests/models/set/prime_attribute_name.rs +++ b/src/unit_tests/models/set/prime_attribute_name.rs @@ -44,8 +44,11 @@ fn test_prime_attribute_name_creation() { assert_eq!(problem.num_attributes(), 6); assert_eq!(problem.num_dependencies(), 3); assert_eq!(problem.query_attribute(), 3); - assert_eq!(problem.num_variables(), 6); - assert_eq!(problem.dimensions(), vec![2, 2, 2, 2, 2, 2]); + assert_eq!(problem.num_variables().unwrap(), 6); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2, 2, 2, 2, 2, 2] + ); assert_eq!(problem.dependencies().len(), 3); } diff --git a/src/unit_tests/models/set/rooted_tree_storage_assignment.rs b/src/unit_tests/models/set/rooted_tree_storage_assignment.rs index 944ef4500..e87bb7c4a 100644 --- a/src/unit_tests/models/set/rooted_tree_storage_assignment.rs +++ b/src/unit_tests/models/set/rooted_tree_storage_assignment.rs @@ -1,6 +1,5 @@ use super::*; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::traits::Problem; fn yes_instance(bound: i64) -> RootedTreeStorageAssignment { @@ -21,7 +20,10 @@ fn test_rooted_tree_storage_assignment_creation() { problem.subsets(), &[vec![0, 2], vec![1, 3], vec![0, 4], vec![2, 4]] ); - assert_eq!(problem.dimensions(), vec![5; 5]); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![5; 5] + ); } #[test] diff --git a/src/unit_tests/models/set/set_basis.rs b/src/unit_tests/models/set/set_basis.rs index a1c0aabdd..913d3bdb2 100644 --- a/src/unit_tests/models/set/set_basis.rs +++ b/src/unit_tests/models/set/set_basis.rs @@ -38,8 +38,11 @@ fn test_set_basis_creation() { assert_eq!(problem.universe_size(), 4); assert_eq!(problem.num_sets(), 4); assert_eq!(problem.basis_size(), 3); - assert_eq!(problem.num_variables(), 12); - assert_eq!(problem.dimensions(), vec![2; 12]); + assert_eq!(problem.num_variables().unwrap(), 12); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2; 12] + ); assert_eq!(problem.get_set(0), Some(&vec![0, 1])); assert_eq!(problem.get_set(4), None); } @@ -183,7 +186,10 @@ fn test_set_basis_is_valid_solution() { fn test_set_basis_k_zero_empty_collection() { // k = 0 with empty collection: trivially satisfiable (no targets to cover). let problem = SetBasis::new(3, vec![], 0); - assert_eq!(problem.dimensions(), Vec::::new()); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + Vec::::new() + ); assert!(problem.evaluate(&vec![]).unwrap()); } @@ -191,7 +197,10 @@ fn test_set_basis_k_zero_empty_collection() { fn test_set_basis_k_zero_nonempty_collection() { // k = 0 with non-empty collection: impossible (no basis sets to cover targets). let problem = SetBasis::new(3, vec![vec![0, 1]], 0); - assert_eq!(problem.dimensions(), Vec::::new()); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + Vec::::new() + ); assert!(!problem.evaluate(&vec![]).unwrap()); } diff --git a/src/unit_tests/models/set/set_splitting.rs b/src/unit_tests/models/set/set_splitting.rs index 476e97af4..ca465327b 100644 --- a/src/unit_tests/models/set/set_splitting.rs +++ b/src/unit_tests/models/set/set_splitting.rs @@ -9,7 +9,7 @@ fn test_set_splitting_creation() { let problem = SetSplitting::new(4, vec![vec![0, 1], vec![1, 2], vec![2, 3]]); assert_eq!(problem.universe_size(), 4); assert_eq!(problem.num_subsets(), 3); - assert_eq!(problem.num_variables(), 4); + assert_eq!(problem.num_variables().unwrap(), 4); } #[test] diff --git a/src/unit_tests/models/set/three_dimensional_matching.rs b/src/unit_tests/models/set/three_dimensional_matching.rs index 3ef8b0df4..d560df96a 100644 --- a/src/unit_tests/models/set/three_dimensional_matching.rs +++ b/src/unit_tests/models/set/three_dimensional_matching.rs @@ -11,8 +11,11 @@ fn test_three_dimensional_matching_creation() { ); assert_eq!(problem.universe_size(), 3); assert_eq!(problem.num_triples(), 5); - assert_eq!(problem.num_variables(), 5); - assert_eq!(problem.dimensions(), vec![2, 2, 2, 2, 2]); + assert_eq!(problem.num_variables().unwrap(), 5); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![2, 2, 2, 2, 2] + ); } #[test] diff --git a/src/unit_tests/models/set/two_dimensional_consecutive_sets.rs b/src/unit_tests/models/set/two_dimensional_consecutive_sets.rs index b67594e2a..57c45aee6 100644 --- a/src/unit_tests/models/set/two_dimensional_consecutive_sets.rs +++ b/src/unit_tests/models/set/two_dimensional_consecutive_sets.rs @@ -17,8 +17,11 @@ fn test_two_dimensional_consecutive_sets_creation() { ); assert_eq!(problem.alphabet_size(), 6); assert_eq!(problem.num_subsets(), 5); - assert_eq!(problem.num_variables(), 6); - assert_eq!(problem.dimensions(), vec![6, 6, 6, 6, 6, 6]); + assert_eq!(problem.num_variables().unwrap(), 6); + assert_eq!( + crate::solvers::cartesian_dimensions(&problem).unwrap(), + vec![6, 6, 6, 6, 6, 6] + ); } #[test] diff --git a/src/unit_tests/reduction_graph.rs b/src/unit_tests/reduction_graph.rs index 6595c0007..133bffee1 100644 --- a/src/unit_tests/reduction_graph.rs +++ b/src/unit_tests/reduction_graph.rs @@ -929,14 +929,10 @@ fn test_decision_minimum_dominating_set_to_minmax_multicenter_has_direct_witness MinimumDominatingSet::new(SimpleGraph::path(4), vec![One; 4]), bound, ); - let aggregate = (edge.reduce_aggregate_fn.unwrap())(&source).unwrap(); + let step = (edge.reduce_fn.unwrap())(&source).unwrap(); assert_eq!( - *aggregate - .extract_value_from_solution_dyn(&witness) - .unwrap() - .downcast::() - .unwrap(), - Or(expected) + step.interpret_optimum.as_ref().unwrap()(&witness).unwrap(), + expected ); } } @@ -1064,16 +1060,18 @@ fn test_find_paths_bounded_returns_shortest_when_truncated() { fn edge() -> ReductionEdgeData { fn reduce( _source: &dyn std::any::Any, - ) -> std::result::Result< - Box, - crate::rules::ReductionError, - > { - Ok(Box::new(crate::rules::VariantReductionResult::< - crate::models::formula::Satisfiability, - crate::models::formula::Satisfiability, - >::new( - crate::models::formula::Satisfiability::new(0, vec![]), - ))) + ) -> std::result::Result + { + Ok(crate::rules::registry::ExecutedStep { + witness: std::rc::Rc::new(crate::rules::VariantReductionResult::< + crate::models::formula::Satisfiability, + crate::models::formula::Satisfiability, + >::new( + crate::models::formula::Satisfiability::new(0, vec![]) + )), + aggregate: None, + interpret_optimum: None, + }) } ReductionEdgeData { diff --git a/src/unit_tests/registry/dispatch.rs b/src/unit_tests/registry/dispatch.rs index 2291f949d..20c6be252 100644 --- a/src/unit_tests/registry/dispatch.rs +++ b/src/unit_tests/registry/dispatch.rs @@ -49,8 +49,12 @@ impl Problem for SolutionProblem { } impl crate::solvers::BruteForceProblem for SolutionProblem { - fn dimensions(&self) -> Vec { - vec![2; self.weights.len()] + fn num_variables(&self) -> Result { + Ok(self.weights.len()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } @@ -93,45 +97,26 @@ fn test_dyn_problem_blanket_impl_exposes_problem_metadata() { } #[test] -fn test_dyn_problem_formats_optimization_values_as_max_min() { +fn test_dyn_evaluation_distinguishes_infeasibility_and_malformed_input() { let problem = MaximumIndependentSet::new(SimpleGraph::new(3, vec![(0, 1)]), vec![1i64; 3]); let dyn_problem: &dyn DynProblem = &problem; - assert_eq!( dyn_problem .evaluate_dyn(&serde_json::json!([true, false, true])) .unwrap(), - "Max(2)" + ("Max(2)".into(), true) ); assert_eq!( dyn_problem .evaluate_dyn(&serde_json::json!([true, true, false])) .unwrap(), - "Max(None)" - ); -} - -#[test] -fn test_dyn_witness_evaluation_distinguishes_infeasibility_and_malformed_input() { - let problem = MaximumIndependentSet::new(SimpleGraph::new(3, vec![(0, 1)]), vec![1i64; 3]); - let dyn_problem: &dyn DynProblem = &problem; - assert_eq!( - dyn_problem - .evaluate_witness_dyn(&serde_json::json!([true, false, true])) - .unwrap(), - Some("Max(2)".into()) - ); - assert_eq!( - dyn_problem - .evaluate_witness_dyn(&serde_json::json!([true, true, false])) - .unwrap(), - None + ("Max(None)".into(), false) ); assert!(dyn_problem - .evaluate_witness_dyn(&serde_json::json!([true])) + .evaluate_dyn(&serde_json::json!([true])) .is_err()); assert!(dyn_problem - .evaluate_witness_dyn(&serde_json::json!([0, 0, 0])) + .evaluate_dyn(&serde_json::json!([0, 0, 0])) .is_err()); } @@ -370,7 +355,10 @@ fn explicit_independent_set_variants_round_trip_through_standard_api() { "Max(2.5)" }; assert_eq!(evaluation, expected, "{variant:?}"); - assert_eq!(loaded.evaluate_dyn(&solution).unwrap(), expected); + assert_eq!( + loaded.evaluate_dyn(&solution).unwrap(), + (expected.into(), true) + ); } } let mut bad = base.clone(); @@ -417,3 +405,63 @@ fn registered_weight_variants_reject_invalid_graphs_and_witnesses() { } } } + +#[derive(Clone, serde::Serialize)] +struct DirectEvaluation; + +#[derive(Clone, serde::Serialize)] +struct DirectValue(bool); + +impl DirectValue { + fn is_valid(&self) -> bool { + self.0 + } +} + +impl std::fmt::Display for DirectValue { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + +impl Problem for DirectEvaluation { + const NAME: &'static str = "DirectEvaluation"; + type Solution = bool; + type Value = DirectValue; + + fn parameter_names() -> &'static [&'static str] { + &[] + } + fn parameters(&self) -> crate::types::ProblemParameters { + Default::default() + } + fn variant() -> Vec<(&'static str, &'static str)> { + vec![] + } + fn evaluate(&self, solution: &bool) -> Result { + Ok(DirectValue(*solution)) + } +} + +crate::impl_dyn_problem!(DirectEvaluation); + +#[test] +fn dynamic_evaluation_needs_neither_aggregation_nor_registration() { + let problem: &dyn DynProblem = &DirectEvaluation; + for feasible in [true, false] { + let input = serde_json::json!(feasible); + assert_eq!( + problem.evaluate_dyn(&input).unwrap(), + (feasible.to_string(), feasible) + ); + assert_eq!(problem.evaluate_json(&input).unwrap(), input); + } + assert!(problem.evaluate_dyn(&serde_json::json!([])).is_err()); + assert!(problem.evaluate_json(&serde_json::json!([])).is_err()); + assert_eq!(problem.problem_name(), "DirectEvaluation"); + assert!(problem.variant_map().is_empty()); + assert!(problem.parameter_names_dyn().is_empty()); + assert_eq!(problem.parameters_dyn(), Default::default()); + assert_eq!(problem.serialize_json(), serde_json::Value::Null); + assert!(problem.as_any().is::()); +} diff --git a/src/unit_tests/registry/variant.rs b/src/unit_tests/registry/variant.rs index eb909bf25..4dcbca44c 100644 --- a/src/unit_tests/registry/variant.rs +++ b/src/unit_tests/registry/variant.rs @@ -320,7 +320,7 @@ fn established_random_generation_models_remain_registered() { DecisionMinimumVertexCover MaximumIndependentSet MinimumVertexCover MaximumClique MinimumDominatingSet MaximalIS KClique MinimumCutIntoBoundedSets HamiltonianCircuit HamiltonianPath HamiltonianPathBetweenTwoVertices LongestCircuit MinimumMaximalMatching - RootedTreeArrangement SteinerTree SteinerTreeInGraphs LengthBoundedDisjointPaths + RootedTreeArrangement SteinerTree LengthBoundedDisjointPaths MaximumAchromaticNumber MaximumDomaticNumber MinimumCoveringByCliques MinimumIntersectionGraphBasis MaximumLeafSpanningTree GeneralizedHex BottleneckTravelingSalesman MaxCut MaximumMatching TravelingSalesman SpinGlass KColoring @@ -367,7 +367,7 @@ fn unit_variants_construct_without_unit_inputs() { "MaximumCoKPlex" => json!({"graph":graph,"k":1}), "MinimumFeedbackVertexSet" => json!({"graph":{"num_vertices":3,"arcs":[[0,1],[1,2]]}}), "MaximumSetPacking" => json!({"subsets":[[0,1],[1,2]]}), - "SteinerTree" | "SteinerTreeInGraphs" => json!({"graph":graph,"terminals":[0,2]}), + "SteinerTree" => json!({"graph":graph,"terminals":[0,2]}), "MaximumIndependentSet" => match entry.variant_map()["graph"].as_str() { "SimpleGraph" => json!({"graph":[[0,1],[1,2]]}), "KingsSubgraph" => json!({"positions":[[0,0],[1,0],[2,0]]}), @@ -377,7 +377,9 @@ fn unit_variants_construct_without_unit_inputs() { graph => panic!("missing construction case for {graph}"), }, "DecisionMaximumIndependentSet" => json!({"graph":[[0,1],[1,2]],"bound":2}), - "DecisionMinimumDominatingSet" => json!({"graph":graph,"bound":1}), + "DecisionMinimumDominatingSet" | "DecisionMinimumVertexCover" => { + json!({"graph":graph,"bound":1}) + } "MaxCut" => json!({"graph":[[0,1],[1,2]]}), "LongestPath" => json!({"graph":[[0,1],[1,2]],"source_vertex":0,"target_vertex":2}), "MinMaxMulticenter" => json!({"graph":[[0,1],[1,2]],"k":1}), @@ -435,13 +437,9 @@ fn unit_construction_preserves_model_validation() { let graph = json!({"num_vertices":3,"edges":[[0,1],[1,2]]}); for (name, data) in [ ("MaximumCoKPlex", json!({"graph":graph,"k":0})), - ("SteinerTree", json!({"graph":graph,"terminals":[0]})), + ("SteinerTree", json!({"graph":graph,"terminals":[]})), ("SteinerTree", json!({"graph":graph,"terminals":[0,0]})), ("SteinerTree", json!({"graph":graph,"terminals":[0,3]})), - ( - "SteinerTreeInGraphs", - json!({"graph":graph,"terminals":[3]}), - ), ( "MinimumTardinessSequencing", json!({"deadlines":[1,2],"precedences":[[0,2]]}), diff --git a/src/unit_tests/rules/acyclicpartition_ilp.rs b/src/unit_tests/rules/acyclicpartition_ilp.rs index 467d43aec..bf5bec2cb 100644 --- a/src/unit_tests/rules/acyclicpartition_ilp.rs +++ b/src/unit_tests/rules/acyclicpartition_ilp.rs @@ -82,7 +82,10 @@ fn test_infeasible_instance() { ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let ilp = reduction.target_problem(); let solver = ILPSolver::new(); - assert!(solver.solve(ilp).is_err()); + assert_eq!( + solver.solve(ilp), + Err(crate::solvers::ILPSolveError::Infeasible) + ); } #[test] diff --git a/src/unit_tests/rules/balancedcompletebipartitesubgraph_ilp.rs b/src/unit_tests/rules/balancedcompletebipartitesubgraph_ilp.rs index f044cf0f9..630fb7c69 100644 --- a/src/unit_tests/rules/balancedcompletebipartitesubgraph_ilp.rs +++ b/src/unit_tests/rules/balancedcompletebipartitesubgraph_ilp.rs @@ -45,7 +45,10 @@ fn test_infeasible_instance() { ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let ilp = reduction.target_problem(); let solver = crate::solvers::ILPSolver::new(); - assert!(solver.solve(ilp).is_err()); + assert_eq!( + solver.solve(ilp), + Err(crate::solvers::ILPSolveError::Infeasible) + ); } #[test] diff --git a/src/unit_tests/rules/biconnectivityaugmentation_ilp.rs b/src/unit_tests/rules/biconnectivityaugmentation_ilp.rs index 249dabc98..9c429ce12 100644 --- a/src/unit_tests/rules/biconnectivityaugmentation_ilp.rs +++ b/src/unit_tests/rules/biconnectivityaugmentation_ilp.rs @@ -142,7 +142,9 @@ fn test_biconnectivityaugmentation_to_ilp_empty_negative_budget() { .is_some(), budget >= 0 ); - assert_eq!(reduction.extract_solution(&vec![]).is_ok(), budget >= 0); + if budget >= 0 { + assert!(reduction.extract_solution(&vec![]).unwrap().is_empty()); + } } } } @@ -160,12 +162,18 @@ fn test_biconnectivityaugmentation_to_ilp_signed_cost_and_certificate_bounds() { .unwrap() .0 ); - assert!(reduction.extract_solution(&vec![0; z.len()]).is_err()); - assert!(reduction.extract_solution(&vec![1; z.len() + 1]).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(reduction.target_problem(), &vec![0; z.len()]), Ok(value) if value.is_valid()) + ); + assert!( + !matches!(crate::traits::Problem::evaluate(reduction.target_problem(), &vec![1; z.len() + 1]), Ok(value) if value.is_valid()) + ); for value in [-1, 2] { let mut bad = z.clone(); bad[0] = value; - assert!(reduction.extract_solution(&bad).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(reduction.target_problem(), &bad), Ok(value) if value.is_valid()) + ); } } } diff --git a/src/unit_tests/rules/bottlenecktravelingsalesman_ilp.rs b/src/unit_tests/rules/bottlenecktravelingsalesman_ilp.rs index 5d92bc4bb..63b831bbd 100644 --- a/src/unit_tests/rules/bottlenecktravelingsalesman_ilp.rs +++ b/src/unit_tests/rules/bottlenecktravelingsalesman_ilp.rs @@ -96,8 +96,9 @@ fn test_no_hamiltonian_cycle_infeasible() { ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp_solver = ILPSolver::new(); let result = ilp_solver.solve(reduction.target_problem()); - assert!( - result.is_err(), + assert_eq!( + result, + Err(crate::solvers::ILPSolveError::Infeasible), "Path graph should have no Hamiltonian cycle" ); } @@ -181,11 +182,13 @@ fn test_bottleneck_ilp_signed_full_range_and_native_cycles() { for variable in 0..witness.len() { let mut invalid = witness.clone(); invalid[variable] = 2; - assert!(result.extract_solution(&invalid).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(result.target_problem(), &invalid), Ok(value) if value.is_valid()) + ); } - assert!(result - .extract_solution(&witness[..witness.len() - 1].to_vec()) - .is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(result.target_problem(), &witness[..witness.len() - 1].to_vec()), Ok(value) if value.is_valid()) + ); } } @@ -196,12 +199,18 @@ fn test_bottleneck_ilp_maximum_must_be_used_and_dominate() { 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); - assert!(result.extract_solution(&config).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(result.target_problem(), &config), Ok(value) if value.is_valid()) + ); config[selector] = 1; // used, but lower than the maximum edge - assert!(result.extract_solution(&config).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(result.target_problem(), &config), Ok(value) if value.is_valid()) + ); config[selector] = 0; config[selector + 1] = 1; // unused - assert!(result.extract_solution(&config).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(result.target_problem(), &config), Ok(value) if value.is_valid()) + ); } #[test] diff --git a/src/unit_tests/rules/boundedcomponentspanningforest_ilp.rs b/src/unit_tests/rules/boundedcomponentspanningforest_ilp.rs index 039d7f2c3..be4313293 100644 --- a/src/unit_tests/rules/boundedcomponentspanningforest_ilp.rs +++ b/src/unit_tests/rules/boundedcomponentspanningforest_ilp.rs @@ -85,7 +85,10 @@ fn test_infeasible_instance() { ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let ilp = reduction.target_problem(); let solver = ILPSolver::new(); - assert!(solver.solve(ilp).is_err()); + assert_eq!( + solver.solve(ilp), + Err(crate::solvers::ILPSolveError::Infeasible) + ); } #[test] diff --git a/src/unit_tests/rules/capacityassignment_ilp.rs b/src/unit_tests/rules/capacityassignment_ilp.rs index e0053dfb1..693805a86 100644 --- a/src/unit_tests/rules/capacityassignment_ilp.rs +++ b/src/unit_tests/rules/capacityassignment_ilp.rs @@ -85,13 +85,11 @@ fn test_solution_extraction() { let reduction: ReductionCAToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); - // link 0 → cap 1, link 1 → cap 0 - // x_{0,0}=0, x_{0,1}=1, x_{0,2}=0, x_{1,0}=1, x_{1,1}=0, x_{1,2}=0 - let ilp_solution = vec![0, 1, 0, 1, 0, 0]; + // Both links choose capacity level 1: total delay 4 + 3 <= 10. + let ilp_solution = vec![0, 1, 0, 0, 1, 0]; let extracted = reduction.extract_solution(&ilp_solution).unwrap(); - assert_eq!(extracted, vec![1, 0]); - // Verify extraction works (evaluation may or may not be feasible) - let _ = problem.evaluate(&extracted).unwrap(); + assert_eq!(extracted, vec![1, 1]); + assert!(problem.evaluate(&extracted).unwrap().is_valid()); } #[test] diff --git a/src/unit_tests/rules/circuit_ilp.rs b/src/unit_tests/rules/circuit_ilp.rs index 044e10148..ea48a23ba 100644 --- a/src/unit_tests/rules/circuit_ilp.rs +++ b/src/unit_tests/rules/circuit_ilp.rs @@ -144,7 +144,9 @@ fn test_circuit_ilp_native_folds_all_feasible_witnesses() { assert!(source.evaluate(&extracted).unwrap().0); actual.insert(extracted); } else { - assert!(reduction.extract_solution(&solution).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(reduction.target_problem(), &solution), Ok(value) if value.is_valid()) + ); } } assert_eq!(actual, expected, "{expr:?}, output={output}"); @@ -161,7 +163,9 @@ fn test_circuit_ilp_rejects_invalid_target_and_supports_empty_circuit() { )])); let reduction = ReduceTo::>::reduce_to(&source).unwrap(); for invalid in [vec![], vec![0], vec![0, 0], vec![2, 1], vec![1, 1, 1]] { - assert!(reduction.extract_solution(&invalid).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(reduction.target_problem(), &invalid), Ok(value) if value.is_valid()) + ); } let empty = CircuitSAT::new(Circuit::new(vec![])); let reduction = ReduceTo::>::reduce_to(&empty).unwrap(); diff --git a/src/unit_tests/rules/circuit_spinglass.rs b/src/unit_tests/rules/circuit_spinglass.rs index d44aaec9d..08cb4e8c3 100644 --- a/src/unit_tests/rules/circuit_spinglass.rs +++ b/src/unit_tests/rules/circuit_spinglass.rs @@ -5,7 +5,6 @@ use crate::solvers::BruteForce; use crate::traits::Problem; use crate::types::{NumericSize, WeightElement}; use num_traits::Num; -include!("../jl_helpers.rs"); /// Verify a gadget has the correct ground states. fn verify_gadget_truth_table(gadget: &LogicGadget, expected: &[(Vec, Vec)]) @@ -359,7 +358,9 @@ fn test_circuit_spinglass_all_threshold_witnesses_native_domain() { assert!(source.evaluate(&decoded).unwrap().0); actual.insert(decoded); } else { - assert!(reduction.extract_solution(&spins).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &spins), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&reduction, value); value.is_valid() }) + ); } } assert_eq!(actual, expected, "expression {expr:?}, output {output}"); @@ -382,10 +383,14 @@ fn test_circuit_spinglass_unsat_threshold_and_invalid_spins() { .find_all_witnesses(ReductionResult::target_problem(&reduction)) .unwrap() { - assert!(reduction.extract_solution(&witness).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &witness), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&reduction, value); value.is_valid() }) + ); } for bad in [vec![], vec![1], vec![0, 0], vec![1, 1, 1]] { - assert!(reduction.extract_solution(&bad).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &bad), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&reduction, value); value.is_valid() }) + ); } let empty = CircuitSAT::new(Circuit::new(vec![])); let reduction = ReduceTo::>::reduce_to(&empty).unwrap(); diff --git a/src/unit_tests/rules/closeststring_ilp.rs b/src/unit_tests/rules/closeststring_ilp.rs index 5714565a7..0611e5574 100644 --- a/src/unit_tests/rules/closeststring_ilp.rs +++ b/src/unit_tests/rules/closeststring_ilp.rs @@ -100,12 +100,10 @@ fn test_closeststring_to_ilp_rejects_missing_one_hot_symbol() { let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let target_solution = vec![0; reduction.target_problem().num_vars()]; - assert_eq!( - reduction - .extract_solution(&target_solution) - .unwrap_err() - .to_string(), - "center position 0 has no selected symbol" + assert!( + !crate::traits::Problem::evaluate(reduction.target_problem(), &target_solution) + .unwrap() + .is_valid() ); } diff --git a/src/unit_tests/rules/closestsubstring_ilp.rs b/src/unit_tests/rules/closestsubstring_ilp.rs index bf3a9fed5..5797cb02d 100644 --- a/src/unit_tests/rules/closestsubstring_ilp.rs +++ b/src/unit_tests/rules/closestsubstring_ilp.rs @@ -77,12 +77,10 @@ fn test_closestsubstring_to_ilp_rejects_missing_one_hot_symbol() { let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let target_solution = vec![0; reduction.target_problem().num_vars()]; - assert_eq!( - reduction - .extract_solution(&target_solution) - .unwrap_err() - .to_string(), - "center position 0 has no selected value" + assert!( + !crate::traits::Problem::evaluate(reduction.target_problem(), &target_solution) + .unwrap() + .is_valid() ); } diff --git a/src/unit_tests/rules/closestvectorproblem_qubo.rs b/src/unit_tests/rules/closestvectorproblem_qubo.rs index aa4afb7b3..22de016f9 100644 --- a/src/unit_tests/rules/closestvectorproblem_qubo.rs +++ b/src/unit_tests/rules/closestvectorproblem_qubo.rs @@ -59,7 +59,10 @@ fn test_closestvectorproblem_to_qubo_twelve_dimensional_identity() { } let solution = reduction.extract_solution(&bits).unwrap(); assert_eq!(solution, vec![1; size]); - assert_eq!(source.evaluate(&solution).unwrap().0, Some(0.0)); + assert_eq!( + source.evaluate(&solution).unwrap().0, + Some(num_rational::BigRational::zero()) + ); } #[test] @@ -73,7 +76,10 @@ 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.0)); + assert_eq!( + source.evaluate(&source_solution).unwrap().0, + Some(num_rational::BigRational::zero()) + ); assert_eq!(reduction.target_problem().num_vars(), 11); } @@ -82,10 +88,10 @@ fn test_closestvectorproblem_to_qubo_coefficients() { let reduction = ReduceTo::>::reduce_to(&canonical_cvp()).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)); + 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)); } #[test] @@ -146,7 +152,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["matrix"]["nrows"], 11); assert_eq!( example.solutions[0].source_config, serde_json::json!([1, 1]) @@ -156,3 +162,21 @@ fn test_closestvectorproblem_to_qubo_canonical_example_spec() { serde_json::to_value(canonical_bits()).unwrap() ); } + +#[test] +fn qubo_energy_matches_squared_distance_up_to_the_dropped_constant() { + let source = ClosestVectorProblem::new(vec![vec![2]], vec![1_i64]).unwrap(); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + let target = reduction.target_problem(); + assert_eq!(target.num_vars(), 3); + // The all-zero encoding represents x=-2, with squared distance (-4-1)^2=25. + for index in 0..8 { + let bits = (0..3).map(|bit| index & (1 << bit) != 0).collect(); + let coefficient = reduction.extract_solution(&bits).unwrap(); + let energy = target.evaluate(&bits).unwrap().unwrap(); + assert_eq!( + source.squared_distance(&coefficient).unwrap(), + num_rational::BigRational::from_integer((energy + 25).into()) + ); + } +} diff --git a/src/unit_tests/rules/clustering_ilp.rs b/src/unit_tests/rules/clustering_ilp.rs index 2142986ba..ab374fa2e 100644 --- a/src/unit_tests/rules/clustering_ilp.rs +++ b/src/unit_tests/rules/clustering_ilp.rs @@ -76,5 +76,8 @@ fn test_clustering_to_ilp_infeasible_instance_is_infeasible() { let reduction: ReductionClusteringToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); - assert!(ILPSolver::new().solve(reduction.target_problem()).is_err()); + assert_eq!( + ILPSolver::new().solve(reduction.target_problem()), + Err(crate::solvers::ILPSolveError::Infeasible) + ); } diff --git a/src/unit_tests/rules/coloring_ilp.rs b/src/unit_tests/rules/coloring_ilp.rs index 238d2cbaf..5625dea4e 100644 --- a/src/unit_tests/rules/coloring_ilp.rs +++ b/src/unit_tests/rules/coloring_ilp.rs @@ -126,8 +126,9 @@ fn test_ilp_infeasible_triangle_2_colors() { // ILP should be infeasible let result = ilp_solver.solve(ilp); - assert!( - result.is_err(), + assert_eq!( + result, + Err(crate::solvers::ILPSolveError::Infeasible), "Triangle with 2 colors should be infeasible" ); } @@ -216,7 +217,11 @@ fn test_complete_graph_k4_with_3_colors_infeasible() { let ilp_solver = ILPSolver::new(); let result = ilp_solver.solve(ilp); - assert!(result.is_err(), "K4 with 3 colors should be infeasible"); + assert_eq!( + result, + Err(crate::solvers::ILPSolveError::Infeasible), + "K4 with 3 colors should be infeasible" + ); } #[test] diff --git a/src/unit_tests/rules/coloring_qubo.rs b/src/unit_tests/rules/coloring_qubo.rs index 2b4ea3793..a96906e80 100644 --- a/src/unit_tests/rules/coloring_qubo.rs +++ b/src/unit_tests/rules/coloring_qubo.rs @@ -69,7 +69,7 @@ fn test_kcoloring_to_qubo_sizes() { let reduction = ReduceTo::>::reduce_to(&kc).expect("reduction should succeed"); // QUBO should have n*K = 3*3 = 9 variables - assert_eq!(reduction.target_problem().num_variables(), 9); + assert_eq!(reduction.target_problem().num_variables().unwrap(), 9); } #[test] @@ -116,13 +116,10 @@ fn test_kcoloring_to_qubo_all_small_graphs_and_configurations() { AggregateReductionResult::extract_value(&reduction, value).0, expected ); - match reduction.extract_solution(&config) { - Ok(coloring) => { - assert!(expected); - assert!(source.evaluate(&coloring).unwrap().0); - any_coloring = true; - } - Err(_) => assert!(!expected), + if expected { + let coloring = reduction.extract_solution(&config).unwrap(); + assert!(source.evaluate(&coloring).unwrap().0); + any_coloring = true; } } assert_eq!( @@ -136,7 +133,9 @@ fn test_kcoloring_to_qubo_all_small_graphs_and_configurations() { assert!( !AggregateReductionResult::extract_value(&reduction, crate::types::Min(None)).0 ); - assert!(reduction.extract_solution(&vec![false; n * k + 1]).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![false; n * k + 1]), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&reduction, value); value.is_valid() }) + ); } } } diff --git a/src/unit_tests/rules/consistencyofdatabasefrequencytables_ilp.rs b/src/unit_tests/rules/consistencyofdatabasefrequencytables_ilp.rs index c9e07b866..a65b3a638 100644 --- a/src/unit_tests/rules/consistencyofdatabasefrequencytables_ilp.rs +++ b/src/unit_tests/rules/consistencyofdatabasefrequencytables_ilp.rs @@ -65,7 +65,10 @@ fn test_cdft_to_ilp_unsat_instance_is_infeasible() { let reduction: ReductionCDFTToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let solver = ILPSolver::new(); - assert!(solver.solve(reduction.target_problem()).is_err()); + assert_eq!( + solver.solve(reduction.target_problem()), + Err(crate::solvers::ILPSolveError::Infeasible) + ); } #[test] diff --git a/src/unit_tests/rules/decisionmaximumindependentset_integralflowbundles.rs b/src/unit_tests/rules/decisionmaximumindependentset_integralflowbundles.rs index 50c1b776d..20fd7a5cf 100644 --- a/src/unit_tests/rules/decisionmaximumindependentset_integralflowbundles.rs +++ b/src/unit_tests/rules/decisionmaximumindependentset_integralflowbundles.rs @@ -102,7 +102,9 @@ fn test_decision_ifb_loops_parallel_edges_and_invalid_witnesses() { vec![1, 1, 0, 0, 1, 1, 1, 1], // self-loop vec![0, 0, 2, 2, 1, 1, 1, 1], // path capacity ] { - assert!(reduction.extract_solution(&flow).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &flow), Ok(value) if { value.is_valid() }) + ); } } diff --git a/src/unit_tests/rules/decisionminimumdominatingset_minimumsummulticenter.rs b/src/unit_tests/rules/decisionminimumdominatingset_minimumsummulticenter.rs index 5f39c738d..41899497a 100644 --- a/src/unit_tests/rules/decisionminimumdominatingset_minimumsummulticenter.rs +++ b/src/unit_tests/rules/decisionminimumdominatingset_minimumsummulticenter.rs @@ -101,7 +101,9 @@ fn test_decisionminimumdominatingset_to_minimumsummulticenter_closed_loop_no_ins ), Or(false) ); - assert!(reduction.extract_solution(&target_solution).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &target_solution), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&reduction, value); value.is_valid() }) + ); } } @@ -144,12 +146,9 @@ fn test_decisionminimumdominatingset_to_minimumsummulticenter_all_small_graphs() } let accepted = crate::rules::AggregateReductionResult::extract_value(&reduction, value).0; - match reduction.extract_solution(&placement) { - Ok(witness) => { - assert!(accepted); - assert_eq!(source.evaluate(&witness).unwrap(), Or(true)); - } - Err(_) => assert!(!accepted), + if accepted { + let witness = reduction.extract_solution(&placement).unwrap(); + assert_eq!(source.evaluate(&witness).unwrap(), Or(true)); } } assert_eq!( @@ -157,9 +156,9 @@ fn test_decisionminimumdominatingset_to_minimumsummulticenter_all_small_graphs() Or(source_yes), "n={n}, edges={edges:?}, K={bound}" ); - assert!(reduction - .extract_solution(&vec![false; target.num_vertices() + 1]) - .is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![false; target.num_vertices() + 1]), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&reduction, value); value.is_valid() }) + ); } } } diff --git a/src/unit_tests/rules/decisionminimumdominatingset_minmaxmulticenter.rs b/src/unit_tests/rules/decisionminimumdominatingset_minmaxmulticenter.rs index 9e15b4e34..933f762f3 100644 --- a/src/unit_tests/rules/decisionminimumdominatingset_minmaxmulticenter.rs +++ b/src/unit_tests/rules/decisionminimumdominatingset_minmaxmulticenter.rs @@ -42,7 +42,9 @@ fn test_decisionminimumdominatingset_to_minmaxmulticenter_closed_loop() { crate::rules::AggregateReductionResult::extract_value(&reduction, optimum), Or(false) ); - assert!(reduction.extract_solution(&witness).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &witness), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&reduction, value); value.is_valid() }) + ); } #[test] @@ -97,7 +99,9 @@ fn test_multicenter_all_small_graphs_bounds_and_placements() { .0 ); } else { - assert!(reduction.extract_solution(&witness).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &witness), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&reduction, value); value.is_valid() }) + ); } } assert_eq!( @@ -119,7 +123,9 @@ fn test_multicenter_duplicate_edges_and_malformed_witness() { vec![true, false, true] ); for bad in [vec![], vec![true; 6]] { - assert!(reduction.extract_solution(&bad).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &bad), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&reduction, value); value.is_valid() }) + ); } assert_eq!( crate::rules::AggregateReductionResult::extract_value(&reduction, Min(None)), diff --git a/src/unit_tests/rules/decisionminimumvertexcover_hamiltoniancircuit.rs b/src/unit_tests/rules/decisionminimumvertexcover_hamiltoniancircuit.rs index 67ba7ee19..4f61dcfd9 100644 --- a/src/unit_tests/rules/decisionminimumvertexcover_hamiltoniancircuit.rs +++ b/src/unit_tests/rules/decisionminimumvertexcover_hamiltoniancircuit.rs @@ -9,13 +9,12 @@ use crate::traits::Problem; fn decision_mvc( num_vertices: usize, edges: &[(usize, usize)], - weights: &[i64], k: i64, -) -> Decision> { +) -> Decision> { Decision::new( MinimumVertexCover::new( SimpleGraph::new(num_vertices, edges.to_vec()), - weights.to_vec(), + vec![One; num_vertices], ), k, ) @@ -23,7 +22,7 @@ fn decision_mvc( #[test] fn test_decisionminimumvertexcover_to_hamiltoniancircuit_structure_counts() { - let source = decision_mvc(3, &[(0, 1), (1, 2)], &[1, 1, 1], 1); + let source = decision_mvc(3, &[(0, 1), (1, 2)], 1); let reduction = ReduceTo::>::reduce_to(&source) .expect("reduction should succeed"); let target = reduction.target_problem(); @@ -35,7 +34,7 @@ fn test_decisionminimumvertexcover_to_hamiltoniancircuit_structure_counts() { #[test] fn test_decisionminimumvertexcover_to_hamiltoniancircuit_closed_loop() { - let source = decision_mvc(3, &[(0, 1), (1, 2)], &[1, 1, 1], 1); + let source = decision_mvc(3, &[(0, 1), (1, 2)], 1); let reduction = ReduceTo::>::reduce_to(&source) .expect("reduction should succeed"); @@ -57,7 +56,7 @@ fn test_decisionminimumvertexcover_to_hamiltoniancircuit_closed_loop() { #[test] fn test_decisionminimumvertexcover_to_hamiltoniancircuit_ignores_isolated_vertices() { - let source = decision_mvc(3, &[(0, 1)], &[1, 1, 1], 1); + let source = decision_mvc(3, &[(0, 1)], 1); let reduction = ReduceTo::>::reduce_to(&source) .expect("reduction should succeed"); @@ -79,7 +78,7 @@ fn test_decisionminimumvertexcover_to_hamiltoniancircuit_ignores_isolated_vertic #[test] fn test_decisionminimumvertexcover_to_hamiltoniancircuit_fixed_yes_when_k_covers_all_active_vertices( ) { - let source = decision_mvc(3, &[(0, 1), (1, 2)], &[1, 1, 1], 3); + let source = decision_mvc(3, &[(0, 1), (1, 2)], 3); let reduction = ReduceTo::>::reduce_to(&source) .expect("reduction should succeed"); let target = reduction.target_problem(); @@ -97,7 +96,7 @@ fn test_decisionminimumvertexcover_to_hamiltoniancircuit_fixed_yes_when_k_covers #[test] fn test_decisionminimumvertexcover_to_hamiltoniancircuit_fixed_no_when_k_zero() { - let source = decision_mvc(2, &[(0, 1)], &[1, 1], 0); + let source = decision_mvc(2, &[(0, 1)], 0); let reduction = ReduceTo::>::reduce_to(&source) .expect("reduction should succeed"); let target = reduction.target_problem(); @@ -107,11 +106,31 @@ fn test_decisionminimumvertexcover_to_hamiltoniancircuit_fixed_no_when_k_zero() } #[test] -fn test_decisionminimumvertexcover_to_hamiltoniancircuit_rejects_non_unit_weights() { - let source = decision_mvc(2, &[(0, 1)], &[2, 1], 1); - let error = ReduceTo::>::reduce_to(&source).unwrap_err(); - assert!(matches!( - error, - crate::rules::ReductionError::InvalidTarget { .. } - )); +fn hamiltonian_edge_registers_only_unit_weight_vertex_cover() { + let sources = inventory::iter:: + .into_iter() + .filter(|entry| { + entry.source_name == "DecisionMinimumVertexCover" + && entry.target_name == "HamiltonianCircuit" + }) + .map(|entry| (entry.source_variant_fn)()) + .collect::>(); + assert_eq!( + sources, + vec![Decision::>::variant()] + ); +} + +#[test] +fn unit_cover_bound_handles_negative_and_empty_graphs() { + for (bound, expected) in [(-1, false), (0, true), (i64::MAX, true)] { + let source = decision_mvc(2, &[], bound); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + let witness = BruteForce::new().solve(reduction.target_problem()).unwrap(); + assert_eq!(witness.is_some(), expected); + if let Some(witness) = witness { + let cover = reduction.extract_solution(&witness).unwrap(); + assert!(source.evaluate(&cover).unwrap().0); + } + } } diff --git a/src/unit_tests/rules/directedhamiltonianpath_ilp.rs b/src/unit_tests/rules/directedhamiltonianpath_ilp.rs index ad9c9f1bc..a0fcccd20 100644 --- a/src/unit_tests/rules/directedhamiltonianpath_ilp.rs +++ b/src/unit_tests/rules/directedhamiltonianpath_ilp.rs @@ -89,8 +89,9 @@ fn test_directedhamiltonianpath_to_ilp_no_path() { ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp_solver = ILPSolver::new(); let result = ilp_solver.solve(reduction.target_problem()); - assert!( - result.is_err(), + assert_eq!( + result, + Err(crate::solvers::ILPSolveError::Infeasible), "Graph with no Hamiltonian path should be infeasible" ); } diff --git a/src/unit_tests/rules/directedtwocommodityintegralflow_ilp.rs b/src/unit_tests/rules/directedtwocommodityintegralflow_ilp.rs index d438da773..61eb1b7a1 100644 --- a/src/unit_tests/rules/directedtwocommodityintegralflow_ilp.rs +++ b/src/unit_tests/rules/directedtwocommodityintegralflow_ilp.rs @@ -97,8 +97,9 @@ fn test_directedtwocommodityintegralflow_to_ilp_infeasible() { let problem = infeasible_instance(); let reduction: ReductionD2CIFToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); - assert!( - ILPSolver::new().solve(reduction.target_problem()).is_err(), + assert_eq!( + ILPSolver::new().solve(reduction.target_problem()), + Err(crate::solvers::ILPSolveError::Infeasible), "infeasible flow instance should produce infeasible ILP" ); } @@ -110,8 +111,9 @@ fn test_directedtwocommodityintegralflow_to_ilp_disallows_using_other_commodity_ let reduction: ReductionD2CIFToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); - assert!( - ILPSolver::new().solve(reduction.target_problem()).is_err(), + assert_eq!( + ILPSolver::new().solve(reduction.target_problem()), + Err(crate::solvers::ILPSolveError::Infeasible), "commodity 1 must conserve flow at commodity 2's source in the ILP reduction" ); } diff --git a/src/unit_tests/rules/ensemblecomputation_ilp.rs b/src/unit_tests/rules/ensemblecomputation_ilp.rs index 3be3c0aaa..319cdca2b 100644 --- a/src/unit_tests/rules/ensemblecomputation_ilp.rs +++ b/src/unit_tests/rules/ensemblecomputation_ilp.rs @@ -30,14 +30,20 @@ fn test_ensemblecomputation_to_ilp_closed_loop() { 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(); - assert!(ILPSolver::new().solve(reduction.target_problem()).is_err()); + assert_eq!( + ILPSolver::new().solve(reduction.target_problem()), + Err(crate::solvers::ILPSolveError::Infeasible) + ); } #[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(); - assert!(ILPSolver::new().solve(reduction.target_problem()).is_err()); + assert_eq!( + ILPSolver::new().solve(reduction.target_problem()), + Err(crate::solvers::ILPSolveError::Infeasible) + ); } #[test] diff --git a/src/unit_tests/rules/eulerianpath_ilp.rs b/src/unit_tests/rules/eulerianpath_ilp.rs index 7d9f5c3c4..3c95e24cb 100644 --- a/src/unit_tests/rules/eulerianpath_ilp.rs +++ b/src/unit_tests/rules/eulerianpath_ilp.rs @@ -87,8 +87,9 @@ fn test_eulerianpath_to_ilp_infeasible_no_instance() { // The ILP must report infeasibility for a NO instance. let solution = ILPSolver::new().solve(reduction.target_problem()); - assert!( - solution.is_err(), + assert_eq!( + solution, + Err(crate::solvers::ILPSolveError::Infeasible), "ILP must be infeasible for a degree-unbalanced NO instance, got {:?}", solution ); diff --git a/src/unit_tests/rules/exactcoverby3sets_algebraicequationsovergf2.rs b/src/unit_tests/rules/exactcoverby3sets_algebraicequationsovergf2.rs index 5d1fc643f..98f072d86 100644 --- a/src/unit_tests/rules/exactcoverby3sets_algebraicequationsovergf2.rs +++ b/src/unit_tests/rules/exactcoverby3sets_algebraicequationsovergf2.rs @@ -49,8 +49,8 @@ fn test_exactcoverby3sets_to_algebraicequationsovergf2_extract_solution_is_ident assert_eq!( reduction - .extract_solution(&vec![true, false, true]) + .extract_solution(&vec![true, true, false]) .unwrap(), - vec![true, false, true] + vec![true, true, false] ); } diff --git a/src/unit_tests/rules/exactcoverby3sets_boundeddiameterspanningtree.rs b/src/unit_tests/rules/exactcoverby3sets_boundeddiameterspanningtree.rs index ba458bb61..a1f12a328 100644 --- a/src/unit_tests/rules/exactcoverby3sets_boundeddiameterspanningtree.rs +++ b/src/unit_tests/rules/exactcoverby3sets_boundeddiameterspanningtree.rs @@ -82,11 +82,15 @@ fn test_exactcoverby3sets_to_boundeddiameterspanningtree_extract_solution() { let mut invalid = vec![false; target_config.len()]; invalid[2] = true; invalid[3] = true; - assert!(reduction.extract_solution(&invalid).is_err()); - assert!(reduction.extract_solution(&vec![]).is_err()); - assert!(reduction - .extract_solution(&vec![true; target_config.len()]) - .is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &invalid), Ok(value) if { value.is_valid() }) + ); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![]), Ok(value) if { value.is_valid() }) + ); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![true; target_config.len()]), Ok(value) if { value.is_valid() }) + ); } #[test] @@ -103,7 +107,9 @@ fn test_exactcoverby3sets_to_boundeddiameterspanningtree_no_instance() { // exist here). Equivalently, the brute-force aggregate evaluates to // Or(false). assert!(BruteForce::new().solve(target).unwrap().is_none()); - assert!(reduction.extract_solution(&vec![]).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![]), Ok(value) if { value.is_valid() }) + ); } #[test] @@ -118,7 +124,9 @@ fn test_exactcoverby3sets_to_boundeddiameterspanningtree_universe_boundaries() { .solve(reduction.target_problem()) .unwrap() .is_none()); - assert!(reduction.extract_solution(&vec![]).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![]), Ok(value) if { value.is_valid() }) + ); } let source = ExactCoverBy3Sets::new(0, vec![]); let reduction = diff --git a/src/unit_tests/rules/exactcoverby3sets_staffscheduling.rs b/src/unit_tests/rules/exactcoverby3sets_staffscheduling.rs index ea050fe01..e9e26ee06 100644 --- a/src/unit_tests/rules/exactcoverby3sets_staffscheduling.rs +++ b/src/unit_tests/rules/exactcoverby3sets_staffscheduling.rs @@ -89,10 +89,10 @@ fn test_exactcoverby3sets_to_staffscheduling_extract_solution() { // Verify the extracted solution is valid in the source assert!(source.evaluate(&extracted).unwrap().0); - // Config with 0 workers everywhere should extract to all-zero (no subsets selected) - let empty_config = vec![0, 0, 0, 0]; - let extracted_empty = result.extract_solution(&empty_config).unwrap(); - assert_eq!(extracted_empty, vec![false, false, false, false]); + // No workers cannot cover the required shifts. + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&result), &vec![0, 0, 0, 0]), Ok(value) if { value.is_valid() }) + ); } #[test] diff --git a/src/unit_tests/rules/exactcoverby3sets_subsetproduct.rs b/src/unit_tests/rules/exactcoverby3sets_subsetproduct.rs index dac72241d..2f2361518 100644 --- a/src/unit_tests/rules/exactcoverby3sets_subsetproduct.rs +++ b/src/unit_tests/rules/exactcoverby3sets_subsetproduct.rs @@ -41,9 +41,9 @@ fn test_exactcoverby3sets_to_subsetproduct_extract_solution_is_identity() { assert_eq!( reduction - .extract_solution(&vec![true, false, true]) + .extract_solution(&vec![true, true, false]) .unwrap(), - vec![true, false, true] + vec![true, true, false] ); } diff --git a/src/unit_tests/rules/expectedretrievalcost_ilp.rs b/src/unit_tests/rules/expectedretrievalcost_ilp.rs index ae9b2af03..86a9b561a 100644 --- a/src/unit_tests/rules/expectedretrievalcost_ilp.rs +++ b/src/unit_tests/rules/expectedretrievalcost_ilp.rs @@ -65,20 +65,25 @@ fn test_solution_extraction() { let reduction: ReductionERCToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); - // record 0 -> sector 0, record 1 -> sector 1 - // x_{0,0}=1, x_{0,1}=0, x_{1,0}=0, x_{1,1}=1 - let mut ilp_solution = vec![0_i64; 4 + 16]; // n + n^2 - // x vars - ilp_solution[0] = 1; // x_{0,0} - ilp_solution[3] = 1; // x_{1,1} - // z vars: z_{r,s,r',s'} at offset 4 + (r*2+s)*4 + (r'*2+s') - // z_{0,0,0,0} = x_{0,0}*x_{0,0} = 1: offset 4 + 0*4 + 0 = 4 - ilp_solution[4] = 1; - // z_{1,1,1,1} = x_{1,1}*x_{1,1} = 1: offset 4 + 3*4 + 3 = 4+15=19 - ilp_solution[19] = 1; - - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); - assert_eq!(extracted, vec![0, 1]); + for assignment in [vec![0, 0], vec![0, 1], vec![1, 0], vec![1, 1]] { + let mut target = vec![0; reduction.target_problem().num_vars()]; + for (r, §or) in assignment.iter().enumerate() { + target[reduction.x_var(r, sector)] = 1; + } + for (r, §or) in assignment.iter().enumerate() { + for (other, &other_sector) in assignment.iter().enumerate() { + target[reduction.z_var(r, sector, other, other_sector)] = 1; + } + } + assert_eq!(reduction.extract_solution(&target).unwrap(), assignment); + assert_eq!( + reduction + .target_problem() + .evaluate_objective(&target) + .unwrap(), + problem.expected_cost(&assignment).unwrap().unwrap() + ); + } } #[test] diff --git a/src/unit_tests/rules/factoring_circuit.rs b/src/unit_tests/rules/factoring_circuit.rs index 5b16f2702..4dacf41cc 100644 --- a/src/unit_tests/rules/factoring_circuit.rs +++ b/src/unit_tests/rules/factoring_circuit.rs @@ -1,10 +1,10 @@ use super::*; +include!("../jl_helpers.rs"); use crate::rules::test_helpers::assert_optimization_round_trip_from_satisfaction_target; use crate::solvers::BruteForce; use crate::traits::Problem; use num_bigint::BigUint; use std::collections::HashMap; -include!("../jl_helpers.rs"); #[test] fn test_read_bit() { @@ -396,10 +396,12 @@ fn test_factoring_to_circuit_zero_width_closed_loop() { fn test_factoring_to_circuit_rejects_invalid_certificates() { let source = Factoring::with_factor_bits(6, 2, 2); let reduction = ReduceTo::::reduce_to(&source).unwrap(); - assert!(reduction.extract_solution(&vec![]).is_err()); - assert!(reduction - .extract_solution(&vec![false; reduction.target_problem().num_variables()]) - .is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![]), Ok(value) if { value.is_valid() }) + ); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![false; reduction.target_problem().num_variables()]), Ok(value) if { value.is_valid() }) + ); let values = evaluate_multiplier_circuit(&reduction, 1, 1); let config = reduction .target_problem() @@ -407,7 +409,9 @@ fn test_factoring_to_circuit_rejects_invalid_certificates() { .iter() .map(|name| values[name]) .collect(); - assert!(reduction.extract_solution(&config).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &config), Ok(value) if { value.is_valid() }) + ); } #[test] diff --git a/src/unit_tests/rules/factoring_ilp.rs b/src/unit_tests/rules/factoring_ilp.rs index 472e97016..8d5c4b2d0 100644 --- a/src/unit_tests/rules/factoring_ilp.rs +++ b/src/unit_tests/rules/factoring_ilp.rs @@ -171,7 +171,11 @@ fn test_infeasible_target_too_large() { let ilp_solver = ILPSolver::new(); let result = ilp_solver.solve(ilp); - assert!(result.is_err(), "Should be infeasible"); + assert_eq!( + result, + Err(crate::solvers::ILPSolveError::Infeasible), + "Should be infeasible" + ); } #[test] @@ -215,7 +219,8 @@ fn test_solution_extraction() { // z_00 = p_0 * q_0 = 0, z_01 = p_0 * q_1 = 0 // z_10 = p_1 * q_0 = 1, z_11 = p_1 * q_1 = 1 // Variables: [p0, p1, q0, q1, z00, z01, z10, z11, c0, c1, c2, c3] - let ilp_solution = vec![0, 1, 1, 1, 0, 0, 1, 1, 0, 1, 0, 0]; + // Each product column already matches 0110, so every carry is zero. + let ilp_solution = vec![0, 1, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0]; let extracted = reduction.extract_solution(&ilp_solution).unwrap(); assert_eq!(extracted, (BigUint::from(2u32), BigUint::from(3u32))); @@ -277,7 +282,10 @@ 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(); - assert!(ILPSolver::new().solve(reduction.target_problem()).is_err()); + assert_eq!( + ILPSolver::new().solve(reduction.target_problem()), + Err(crate::solvers::ILPSolveError::Infeasible) + ); } #[test] diff --git a/src/unit_tests/rules/feasibleregisterassignment_ilp.rs b/src/unit_tests/rules/feasibleregisterassignment_ilp.rs index 6b18e1e2a..5a58bdaef 100644 --- a/src/unit_tests/rules/feasibleregisterassignment_ilp.rs +++ b/src/unit_tests/rules/feasibleregisterassignment_ilp.rs @@ -40,8 +40,9 @@ 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"); - assert!( - ILPSolver::new().solve(reduction.target_problem()).is_err(), + assert_eq!( + ILPSolver::new().solve(reduction.target_problem()), + Err(crate::solvers::ILPSolveError::Infeasible), "register-conflict source instance should reduce to an infeasible ILP" ); } diff --git a/src/unit_tests/rules/flowshopscheduling_ilp.rs b/src/unit_tests/rules/flowshopscheduling_ilp.rs index 34da74e87..6bf8a37e6 100644 --- a/src/unit_tests/rules/flowshopscheduling_ilp.rs +++ b/src/unit_tests/rules/flowshopscheduling_ilp.rs @@ -33,8 +33,9 @@ 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"); - assert!( - ILPSolver::new().solve(reduction.target_problem()).is_err(), + assert_eq!( + ILPSolver::new().solve(reduction.target_problem()), + Err(crate::solvers::ILPSolveError::Infeasible), "infeasible FSS should produce infeasible ILP" ); } diff --git a/src/unit_tests/rules/graph.rs b/src/unit_tests/rules/graph.rs index df044287b..4918275a6 100644 --- a/src/unit_tests/rules/graph.rs +++ b/src/unit_tests/rules/graph.rs @@ -12,7 +12,6 @@ use crate::registry::ProblemCategory; use crate::rules::graph::{ReductionMode, ReductionStep}; use crate::rules::registry::{ReductionEntry, ReductionParameterDeclarations}; use crate::rules::traits::{AggregateReductionResult, ReductionResult}; -use crate::solvers::BruteForceProblem as _; use crate::topology::SimpleGraph; use crate::traits::Problem; use crate::types::{One, ProblemParameters, Sum}; @@ -84,7 +83,12 @@ impl Problem for AggregateChainSource { type Solution = Vec; type Value = Sum; - crate::problem_parameters![("num_variables", num_variables)]; + fn parameter_names() -> &'static [&'static str] { + &["num_variables"] + } + fn parameters(&self) -> crate::types::ProblemParameters { + crate::types::ProblemParameters::new(vec![("num_variables", 1usize as u64)]) + } fn evaluate( &self, @@ -99,8 +103,12 @@ impl Problem for AggregateChainSource { } impl crate::solvers::BruteForceProblem for AggregateChainSource { - fn dimensions(&self) -> Vec { - vec![1] + fn num_variables(&self) -> Result { + Ok(1usize) + } + + fn dimension(&self, variable: usize) -> Result { + Ok([1][variable]) } } @@ -109,7 +117,12 @@ impl Problem for AggregateChainMiddle { type Solution = Vec; type Value = Sum; - crate::problem_parameters![("num_variables", num_variables)]; + fn parameter_names() -> &'static [&'static str] { + &["num_variables"] + } + fn parameters(&self) -> crate::types::ProblemParameters { + crate::types::ProblemParameters::new(vec![("num_variables", 1usize as u64)]) + } fn evaluate( &self, @@ -124,8 +137,12 @@ impl Problem for AggregateChainMiddle { } impl crate::solvers::BruteForceProblem for AggregateChainMiddle { - fn dimensions(&self) -> Vec { - vec![1] + fn num_variables(&self) -> Result { + Ok(1usize) + } + + fn dimension(&self, variable: usize) -> Result { + Ok([1][variable]) } } @@ -134,7 +151,12 @@ impl Problem for AggregateChainTarget { type Solution = Vec; type Value = Sum; - crate::problem_parameters![("num_variables", num_variables)]; + fn parameter_names() -> &'static [&'static str] { + &["num_variables"] + } + fn parameters(&self) -> crate::types::ProblemParameters { + crate::types::ProblemParameters::new(vec![("num_variables", 1usize as u64)]) + } fn evaluate( &self, @@ -149,23 +171,32 @@ impl Problem for AggregateChainTarget { } impl crate::solvers::BruteForceProblem for AggregateChainTarget { - fn dimensions(&self) -> Vec { - vec![1] + fn num_variables(&self) -> Result { + Ok(1usize) + } + + fn dimension(&self, variable: usize) -> Result { + Ok([1][variable]) } } impl Problem for NaturalVariantProblem { const NAME: &'static str = "NaturalVariantProblem"; type Solution = Vec; - type Value = Sum; + type Value = crate::types::Max; - crate::problem_parameters![("num_variables", num_variables)]; + fn parameter_names() -> &'static [&'static str] { + &["num_variables"] + } + fn parameters(&self) -> crate::types::ProblemParameters { + crate::types::ProblemParameters::new(vec![("num_variables", 1usize as u64)]) + } fn evaluate( &self, config: &Self::Solution, ) -> Result { - Ok(Sum(config.iter().sum::() as u64)) + Ok(crate::types::Max(Some(config.iter().sum::() as u64))) } fn variant() -> Vec<(&'static str, &'static str)> { @@ -174,8 +205,12 @@ impl Problem for NaturalVariantProblem { } impl crate::solvers::BruteForceProblem for NaturalVariantProblem { - fn dimensions(&self) -> Vec { - vec![1] + fn num_variables(&self) -> Result { + Ok(1usize) + } + + fn dimension(&self, variable: usize) -> Result { + Ok([1][variable]) } } @@ -267,7 +302,7 @@ impl ReductionResult for SourceToMiddleWitnessResult { fn reduce_source_to_middle_witness( any: &dyn Any, -) -> Result, crate::rules::ReductionError> { +) -> Result { any.downcast_ref::().ok_or( crate::rules::ReductionError::SourceTypeMismatch { source_problem: AggregateChainSource::NAME, @@ -275,14 +310,18 @@ fn reduce_source_to_middle_witness( expected: std::any::type_name::(), }, )?; - Ok(Box::new(SourceToMiddleWitnessResult { - target: AggregateChainMiddle, - })) + Ok(crate::rules::registry::ExecutedStep { + witness: std::rc::Rc::new(SourceToMiddleWitnessResult { + target: AggregateChainMiddle, + }), + aggregate: None, + interpret_optimum: None, + }) } fn fail_source_to_middle_witness( _any: &dyn Any, -) -> Result, crate::rules::ReductionError> { +) -> Result { Err(crate::rules::ReductionError::InvalidTarget { source_problem: AggregateChainSource::NAME, target_problem: AggregateChainMiddle::NAME, @@ -294,7 +333,7 @@ static SHARED_PREFIX_EXECUTIONS: AtomicUsize = AtomicUsize::new(0); fn reduce_counted_source_to_middle_witness( any: &dyn Any, -) -> Result, crate::rules::ReductionError> { +) -> Result { SHARED_PREFIX_EXECUTIONS.fetch_add(1, Ordering::SeqCst); reduce_source_to_middle_witness(any) } @@ -321,7 +360,7 @@ impl ReductionResult for MiddleToTargetWitnessResult { fn reduce_middle_to_target_witness( any: &dyn Any, -) -> Result, crate::rules::ReductionError> { +) -> Result { any.downcast_ref::().ok_or( crate::rules::ReductionError::SourceTypeMismatch { source_problem: AggregateChainMiddle::NAME, @@ -329,14 +368,18 @@ fn reduce_middle_to_target_witness( expected: std::any::type_name::(), }, )?; - Ok(Box::new(MiddleToTargetWitnessResult { - target: AggregateChainTarget, - })) + Ok(crate::rules::registry::ExecutedStep { + witness: std::rc::Rc::new(MiddleToTargetWitnessResult { + target: AggregateChainTarget, + }), + aggregate: None, + interpret_optimum: None, + }) } fn reduce_natural_variant_witness( any: &dyn Any, -) -> Result, crate::rules::ReductionError> { +) -> Result { let source = any.downcast_ref::().ok_or( crate::rules::ReductionError::SourceTypeMismatch { source_problem: NaturalVariantProblem::NAME, @@ -344,10 +387,14 @@ fn reduce_natural_variant_witness( expected: std::any::type_name::(), }, )?; - Ok(Box::new(crate::rules::VariantReductionResult::< - NaturalVariantProblem, - NaturalVariantProblem, - >::new(source.clone()))) + Ok(crate::rules::registry::ExecutedStep { + witness: std::rc::Rc::new(crate::rules::VariantReductionResult::< + NaturalVariantProblem, + NaturalVariantProblem, + >::new(source.clone())), + aggregate: None, + interpret_optimum: None, + }) } fn build_two_node_graph( @@ -418,7 +465,7 @@ fn execute_paths_executes_a_shared_prefix_once() { ), ], ); - let paths = vec![ + let mut paths = vec![ named_path(&[AggregateChainSource::NAME, AggregateChainMiddle::NAME]), named_path(&[ AggregateChainSource::NAME, @@ -427,11 +474,31 @@ fn execute_paths_executes_a_shared_prefix_once() { ]), ]; + paths.push(paths[0].clone()); + paths.push(paths[1].clone()); + let executed = graph .execute_paths(&paths, &AggregateChainSource) .expect("both paths are executable"); - assert_eq!(executed.len(), 2); + assert_eq!(executed.len(), 4); + for (path, execution) in paths.iter().zip(&executed) { + assert_eq!(execution.steps.len(), path.len()); + assert_eq!( + execution + .extract_solution::, _>(&vec![1usize]) + .unwrap(), + vec![1] + ); + } + assert!(std::rc::Rc::ptr_eq( + &executed[0].steps[0].witness, + &executed[3].steps[0].witness + )); + assert!(std::rc::Rc::ptr_eq( + &executed[1].steps[1].witness, + &executed[3].steps[1].witness + )); assert_eq!(SHARED_PREFIX_EXECUTIONS.load(Ordering::SeqCst), 1); } @@ -693,7 +760,8 @@ fn test_aggregate_reduction_chain_extracts_value_backwards() { .expect("expected aggregate reduction chain"); assert_eq!( - chain.target_problem::().dimensions(), + crate::solvers::cartesian_dimensions(chain.target_problem::()) + .unwrap(), vec![1] ); assert_eq!(chain.extract_value_dyn(json!(7)), json!(12)); @@ -1916,3 +1984,115 @@ fn test_composed_path_parameters_transform_evaluation() { assert_eq!(final_size.get("num_vertices"), Some(10)); assert_eq!(final_size.get("num_edges"), Some(20)); } + +#[test] +fn witness_and_value_mapping_share_one_executed_construction() { + use crate::rules::registry::ExecutedStep; + use std::rc::Rc; + + static CONSTRUCTIONS: AtomicUsize = AtomicUsize::new(0); + + let chain = crate::rules::ReductionChain::execute( + &AggregateChainSource, + &[|_| { + CONSTRUCTIONS.fetch_add(1, Ordering::SeqCst); + let result = Rc::new(SourceToMiddleWitnessResult { + target: AggregateChainMiddle, + }); + Ok(ExecutedStep { + aggregate: Some(result.clone()), + interpret_optimum: None, + witness: result, + }) + }], + ) + .unwrap(); + let step = &chain.steps[0]; + let aggregate = step.aggregate.as_ref().unwrap(); + assert!(std::ptr::eq( + step.witness.target_problem_any(), + aggregate.target_problem_any(), + )); + let witness = vec![1usize]; + assert_eq!( + chain.extract_solution::, _>(&witness).unwrap(), + witness + ); + assert_eq!( + aggregate.extract_value_dyn(serde_json::json!(7)), + serde_json::json!(7) + ); + assert_eq!(CONSTRUCTIONS.load(Ordering::SeqCst), 1); +} + +impl AggregateReductionResult for SourceToMiddleWitnessResult { + type Source = AggregateChainSource; + type Target = AggregateChainMiddle; + fn target_problem(&self) -> &Self::Target { + &self.target + } + fn extract_value(&self, value: Sum) -> Sum { + value + } +} + +#[test] +fn composed_witness_agrees_across_direct_chain_path_and_json() { + use crate::rules::ReduceTo; + type Cover = MinimumVertexCover; + type IndependentSet = MaximumIndependentSet; + let source = Cover::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)]), vec![1; 3]); + let first = ReduceTo::::reduce_to(&source).unwrap(); + let second = ReduceTo::>::reduce_to(first.target_problem()).unwrap(); + let third = ReduceTo::>::reduce_to(second.target_problem()).unwrap(); + let target_solution = vec![1i64, 0, 1]; + assert!(third + .target_problem() + .evaluate(&target_solution) + .unwrap() + .is_valid()); + let expected = first + .extract_solution( + &second + .extract_solution(&third.extract_solution(&target_solution).unwrap()) + .unwrap(), + ) + .unwrap(); + assert_eq!(expected, vec![false, true, false]); + let path = ReductionPath { + steps: [ + (Cover::NAME, Cover::variant()), + (IndependentSet::NAME, IndependentSet::variant()), + ( + MaximumSetPacking::::NAME, + MaximumSetPacking::::variant(), + ), + (ILP::::NAME, ILP::::variant()), + ] + .into_iter() + .map(|(name, variant)| ReductionStep { + name: name.into(), + variant: ReductionGraph::variant_to_map(&variant), + }) + .collect(), + }; + let graph = ReductionGraph::new(); + let chain = graph.reduce_along_path(&path, &source).unwrap().unwrap(); + let executed = graph.execute_paths(&[path], &source).unwrap(); + assert_eq!( + chain + .extract_solution::, _>(&target_solution) + .unwrap(), + expected + ); + assert_eq!( + executed[0] + .extract_solution::, _>(&target_solution) + .unwrap(), + expected + ); + assert_eq!( + chain.extract_solution_json(json!(target_solution)).unwrap(), + json!(expected) + ); +} diff --git a/src/unit_tests/rules/graphpartitioning_qubo.rs b/src/unit_tests/rules/graphpartitioning_qubo.rs index b378a227c..4b163d528 100644 --- a/src/unit_tests/rules/graphpartitioning_qubo.rs +++ b/src/unit_tests/rules/graphpartitioning_qubo.rs @@ -42,7 +42,7 @@ fn test_graphpartitioning_to_qubo_matrix_matches_issue_example() { let expected_diagonal = [-48, -47, -46, -46, -47, -48]; for (index, expected) in expected_diagonal.into_iter().enumerate() { - assert_eq!(qubo.get(index, index), Some(&expected)); + assert_eq!(qubo.get(index, index), Some(expected)); } let edge_pairs = [ @@ -57,12 +57,12 @@ fn test_graphpartitioning_to_qubo_matrix_matches_issue_example() { (4, 5), ]; for &(u, v) in &edge_pairs { - assert_eq!(qubo.get(u, v), Some(&18), "edge ({u}, {v})"); + assert_eq!(qubo.get(u, v), Some(18), "edge ({u}, {v})"); } let non_edge_pairs = [(0, 3), (0, 4), (0, 5), (1, 4), (1, 5), (2, 5)]; for &(u, v) in &non_edge_pairs { - assert_eq!(qubo.get(u, v), Some(&20), "non-edge ({u}, {v})"); + assert_eq!(qubo.get(u, v), Some(20), "non-edge ({u}, {v})"); } } @@ -77,6 +77,6 @@ fn test_graphpartitioning_to_qubo_canonical_example_spec() { assert_eq!(example.source.problem, "GraphPartitioning"); assert_eq!(example.target.problem, "QUBO"); - assert_eq!(example.target.instance["num_vars"], 6); + assert_eq!(example.target.instance["matrix"]["nrows"], 6); assert!(!example.solutions.is_empty()); } diff --git a/src/unit_tests/rules/hamiltoniancircuit_biconnectivityaugmentation.rs b/src/unit_tests/rules/hamiltoniancircuit_biconnectivityaugmentation.rs index 490356692..f94f0f2bd 100644 --- a/src/unit_tests/rules/hamiltoniancircuit_biconnectivityaugmentation.rs +++ b/src/unit_tests/rules/hamiltoniancircuit_biconnectivityaugmentation.rs @@ -143,7 +143,9 @@ fn test_hamiltoniancircuit_to_biconnectivityaugmentation_small_graphs() { assert_eq!(target.num_potential_edges(), 0); assert_eq!(*target.budget(), 0); assert!(!target.evaluate(&vec![]).unwrap().0); - assert!(reduction.extract_solution(&vec![]).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![]), Ok(value) if { value.is_valid() }) + ); assert!(BruteForce::new().solve(&source).unwrap().is_none()); assert!(BruteForce::new().solve(target).unwrap().is_none()); } @@ -173,9 +175,8 @@ fn test_hamiltoniancircuit_to_biconnectivityaugmentation_all_graphs_and_certific for mask in 0..1usize << pairs.len() { let config: Vec<_> = (0..pairs.len()).map(|i| mask & (1 << i) != 0).collect(); let feasible = reduction.target_problem().evaluate(&config).unwrap().0; - let extracted = reduction.extract_solution(&config); - assert_eq!(extracted.is_ok(), feasible); - if let Ok(circuit) = extracted { + if feasible { + let circuit = reduction.extract_solution(&config).unwrap(); assert!(source.evaluate(&circuit).unwrap().0); target_yes = true; } @@ -194,10 +195,18 @@ fn test_hamiltoniancircuit_to_biconnectivityaugmentation_rejects_infeasible_cert let reduction = ReduceTo::>::reduce_to(&source).unwrap(); // A spanning cycle made only of non-edges exceeds the budget and is not a source cycle. - assert!(reduction.extract_solution(&vec![true; 3]).is_err()); - assert!(reduction.extract_solution(&vec![false; 3]).is_err()); - assert!(reduction.extract_solution(&vec![true; 2]).is_err()); - assert!(reduction.extract_solution(&vec![true; 4]).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![true; 3]), Ok(value) if { value.is_valid() }) + ); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![false; 3]), Ok(value) if { value.is_valid() }) + ); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![true; 2]), Ok(value) if { value.is_valid() }) + ); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![true; 4]), Ok(value) if { value.is_valid() }) + ); let source = HamiltonianCircuit::new(SimpleGraph::complete(6)); let reduction = ReduceTo::>::reduce_to(&source).unwrap(); @@ -208,5 +217,7 @@ fn test_hamiltoniancircuit_to_biconnectivityaugmentation_rejects_infeasible_cert .iter() .map(|&(u, v, _)| (u < 3) == (v < 3)) .collect(); - assert!(reduction.extract_solution(&config).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &config), Ok(value) if { value.is_valid() }) + ); } diff --git a/src/unit_tests/rules/hamiltoniancircuit_longestcircuit.rs b/src/unit_tests/rules/hamiltoniancircuit_longestcircuit.rs index e7d230743..571623624 100644 --- a/src/unit_tests/rules/hamiltoniancircuit_longestcircuit.rs +++ b/src/unit_tests/rules/hamiltoniancircuit_longestcircuit.rs @@ -26,7 +26,9 @@ fn test_hamiltoniancircuit_aggregate_requires_a_spanning_cycle() { } let short_cycle = HamiltonianCircuit::new(SimpleGraph::new(4, vec![(0, 1), (1, 2), (0, 2)])); let reduction = ReduceTo::>::reduce_to(&short_cycle).unwrap(); - assert!(reduction.extract_solution(&vec![true; 3]).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![true; 3]), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&reduction, value); value.is_valid() }) + ); } #[test] @@ -123,19 +125,14 @@ fn test_hamiltoniancircuit_extraction_matches_all_small_target_configurations() .collect(); let value = target.evaluate(&config).unwrap(); let certifies = value.0 == Some(n as i64); - let extracted = reduction.extract_solution(&config); - assert_eq!( - extracted.is_ok(), - certifies, - "n={n}, graph={graph_mask}, config={mask}" - ); - if let Ok(order) = extracted { + if certifies { + let order = reduction.extract_solution(&config).unwrap(); assert!(source.evaluate(&order).unwrap().0); } } - assert!(reduction - .extract_solution(&vec![false; target.num_edges() + 1]) - .is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![false; target.num_edges() + 1]), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&reduction, value); value.is_valid() }) + ); } } } diff --git a/src/unit_tests/rules/hamiltoniancircuit_quadraticassignment.rs b/src/unit_tests/rules/hamiltoniancircuit_quadraticassignment.rs index 466d811f9..4abdae217 100644 --- a/src/unit_tests/rules/hamiltoniancircuit_quadraticassignment.rs +++ b/src/unit_tests/rules/hamiltoniancircuit_quadraticassignment.rs @@ -167,7 +167,9 @@ fn test_hamiltoniancircuit_to_quadraticassignment_small_graphs_are_no() { let value = target.evaluate(&best).unwrap(); assert_eq!(value, Min(Some(3))); assert!(!crate::rules::AggregateReductionResult::extract_value(&reduction, value).0); - assert!(reduction.extract_solution(&best).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &best), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&reduction, value); value.is_valid() }) + ); } } @@ -181,7 +183,8 @@ fn test_hamiltoniancircuit_to_quadraticassignment_rejects_invalid_certificates() vec![0, 0, 1, 2], vec![0, 2, 1, 3], ] { - assert!(reduction.extract_solution(&config).is_err(), "{config:?}"); + assert!(!matches!(reduction.target_problem().evaluate(&config), + Ok(value) if crate::rules::AggregateReductionResult::extract_value(&reduction, value).0)); } for value in [Min(None), Min(Some(-1)), Min(Some(1))] { assert!(!crate::rules::AggregateReductionResult::extract_value(&reduction, value).0); @@ -224,7 +227,6 @@ fn test_hamiltoniancircuit_to_quadraticassignment_all_small_graphs_and_orders() reduction.target_problem().evaluate(&order).unwrap(), Min(None) ); - assert!(reduction.extract_solution(&order).is_err()); continue; } let missing = (0..n) @@ -240,7 +242,9 @@ fn test_hamiltoniancircuit_to_quadraticassignment_all_small_graphs_and_orders() if expected { assert_eq!(reduction.extract_solution(&order).unwrap(), order); } else { - assert!(reduction.extract_solution(&order).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &order), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&reduction, value); value.is_valid() }) + ); } } } diff --git a/src/unit_tests/rules/hamiltoniancircuit_ruralpostman.rs b/src/unit_tests/rules/hamiltoniancircuit_ruralpostman.rs index b13ad1365..1e7227fe9 100644 --- a/src/unit_tests/rules/hamiltoniancircuit_ruralpostman.rs +++ b/src/unit_tests/rules/hamiltoniancircuit_ruralpostman.rs @@ -147,3 +147,31 @@ fn test_hamiltoniancircuit_to_ruralpostman_extract_solution() { "extracted solution should be a valid Hamiltonian circuit" ); } + +#[test] +fn aggregate_distinguishes_hamiltonian_tour_cost() { + for (edges, expected) in [ + (vec![(0, 1), (1, 2), (0, 2)], true), + (vec![(0, 1), (1, 2)], false), + ] { + let source = HamiltonianCircuit::new(SimpleGraph::new(3, edges)); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + let target = reduction.target_problem(); + let solution = crate::solvers::ILPSolver::new().solve(target).unwrap(); + assert_eq!( + crate::rules::AggregateReductionResult::extract_value( + &reduction, + target.evaluate(&solution).unwrap() + ), + crate::types::Or(expected) + ); + if expected { + assert!( + source + .evaluate(&reduction.extract_solution(&solution).unwrap()) + .unwrap() + .0 + ); + } + } +} diff --git a/src/unit_tests/rules/hamiltoniancircuit_stackercrane.rs b/src/unit_tests/rules/hamiltoniancircuit_stackercrane.rs index 58121a30f..d11e33732 100644 --- a/src/unit_tests/rules/hamiltoniancircuit_stackercrane.rs +++ b/src/unit_tests/rules/hamiltoniancircuit_stackercrane.rs @@ -157,19 +157,18 @@ fn test_stackercrane_certificate_for_all_small_configurations() { crate::rules::AggregateReductionResult::extract_value(&reduction, value).0, expected ); - let decoded = reduction.extract_solution(&config); - assert_eq!( - decoded.is_ok(), - expected, - "n={n}, mask={mask}, config={config:?}" - ); - if let Ok(order) = decoded { + if expected { + let order = reduction.extract_solution(&config).unwrap(); assert!(source.evaluate(&order).unwrap().0); } } - assert!(reduction.extract_solution(&vec![0; n + 1]).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![0; n + 1]), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&reduction, value); value.is_valid() }) + ); if n > 0 { - assert!(reduction.extract_solution(&vec![n; n]).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![n; n]), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&reduction, value); value.is_valid() }) + ); } } } diff --git a/src/unit_tests/rules/hamiltonianpath_ilp.rs b/src/unit_tests/rules/hamiltonianpath_ilp.rs index 66a2a897e..e9bd5b743 100644 --- a/src/unit_tests/rules/hamiltonianpath_ilp.rs +++ b/src/unit_tests/rules/hamiltonianpath_ilp.rs @@ -83,8 +83,9 @@ fn test_hamiltonianpath_to_ilp_no_path() { ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp_solver = ILPSolver::new(); let result = ilp_solver.solve(reduction.target_problem()); - assert!( - result.is_err(), + assert_eq!( + result, + Err(crate::solvers::ILPSolveError::Infeasible), "Disconnected graph should have no Hamiltonian path" ); } diff --git a/src/unit_tests/rules/hamiltonianpathbetweentwovertices_longestpath.rs b/src/unit_tests/rules/hamiltonianpathbetweentwovertices_longestpath.rs index 4e87892d5..c73cb3f95 100644 --- a/src/unit_tests/rules/hamiltonianpathbetweentwovertices_longestpath.rs +++ b/src/unit_tests/rules/hamiltonianpathbetweentwovertices_longestpath.rs @@ -152,19 +152,14 @@ fn test_hamiltonian_path_extraction_for_all_small_graphs_and_endpoints() { .0, expected ); - let result = reduction.extract_solution(&config); - assert_eq!( - result.is_ok(), - expected, - "n={n}, graph={graph_mask}, s={start}, t={end}, config={mask}" - ); - if let Ok(order) = result { + if expected { + let order = reduction.extract_solution(&config).unwrap(); assert!(source.evaluate(&order).unwrap().0); } } - assert!(reduction - .extract_solution(&vec![false; edges.len() + 1]) - .is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![false; edges.len() + 1]), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&reduction, value); value.is_valid() }) + ); } } } diff --git a/src/unit_tests/rules/highlyconnecteddeletion_ilp.rs b/src/unit_tests/rules/highlyconnecteddeletion_ilp.rs index 2dc13ee79..e56985e6c 100644 --- a/src/unit_tests/rules/highlyconnecteddeletion_ilp.rs +++ b/src/unit_tests/rules/highlyconnecteddeletion_ilp.rs @@ -85,12 +85,10 @@ fn test_highlyconnecteddeletion_to_ilp_rejects_unassigned_vertex() { let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let target_solution = vec![0; reduction.target_problem().num_vars()]; - assert_eq!( - reduction - .extract_solution(&target_solution) - .unwrap_err() - .to_string(), - "vertex 0 has no selected cluster" + assert!( + !crate::traits::Problem::evaluate(reduction.target_problem(), &target_solution) + .unwrap() + .is_valid() ); } @@ -125,3 +123,16 @@ fn test_highlyconnecteddeletion_to_ilp_disconnected_no_cluster() { assert_bf_vs_ilp(&source, &reduction); } + +#[test] +fn subset_mask_limit_belongs_to_the_reduction() { + let source = HighlyConnectedDeletion::new(SimpleGraph::new(64, vec![])); + assert_eq!( + source.evaluate(&vec![]).unwrap(), + crate::types::Min(Some(0)) + ); + assert!(matches!( + ReduceTo::>::reduce_to(&source), + Err(crate::rules::ReductionError::IntegerOverflow { .. }) + )); +} diff --git a/src/unit_tests/rules/ilp_helpers.rs b/src/unit_tests/rules/ilp_helpers.rs index 9ca18265d..9d82c06dd 100644 --- a/src/unit_tests/rules/ilp_helpers.rs +++ b/src/unit_tests/rules/ilp_helpers.rs @@ -153,7 +153,7 @@ fn test_one_hot_decode_permutation() { solution[2] = 1; // item 0 -> slot 2 solution[3] = 1; // item 1 -> slot 0 solution[7] = 1; // item 2 -> slot 1 - let decoded = one_hot_decode(&solution, 3, 3, 0).unwrap(); + let decoded = one_hot_decode(&solution, 3, 3, 0); assert_eq!(decoded, vec![1, 2, 0]); // slot 0 gets item 1, slot 1 gets item 2, slot 2 gets item 0 } @@ -164,25 +164,16 @@ fn test_one_hot_decode_with_offset() { solution[7] = 1; // 5 + 2 solution[8] = 1; // 5 + 3 solution[12] = 1; // 5 + 7 - let decoded = one_hot_decode(&solution, 3, 3, 5).unwrap(); + let decoded = one_hot_decode(&solution, 3, 3, 5); assert_eq!(decoded, vec![1, 2, 0]); } -#[test] -fn test_one_hot_decode_rejects_missing_and_duplicate_items() { - assert!(one_hot_decode(&[0, 0, 0, 0], 2, 2, 0).is_err()); - assert!(one_hot_decode(&[1, 0, 1, 0], 2, 2, 0).is_err()); - assert!(one_hot_decode(&[1, 1, 0, 0], 2, 2, 0).is_err()); -} - #[test] fn test_one_hot_decode_rows_accepts_exactly_one_column_per_row() { assert_eq!( - one_hot_decode_rows(&[0, 1, 0, 1, 0, 0], 2, 3, 0).unwrap(), + one_hot_decode_rows(&[0, 1, 0, 1, 0, 0], 2, 3, 0), vec![1, 0] ); - assert!(one_hot_decode_rows(&[0, 0, 0, 1, 0, 0], 2, 3, 0).is_err()); - assert!(one_hot_decode_rows(&[1, 1, 0, 1, 0, 0], 2, 3, 0).is_err()); } #[test] diff --git a/src/unit_tests/rules/ilp_i64_ilp_bool.rs b/src/unit_tests/rules/ilp_i64_ilp_bool.rs index 75be4dc93..027b69157 100644 --- a/src/unit_tests/rules/ilp_i64_ilp_bool.rs +++ b/src/unit_tests/rules/ilp_i64_ilp_bool.rs @@ -22,8 +22,16 @@ fn integer_ilp( 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 witness = match ILPSolver::new().solve(reduction.target_problem()) { + Ok(solution) => solution, + Err(crate::solvers::ILPSolveError::Infeasible) => return None, + Err(error) => panic!("ILP execution failed: {error}"), + }; let source_solution = reduction.extract_solution(&witness).unwrap(); + assert!( + source.is_feasible(&source_solution).unwrap(), + "decoded integer ILP solution must be feasible" + ); let objective = source.evaluate_objective(&source_solution).unwrap(); Some((source_solution, objective)) } @@ -39,8 +47,7 @@ fn test_ilp_i64_to_ilp_bool_closed_loop() { vec![(0, -5), (1, -6)], ObjectiveSense::Minimize, ); - let (solution, objective) = solve_via_bool(&source).unwrap(); - assert!(source.is_feasible(&solution).unwrap()); + let (_, objective) = solve_via_bool(&source).unwrap(); assert_eq!(objective, -27); } @@ -52,8 +59,7 @@ fn test_ilp_i64_to_ilp_bool_maximize() { vec![(0, 3), (1, 5)], ObjectiveSense::Maximize, ); - let (solution, objective) = solve_via_bool(&source).unwrap(); - assert!(source.is_feasible(&solution).unwrap()); + let (_, objective) = solve_via_bool(&source).unwrap(); assert_eq!(objective, 24); } @@ -98,8 +104,7 @@ fn test_ilp_i64_to_ilp_bool_equality_constraint() { vec![(0, 1)], ObjectiveSense::Minimize, ); - let (solution, objective) = solve_via_bool(&source).unwrap(); - assert!(source.is_feasible(&solution).unwrap()); + let (_, objective) = solve_via_bool(&source).unwrap(); assert_eq!(objective, 1); } @@ -130,7 +135,10 @@ fn test_ilp_i64_to_ilp_bool_infeasible() { ObjectiveSense::Minimize, ); let reduction = ReduceTo::>::reduce_to(&source).unwrap(); - assert!(ILPSolver::new().solve(reduction.target_problem()).is_err()); + assert_eq!( + ILPSolver::new().solve(reduction.target_problem()), + Err(crate::solvers::ILPSolveError::Infeasible) + ); } #[test] diff --git a/src/unit_tests/rules/ilp_casts.rs b/src/unit_tests/rules/ilp_i64_ilp_f64.rs similarity index 71% rename from src/unit_tests/rules/ilp_casts.rs rename to src/unit_tests/rules/ilp_i64_ilp_f64.rs index 2784c1f0b..5ecd98af0 100644 --- a/src/unit_tests/rules/ilp_casts.rs +++ b/src/unit_tests/rules/ilp_i64_ilp_f64.rs @@ -1,6 +1,6 @@ use super::*; use crate::models::algebraic::{IntegerVariable, ObjectiveSense}; -use crate::rules::ReductionGraph; +use crate::rules::{ReductionGraph, ReductionResult}; use crate::solvers::ILPSolver; use crate::types::MAX_EXACT_F64_INTEGER; @@ -35,7 +35,7 @@ fn test_ilp_i64_coefficients_to_f64_rejects_inexact_value() { let source = ILP::::new( 1, vec![], - vec![(0, MAX_EXACT_F64_INTEGER + 1)], + vec![(0, MAX_EXACT_F64_INTEGER + 2)], ObjectiveSense::Minimize, ) .unwrap(); @@ -47,7 +47,7 @@ fn test_ilp_i64_coefficients_to_f64_rejects_inexact_value() { } #[test] -fn test_ilp_cast_rechecks_source_feasibility() { +fn test_ilp_integer_coefficients_preserve_large_exact_constraint() { let rhs = 1_000_000_000_000_i64; let source = ILP::::with_variables( vec![IntegerVariable::nonnegative()], @@ -57,13 +57,28 @@ fn test_ilp_cast_rechecks_source_feasibility() { ) .unwrap(); let reduction = ReduceTo::>::reduce_to(&source).unwrap(); - let target_solution = vec![rhs + 1]; + assert_eq!(reduction.target_problem().variables(), source.variables()); + assert_eq!( + reduction.target_problem().constraints()[0].terms(), + &[(0, 1.0)] + ); + assert_eq!( + reduction.target_problem().constraints()[0].rhs(), + rhs as f64 + ); + let target_solution = vec![rhs]; assert!(reduction .target_problem() .is_feasible(&target_solution) .unwrap()); - assert!(reduction.extract_solution(&target_solution).is_err()); + assert_eq!( + reduction.extract_solution(&target_solution).unwrap(), + target_solution + ); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![]), Ok(value) if { value.is_valid() }) + ); } #[test] diff --git a/src/unit_tests/rules/ilp_qubo.rs b/src/unit_tests/rules/ilp_qubo.rs index 08300e2c8..59b817492 100644 --- a/src/unit_tests/rules/ilp_qubo.rs +++ b/src/unit_tests/rules/ilp_qubo.rs @@ -105,7 +105,7 @@ fn test_ilp_to_qubo_ge_with_slack() { let qubo = reduction.target_problem(); // 3 original + ceil(log2(3))=2 slack = 5 QUBO variables - assert_eq!(qubo.num_variables(), 5); + assert_eq!(qubo.num_variables().unwrap(), 5); let solver = BruteForce::new(); let qubo_solutions = solver.find_all_witnesses(qubo).unwrap(); @@ -136,7 +136,7 @@ fn test_ilp_to_qubo_le_with_slack() { let qubo = reduction.target_problem(); // 3 original + ceil(log2(3))=2 slack = 5 QUBO variables - assert_eq!(qubo.num_variables(), 5); + assert_eq!(qubo.num_variables().unwrap(), 5); let solver = BruteForce::new(); let qubo_solutions = solver.find_all_witnesses(qubo).unwrap(); @@ -164,7 +164,7 @@ fn test_ilp_to_qubo_structure() { let qubo = reduction.target_problem(); // Verify QUBO has appropriate structure - assert!(qubo.num_variables() >= ilp.num_vars()); + assert!(qubo.num_variables().unwrap() >= ilp.num_vars()); } #[test] @@ -215,12 +215,11 @@ fn test_ilp_qubo_all_small_rows_and_target_assignments() { // Independently detect zero squared-residual penalty. let certifies = source_value.is_valid() && energy.0.unwrap() + constant == normalized_objective; - let decoded = reduction.extract_solution(&config); - assert_eq!(decoded.is_ok(), certifies); let extracted_value = AggregateReductionResult::extract_value(&reduction, energy); assert_eq!(extracted_value.is_valid(), certifies); - if let Ok(solution) = decoded { + if certifies { + let solution = reduction.extract_solution(&config).unwrap(); assert_eq!(source.evaluate(&solution).unwrap(), source_value); assert_eq!(extracted_value, source_value); } @@ -244,9 +243,9 @@ fn test_ilp_qubo_all_small_rows_and_target_assignments() { .unwrap(); } assert_eq!(actual, expected); - assert!(reduction - .extract_solution(&vec![false; target.num_vars() + 1]) - .is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![false; target.num_vars() + 1]), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&reduction, value); value.is_valid() }) + ); } } } @@ -275,7 +274,9 @@ fn test_ilp_qubo_inconsistent_rows_and_absent_aggregate() { .evaluate(&config) .unwrap(); assert!(!AggregateReductionResult::extract_value(&reduction, value).is_valid()); - assert!(reduction.extract_solution(&config).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &config), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&reduction, value); value.is_valid() }) + ); } assert!( !AggregateReductionResult::extract_value(&reduction, crate::types::Min(None)) diff --git a/src/unit_tests/rules/integralflowbundles_ilp.rs b/src/unit_tests/rules/integralflowbundles_ilp.rs index b9bea620c..aad2e688b 100644 --- a/src/unit_tests/rules/integralflowbundles_ilp.rs +++ b/src/unit_tests/rules/integralflowbundles_ilp.rs @@ -99,7 +99,10 @@ 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"); - assert!(ILPSolver::new().solve(reduction.target_problem()).is_err()); + assert_eq!( + ILPSolver::new().solve(reduction.target_problem()), + Err(crate::solvers::ILPSolveError::Infeasible) + ); } #[test] diff --git a/src/unit_tests/rules/kcoloring_bicliquecover.rs b/src/unit_tests/rules/kcoloring_bicliquecover.rs index 9d86c589b..6246f6005 100644 --- a/src/unit_tests/rules/kcoloring_bicliquecover.rs +++ b/src/unit_tests/rules/kcoloring_bicliquecover.rs @@ -262,7 +262,9 @@ fn test_kcoloring_to_bicliquecover_native_loops_are_infeasible() { assert_eq!(target.graph().left_edges(), &[(0, 0)]); assert!(target.evaluate(&vec![]).unwrap().0.is_none()); assert!(BruteForce::new().solve(target).unwrap().is_none()); - assert!(reduction.extract_solution(&vec![]).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![]), Ok(value) if { value.is_valid() }) + ); if q == 0 { assert!(source.evaluate(&vec![0; n]).is_err()); } else { @@ -304,7 +306,9 @@ fn test_kcoloring_to_bicliquecover_rejects_invalid_certificates() { vec![vec![true; 8]; 4], vec![vec![false; 8]; 4], ] { - assert!(reduction.extract_solution(&invalid).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &invalid), Ok(value) if { value.is_valid() }) + ); } let valid = forward_witness(&source, &[0, 1]); assert!(target.evaluate(&valid).unwrap().0.is_some()); @@ -355,9 +359,8 @@ fn test_kcoloring_to_bicliquecover_all_single_vertex_target_configs() { }) .collect(); let value = target.evaluate(&config).unwrap(); - let decoded = reduction.extract_solution(&config); - assert_eq!(decoded.is_ok(), value.0.is_some()); - if let Ok(coloring) = decoded { + if value.0.is_some() { + let coloring = reduction.extract_solution(&config).unwrap(); feasible = true; assert!(source.evaluate(&coloring).unwrap().0); } diff --git a/src/unit_tests/rules/kcoloring_twodimensionalconsecutivesets.rs b/src/unit_tests/rules/kcoloring_twodimensionalconsecutivesets.rs index 46338d9c3..6be37cedb 100644 --- a/src/unit_tests/rules/kcoloring_twodimensionalconsecutivesets.rs +++ b/src/unit_tests/rules/kcoloring_twodimensionalconsecutivesets.rs @@ -145,7 +145,9 @@ fn test_kcoloring_to_tdcs_native_loops_are_no() { for a in 0..3 { for b in 0..3 { for c in 0..3 { - assert!(reduction.extract_solution(&vec![a, b, c]).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![a, b, c]), Ok(value) if { value.is_valid() }) + ); } } } @@ -157,7 +159,9 @@ fn test_kcoloring_to_tdcs_rejects_noncertificates() { let source = KColoring::::new(SimpleGraph::new(2, vec![(0, 1)])); let reduction = ReduceTo::::reduce_to(&source).unwrap(); for config in [vec![], vec![0, 1], vec![0, 1, 3], vec![0, 0, 0]] { - assert!(reduction.extract_solution(&config).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &config), Ok(value) if { value.is_valid() }) + ); } } @@ -214,9 +218,8 @@ fn test_kcoloring_to_tdcs_all_tiny_graphs_and_target_assignments() { }) .collect(); let feasible = target.evaluate(&grouping).unwrap().0; - let extracted = reduction.extract_solution(&grouping); - assert_eq!(extracted.is_ok(), feasible); - if let Ok(coloring) = extracted { + if feasible { + let coloring = reduction.extract_solution(&grouping).unwrap(); assert!(source.evaluate(&coloring).unwrap().0); target_yes = true; } diff --git a/src/unit_tests/rules/knapsack_qubo.rs b/src/unit_tests/rules/knapsack_qubo.rs index 29ff7203c..4827ce90b 100644 --- a/src/unit_tests/rules/knapsack_qubo.rs +++ b/src/unit_tests/rules/knapsack_qubo.rs @@ -77,6 +77,6 @@ fn test_knapsack_to_qubo_canonical_example_spec() { assert_eq!(example.source.problem, "Knapsack"); assert_eq!(example.target.problem, "QUBO"); assert_eq!(example.source.instance["capacity"], 7); - assert_eq!(example.target.instance["num_vars"], 7); + assert_eq!(example.target.instance["matrix"]["nrows"], 7); assert!(!example.solutions.is_empty()); } diff --git a/src/unit_tests/rules/ksatisfiability_acyclicpartition.rs b/src/unit_tests/rules/ksatisfiability_acyclicpartition.rs index d12fc33e7..803d7cba1 100644 --- a/src/unit_tests/rules/ksatisfiability_acyclicpartition.rs +++ b/src/unit_tests/rules/ksatisfiability_acyclicpartition.rs @@ -35,7 +35,9 @@ fn test_ksatisfiability_to_acyclicpartition_closed_loop() { .0 ); } else { - assert!(reduction.extract_solution(&labels).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &labels), Ok(value) if { value.is_valid() }) + ); } } assert_eq!(count, 3); @@ -53,7 +55,9 @@ fn test_acyclicpartition_extraction_rejects_invalid_targets() { vec![0; 9], vec![2, 1, 1, 0, 0, 1, 1, 0, 1], ] { - assert!(reduction.extract_solution(&labels).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &labels), Ok(value) if { value.is_valid() }) + ); } } diff --git a/src/unit_tests/rules/ksatisfiability_bicliquecover.rs b/src/unit_tests/rules/ksatisfiability_bicliquecover.rs index 9c6b4b2c9..22e336c8a 100644 --- a/src/unit_tests/rules/ksatisfiability_bicliquecover.rs +++ b/src/unit_tests/rules/ksatisfiability_bicliquecover.rs @@ -125,7 +125,9 @@ fn test_ksatisfiability_to_bicliquecover_rejects_invalid_covers() { vec![vec![true; target.num_vertices()]; target.k()], vec![vec![false; target.num_vertices() - 1]; target.k()], ] { - assert!(reduction.extract_solution(&invalid).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &invalid), Ok(value) if { value.is_valid() }) + ); } } @@ -252,7 +254,9 @@ fn test_ksatisfiability_to_bicliquecover_empty_conjunction_and_clause() { .unwrap() .0 .is_none()); - assert!(reduction.extract_solution(&vec![]).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![]), Ok(value) if { value.is_valid() }) + ); assert!(!no.evaluate(&vec![false; n]).unwrap().0); } } diff --git a/src/unit_tests/rules/ksatisfiability_cyclicordering.rs b/src/unit_tests/rules/ksatisfiability_cyclicordering.rs index 75c1ab6f7..c8e312447 100644 --- a/src/unit_tests/rules/ksatisfiability_cyclicordering.rs +++ b/src/unit_tests/rules/ksatisfiability_cyclicordering.rs @@ -271,7 +271,9 @@ fn empty_formula_and_empty_clause_have_opposite_fixed_targets() { vec![2, 1, 0], ] { assert!(!reduction.target_problem().evaluate(&config).unwrap().0); - assert!(reduction.extract_solution(&config).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &config), Ok(value) if { value.is_valid() }) + ); } } } @@ -293,7 +295,9 @@ fn reject_invalid_orderings_and_accept_every_rotation() { ); } for config in [vec![], vec![0; n], vec![n; n], (0..n).collect()] { - assert!(reduction.extract_solution(&config).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &config), Ok(value) if { value.is_valid() }) + ); } } diff --git a/src/unit_tests/rules/ksatisfiability_directedtwocommodityintegralflow.rs b/src/unit_tests/rules/ksatisfiability_directedtwocommodityintegralflow.rs index 6ad979fc3..908cf2509 100644 --- a/src/unit_tests/rules/ksatisfiability_directedtwocommodityintegralflow.rs +++ b/src/unit_tests/rules/ksatisfiability_directedtwocommodityintegralflow.rs @@ -40,9 +40,17 @@ fn solve_target_via_ilp( problem: &crate::models::graph::DirectedTwoCommodityIntegralFlow, ) -> Option> { let reduction = ReduceTo::>::reduce_to(problem).expect("reduction should succeed"); - let ilp_solution = ILPSolver::new().solve(reduction.target_problem()).ok()?; + let ilp_solution = match ILPSolver::new().solve(reduction.target_problem()) { + Ok(solution) => solution, + Err(crate::solvers::ILPSolveError::Infeasible) => return None, + Err(error) => panic!("ILP execution failed: {error}"), + }; let extracted = reduction.extract_solution(&ilp_solution).unwrap(); - problem.evaluate(&extracted).unwrap().0.then_some(extracted) + assert!( + problem.evaluate(&extracted).unwrap().0, + "decoded flow must be feasible" + ); + Some(extracted) } #[test] diff --git a/src/unit_tests/rules/ksatisfiability_feasibleregisterassignment.rs b/src/unit_tests/rules/ksatisfiability_feasibleregisterassignment.rs index ef7624f3d..526812d9e 100644 --- a/src/unit_tests/rules/ksatisfiability_feasibleregisterassignment.rs +++ b/src/unit_tests/rules/ksatisfiability_feasibleregisterassignment.rs @@ -170,8 +170,9 @@ fn test_ksatisfiability_to_feasible_register_assignment_unsatisfiable_instance() let fra_to_ilp = ReduceTo::>::reduce_to(reduction.target_problem()) .expect("reduction should succeed"); - assert!( - ILPSolver::new().solve(fra_to_ilp.target_problem()).is_err(), + assert_eq!( + ILPSolver::new().solve(fra_to_ilp.target_problem()), + Err(crate::solvers::ILPSolveError::Infeasible), "an unsatisfiable source formula should yield an infeasible FRA instance" ); } @@ -193,7 +194,9 @@ fn native_empty_clause_is_infeasible_and_empty_conjunction_is_feasible() { vec![2, 1, 0], ] { assert!(!reduction.target_problem().evaluate(&config).unwrap().0); - assert!(reduction.extract_solution(&config).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &config), Ok(value) if { value.is_valid() }) + ); } let source = KSatisfiability::::new(num_vars, vec![]); let reduction = ReduceTo::::reduce_to(&source).unwrap(); @@ -282,7 +285,9 @@ fn invalid_realizations_are_rejected() { let reduction = ReduceTo::::reduce_to(&source).unwrap(); let n = reduction.target_problem().num_vertices(); for config in [vec![], vec![n; n], vec![0; n], (0..n).collect()] { - assert!(reduction.extract_solution(&config).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &config), Ok(value) if { value.is_valid() }) + ); } } diff --git a/src/unit_tests/rules/ksatisfiability_kclique.rs b/src/unit_tests/rules/ksatisfiability_kclique.rs index 0e0bf789b..157f25686 100644 --- a/src/unit_tests/rules/ksatisfiability_kclique.rs +++ b/src/unit_tests/rules/ksatisfiability_kclique.rs @@ -147,7 +147,9 @@ fn test_kclique_all_two_clause_formulas_and_target_selections() { .0 ); } else { - assert!(reduction.extract_solution(&witness).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &witness), Ok(value) if { value.is_valid() }) + ); } } assert_eq!(source_yes, target_yes); @@ -169,7 +171,9 @@ fn test_kclique_rejects_malformed_or_non_clique_selections() { vec![true, true, false, false, true], vec![true, false, true, false, true], ] { - assert!(reduction.extract_solution(&bad).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &bad), Ok(value) if { value.is_valid() }) + ); } } diff --git a/src/unit_tests/rules/ksatisfiability_kernel.rs b/src/unit_tests/rules/ksatisfiability_kernel.rs index 200045a36..902b13f78 100644 --- a/src/unit_tests/rules/ksatisfiability_kernel.rs +++ b/src/unit_tests/rules/ksatisfiability_kernel.rs @@ -174,7 +174,9 @@ fn test_ksatisfiability_to_kernel_rejects_non_kernel() { let source = KSatisfiability::::new(1, vec![CNFClause::new(vec![1, 1, 1])]); let reduction = ReduceTo::::reduce_to(&source).unwrap(); for config in [vec![], vec![false; 5], vec![true; 5], vec![false; 6]] { - assert!(reduction.extract_solution(&config).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &config), Ok(value) if { value.is_valid() }) + ); } } diff --git a/src/unit_tests/rules/ksatisfiability_monochromatictriangle.rs b/src/unit_tests/rules/ksatisfiability_monochromatictriangle.rs index eaa8484bd..9256fc0bd 100644 --- a/src/unit_tests/rules/ksatisfiability_monochromatictriangle.rs +++ b/src/unit_tests/rules/ksatisfiability_monochromatictriangle.rs @@ -99,7 +99,9 @@ fn test_ksatisfiability_to_monochromatic_triangle_closed_loop() { .unwrap() .0 ); - assert!(reduction.extract_solution(&vec![]).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![]), Ok(value) if { value.is_valid() }) + ); } #[test] diff --git a/src/unit_tests/rules/ksatisfiability_oneinthreesatisfiability.rs b/src/unit_tests/rules/ksatisfiability_oneinthreesatisfiability.rs index 5792a4456..69b2b5e8d 100644 --- a/src/unit_tests/rules/ksatisfiability_oneinthreesatisfiability.rs +++ b/src/unit_tests/rules/ksatisfiability_oneinthreesatisfiability.rs @@ -181,7 +181,9 @@ fn test_oneinthree_rejects_infeasible_target_assignments() { let source = KSatisfiability::::new(1, vec![CNFClause::new(vec![1; 3])]); let reduction = ReduceTo::::reduce_to(&source).unwrap(); for config in [vec![], vec![false; 9], vec![true; 9], vec![false; 10]] { - assert!(reduction.extract_solution(&config).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &config), Ok(value) if { value.is_valid() }) + ); } } diff --git a/src/unit_tests/rules/ksatisfiability_preemptivescheduling.rs b/src/unit_tests/rules/ksatisfiability_preemptivescheduling.rs index 375509297..eb94c7895 100644 --- a/src/unit_tests/rules/ksatisfiability_preemptivescheduling.rs +++ b/src/unit_tests/rules/ksatisfiability_preemptivescheduling.rs @@ -32,7 +32,11 @@ fn solve_threshold_schedule_via_ilp( target.precedences().to_vec(), ); let pcs_to_ilp = ReduceTo::>::reduce_to(&pcs).expect("reduction should succeed"); - let ilp_solution = ILPSolver::new().solve(pcs_to_ilp.target_problem()).ok()?; + let ilp_solution = match ILPSolver::new().solve(pcs_to_ilp.target_problem()) { + Ok(solution) => solution, + Err(crate::solvers::ILPSolveError::Infeasible) => return None, + Err(error) => panic!("ILP execution failed: {error}"), + }; let slot_assignment = pcs_to_ilp.extract_solution(&ilp_solution).unwrap(); let mut config = vec![vec![false; target.d_max()]; target.num_tasks()]; diff --git a/src/unit_tests/rules/ksatisfiability_quadraticcongruences.rs b/src/unit_tests/rules/ksatisfiability_quadraticcongruences.rs index 098a88b18..c1e7a6287 100644 --- a/src/unit_tests/rules/ksatisfiability_quadraticcongruences.rs +++ b/src/unit_tests/rules/ksatisfiability_quadraticcongruences.rs @@ -69,8 +69,6 @@ fn test_native_clauses_and_arbitrary_crt_signs() { ); } recovered.insert(extracted); - } else { - assert!(extracted.is_err()); } } // Enumerate only appearing variables; unused coordinates are free. @@ -177,7 +175,9 @@ fn test_rejects_infeasible_and_out_of_bound_integers() { reduction.target.c() + 1u32, ] { assert_eq!(reduction.target.evaluate(&witness).unwrap(), Or(false)); - assert!(reduction.extract_solution(&witness).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &witness), Ok(value) if { value.is_valid() }) + ); } } diff --git a/src/unit_tests/rules/ksatisfiability_qubo.rs b/src/unit_tests/rules/ksatisfiability_qubo.rs index 22aad0944..1b91ea920 100644 --- a/src/unit_tests/rules/ksatisfiability_qubo.rs +++ b/src/unit_tests/rules/ksatisfiability_qubo.rs @@ -102,7 +102,7 @@ fn test_ksatisfiability_to_qubo_structure() { let qubo = reduction.target_problem(); // QUBO should have at least the original variables - assert!(qubo.num_variables() >= ksat.num_vars()); + assert!(qubo.num_variables().unwrap() >= ksat.num_vars()); } #[test] @@ -124,7 +124,7 @@ fn test_k3satisfiability_to_qubo_closed_loop() { let qubo = reduction.target_problem(); // QUBO should have 5 + 7 = 12 variables - assert_eq!(qubo.num_variables(), 12); + assert_eq!(qubo.num_variables().unwrap(), 12); let solver = BruteForce::new(); let qubo_solutions = solver.find_all_witnesses(qubo).unwrap(); @@ -146,7 +146,7 @@ fn test_k3satisfiability_to_qubo_single_clause() { let qubo = reduction.target_problem(); // 3 vars + 1 auxiliary = 4 total - assert_eq!(qubo.num_variables(), 4); + assert_eq!(qubo.num_variables().unwrap(), 4); let solver = BruteForce::new(); let qubo_solutions = solver.find_all_witnesses(qubo).unwrap(); @@ -235,7 +235,7 @@ fn test_sat_qubo_all_short_clauses_and_raw_targets() { let decoded = reduction.extract_solution(&witness).unwrap(); assert!(source.evaluate(&decoded).unwrap().0); } else { - assert!(reduction.extract_solution(&witness).is_err()); + assert!(!matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &witness), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&reduction, value); value.is_valid() })); } minimum = minimum.min(energy); } @@ -250,10 +250,8 @@ fn test_sat_qubo_all_short_clauses_and_raw_targets() { AggregateReductionResult::extract_value(&reduction, Min(None)), Or(false) ); - assert!(reduction.extract_solution(&vec![]).is_err()); - assert!(reduction - .extract_solution(&vec![false; target.num_vars() + 1]) - .is_err()); + assert!(!matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![]), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&reduction, value); value.is_valid() })); + assert!(!matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![false; target.num_vars() + 1]), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&reduction, value); value.is_valid() })); } } for n in [0, 3] { @@ -272,29 +270,15 @@ fn test_sat_qubo_all_short_clauses_and_raw_targets() { #[test] fn test_sat_qubo_checked_numeric_boundaries() { - let mut matrix = vec![vec![i64::MAX]]; + let mut matrix = vec![std::collections::BTreeMap::from([(0, i64::MAX)])]; assert!(add_coefficient(&mut matrix, 0, 0, 1).is_err()); - let mut matrix = vec![vec![i64::MIN]]; + let mut matrix = vec![std::collections::BTreeMap::from([(0, i64::MIN)])]; assert!(add_coefficient(&mut matrix, 0, 0, -1).is_err()); assert!(build_qubo_matrix(usize::MAX, &[], 1).is_err()); - // This variable count is legal for the source on both 32- and 64-bit hosts, - // but its dense target cannot have an addressable number of entries. - let n = usize::MAX / 2; - let k2 = KSatisfiability::::new(n, vec![]); - let k3 = KSatisfiability::::new(n, vec![]); - assert!(matches!( - ReduceTo::>::reduce_to(&k2), - Err(crate::rules::ReductionError::IntegerOverflow { .. }) - )); - assert!(matches!( - ReduceTo::>::reduce_to(&k3), - Err(crate::rules::ReductionError::IntegerOverflow { .. }) - )); } #[test] fn test_sat_qubo_registered_aggregate_threshold() { - use crate::types::Or; macro_rules! check { ($k:ty) => { for (clauses, expected) in [(vec![vec![1]], true), (vec![vec![1], vec![-1]], false)] { @@ -315,14 +299,10 @@ fn test_sat_qubo_registered_aggregate_threshold() { && (e.target_variant_fn)() == QUBO::::variant() }) .unwrap(); - let aggregate = (edge.reduce_aggregate_fn.unwrap())(&source).unwrap(); + let step = (edge.reduce_fn.unwrap())(&source).unwrap(); assert_eq!( - *aggregate - .extract_value_from_solution_dyn(&witness) - .unwrap() - .downcast::() - .unwrap(), - Or(expected) + step.interpret_optimum.as_ref().unwrap()(&witness).unwrap(), + expected ); } }; diff --git a/src/unit_tests/rules/ksatisfiability_registersufficiency.rs b/src/unit_tests/rules/ksatisfiability_registersufficiency.rs index 301cf7af3..14c94748f 100644 --- a/src/unit_tests/rules/ksatisfiability_registersufficiency.rs +++ b/src/unit_tests/rules/ksatisfiability_registersufficiency.rs @@ -92,7 +92,9 @@ fn test_ksatisfiability_to_register_sufficiency_rejects_invalid_snapshot_order() let positions = positions_from_order(&order, target.num_vertices()); assert_eq!(target.evaluate(&positions).unwrap(), Or(false)); - assert!(reduction.extract_solution(&positions).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &positions), Ok(value) if { value.is_valid() }) + ); } #[test] @@ -163,7 +165,9 @@ fn test_ksatisfiability_to_registersufficiency_closed_loop_boundaries() { assert_eq!(extracted, vec![false; declared]); assert_eq!(source.evaluate(&extracted).unwrap(), Or(true)); } else { - assert!(reduction.extract_solution(&vec![0]).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![0]), Ok(value) if { value.is_valid() }) + ); } } } @@ -217,7 +221,9 @@ fn test_short_repeated_and_tautological_clauses() { assert_eq!(decoded[i], original[i]); } } else { - assert!(reduction.extract_solution(&positions).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &positions), Ok(value) if { value.is_valid() }) + ); } } } diff --git a/src/unit_tests/rules/ksatisfiability_simultaneousincongruences.rs b/src/unit_tests/rules/ksatisfiability_simultaneousincongruences.rs index 6646e6d14..f36bb8157 100644 --- a/src/unit_tests/rules/ksatisfiability_simultaneousincongruences.rs +++ b/src/unit_tests/rules/ksatisfiability_simultaneousincongruences.rs @@ -17,7 +17,7 @@ fn test_ksatisfiability_to_simultaneous_incongruences_closed_loop() { .expect("reduction should succeed"); let target = reduction.target_problem(); - assert_eq!(target.lcm_moduli(), 15); + assert_eq!(target.lcm_moduli().unwrap(), 15); assert_eq!(target.num_pairs(), 6); let solver = BruteForce::new(); diff --git a/src/unit_tests/rules/ksatisfiability_timetabledesign.rs b/src/unit_tests/rules/ksatisfiability_timetabledesign.rs index e9c62c153..49d711323 100644 --- a/src/unit_tests/rules/ksatisfiability_timetabledesign.rs +++ b/src/unit_tests/rules/ksatisfiability_timetabledesign.rs @@ -118,10 +118,9 @@ fn test_ksatisfiability_to_timetabledesign_unsatisfiable() { let target_reduction = ReduceTo::>::reduce_to(reduction.target_problem()) .expect("timetable reduction should succeed"); - assert!( - ILPSolver::new() - .solve(target_reduction.target_problem()) - .is_err(), + assert_eq!( + ILPSolver::new().solve(target_reduction.target_problem()), + Err(crate::solvers::ILPSolveError::Infeasible), "unsatisfiable 3SAT instance should produce an infeasible timetable" ); } diff --git a/src/unit_tests/rules/lengthboundeddisjointpaths_ilp.rs b/src/unit_tests/rules/lengthboundeddisjointpaths_ilp.rs index c9ca738b9..8455aaa37 100644 --- a/src/unit_tests/rules/lengthboundeddisjointpaths_ilp.rs +++ b/src/unit_tests/rules/lengthboundeddisjointpaths_ilp.rs @@ -113,6 +113,8 @@ fn test_lengthboundeddisjointpaths_to_ilp_rejects_invalid_target_solutions() { let source = LengthBoundedDisjointPaths::new(SimpleGraph::new(2, vec![(0, 1)]), 0, 1, 1); let reduction = ReduceTo::>::reduce_to(&source).unwrap(); for solution in [vec![], vec![2, 0, 1], vec![0, 0, 1], vec![1, 0, 0]] { - assert!(reduction.extract_solution(&solution).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(reduction.target_problem(), &solution), Ok(value) if value.is_valid()) + ); } } diff --git a/src/unit_tests/rules/maximumindependentset_maximumsetpacking.rs b/src/unit_tests/rules/maximumindependentset_maximumsetpacking.rs index 202b4819b..9388cc29a 100644 --- a/src/unit_tests/rules/maximumindependentset_maximumsetpacking.rs +++ b/src/unit_tests/rules/maximumindependentset_maximumsetpacking.rs @@ -1,8 +1,8 @@ use super::*; +include!("../jl_helpers.rs"); use crate::rules::test_helpers::assert_optimization_round_trip_from_optimization_target; use crate::solvers::BruteForce; use crate::types::One; -include!("../jl_helpers.rs"); #[test] fn test_maximumindependentset_to_maximumsetpacking_closed_loop() { diff --git a/src/unit_tests/rules/maximumindependentset_qubo.rs b/src/unit_tests/rules/maximumindependentset_qubo.rs index 99db973b3..ee99c7302 100644 --- a/src/unit_tests/rules/maximumindependentset_qubo.rs +++ b/src/unit_tests/rules/maximumindependentset_qubo.rs @@ -42,7 +42,7 @@ fn test_maximumindependentset_to_qubo_via_path_closed_loop() { path.type_names(), vec!["MaximumIndependentSet", "MaximumSetPacking", "QUBO"] ); - assert_eq!(qubo.num_variables(), 4); + assert_eq!(qubo.num_variables().unwrap(), 4); let solver = BruteForce::new(); let qubo_solutions = solver.find_all_witnesses(qubo).unwrap(); @@ -77,7 +77,7 @@ fn test_maximumindependentset_to_qubo_via_path_empty_graph() { let (_, chain) = reduce_mis_to_qubo(&problem); let qubo: &QUBO = chain.target_problem(); - assert_eq!(qubo.num_variables(), 3); + assert_eq!(qubo.num_variables().unwrap(), 3); let solver = BruteForce::new(); let qubo_solution = solver diff --git a/src/unit_tests/rules/maximumleafspanningtree_ilp.rs b/src/unit_tests/rules/maximumleafspanningtree_ilp.rs index be3068b63..91065c778 100644 --- a/src/unit_tests/rules/maximumleafspanningtree_ilp.rs +++ b/src/unit_tests/rules/maximumleafspanningtree_ilp.rs @@ -94,6 +94,13 @@ fn test_solution_extraction_reads_edge_selector_prefix() { target_solution[0] = 1; // edge (0,1) target_solution[1] = 1; // edge (1,2) target_solution[2] = 1; // edge (2,3) + target_solution[4] = 1; // vertex 0 is a leaf + target_solution[7] = 1; // vertex 3 is a leaf + + // Root 0 supplies one unit to each other vertex along the path. + target_solution[8] = 3; // 0 -> 1 + target_solution[10] = 2; // 1 -> 2 + target_solution[12] = 1; // 2 -> 3 assert_eq!( reduction.extract_solution(&target_solution).unwrap(), diff --git a/src/unit_tests/rules/maximummatching_maximumsetpacking.rs b/src/unit_tests/rules/maximummatching_maximumsetpacking.rs index e2a96c257..c3fb58b8f 100644 --- a/src/unit_tests/rules/maximummatching_maximumsetpacking.rs +++ b/src/unit_tests/rules/maximummatching_maximumsetpacking.rs @@ -1,10 +1,10 @@ use super::*; +include!("../jl_helpers.rs"); use crate::rules::test_helpers::assert_optimization_round_trip_from_optimization_target; use crate::solvers::BruteForce; use crate::topology::SimpleGraph; use crate::traits::Problem; use crate::types::Max; -include!("../jl_helpers.rs"); #[test] fn test_maximummatching_to_maximumsetpacking_closed_loop() { diff --git a/src/unit_tests/rules/maximumsetpacking_ilp.rs b/src/unit_tests/rules/maximumsetpacking_ilp.rs index 8a7e838e8..d0e6b68e7 100644 --- a/src/unit_tests/rules/maximumsetpacking_ilp.rs +++ b/src/unit_tests/rules/maximumsetpacking_ilp.rs @@ -142,3 +142,63 @@ 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 extraction_maps_feasible_witnesses_through_typed_and_dynamic_paths() { + use crate::rules::{DynReductionResult, ReductionGraph}; + use serde_json::json; + + let source = MaximumSetPacking::with_weights(vec![vec![0]], vec![1i64]).unwrap(); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + let graph = ReductionGraph::new(); + let path = graph + .find_all_paths( + MaximumSetPacking::::NAME, + &ReductionGraph::variant_to_map(&MaximumSetPacking::::variant()), + ILP::::NAME, + &ReductionGraph::variant_to_map(&ILP::::variant()), + ) + .into_iter() + .find(|path| path.len() == 1) + .unwrap(); + let chain = graph.reduce_along_path(&path, &source).unwrap().unwrap(); + assert_eq!(reduction.extract_solution(&vec![1]).unwrap(), vec![true]); + let extracted = reduction.extract_solution_dyn(&vec![1i64]).unwrap(); + assert_eq!(*extracted.downcast::>().unwrap(), vec![true]); + // An unselected set is feasible even though it is not optimal. + assert_eq!(reduction.extract_solution(&vec![0]).unwrap(), vec![false]); + assert_eq!( + chain.extract_solution_json(json!([0])).unwrap(), + json!([false]) + ); +} + +#[test] +fn parameter_upper_bounds_cover_single_and_shared_elements() { + use crate::parameters::ParameterRelation; + use crate::rules::registry::ReductionEntry; + let entry = inventory::iter:: + .into_iter() + .find(|entry| { + entry.source_name == MaximumSetPacking::::NAME + && entry.target_name == ILP::::NAME + && (entry.source_variant_fn)() == MaximumSetPacking::::variant() + && (entry.target_variant_fn)() == ILP::::variant() + }) + .unwrap(); + let contract = entry.parameter_contract().unwrap(); + let transform = contract.transform().unwrap(); + assert_eq!(transform.relation(), ParameterRelation::UpperBound); + for (sets, constraints) in [ + (vec![vec![0]], 0), + (vec![vec![0, 1], vec![1, 2], vec![2, 3]], 2), + ] { + let source = MaximumSetPacking::::new(sets); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + let actual = reduction.target_problem().parameters(); + let declared = transform.evaluate(&source.parameters()).unwrap(); + assert_eq!(actual.get("num_constraints"), Some(constraints)); + assert_eq!(actual.get("num_vars"), declared.get("num_vars")); + assert!(actual.get("num_constraints").unwrap() <= declared.get("num_constraints").unwrap()); + } +} diff --git a/src/unit_tests/rules/maximumsetpacking_qubo.rs b/src/unit_tests/rules/maximumsetpacking_qubo.rs index c523def04..503e7c213 100644 --- a/src/unit_tests/rules/maximumsetpacking_qubo.rs +++ b/src/unit_tests/rules/maximumsetpacking_qubo.rs @@ -64,7 +64,7 @@ fn test_setpacking_to_qubo_structure() { let qubo = reduction.target_problem(); // QUBO should have same number of variables as sets - assert_eq!(qubo.num_variables(), 3); + assert_eq!(qubo.num_variables().unwrap(), 3); } #[test] @@ -106,7 +106,7 @@ fn test_setpacking_to_qubo_penalty_strict_at_large_weights() { let source = MaximumSetPacking::with_weights(vec![vec![0], vec![0]], vec![weight, weight]).unwrap(); let reduction = ReduceTo::>::reduce_to(&source).unwrap(); - assert!(reduction.target_problem().matrix()[0][1] > weight); + assert!(reduction.target_problem().matrix()[[0, 1]] > weight); crate::rules::test_helpers::assert_optimization_round_trip_from_optimization_target( &source, &reduction, diff --git a/src/unit_tests/rules/minimumcapacitatedspanningtree_ilp.rs b/src/unit_tests/rules/minimumcapacitatedspanningtree_ilp.rs index e3725ece6..07121cda3 100644 --- a/src/unit_tests/rules/minimumcapacitatedspanningtree_ilp.rs +++ b/src/unit_tests/rules/minimumcapacitatedspanningtree_ilp.rs @@ -102,6 +102,13 @@ fn test_solution_extraction_reads_edge_selector_prefix() { target_solution[1] = 1; // edge (0,2) target_solution[3] = 1; // edge (1,3) + // Requirement and connectivity flows run toward root 0. + for offset in [5, 15] { + target_solution[offset + 1] = 2; // 1 -> 0 + target_solution[offset + 3] = 1; // 2 -> 0 + target_solution[offset + 7] = 1; // 3 -> 1 + } + assert_eq!( reduction.extract_solution(&target_solution).unwrap(), vec![true, true, false, true, false] @@ -167,5 +174,8 @@ fn test_zero_requirement_vertex_still_must_be_connected() { ); let reduction: ReductionMinimumCapacitatedSpanningTreeToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); - assert!(ILPSolver::new().solve(reduction.target_problem()).is_err()); + assert_eq!( + ILPSolver::new().solve(reduction.target_problem()), + Err(crate::solvers::ILPSolveError::Infeasible) + ); } diff --git a/src/unit_tests/rules/minimumcostmaximumflow_minimumcostcirculation.rs b/src/unit_tests/rules/minimumcostmaximumflow_minimumcostcirculation.rs index 8bdc090ed..96f287f3a 100644 --- a/src/unit_tests/rules/minimumcostmaximumflow_minimumcostcirculation.rs +++ b/src/unit_tests/rules/minimumcostmaximumflow_minimumcostcirculation.rs @@ -239,13 +239,9 @@ fn test_minimumcostmaximumflow_to_minimumcostcirculation_extract_solution_length let source = canonical_source(); let reduction = ReduceTo::::reduce_to(&source).expect("reduction should succeed"); - // Provide a dummy target config of the right length; extract_solution - // must truncate to num_original_arcs. + // A value-3 flow closes through the added sink-to-source return arc. let m = source.num_arcs(); - let mut padded = vec![0_usize; m + 1]; - for (i, v) in padded.iter_mut().enumerate().take(m) { - *v = i % 2; - } + let padded = vec![2_usize, 1, 1, 1, 2, 3]; let extracted = reduction.extract_solution(&padded).unwrap(); assert_eq!(extracted.len(), m); assert_eq!(extracted, padded[..m].to_vec()); diff --git a/src/unit_tests/rules/minimumcoveringbycliques_minimumintersectiongraphbasis.rs b/src/unit_tests/rules/minimumcoveringbycliques_minimumintersectiongraphbasis.rs index 0737f7f5d..357591ad0 100644 --- a/src/unit_tests/rules/minimumcoveringbycliques_minimumintersectiongraphbasis.rs +++ b/src/unit_tests/rules/minimumcoveringbycliques_minimumintersectiongraphbasis.rs @@ -60,14 +60,6 @@ fn test_minimumcoveringbycliques_to_minimumintersectiongraphbasis_invalid_target target.evaluate(&invalid_target_solution).unwrap(), Min(None) ); - - let error = reduction - .extract_solution(&invalid_target_solution) - .unwrap_err(); - assert_eq!( - error.to_string(), - "target configuration is not a valid intersection graph basis" - ); } #[test] diff --git a/src/unit_tests/rules/minimumdiscreteplanarinversekinematics_qubo.rs b/src/unit_tests/rules/minimumdiscreteplanarinversekinematics_qubo.rs index fe188e749..943a4e257 100644 --- a/src/unit_tests/rules/minimumdiscreteplanarinversekinematics_qubo.rs +++ b/src/unit_tests/rules/minimumdiscreteplanarinversekinematics_qubo.rs @@ -96,8 +96,14 @@ fn test_minimumdiscreteplanarinversekinematics_to_qubo_empty_allowed_pairs() { assert!(solver.solve(&source).unwrap().is_none()); assert!(!qubo_solutions.is_empty(), "QUBO solver found no solutions"); for target_solution in qubo_solutions { - let extracted = reduction.extract_solution(&target_solution).unwrap(); - assert_eq!(source.evaluate(&extracted).unwrap(), Min(None)); + let value = reduction + .target_problem() + .evaluate(&target_solution) + .unwrap(); + assert_eq!( + crate::rules::AggregateReductionResult::extract_value(&reduction, value), + Min(None) + ); } } @@ -115,7 +121,7 @@ fn test_minimumdiscreteplanarinversekinematics_to_qubo_canonical_example_spec() "MinimumDiscretePlanarInverseKinematics" ); assert_eq!(example.target.problem, "QUBO"); - assert_eq!(example.target.instance["num_vars"], 4); + assert_eq!(example.target.instance["matrix"]["nrows"], 4); assert_eq!( example.solutions[0].source_config, serde_json::json!([0, 1]) @@ -125,3 +131,89 @@ fn test_minimumdiscreteplanarinversekinematics_to_qubo_canonical_example_spec() serde_json::json!([true, false, false, true]) ); } + +#[test] +fn optimum_energy_recovers_distance_and_infeasibility() { + for source in [ + worked_example(), + MinimumDiscretePlanarInverseKinematics::new( + vec![1.0, 1.0, 1.0], + (0.0, 0.0), + vec![vec![0.0, PI]; 3], + vec![vec![(0, 0)], vec![(1, 0)]], + ) + .unwrap(), + ] { + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + let entry = inventory::iter:: + .into_iter() + .find(|entry| { + entry.source_name == "MinimumDiscretePlanarInverseKinematics" + && entry.target_name == "QUBO" + }) + .unwrap(); + let chain = + crate::rules::ReductionChain::execute(&source, &[entry.reduce_fn.unwrap()]).unwrap(); + + let solver = BruteForce::new(); + let expected = solver + .solve(&source) + .unwrap() + .map(|solution| source.evaluate(&solution).unwrap().0.unwrap()); + for solution in solver + .find_all_witnesses(reduction.target_problem()) + .unwrap() + { + let completed = crate::solvers::complete_reduction( + &source, + &chain, + &crate::solvers::SolveOutcome::Optimal { + solution: serde_json::to_value(&solution).unwrap(), + evaluation: String::new(), + }, + ) + .unwrap(); + assert_eq!( + matches!(completed, crate::solvers::SolveOutcome::Optimal { .. }), + expected.is_some() + ); + let recovered = crate::rules::AggregateReductionResult::extract_value( + &reduction, + reduction.target_problem().evaluate(&solution).unwrap(), + ) + .0; + match (expected, recovered) { + (Some(expected), Some(actual)) => { + assert!((actual - expected).abs() < EPS); + assert_eq!( + source + .evaluate(&reduction.extract_solution(&solution).unwrap()) + .unwrap(), + Min(Some(expected)) + ); + } + (None, None) => {} + other => panic!("source and recovered outcomes disagree: {other:?}"), + } + } + assert_eq!( + crate::rules::AggregateReductionResult::extract_value(&reduction, Min(None)), + Min(None) + ); + } +} + +#[test] +fn nonfinite_energy_relation_is_a_construction_error() { + let source = MinimumDiscretePlanarInverseKinematics::new( + vec![1e200], + (0.0, 0.0), + vec![vec![0.0]], + vec![], + ) + .unwrap(); + assert!(matches!( + ReduceTo::>::reduce_to(&source), + Err(crate::rules::ReductionError::NonFiniteResult { .. }) + )); +} diff --git a/src/unit_tests/rules/minimumedgecostflow_ilp.rs b/src/unit_tests/rules/minimumedgecostflow_ilp.rs index 683d75059..fa9a3c65f 100644 --- a/src/unit_tests/rules/minimumedgecostflow_ilp.rs +++ b/src/unit_tests/rules/minimumedgecostflow_ilp.rs @@ -109,8 +109,9 @@ fn test_minimumedgecostflow_to_ilp_infeasible() { let problem = infeasible_instance(); let reduction: ReductionMECFToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); - assert!( - ILPSolver::new().solve(reduction.target_problem()).is_err(), + assert_eq!( + ILPSolver::new().solve(reduction.target_problem()), + Err(crate::solvers::ILPSolveError::Infeasible), "infeasible instance should produce infeasible ILP" ); } diff --git a/src/unit_tests/rules/minimumfaultdetectiontestset_ilp.rs b/src/unit_tests/rules/minimumfaultdetectiontestset_ilp.rs index f2400cdfc..7726cafac 100644 --- a/src/unit_tests/rules/minimumfaultdetectiontestset_ilp.rs +++ b/src/unit_tests/rules/minimumfaultdetectiontestset_ilp.rs @@ -83,7 +83,10 @@ fn test_reduction_is_infeasible_when_an_internal_vertex_has_no_covering_pair() { assert_eq!(problem.evaluate(&vec![vec![false]]).unwrap(), Min(None)); assert_eq!(problem.evaluate(&vec![vec![true]]).unwrap(), Min(None)); - assert!(ILPSolver::new().solve(ilp).is_err()); + assert_eq!( + ILPSolver::new().solve(ilp), + Err(crate::solvers::ILPSolveError::Infeasible) + ); } #[test] diff --git a/src/unit_tests/rules/minimumfeedbackvertexset_minimumcodegenerationunlimitedregisters.rs b/src/unit_tests/rules/minimumfeedbackvertexset_minimumcodegenerationunlimitedregisters.rs index 3712e5c42..8780a8d97 100644 --- a/src/unit_tests/rules/minimumfeedbackvertexset_minimumcodegenerationunlimitedregisters.rs +++ b/src/unit_tests/rules/minimumfeedbackvertexset_minimumcodegenerationunlimitedregisters.rs @@ -70,7 +70,9 @@ fn test_codegen_empty_graph_and_invalid_orders() { let reduction = ReduceTo::::reduce_to(&source).unwrap(); for config in [vec![], vec![9; 6], vec![0; 6], vec![1, 0, 2, 3, 4, 5]] { - assert!(reduction.extract_solution(&config).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &config), Ok(value) if { value.is_valid() }) + ); } } diff --git a/src/unit_tests/rules/minimummatrixcover_ilp.rs b/src/unit_tests/rules/minimummatrixcover_ilp.rs index da371feef..950ec637b 100644 --- a/src/unit_tests/rules/minimummatrixcover_ilp.rs +++ b/src/unit_tests/rules/minimummatrixcover_ilp.rs @@ -13,7 +13,8 @@ fn test_minimum_matrix_cover_to_ilp_closed_loop() { vec![3, 0, 0, 2], vec![1, 0, 0, 4], vec![0, 2, 4, 0], - ]); + ]) + .unwrap(); let reduction = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); assert_bf_vs_ilp(&problem, &reduction); @@ -28,7 +29,7 @@ fn test_minimum_matrix_cover_to_ilp_closed_loop() { #[test] fn test_minimum_matrix_cover_to_ilp_structure() { - let problem = MinimumMatrixCover::new(vec![vec![0, 3], vec![2, 0]]); + let problem = MinimumMatrixCover::new(vec![vec![0, 3], vec![2, 0]]).unwrap(); let reduction = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -54,7 +55,8 @@ fn test_minimum_matrix_cover_to_ilp_bf_vs_ilp() { vec![3, 0, 0, 2], vec![1, 0, 0, 4], vec![0, 2, 4, 0], - ]); + ]) + .unwrap(); let reduction = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let bf_value_solution = BruteForce::new().solve(&problem).unwrap().unwrap(); @@ -72,7 +74,7 @@ fn test_minimum_matrix_cover_to_ilp_bf_vs_ilp() { #[test] fn test_minimum_matrix_cover_to_ilp_2x2() { - let problem = MinimumMatrixCover::new(vec![vec![0, 3], vec![2, 0]]); + let problem = MinimumMatrixCover::new(vec![vec![0, 3], vec![2, 0]]).unwrap(); let reduction = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp_solution = ILPSolver::new() @@ -86,7 +88,7 @@ fn test_minimum_matrix_cover_to_ilp_2x2() { #[test] fn test_minimum_matrix_cover_to_ilp_1x1() { - let problem = MinimumMatrixCover::new(vec![vec![5]]); + let problem = MinimumMatrixCover::new(vec![vec![5]]).unwrap(); let reduction = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); @@ -108,7 +110,8 @@ fn test_minimum_matrix_cover_to_ilp_1x1() { fn test_minimum_matrix_cover_to_ilp_diagonal_matrix() { // Diagonal matrix: all off-diagonal entries are 0 // Value is always Σ a_ii (constant), since f(i)²=1 - let problem = MinimumMatrixCover::new(vec![vec![2, 0, 0], vec![0, 3, 0], vec![0, 0, 1]]); + let problem = + MinimumMatrixCover::new(vec![vec![2, 0, 0], vec![0, 3, 0], vec![0, 0, 1]]).unwrap(); let reduction = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp_solution = ILPSolver::new() @@ -122,7 +125,7 @@ fn test_minimum_matrix_cover_to_ilp_diagonal_matrix() { #[test] fn test_minimum_matrix_cover_to_ilp_asymmetric() { // Non-symmetric matrix - let problem = MinimumMatrixCover::new(vec![vec![0, 5], vec![1, 0]]); + let problem = MinimumMatrixCover::new(vec![vec![0, 5], vec![1, 0]]).unwrap(); let reduction = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let bf_value_solution = BruteForce::new().solve(&problem).unwrap().unwrap(); diff --git a/src/unit_tests/rules/minimummultiwaycut_qubo.rs b/src/unit_tests/rules/minimummultiwaycut_qubo.rs index b300fa285..d7f76eb19 100644 --- a/src/unit_tests/rules/minimummultiwaycut_qubo.rs +++ b/src/unit_tests/rules/minimummultiwaycut_qubo.rs @@ -4,6 +4,45 @@ use crate::solvers::BruteForceProblem as _; use crate::traits::Problem; use crate::types::Min; +#[test] +fn signed_cut_weights_preserve_every_target_optimum() { + let solver = BruteForce::new(); + for weights in [ + vec![-1, -1, -1], + vec![-3, 2, 1], + vec![0, -1, 2], + vec![i64::MIN, 0, 0], + ] { + let source = MinimumMultiwayCut::new( + SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)]), + vec![0, 1], + weights, + ); + let optimum = (0..8) + .filter_map(|bits| { + source + .evaluate(&(0..3).map(|i| bits & (1 << i) != 0).collect()) + .unwrap() + .0 + }) + .min() + .unwrap(); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + let solutions = solver + .find_all_witnesses(reduction.target_problem()) + .unwrap(); + assert!(!solutions.is_empty()); + for solution in solutions { + assert_eq!( + source + .evaluate(&reduction.extract_solution(&solution).unwrap()) + .unwrap(), + Min(Some(optimum)) + ); + } + } +} + #[test] fn test_minimummultiwaycut_to_qubo_closed_loop() { // 5 vertices, terminals {0,2,4}, 6 edges with weights [2,3,1,2,4,5] @@ -56,7 +95,7 @@ fn test_minimummultiwaycut_to_qubo_sizes() { let source = MinimumMultiwayCut::new(graph, vec![0, 2, 4], vec![2, 3, 1, 2, 4, 5]); let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); - assert_eq!(reduction.target_problem().num_variables(), 15); + assert_eq!(reduction.target_problem().num_variables().unwrap(), 15); } #[test] diff --git a/src/unit_tests/rules/minimumvertexcover_comparativecontainment.rs b/src/unit_tests/rules/minimumvertexcover_comparativecontainment.rs index 4786f0a13..14c27cbc6 100644 --- a/src/unit_tests/rules/minimumvertexcover_comparativecontainment.rs +++ b/src/unit_tests/rules/minimumvertexcover_comparativecontainment.rs @@ -87,7 +87,9 @@ fn test_signed_containment_all_small_graphs_and_witnesses() { if valid { assert_eq!(reduction.extract_solution(&witness).unwrap(), witness); } else { - assert!(reduction.extract_solution(&witness).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &witness), Ok(value) if { value.is_valid() }) + ); } } } @@ -109,7 +111,9 @@ fn test_signed_containment_duplicate_edges_and_invalid_length() { let witness = vec![true, false, false]; assert_eq!(reduction.extract_solution(&witness).unwrap(), witness); for bad in [vec![], vec![true; 4]] { - assert!(reduction.extract_solution(&bad).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &bad), Ok(value) if { value.is_valid() }) + ); } } diff --git a/src/unit_tests/rules/minimumvertexcover_ensemblecomputation.rs b/src/unit_tests/rules/minimumvertexcover_ensemblecomputation.rs index b44823425..0249d4a50 100644 --- a/src/unit_tests/rules/minimumvertexcover_ensemblecomputation.rs +++ b/src/unit_tests/rules/minimumvertexcover_ensemblecomputation.rs @@ -165,7 +165,9 @@ fn test_minimumvertexcover_to_ensemblecomputation_rejects_invalid_programs() { let source = MinimumVertexCover::new(SimpleGraph::new(2, vec![(0, 1)]), vec![One; 2]); let reduction = ReduceTo::::reduce_to(&source).unwrap(); for program in [vec![], vec![0; 6], vec![3, 0, 1, 2, 0, 1]] { - assert!(reduction.extract_solution(&program).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &program), Ok(value) if { value.is_valid() }) + ); } } diff --git a/src/unit_tests/rules/minimumvertexcover_maximumindependentset.rs b/src/unit_tests/rules/minimumvertexcover_maximumindependentset.rs index 53fd77c03..4723ff7dc 100644 --- a/src/unit_tests/rules/minimumvertexcover_maximumindependentset.rs +++ b/src/unit_tests/rules/minimumvertexcover_maximumindependentset.rs @@ -1,7 +1,7 @@ use super::*; +include!("../jl_helpers.rs"); use crate::rules::test_helpers::assert_optimization_round_trip_from_optimization_target; use crate::solvers::BruteForce; -include!("../jl_helpers.rs"); #[test] fn test_minimumvertexcover_to_maximumindependentset_closed_loop() { diff --git a/src/unit_tests/rules/minimumvertexcover_minimumsetcovering.rs b/src/unit_tests/rules/minimumvertexcover_minimumsetcovering.rs index 11accfa1f..176eb58ae 100644 --- a/src/unit_tests/rules/minimumvertexcover_minimumsetcovering.rs +++ b/src/unit_tests/rules/minimumvertexcover_minimumsetcovering.rs @@ -1,7 +1,7 @@ use super::*; +include!("../jl_helpers.rs"); use crate::rules::test_helpers::assert_optimization_round_trip_from_optimization_target; use crate::solvers::BruteForce; -include!("../jl_helpers.rs"); #[test] fn test_minimumvertexcover_to_minimumsetcovering_closed_loop() { diff --git a/src/unit_tests/rules/minimumvertexcover_qubo.rs b/src/unit_tests/rules/minimumvertexcover_qubo.rs index 98c1d4935..6fc0509bb 100644 --- a/src/unit_tests/rules/minimumvertexcover_qubo.rs +++ b/src/unit_tests/rules/minimumvertexcover_qubo.rs @@ -55,7 +55,7 @@ fn test_minimumvertexcover_to_qubo_via_path_closed_loop() { "QUBO", ] ); - assert_eq!(qubo.num_variables(), 4); + assert_eq!(qubo.num_variables().unwrap(), 4); let solver = BruteForce::new(); let qubo_solutions = solver.find_all_witnesses(qubo).unwrap(); @@ -93,7 +93,7 @@ fn test_minimumvertexcover_to_qubo_via_path_star_graph() { let (_, chain) = reduce_vc_to_qubo(&problem); let qubo: &QUBO = chain.target_problem(); - assert_eq!(qubo.num_variables(), 4); + assert_eq!(qubo.num_variables().unwrap(), 4); let solver = BruteForce::new(); let qubo_solution = solver diff --git a/src/unit_tests/rules/minimumweightdecoding_ilp.rs b/src/unit_tests/rules/minimumweightdecoding_ilp.rs index 09eb28337..fa462ed7d 100644 --- a/src/unit_tests/rules/minimumweightdecoding_ilp.rs +++ b/src/unit_tests/rules/minimumweightdecoding_ilp.rs @@ -96,8 +96,9 @@ fn test_minimumweightdecoding_to_ilp_infeasible() { let problem = infeasible_instance(); let reduction: ReductionMinimumWeightDecodingToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); - assert!( - ILPSolver::new().solve(reduction.target_problem()).is_err(), + assert_eq!( + ILPSolver::new().solve(reduction.target_problem()), + Err(crate::solvers::ILPSolveError::Infeasible), "infeasible instance should produce infeasible ILP" ); } diff --git a/src/unit_tests/rules/monochromatictriangle_ilp.rs b/src/unit_tests/rules/monochromatictriangle_ilp.rs index 0dbd944fd..b2c0ba372 100644 --- a/src/unit_tests/rules/monochromatictriangle_ilp.rs +++ b/src/unit_tests/rules/monochromatictriangle_ilp.rs @@ -69,8 +69,9 @@ fn test_monochromatic_triangle_to_ilp_infeasible_k6() { let problem = MonochromaticTriangle::new(SimpleGraph::new(6, edges)); let reduction = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); - assert!( - ILPSolver::new().solve(reduction.target_problem()).is_err(), + assert_eq!( + ILPSolver::new().solve(reduction.target_problem()), + Err(crate::solvers::ILPSolveError::Infeasible), "K6 should be infeasible by R(3,3)=6" ); } diff --git a/src/unit_tests/rules/multiplechoicebranching_ilp.rs b/src/unit_tests/rules/multiplechoicebranching_ilp.rs index 6166a1180..6c995f893 100644 --- a/src/unit_tests/rules/multiplechoicebranching_ilp.rs +++ b/src/unit_tests/rules/multiplechoicebranching_ilp.rs @@ -21,7 +21,10 @@ fn test_multiplechoicebranching_to_ilp_closed_loop() { let actual = reduction.extract_solution(&target).unwrap(); assert!(problem.evaluate(&actual).unwrap().0); } - None => assert!(ILPSolver::new().solve(reduction.target_problem()).is_err()), + None => assert_eq!( + ILPSolver::new().solve(reduction.target_problem()), + Err(crate::solvers::ILPSolveError::Infeasible) + ), } } } @@ -35,7 +38,10 @@ fn test_multiplechoicebranching_to_ilp_rejects_forced_cycle() { 2, ); let reduction = ReduceTo::>::reduce_to(&problem).unwrap(); - assert!(ILPSolver::new().solve(reduction.target_problem()).is_err()); + assert_eq!( + ILPSolver::new().solve(reduction.target_problem()), + Err(crate::solvers::ILPSolveError::Infeasible) + ); } #[test] diff --git a/src/unit_tests/rules/naesatisfiability_ilp.rs b/src/unit_tests/rules/naesatisfiability_ilp.rs index 287d57652..4e168df0c 100644 --- a/src/unit_tests/rules/naesatisfiability_ilp.rs +++ b/src/unit_tests/rules/naesatisfiability_ilp.rs @@ -80,8 +80,9 @@ fn test_naesatisfiability_to_ilp_infeasible() { let ilp_solver = ILPSolver::new(); // The ILP should be infeasible: x1 ≥ 1 (at least one true) AND x1 ≤ 0 (at least one false) - assert!( - ilp_solver.solve(ilp).is_err(), + assert_eq!( + ilp_solver.solve(ilp), + Err(crate::solvers::ILPSolveError::Infeasible), "ILP should be infeasible for unsatisfiable NAE-SAT" ); } diff --git a/src/unit_tests/rules/naesatisfiability_maxcut.rs b/src/unit_tests/rules/naesatisfiability_maxcut.rs index 9f07e13f0..165fecb08 100644 --- a/src/unit_tests/rules/naesatisfiability_maxcut.rs +++ b/src/unit_tests/rules/naesatisfiability_maxcut.rs @@ -183,23 +183,20 @@ fn check_every_cut(source: &NAESatisfiability) { let value = target.evaluate(&cut).unwrap(); best = best.max(value.0.unwrap()); let certificate = AggregateReductionResult::extract_value(&reduction, value).0; - match reduction.extract_solution(&cut) { - Ok(assignment) => { - assert!(certificate); - assert!(source.evaluate(&assignment).unwrap().0); - assert_eq!( - assignment, - (0..source.num_vars()) - .map(|i| cut[2 * i]) - .collect::>() - ); - let index = assignment - .iter() - .enumerate() - .fold(0, |index, (i, &bit)| index | (usize::from(bit) << i)); - decoded[index] = true; - } - Err(_) => assert!(!certificate), + if certificate { + let assignment = reduction.extract_solution(&cut).unwrap(); + assert!(source.evaluate(&assignment).unwrap().0); + assert_eq!( + assignment, + (0..source.num_vars()) + .map(|i| cut[2 * i]) + .collect::>() + ); + let index = assignment + .iter() + .enumerate() + .fold(0, |index, (i, &bit)| index | (usize::from(bit) << i)); + decoded[index] = true; } } for (mask, &has_extension) in decoded.iter().enumerate() { @@ -213,9 +210,9 @@ fn check_every_cut(source: &NAESatisfiability) { decoded.iter().any(|&valid| valid) ); assert!(!AggregateReductionResult::extract_value(&reduction, crate::types::Max(None)).0); - assert!(reduction - .extract_solution(&vec![false; target.num_vertices() + 1]) - .is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![false; target.num_vertices() + 1]), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&reduction, value); value.is_valid() }) + ); } #[test] diff --git a/src/unit_tests/rules/naesatisfiability_setsplitting.rs b/src/unit_tests/rules/naesatisfiability_setsplitting.rs index 522d9bf04..e35cfbfd7 100644 --- a/src/unit_tests/rules/naesatisfiability_setsplitting.rs +++ b/src/unit_tests/rules/naesatisfiability_setsplitting.rs @@ -54,9 +54,9 @@ fn test_naesatisfiability_to_setsplitting_extract_solution_uses_positive_literal assert_eq!( reduction - .extract_solution(&vec![true, false, true, false, true, false]) + .extract_solution(&vec![true, true, false, false, false, true]) .unwrap(), - vec![true, false, true] + vec![true, true, false] ); } diff --git a/src/unit_tests/rules/numericalmatchingwithtargetsums_ilp.rs b/src/unit_tests/rules/numericalmatchingwithtargetsums_ilp.rs index 697129347..50dc188a1 100644 --- a/src/unit_tests/rules/numericalmatchingwithtargetsums_ilp.rs +++ b/src/unit_tests/rules/numericalmatchingwithtargetsums_ilp.rs @@ -55,8 +55,9 @@ fn test_numericalmatchingwithtargetsums_to_ilp_unsatisfiable() { let problem = NumericalMatchingWithTargetSums::new(vec![1, 2], vec![3, 4], vec![10, 20]); let reduction = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let result = ILPSolver::new().solve(reduction.target_problem()); - assert!( - result.is_err(), + assert_eq!( + result, + Err(crate::solvers::ILPSolveError::Infeasible), "Unsatisfiable instance should have no ILP solution" ); } diff --git a/src/unit_tests/rules/optimallineararrangement_consecutiveonesmatrixaugmentation.rs b/src/unit_tests/rules/optimallineararrangement_consecutiveonesmatrixaugmentation.rs index a081c4de7..fff241b12 100644 --- a/src/unit_tests/rules/optimallineararrangement_consecutiveonesmatrixaugmentation.rs +++ b/src/unit_tests/rules/optimallineararrangement_consecutiveonesmatrixaugmentation.rs @@ -102,7 +102,9 @@ fn test_optimallineararrangement_to_consecutiveonesmatrixaugmentation_edgeless_s let arrangement = reduction.extract_solution(&witness).unwrap(); assert_eq!(arrangement.len(), 3); assert_eq!(source.evaluate(&arrangement).unwrap(), Or(true)); - assert!(reduction.extract_solution(&vec![]).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![]), Ok(value) if { value.is_valid() }) + ); } #[test] @@ -134,7 +136,9 @@ fn test_optimallineararrangement_to_consecutiveonesmatrixaugmentation_negative_b BruteForce::new().solve(&source).unwrap().is_none(), "P_6 has no arrangement of length <= 4" ); - assert!(reduction.extract_solution(&vec![]).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![]), Ok(value) if { value.is_valid() }) + ); } #[test] @@ -143,19 +147,13 @@ fn test_optimallineararrangement_to_consecutiveonesmatrixaugmentation_extract_in let reduction = ReduceTo::::reduce_to(&source) .expect("reduction should succeed"); + assert!(reduction.target_problem().evaluate(&vec![0, 1, 2]).is_err()); assert_eq!( reduction - .extract_solution(&vec![0, 1, 2]) - .unwrap_err() - .to_string(), - "target evaluation failed during extraction: invalid configuration: column ordering length does not match the matrix" - ); - assert_eq!( - reduction - .extract_solution(&vec![0, 0, 1, 2, 3, 4]) - .unwrap_err() - .to_string(), - "target column order is not a satisfying augmentation certificate" + .target_problem() + .evaluate(&vec![0, 0, 1, 2, 3, 4]) + .unwrap(), + crate::types::Or(false) ); } @@ -191,9 +189,9 @@ fn test_optimallineararrangement_to_consecutiveonesmatrixaugmentation_native_dom Or(true) ); } else { - assert!(reduction - .extract_solution(&(0..target.num_cols()).collect()) - .is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &(0..target.num_cols()).collect()), Ok(value) if { value.is_valid() }) + ); } } } @@ -202,8 +200,12 @@ fn test_optimallineararrangement_to_consecutiveonesmatrixaugmentation_native_dom fn test_optimallineararrangement_to_consecutiveonesmatrixaugmentation_certificate() { let source = decision_ola(SimpleGraph::new(3, vec![(0, 2)]), 1); let reduction = ReduceTo::::reduce_to(&source).unwrap(); - assert!(reduction.extract_solution(&vec![0, 1, 2]).is_err()); - assert!(reduction.extract_solution(&vec![0, 1, 3]).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![0, 1, 2]), Ok(value) if { value.is_valid() }) + ); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![0, 1, 3]), Ok(value) if { value.is_valid() }) + ); let arrangement = reduction.extract_solution(&vec![2, 0, 1]).unwrap(); assert_eq!(arrangement, vec![1, 2, 0]); assert_eq!(source.evaluate(&arrangement).unwrap(), Or(true)); diff --git a/src/unit_tests/rules/paintshop_qubo.rs b/src/unit_tests/rules/paintshop_qubo.rs index e5b6343f2..69ec5351d 100644 --- a/src/unit_tests/rules/paintshop_qubo.rs +++ b/src/unit_tests/rules/paintshop_qubo.rs @@ -56,27 +56,26 @@ fn test_paintshop_to_qubo_optimal_value() { #[test] fn test_paintshop_to_qubo_matrix_structure() { - // Issue example: verify the Q matrix matches expected values + // Verify the Q matrix matches expected values let source = PaintShop::new(vec!["A", "B", "C", "A", "D", "B", "D", "C"]); let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let qubo = reduction.target_problem(); - let m = qubo.matrix(); - // From the issue: + // Expected coefficients: // Q = [ -1, -2, 2, 2 ] // [ 0, 2, -2, 0 ] // [ 0, 0, 1, -2 ] // [ 0, 0, 0, 0 ] - assert_eq!(m[0][0], -1); - assert_eq!(m[0][1], -2); - assert_eq!(m[0][2], 2); - assert_eq!(m[0][3], 2); - assert_eq!(m[1][1], 2); - assert_eq!(m[1][2], -2); - assert_eq!(m[1][3], 0); - assert_eq!(m[2][2], 1); - assert_eq!(m[2][3], -2); - assert_eq!(m[3][3], 0); + assert_eq!(qubo.get(0, 0).unwrap(), -1); + assert_eq!(qubo.get(0, 1).unwrap(), -2); + assert_eq!(qubo.get(0, 2).unwrap(), 2); + assert_eq!(qubo.get(0, 3).unwrap(), 2); + assert_eq!(qubo.get(1, 1).unwrap(), 2); + assert_eq!(qubo.get(1, 2).unwrap(), -2); + assert_eq!(qubo.get(1, 3).unwrap(), 0); + assert_eq!(qubo.get(2, 2).unwrap(), 1); + assert_eq!(qubo.get(2, 3).unwrap(), -2); + assert_eq!(qubo.get(3, 3).unwrap(), 0); } #[test] @@ -113,6 +112,6 @@ fn test_paintshop_to_qubo_canonical_example_spec() { assert_eq!(example.source.problem, "PaintShop"); assert_eq!(example.target.problem, "QUBO"); assert_eq!(example.source.instance["num_cars"], 4); - assert_eq!(example.target.instance["num_vars"], 4); + assert_eq!(example.target.instance["matrix"]["nrows"], 4); assert!(!example.solutions.is_empty()); } diff --git a/src/unit_tests/rules/partition_integralflowwithmultipliers.rs b/src/unit_tests/rules/partition_integralflowwithmultipliers.rs index cb72e3e25..b0289975d 100644 --- a/src/unit_tests/rules/partition_integralflowwithmultipliers.rs +++ b/src/unit_tests/rules/partition_integralflowwithmultipliers.rs @@ -75,10 +75,6 @@ fn test_partition_to_integralflowwithmultipliers_odd_total_is_fixed_no_instance( assert_eq!(target.capacities(), &[1, 1]); assert_eq!(target.requirement(), 1); assert!(BruteForce::new().solve(target).unwrap().is_none()); - assert_eq!( - reduction.extract_solution(&vec![]).unwrap_err().to_string(), - "the fixed infeasible target instance has no extractable witness" - ); } #[test] diff --git a/src/unit_tests/rules/partition_openshopscheduling.rs b/src/unit_tests/rules/partition_openshopscheduling.rs index 40bc779df..3145ec02b 100644 --- a/src/unit_tests/rules/partition_openshopscheduling.rs +++ b/src/unit_tests/rules/partition_openshopscheduling.rs @@ -51,7 +51,9 @@ fn test_partition_to_open_shop_scheduling_odd_total_is_not_satisfying() { let source = Partition::new(vec![2, 4, 5]).unwrap(); let reduction = ReduceTo::::reduce_to(&source).unwrap(); let best = solve_target(reduction.target_problem()); - assert!(reduction.extract_solution(&best).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &best), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&reduction, value); value.is_valid() }) + ); } #[test] @@ -122,13 +124,17 @@ fn test_partition_to_open_shop_all_small_partitions_and_machine_orders() { assert_eq!(reduction.extract_solution(&schedule).unwrap(), assignment); let delayed: Vec<_> = schedule.iter().map(|&time| time + 1).collect(); assert!(target.evaluate(&delayed).unwrap().0.is_some()); - assert!(reduction.extract_solution(&delayed).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &delayed), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&reduction, value); value.is_valid() }) + ); } } - assert!(reduction.extract_solution(&vec![0; (n + 1) * 3]).is_err()); - assert!(reduction - .extract_solution(&vec![0; (n + 1) * 3 + 1]) - .is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![0; (n + 1) * 3]), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&reduction, value); value.is_valid() }) + ); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![0; (n + 1) * 3 + 1]), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&reduction, value); value.is_valid() }) + ); } } } @@ -144,7 +150,9 @@ fn test_partition_to_open_shop_odd_singleton_certificate() { .unwrap(); assert_eq!(value, crate::types::Min(Some(3))); assert!(!AggregateReductionResult::extract_value(&reduction, value).0); - assert!(reduction.extract_solution(&schedule).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &schedule), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&reduction, value); value.is_valid() }) + ); } #[test] diff --git a/src/unit_tests/rules/partition_sequencingtominimizetardytaskweight.rs b/src/unit_tests/rules/partition_sequencingtominimizetardytaskweight.rs index 8bb870c3e..79e249fa3 100644 --- a/src/unit_tests/rules/partition_sequencingtominimizetardytaskweight.rs +++ b/src/unit_tests/rules/partition_sequencingtominimizetardytaskweight.rs @@ -65,7 +65,9 @@ fn test_partition_to_sequencing_to_minimize_tardy_task_weight_odd_total_is_unsat ) .0 ); - assert!(reduction.extract_solution(&best).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &best), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&reduction, value); value.is_valid() }) + ); } #[cfg(feature = "example-db")] @@ -152,9 +154,8 @@ fn test_partition_to_tardy_weight_all_small_configurations() { } let certified = crate::rules::AggregateReductionResult::extract_value(&reduction, value).0; - let extracted = reduction.extract_solution(&schedule); - assert_eq!(extracted.is_ok(), certified); - if let Ok(bits) = extracted { + if certified { + let bits = reduction.extract_solution(&schedule).unwrap(); assert!(source.evaluate(&bits).unwrap().0); } } @@ -166,10 +167,12 @@ fn test_partition_to_tardy_weight_all_small_configurations() { .0, source_feasible ); - assert!(reduction.extract_solution(&vec![]).is_err()); - assert!(reduction - .extract_solution(&vec![n as usize; n as usize]) - .is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![]), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&reduction, value); value.is_valid() }) + ); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![n as usize; n as usize]), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&reduction, value); value.is_valid() }) + ); assert!( !crate::rules::AggregateReductionResult::extract_value(&reduction, Min(None),).0 ); @@ -195,9 +198,8 @@ fn test_partition_to_tardy_weight_full_i64_domain() { crate::rules::AggregateReductionResult::extract_value(&reduction, value).0, balanced ); - let extracted = reduction.extract_solution(&schedule); - assert_eq!(extracted.is_ok(), balanced); - if let Ok(bits) = extracted { + if balanced { + let bits = reduction.extract_solution(&schedule).unwrap(); assert!(source.evaluate(&bits).unwrap().0); } } diff --git a/src/unit_tests/rules/partition_subsetsum.rs b/src/unit_tests/rules/partition_subsetsum.rs index 2e87577e8..f6104bb9a 100644 --- a/src/unit_tests/rules/partition_subsetsum.rs +++ b/src/unit_tests/rules/partition_subsetsum.rs @@ -46,12 +46,6 @@ fn test_partition_to_subsetsum_odd_total() { // No witness should exist for the target let witness = BruteForce::new().solve(target).unwrap(); assert!(witness.is_none()); - - let error = reduction.extract_solution(&vec![]).unwrap_err(); - assert_eq!( - error.to_string(), - "expected 3 subset-selection values, got 0" - ); } #[test] @@ -72,7 +66,7 @@ fn test_partition_to_subsetsum_rejects_wrong_solution_length() { let source = Partition::new(vec![1, 1, 2, 2]).unwrap(); let reduction = ReduceTo::::reduce_to(&source).expect("reduction should succeed"); - assert!(reduction - .extract_solution(&vec![false, true, false]) - .is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![false, true, false]), Ok(value) if { value.is_valid() }) + ); } diff --git a/src/unit_tests/rules/partition_sumofsquarespartition.rs b/src/unit_tests/rules/partition_sumofsquarespartition.rs index 5f5a46fc6..89bd1f915 100644 --- a/src/unit_tests/rules/partition_sumofsquarespartition.rs +++ b/src/unit_tests/rules/partition_sumofsquarespartition.rs @@ -153,5 +153,7 @@ fn test_partition_to_sumofsquarespartition_solution_extraction_identity() { ); } - assert!(reduction.extract_solution(&vec![0]).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![0]), Ok(value) if { value.is_valid() }) + ); } diff --git a/src/unit_tests/rules/partitionintocliques_ilp.rs b/src/unit_tests/rules/partitionintocliques_ilp.rs index 564c1af27..39a13c9c8 100644 --- a/src/unit_tests/rules/partitionintocliques_ilp.rs +++ b/src/unit_tests/rules/partitionintocliques_ilp.rs @@ -29,5 +29,8 @@ fn test_partitionintocliques_to_ilp_preserves_infeasibility() { let problem = PartitionIntoCliques::new(SimpleGraph::new(3, vec![]), 2); let reduction = ReduceTo::>::reduce_to(&problem).unwrap(); - assert!(ILPSolver::new().solve(reduction.target_problem()).is_err()); + assert_eq!( + ILPSolver::new().solve(reduction.target_problem()), + Err(crate::solvers::ILPSolveError::Infeasible) + ); } diff --git a/src/unit_tests/rules/partitionintocliques_minimumcoveringbycliques.rs b/src/unit_tests/rules/partitionintocliques_minimumcoveringbycliques.rs index c3e5fb245..4a04b991f 100644 --- a/src/unit_tests/rules/partitionintocliques_minimumcoveringbycliques.rs +++ b/src/unit_tests/rules/partitionintocliques_minimumcoveringbycliques.rs @@ -135,12 +135,12 @@ fn test_partitionintocliques_to_minimumcoveringbycliques_unsat_extracts_invalid_ ); assert_eq!(target.evaluate(&target_solution).unwrap(), Min(Some(4))); - assert_eq!( - reduction - .extract_solution(&target_solution) - .unwrap_err() - .to_string(), - "target cover does not certify the source clique bound" + assert!( + !crate::rules::AggregateReductionResult::extract_value( + &reduction, + target.evaluate(&target_solution).unwrap() + ) + .0 ); } @@ -191,12 +191,16 @@ fn test_partitionintocliques_native_bounds_and_adjacency_semantics() { assert!(source.evaluate(&decoded).unwrap().0); } } else { - assert!(reduction.extract_solution(&witness).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &witness), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&reduction, value); value.is_valid() }) + ); } - assert!(reduction.extract_solution(&vec![0; witness.len()]).is_err()); - assert!(reduction - .extract_solution(&vec![0; witness.len() + 1]) - .is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![0; witness.len()]), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&reduction, value); value.is_valid() }) + ); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![0; witness.len() + 1]), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&reduction, value); value.is_valid() }) + ); let q = layout.num_directed_pairs(); assert_eq!(target.num_vertices(), 2 * n + 2 * q + 4); assert_eq!(target.num_edges(), (n + q) * (n + q) + 4 * n + 7 * q + 2); diff --git a/src/unit_tests/rules/precedenceconstrainedscheduling_ilp.rs b/src/unit_tests/rules/precedenceconstrainedscheduling_ilp.rs index 3f54767c4..a454000dd 100644 --- a/src/unit_tests/rules/precedenceconstrainedscheduling_ilp.rs +++ b/src/unit_tests/rules/precedenceconstrainedscheduling_ilp.rs @@ -60,8 +60,9 @@ fn test_precedenceconstrainedscheduling_to_ilp_infeasible() { let problem = infeasible_instance(); let reduction: ReductionPCSToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); - assert!( - ILPSolver::new().solve(reduction.target_problem()).is_err(), + assert_eq!( + ILPSolver::new().solve(reduction.target_problem()), + Err(crate::solvers::ILPSolveError::Infeasible), "infeasible scheduling instance should produce infeasible ILP" ); } diff --git a/src/unit_tests/rules/prizecollectingsteinerforest_steinertree.rs b/src/unit_tests/rules/prizecollectingsteinerforest_steinertree.rs index 252a965c7..e7694a537 100644 --- a/src/unit_tests/rules/prizecollectingsteinerforest_steinertree.rs +++ b/src/unit_tests/rules/prizecollectingsteinerforest_steinertree.rs @@ -33,14 +33,14 @@ fn test_prizecollectingsteinerforest_to_steinertree_canonical_closed_loop() { "PCSF -> SteinerTree canonical closed loop", ); - // Numeric sanity: both optima must agree, and equal 3 on this instance. + // Three gadget terminals each add M = omega + 1 = 2. let target = reduction.target_problem(); let source_opt_solution = BruteForce::new().solve(&source).unwrap().unwrap(); let source_opt = source.evaluate(&source_opt_solution).unwrap(); let target_opt_solution = BruteForce::new().solve(target).unwrap().unwrap(); let target_opt = target.evaluate(&target_opt_solution).unwrap(); assert_eq!(source_opt, Min(Some(3))); - assert_eq!(target_opt, Min(Some(3))); + assert_eq!(target_opt, Min(Some(9))); } #[test] @@ -128,17 +128,9 @@ fn test_prizecollectingsteinerforest_to_steinertree_all_prizes() { let target_opt_solution = BruteForce::new().solve(target).unwrap().unwrap(); let target_opt = target.evaluate(&target_opt_solution).unwrap(); assert_eq!(source_opt, Min(Some(3))); - assert_eq!(target_opt, Min(Some(3))); + assert_eq!(target_opt, Min(Some(9))); } -/// No vertex carries a positive prize, so no gadget terminals are added. -/// Only the artificial root remains as a terminal, but SteinerTree requires -/// at least two terminals — so this corner case is covered by size-contract -/// inspection plus a degenerate single-vertex source case that still has -/// the construction proceed when `omega = 0`. We skip the SteinerTree -/// instantiation when `k = 0` (which would produce a single-terminal -/// SteinerTree); the closed-loop check uses a near-empty case where one -/// vertex has prize 0 and one has a positive prize. #[test] fn test_prizecollectingsteinerforest_to_steinertree_mixed_zero_prize() { // Two-vertex path with one prize-zero vertex. @@ -189,3 +181,91 @@ fn test_prizecollectingsteinerforest_to_steinertree_path_with_omission() { "PCSF -> SteinerTree path-with-omission case", ); } + +#[test] +fn test_zero_prize_forest_through_steiner_tree_and_ilp() { + use crate::models::algebraic::ILP; + use crate::solvers::ILPSolver; + for (n, edges, costs, expected) in [(0, vec![], vec![], 0), (2, vec![(0, 1)], vec![5], 0)] { + let source = + PrizeCollectingSteinerForest::new(SimpleGraph::new(n, edges), vec![0; n], costs, 1, 1) + .unwrap(); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + assert_eq!(reduction.target_problem().num_terminals(), 1); + let ilp = ReduceTo::>::reduce_to(reduction.target_problem()).unwrap(); + let raw = ILPSolver::new().solve(ilp.target_problem()).unwrap(); + let tree = ilp.extract_solution(&raw).unwrap(); + let forest = reduction.extract_solution(&tree).unwrap(); + assert_eq!(source.evaluate(&forest).unwrap(), Min(Some(expected))); + assert_optimization_round_trip_from_optimization_target( + &source, + &reduction, + "zero-prize forest", + ); + } +} + +#[test] +fn low_prizes_do_not_bypass_component_costs() { + for (prizes, beta, omega, expected) in [ + (vec![1, 2], 1, 5, 3), + (vec![1, 2], 0, 5, 0), + (vec![1, 2], 1, 0, 0), + (vec![0, 2], 1, 5, 2), + ] { + let source = PrizeCollectingSteinerForest::new( + SimpleGraph::new(2, vec![(0, 1)]), + prizes, + vec![0], + beta, + omega, + ) + .unwrap(); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + let offset = (omega + 1) * source.num_vertices_with_prize() as i64; + let trees = BruteForce::new() + .find_all_witnesses(reduction.target_problem()) + .unwrap(); + assert!(!trees.is_empty()); + for tree in trees { + assert_eq!( + reduction.target_problem().evaluate(&tree).unwrap(), + Min(Some(expected + offset)) + ); + let forest = reduction.extract_solution(&tree).unwrap(); + assert_eq!(source.evaluate(&forest).unwrap(), Min(Some(expected))); + } + } +} + +#[test] +fn gadget_coefficients_report_native_integer_overflow() { + for (prize, beta, omega) in [(1, 1, i64::MAX), (i64::MAX, 2, 0), (i64::MAX, 1, 0)] { + let source = PrizeCollectingSteinerForest::new( + SimpleGraph::new(1, vec![]), + vec![prize], + vec![], + beta, + omega, + ) + .unwrap(); + assert!(matches!( + ReduceTo::>::reduce_to(&source), + Err(crate::rules::ReductionError::IntegerOverflow { .. }) + )); + } + // No prize gadget is constructed, so its inclusion cost is not needed. + let source = PrizeCollectingSteinerForest::new( + SimpleGraph::new(1, vec![]), + vec![0], + vec![], + 1, + i64::MAX, + ) + .unwrap(); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + assert_eq!( + reduction.target_problem().evaluate(&vec![false]).unwrap(), + Min(Some(0)) + ); +} diff --git a/src/unit_tests/rules/qubo_casts.rs b/src/unit_tests/rules/qubo_casts.rs index 9ac1eb48a..002a36469 100644 --- a/src/unit_tests/rules/qubo_casts.rs +++ b/src/unit_tests/rules/qubo_casts.rs @@ -9,7 +9,9 @@ fn test_qubo_i64_to_f64_closed_loop() { assert_eq!( reduction.target_problem().matrix(), - &[vec![1.0, -2.0], vec![0.0, 3.0]] + QUBO::from_matrix(vec![vec![1.0, -2.0], vec![0.0, 3.0]]) + .unwrap() + .matrix() ); assert_eq!( reduction.extract_solution(&vec![true, false]).unwrap(), diff --git a/src/unit_tests/rules/reduction_path_parity.rs b/src/unit_tests/rules/reduction_path_parity.rs index d2011bcd4..bb10d89a9 100644 --- a/src/unit_tests/rules/reduction_path_parity.rs +++ b/src/unit_tests/rules/reduction_path_parity.rs @@ -131,7 +131,7 @@ fn test_jl_parity_factoring_to_spinglass_path() { // Verify reduction produces a valid SpinGlass problem assert!( - target.num_variables() > 0, + target.num_variables().unwrap() > 0, "SpinGlass should have variables" ); diff --git a/src/unit_tests/rules/registersufficiency_ilp.rs b/src/unit_tests/rules/registersufficiency_ilp.rs index beaef71d7..aa1621b4b 100644 --- a/src/unit_tests/rules/registersufficiency_ilp.rs +++ b/src/unit_tests/rules/registersufficiency_ilp.rs @@ -63,8 +63,9 @@ fn test_register_sufficiency_to_ilp_infeasible() { let source = infeasible_example(); let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); - assert!( - ILPSolver::new().solve(reduction.target_problem()).is_err(), + assert_eq!( + ILPSolver::new().solve(reduction.target_problem()), + Err(crate::solvers::ILPSolveError::Infeasible), "register-sufficiency instance with bound one should be infeasible" ); } diff --git a/src/unit_tests/rules/resourceconstrainedscheduling_ilp.rs b/src/unit_tests/rules/resourceconstrainedscheduling_ilp.rs index 9e811bbd4..eb2438fb6 100644 --- a/src/unit_tests/rules/resourceconstrainedscheduling_ilp.rs +++ b/src/unit_tests/rules/resourceconstrainedscheduling_ilp.rs @@ -50,8 +50,9 @@ fn test_resourceconstrainedscheduling_to_ilp_infeasible() { let problem = ResourceConstrainedScheduling::new(1, vec![5], vec![vec![6], vec![6], vec![6]], 1).unwrap(); let reduction = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); - assert!( - ILPSolver::new().solve(reduction.target_problem()).is_err(), + assert_eq!( + ILPSolver::new().solve(reduction.target_problem()), + Err(crate::solvers::ILPSolveError::Infeasible), "infeasible RCS should produce infeasible ILP" ); } diff --git a/src/unit_tests/rules/rootedtreestorageassignment_ilp.rs b/src/unit_tests/rules/rootedtreestorageassignment_ilp.rs index 737aec834..a19c78a93 100644 --- a/src/unit_tests/rules/rootedtreestorageassignment_ilp.rs +++ b/src/unit_tests/rules/rootedtreestorageassignment_ilp.rs @@ -42,9 +42,10 @@ fn test_rootedtreestorageassignment_to_ilp_bf_vs_ilp() { assert!(ilp_value.0, "ILP solution should be feasible"); assert!(bf_value.0, "BF should also find feasible solution"); } - Err(_) => { + Err(crate::solvers::ILPSolveError::Infeasible) => { assert!(!bf_value.0, "both should agree on infeasibility"); } + Err(error) => panic!("ILP execution failed: {error}"), } } @@ -66,7 +67,11 @@ fn test_rootedtreestorageassignment_to_ilp_infeasible() { let ilp_solver = ILPSolver::new(); let ilp_result = ilp_solver.solve(reduction.target_problem()); assert!(bf_witness.is_none(), "source should be infeasible"); - assert!(ilp_result.is_err(), "reduced ILP should also be infeasible"); + assert_eq!( + ilp_result, + Err(crate::solvers::ILPSolveError::Infeasible), + "reduced ILP should also be infeasible" + ); } #[test] diff --git a/src/unit_tests/rules/sat_coloring.rs b/src/unit_tests/rules/sat_coloring.rs index 42fc5e598..bae7be66f 100644 --- a/src/unit_tests/rules/sat_coloring.rs +++ b/src/unit_tests/rules/sat_coloring.rs @@ -1,10 +1,10 @@ use super::*; +include!("../jl_helpers.rs"); use crate::models::formula::CNFClause; use crate::solvers::BruteForce; use crate::topology::Graph; use crate::traits::Problem; use crate::variant::K3; -include!("../jl_helpers.rs"); #[test] fn test_constructor_basic_structure() { diff --git a/src/unit_tests/rules/sat_ksat.rs b/src/unit_tests/rules/sat_ksat.rs index 4e4cfa7c5..e7b3bf744 100644 --- a/src/unit_tests/rules/sat_ksat.rs +++ b/src/unit_tests/rules/sat_ksat.rs @@ -1,9 +1,9 @@ use super::*; +include!("../jl_helpers.rs"); use crate::rules::test_helpers::assert_satisfaction_round_trip_from_satisfaction_target; use crate::solvers::BruteForce; use crate::traits::Problem; use crate::variant::K3; -include!("../jl_helpers.rs"); #[test] fn test_sat_to_3sat_exact_size() { diff --git a/src/unit_tests/rules/sat_maximumindependentset.rs b/src/unit_tests/rules/sat_maximumindependentset.rs index 47b23dd0d..f4fb3b2c5 100644 --- a/src/unit_tests/rules/sat_maximumindependentset.rs +++ b/src/unit_tests/rules/sat_maximumindependentset.rs @@ -1,10 +1,10 @@ use super::*; +include!("../jl_helpers.rs"); use crate::models::formula::CNFClause; use crate::rules::test_helpers::assert_satisfaction_round_trip_from_optimization_target; use crate::solvers::BruteForce; use crate::topology::Graph; use crate::traits::Problem; -include!("../jl_helpers.rs"); #[test] fn test_boolvar_creation() { @@ -230,7 +230,9 @@ fn test_jl_parity_sat_to_independentset() { .solve(result.target_problem()) .unwrap() .expect("SAT->IS: target should have an optimal solution"); - assert!(result.extract_solution(&target_solution).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&result), &target_solution), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&result, value); value.is_valid() }) + ); assert_eq!( crate::rules::AggregateReductionResult::extract_value( &result, @@ -292,22 +294,19 @@ fn test_sat_to_independentset_all_certificates() { crate::rules::AggregateReductionResult::extract_value(&reduction, value), Or(certificate) ); - match reduction.extract_solution(&config) { - Ok(assignment) => { - assert!(certificate); - assert_eq!(source.evaluate(&assignment).unwrap(), Or(true)); - accepted = true; - } - Err(_) => assert!(!certificate), + if certificate { + let assignment = reduction.extract_solution(&config).unwrap(); + assert_eq!(source.evaluate(&assignment).unwrap(), Or(true)); + accepted = true; } } assert_eq!( accepted, BruteForce::new().solve(&source).unwrap().is_some() ); - assert!(reduction - .extract_solution(&vec![false; target.num_vertices() + 1]) - .is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![false; target.num_vertices() + 1]), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&reduction, value); value.is_valid() }) + ); } } for num_vars in [0, 3] { diff --git a/src/unit_tests/rules/sat_minimumdominatingset.rs b/src/unit_tests/rules/sat_minimumdominatingset.rs index c6db9615d..21c6331a0 100644 --- a/src/unit_tests/rules/sat_minimumdominatingset.rs +++ b/src/unit_tests/rules/sat_minimumdominatingset.rs @@ -1,9 +1,10 @@ use super::*; +use crate::traits::Problem; +include!("../jl_helpers.rs"); use crate::models::formula::CNFClause; use crate::rules::test_helpers::assert_satisfaction_round_trip_from_optimization_target; use crate::solvers::BruteForce; use crate::topology::Graph; -include!("../jl_helpers.rs"); #[test] fn test_sat_to_minimumdominatingset_closed_loop() { @@ -144,9 +145,12 @@ fn test_extract_solution_too_many_selected() { .expect("reduction should succeed"); let ds_sol = vec![true, true, false, false]; - assert_eq!( - reduction.extract_solution(&ds_sol).unwrap_err().to_string(), - "target dominating set does not certify satisfiability" + assert!( + !crate::rules::AggregateReductionResult::extract_value( + &reduction, + reduction.target_problem().evaluate(&ds_sol).unwrap() + ) + .0 ); } @@ -156,12 +160,15 @@ fn test_extract_solution_rejects_unselected_variable_gadget() { let reduction = ReduceTo::>::reduce_to(&sat) .expect("reduction should succeed"); - assert_eq!( - reduction - .extract_solution(&vec![false, false, false, false]) - .unwrap_err() - .to_string(), - "target dominating set does not certify satisfiability" + assert!( + !crate::rules::AggregateReductionResult::extract_value( + &reduction, + reduction + .target_problem() + .evaluate(&vec![false, false, false, false]) + .unwrap() + ) + .0 ); } @@ -171,12 +178,15 @@ fn test_extract_solution_rejects_selected_clause_vertex() { let reduction = ReduceTo::>::reduce_to(&sat) .expect("reduction should succeed"); - assert_eq!( - reduction - .extract_solution(&vec![true, false, false, true]) - .unwrap_err() - .to_string(), - "target dominating set does not certify satisfiability" + assert!( + !crate::rules::AggregateReductionResult::extract_value( + &reduction, + reduction + .target_problem() + .evaluate(&vec![true, false, false, true]) + .unwrap() + ) + .0 ); } @@ -247,7 +257,9 @@ fn test_jl_parity_sat_to_dominatingset() { .solve(result.target_problem()) .unwrap() .expect("SAT->DS: target should have an optimal solution"); - assert!(result.extract_solution(&target_solution).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&result), &target_solution), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&result, value); value.is_valid() }) + ); } else { assert_satisfaction_round_trip_from_optimization_target( &source, @@ -296,22 +308,19 @@ fn test_sat_to_dominatingset_native_certificates() { crate::rules::AggregateReductionResult::extract_value(&result, value), Or(certificate) ); - match result.extract_solution(&config) { - Ok(x) => { - assert!(certificate); - assert_eq!(source.evaluate(&x).unwrap(), Or(true)); - accepted = true; - } - Err(_) => assert!(!certificate), + if certificate { + let x = result.extract_solution(&config).unwrap(); + assert_eq!(source.evaluate(&x).unwrap(), Or(true)); + accepted = true; } } assert_eq!( accepted, BruteForce::new().solve(&source).unwrap().is_some() ); - assert!(result - .extract_solution(&vec![false; target.num_vertices() + 1]) - .is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&result), &vec![false; target.num_vertices() + 1]), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&result, value); value.is_valid() }) + ); } } diff --git a/src/unit_tests/rules/satisfiability_maximum2satisfiability.rs b/src/unit_tests/rules/satisfiability_maximum2satisfiability.rs index 3db2ebc6d..d5ed64c13 100644 --- a/src/unit_tests/rules/satisfiability_maximum2satisfiability.rs +++ b/src/unit_tests/rules/satisfiability_maximum2satisfiability.rs @@ -73,7 +73,9 @@ fn test_satisfiability_to_maximum2satisfiability_unsatisfiable_gap() { .solve(target) .unwrap() .expect("MAX-2-SAT target should always have a witness"); - assert!(reduction.extract_solution(&target_solution).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &target_solution), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&reduction, value); value.is_valid() }) + ); assert_eq!( crate::rules::AggregateReductionResult::extract_value(&reduction, Max(Some(55))), Or(false) @@ -162,14 +164,16 @@ fn test_satisfiability_to_maximum2satisfiability_every_target_witness() { let decoded = reduction.extract_solution(&assignment).unwrap(); assert!(source.evaluate(&decoded).unwrap().0); } else { - assert!(reduction.extract_solution(&assignment).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &assignment), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&reduction, value); value.is_valid() }) + ); } } let source_yes = BruteForce::new().solve(&source).unwrap().is_some(); assert_eq!(best == threshold, source_yes); - assert!(reduction - .extract_solution(&vec![false; target.num_vars() + 1]) - .is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![false; target.num_vars() + 1]), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&reduction, value); value.is_valid() }) + ); assert_eq!( crate::rules::AggregateReductionResult::extract_value(&reduction, Max(None)), Or(false) diff --git a/src/unit_tests/rules/satisfiability_naesatisfiability.rs b/src/unit_tests/rules/satisfiability_naesatisfiability.rs index e1ee71120..a88b6fcb4 100644 --- a/src/unit_tests/rules/satisfiability_naesatisfiability.rs +++ b/src/unit_tests/rules/satisfiability_naesatisfiability.rs @@ -86,14 +86,13 @@ fn test_solution_extraction_distinguishes_zero_assignment_from_malformed_input() vec![false, false] ); - let error = reduction.extract_solution(&vec![false, false]).unwrap_err(); - assert_eq!( - error.to_string(), - "target evaluation failed during extraction: invalid configuration: assignment length does not match the formula variables" - ); assert!(reduction - .extract_solution(&vec![false, false, false, false]) + .target_problem() + .evaluate(&vec![false, false]) .is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![false, false, false, false]), Ok(value) if { value.is_valid() }) + ); assert!(crate::rules::DynReductionResult::target_solution_from_json( &reduction, serde_json::json!([false, 2, false]) diff --git a/src/unit_tests/rules/schedulingwithindividualdeadlines_ilp.rs b/src/unit_tests/rules/schedulingwithindividualdeadlines_ilp.rs index 8838a3843..b5f904249 100644 --- a/src/unit_tests/rules/schedulingwithindividualdeadlines_ilp.rs +++ b/src/unit_tests/rules/schedulingwithindividualdeadlines_ilp.rs @@ -86,8 +86,9 @@ fn test_schedulingwithindividualdeadlines_to_ilp_infeasible() { let problem = infeasible_instance(); let reduction: ReductionSWIDToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); - assert!( - ILPSolver::new().solve(reduction.target_problem()).is_err(), + assert_eq!( + ILPSolver::new().solve(reduction.target_problem()), + Err(crate::solvers::ILPSolveError::Infeasible), "infeasible instance should yield infeasible ILP" ); } diff --git a/src/unit_tests/rules/sequencingtominimizetardytaskweight_ilp.rs b/src/unit_tests/rules/sequencingtominimizetardytaskweight_ilp.rs index baac86613..c90ea934a 100644 --- a/src/unit_tests/rules/sequencingtominimizetardytaskweight_ilp.rs +++ b/src/unit_tests/rules/sequencingtominimizetardytaskweight_ilp.rs @@ -134,24 +134,23 @@ fn test_tardy_ilp_signed_permutations_and_all_indicators() { (0..count).all(|job| (bits[count * count + job] == 1) == expected[job]); let value = target.evaluate(&bits).unwrap(); assert_eq!(value.is_valid(), exact); - let extracted = reduction.extract_solution(&bits); - assert_eq!(extracted.is_ok(), exact); if exact { + let extracted = reduction.extract_solution(&bits); assert_eq!(value.value, source_value.0); assert_eq!(extracted.unwrap(), schedule); } } } - assert!(reduction - .extract_solution(&vec![0; target.num_vars() + 1]) - .is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(reduction.target_problem(), &vec![0; target.num_vars() + 1]), Ok(value) if value.is_valid()) + ); if count > 0 { - assert!(reduction - .extract_solution(&vec![0; target.num_vars()]) - .is_err()); - assert!(reduction - .extract_solution(&vec![2; target.num_vars()]) - .is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(reduction.target_problem(), &vec![0; target.num_vars()]), Ok(value) if value.is_valid()) + ); + assert!( + !matches!(crate::traits::Problem::evaluate(reduction.target_problem(), &vec![2; target.num_vars()]), Ok(value) if value.is_valid()) + ); } } } @@ -170,9 +169,8 @@ fn test_tardy_ilp_complete_small_binary_target_space() { .map(|i| i64::from(mask & (1 << i) != 0)) .collect(); let value = target.evaluate(&bits).unwrap(); - let extracted = reduction.extract_solution(&bits); - assert_eq!(extracted.is_ok(), value.is_valid()); - if let Ok(schedule) = extracted { + if value.is_valid() { + let schedule = reduction.extract_solution(&bits).unwrap(); assert_eq!(source.evaluate(&schedule).unwrap().0, value.value); feasible += 1; } diff --git a/src/unit_tests/rules/sequencingtominimizeweightedcompletiontime_ilp.rs b/src/unit_tests/rules/sequencingtominimizeweightedcompletiontime_ilp.rs index 62b7503de..0d00777ef 100644 --- a/src/unit_tests/rules/sequencingtominimizeweightedcompletiontime_ilp.rs +++ b/src/unit_tests/rules/sequencingtominimizeweightedcompletiontime_ilp.rs @@ -104,8 +104,9 @@ fn test_cyclic_precedence_instance_is_infeasible() { ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); - assert!( - ILPSolver::new().solve(ilp).is_err(), + assert_eq!( + ILPSolver::new().solve(ilp), + Err(crate::solvers::ILPSolveError::Infeasible), "cyclic precedences should make the ILP infeasible" ); } diff --git a/src/unit_tests/rules/sequencingtominimizeweightedtardiness_ilp.rs b/src/unit_tests/rules/sequencingtominimizeweightedtardiness_ilp.rs index 3768789a1..6c85b75bd 100644 --- a/src/unit_tests/rules/sequencingtominimizeweightedtardiness_ilp.rs +++ b/src/unit_tests/rules/sequencingtominimizeweightedtardiness_ilp.rs @@ -43,8 +43,9 @@ fn test_sequencingtominimizeweightedtardiness_to_ilp_infeasible() { let problem = SequencingToMinimizeWeightedTardiness::new(vec![10, 10], vec![1, 1], vec![1, 1], 0); let reduction = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); - assert!( - ILPSolver::new().solve(reduction.target_problem()).is_err(), + assert_eq!( + ILPSolver::new().solve(reduction.target_problem()), + Err(crate::solvers::ILPSolveError::Infeasible), "infeasible STMWT should produce infeasible ILP" ); } diff --git a/src/unit_tests/rules/sequencingwithdeadlinesandsetuptimes_ilp.rs b/src/unit_tests/rules/sequencingwithdeadlinesandsetuptimes_ilp.rs index 30a4bcccc..c839db5b9 100644 --- a/src/unit_tests/rules/sequencingwithdeadlinesandsetuptimes_ilp.rs +++ b/src/unit_tests/rules/sequencingwithdeadlinesandsetuptimes_ilp.rs @@ -49,8 +49,9 @@ fn test_sequencingwithdeadlinesandsetuptimes_to_ilp_infeasible() { let problem = SequencingWithDeadlinesAndSetUpTimes::new(vec![2, 2], vec![1, 1], vec![0, 0], vec![0]); let reduction = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); - assert!( - ILPSolver::new().solve(reduction.target_problem()).is_err(), + assert_eq!( + ILPSolver::new().solve(reduction.target_problem()), + Err(crate::solvers::ILPSolveError::Infeasible), "infeasible instance should produce infeasible ILP" ); } diff --git a/src/unit_tests/rules/sequencingwithinintervals_ilp.rs b/src/unit_tests/rules/sequencingwithinintervals_ilp.rs index 4f4a6c7e6..aaf208a33 100644 --- a/src/unit_tests/rules/sequencingwithinintervals_ilp.rs +++ b/src/unit_tests/rules/sequencingwithinintervals_ilp.rs @@ -72,8 +72,9 @@ fn test_sequencingwithinintervals_to_ilp_infeasible() { let problem = infeasible_instance(); let reduction: ReductionSWIToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); - assert!( - ILPSolver::new().solve(reduction.target_problem()).is_err(), + assert_eq!( + ILPSolver::new().solve(reduction.target_problem()), + Err(crate::solvers::ILPSolveError::Infeasible), "infeasible instance (forced overlap) should yield infeasible ILP" ); } diff --git a/src/unit_tests/rules/sequencingwithreleasetimesanddeadlines_ilp.rs b/src/unit_tests/rules/sequencingwithreleasetimesanddeadlines_ilp.rs index 2598ef274..a4b81ef0d 100644 --- a/src/unit_tests/rules/sequencingwithreleasetimesanddeadlines_ilp.rs +++ b/src/unit_tests/rules/sequencingwithreleasetimesanddeadlines_ilp.rs @@ -38,8 +38,9 @@ fn test_sequencingwithreleasetimesanddeadlines_to_ilp_infeasible() { // Two tasks that can't both fit: both need time 0-1, but overlap let problem = SequencingWithReleaseTimesAndDeadlines::new(vec![2, 2], vec![0, 0], vec![2, 2]); let reduction = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); - assert!( - ILPSolver::new().solve(reduction.target_problem()).is_err(), + assert_eq!( + ILPSolver::new().solve(reduction.target_problem()), + Err(crate::solvers::ILPSolveError::Infeasible), "infeasible SWRTD should produce infeasible ILP" ); } @@ -51,8 +52,9 @@ fn test_sequencingwithreleasetimesanddeadlines_to_ilp_rejects_empty_start_window let problem = SequencingWithReleaseTimesAndDeadlines::new(vec![14], vec![0], vec![13]); let reduction = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); - assert!( - ILPSolver::new().solve(reduction.target_problem()).is_err(), + assert_eq!( + ILPSolver::new().solve(reduction.target_problem()), + Err(crate::solvers::ILPSolveError::Infeasible), "a task longer than its release-deadline window must make the ILP infeasible" ); } diff --git a/src/unit_tests/rules/setsplitting_ilp.rs b/src/unit_tests/rules/setsplitting_ilp.rs index c4cb8d97c..ef7505faf 100644 --- a/src/unit_tests/rules/setsplitting_ilp.rs +++ b/src/unit_tests/rules/setsplitting_ilp.rs @@ -70,8 +70,9 @@ fn test_setsplitting_to_ilp_infeasible() { let ilp = reduction.target_problem(); let ilp_solver = ILPSolver::new(); - assert!( - ilp_solver.solve(ilp).is_err(), + assert_eq!( + ilp_solver.solve(ilp), + Err(crate::solvers::ILPSolveError::Infeasible), "ILP should be infeasible for unsplittable instance" ); } diff --git a/src/unit_tests/rules/shortestweightconstrainedpath_ilp.rs b/src/unit_tests/rules/shortestweightconstrainedpath_ilp.rs index 8fe434aab..1b2f1bb32 100644 --- a/src/unit_tests/rules/shortestweightconstrainedpath_ilp.rs +++ b/src/unit_tests/rules/shortestweightconstrainedpath_ilp.rs @@ -61,10 +61,11 @@ fn test_shortestweightconstrainedpath_to_ilp_bf_vs_ilp() { // Both should agree on the optimal length assert_eq!(ilp_value, bf_value); } - Err(_) => { + Err(crate::solvers::ILPSolveError::Infeasible) => { // ILP found no feasible solution; brute force should agree assert_eq!(bf_value, Min(None)); } + Err(error) => panic!("ILP execution failed: {error}"), } } diff --git a/src/unit_tests/rules/spinglass_maxcut.rs b/src/unit_tests/rules/spinglass_maxcut.rs index 75faf6fd8..2acf06eba 100644 --- a/src/unit_tests/rules/spinglass_maxcut.rs +++ b/src/unit_tests/rules/spinglass_maxcut.rs @@ -1,7 +1,7 @@ use super::*; +include!("../jl_helpers.rs"); use crate::rules::test_helpers::assert_optimization_round_trip_from_optimization_target; use crate::solvers::BruteForce; -include!("../jl_helpers.rs"); #[test] fn test_spinglass_to_maxcut_closed_loop() { diff --git a/src/unit_tests/rules/spinglass_qubo.rs b/src/unit_tests/rules/spinglass_qubo.rs index 7894a553b..4d1443b1d 100644 --- a/src/unit_tests/rules/spinglass_qubo.rs +++ b/src/unit_tests/rules/spinglass_qubo.rs @@ -1,8 +1,9 @@ use super::*; +include!("../jl_helpers.rs"); use crate::rules::test_helpers::assert_optimization_round_trip_from_optimization_target; use crate::solvers::BruteForce; use crate::solvers::BruteForceProblem as _; -include!("../jl_helpers.rs"); +use crate::traits::Problem; #[test] fn test_spinglass_to_qubo_closed_loop() { @@ -68,7 +69,7 @@ fn test_reduction_structure() { let reduction2 = ReduceTo::>::reduce_to(&sg2).expect("reduction should succeed"); let qubo2 = reduction2.target_problem(); - assert_eq!(qubo2.num_variables(), 3); + assert_eq!(qubo2.num_variables().unwrap(), 3); } #[test] @@ -207,3 +208,30 @@ fn test_jl_parity_rule_qubo_to_spinglass() { assert_eq!(best_source, jl_parse_bool_configs_set(&case["best_source"])); } } + +#[test] +fn test_qubo_to_spinglass_preserves_small_nonzero_coefficients() { + // Exact powers of two distinguish algebraic coefficient preservation from + // backend tolerances. The two scales expose both former pruning branches: + // q < 1e-10, and q > 1e-10 but q/4 < 1e-10. + for magnitude in [2.0_f64.powi(-40), 2.0_f64.powi(-32)] { + for sign in [-1.0, 1.0] { + let q = sign * magnitude; + let source = QUBO::::from_matrix(vec![vec![q, q], vec![0.0, 0.0]]).unwrap(); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + let target = reduction.target_problem(); + assert_eq!(target.fields(), &[3.0 * q / 4.0, q / 4.0]); + assert_eq!(target.interactions(), vec![((0, 1), q / 4.0)]); + let offset = 3.0 * q / 4.0; + for left in [-1, 1] { + for right in [-1, 1] { + let spins = vec![left, right]; + let bits = reduction.extract_solution(&spins).unwrap(); + let source_value = source.evaluate(&bits).unwrap().0.unwrap(); + let target_value = target.evaluate(&spins).unwrap().0.unwrap(); + assert_eq!(source_value, target_value + offset); + } + } + } + } +} diff --git a/src/unit_tests/rules/steinertree_ilp.rs b/src/unit_tests/rules/steinertree_ilp.rs index 6b98b506c..ad07a1f90 100644 --- a/src/unit_tests/rules/steinertree_ilp.rs +++ b/src/unit_tests/rules/steinertree_ilp.rs @@ -10,6 +10,7 @@ fn lift(source: &SteinerTree, chosen: &[bool]) -> Vec { let root = source.terminals()[0]; let edges = source.graph().edges(); let mut witness = vec![0; tree_ilp_sizes(n, m, source.terminals().len()).unwrap().0]; + witness[m + root] = 1; let mut adj = vec![vec![]; n]; for (e, &(u, v)) in edges.iter().enumerate() { if chosen[e] { @@ -65,6 +66,9 @@ fn test_steinertree_to_ilp_closed_loop() { (4, vec![(0, 1), (2, 3)], vec![1, -10], vec![0, 1], 1), (3, vec![(0, 1), (1, 2)], vec![1, -5], vec![0, 1], -4), (3, vec![(0, 1), (1, 2)], vec![2, 3], vec![2, 0], 5), + (1, vec![], vec![], vec![0], 0), + (2, vec![(0, 1)], vec![5], vec![0], 0), + (2, vec![(0, 1)], vec![-5], vec![0], -5), ] { let source = SteinerTree::new(SimpleGraph::new(n, edges), weights, terminals); let reduction = ReduceTo::>::reduce_to(&source).unwrap(); @@ -121,7 +125,9 @@ fn test_steiner_every_small_raw_target_and_malformed_witness() { let decoded = reduction.extract_solution(&witness).unwrap(); assert_eq!(source.evaluate(&decoded).unwrap(), Min(Some(-3))); } else { - assert!(reduction.extract_solution(&witness).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(reduction.target_problem(), &witness), Ok(value) if value.is_valid()) + ); } } for bad in [ @@ -129,7 +135,9 @@ fn test_steiner_every_small_raw_target_and_malformed_witness() { vec![1; target.num_vars() + 1], vec![2; target.num_vars()], ] { - assert!(reduction.extract_solution(&bad).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(reduction.target_problem(), &bad), Ok(value) if value.is_valid()) + ); } assert_eq!(feasible_count, 1); } @@ -151,3 +159,17 @@ fn test_steiner_count_boundaries() { )); } } + +#[test] +fn test_single_terminal_tree_lifts_include_empty_tree() { + let source = SteinerTree::new(SimpleGraph::new(2, vec![(0, 1)]), vec![-5], vec![1]); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + for selected in [vec![false], vec![true]] { + let witness = lift(&source, &selected); + assert_eq!( + reduction.target_problem().evaluate(&witness).unwrap().value, + source.evaluate(&selected).unwrap().0 + ); + assert_eq!(reduction.extract_solution(&witness).unwrap(), selected); + } +} diff --git a/src/unit_tests/rules/steinertreeingraphs_ilp.rs b/src/unit_tests/rules/steinertreeingraphs_ilp.rs deleted file mode 100644 index d09885b41..000000000 --- a/src/unit_tests/rules/steinertreeingraphs_ilp.rs +++ /dev/null @@ -1,30 +0,0 @@ -use super::*; -use crate::models::algebraic::ILP; -use crate::rules::test_helpers::assert_bf_vs_ilp; -use crate::rules::ReduceTo; -use crate::topology::SimpleGraph; - -#[test] -fn test_steinertreeingraphs_to_ilp_closed_loop() { - // Path graph: 0 - 1 - 2, terminals {0, 2}, weights [1, 1] - // Optimal Steiner tree: use both edges (cost 2) - // ILP variables: 2 + 2*2*1 = 6 binary = 64 configs - let source = SteinerTreeInGraphs::new( - SimpleGraph::new(3, vec![(0, 1), (1, 2)]), - vec![0, 2], - vec![1, 1], - ); - let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); - assert_bf_vs_ilp(&source, &reduction); -} - -#[test] -fn test_steinertreeingraphs_to_ilp_bf_vs_ilp() { - let source = SteinerTreeInGraphs::new( - SimpleGraph::new(3, vec![(0, 1), (1, 2)]), - vec![0, 2], - vec![1, 1], - ); - 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/stringtostringcorrection_ilp.rs b/src/unit_tests/rules/stringtostringcorrection_ilp.rs index 83354d82f..9132a7132 100644 --- a/src/unit_tests/rules/stringtostringcorrection_ilp.rs +++ b/src/unit_tests/rules/stringtostringcorrection_ilp.rs @@ -65,8 +65,9 @@ fn test_stringtostringcorrection_to_ilp_infeasible() { let reduction: ReductionSTSCToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp_solver = ILPSolver::new(); - assert!( - ilp_solver.solve(reduction.target_problem()).is_err(), + assert_eq!( + ilp_solver.solve(reduction.target_problem()), + Err(crate::solvers::ILPSolveError::Infeasible), "reduced ILP should also be infeasible" ); } diff --git a/src/unit_tests/rules/strongconnectivityaugmentation_ilp.rs b/src/unit_tests/rules/strongconnectivityaugmentation_ilp.rs index 0fd893b32..7d1b060dc 100644 --- a/src/unit_tests/rules/strongconnectivityaugmentation_ilp.rs +++ b/src/unit_tests/rules/strongconnectivityaugmentation_ilp.rs @@ -95,7 +95,10 @@ fn test_infeasible_budget() { ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let ilp = reduction.target_problem(); let solver = ILPSolver::new(); - assert!(solver.solve(ilp).is_err()); + assert_eq!( + solver.solve(ilp), + Err(crate::solvers::ILPSolveError::Infeasible) + ); } #[test] diff --git a/src/unit_tests/rules/subgraphisomorphism_ilp.rs b/src/unit_tests/rules/subgraphisomorphism_ilp.rs index cd7fe8f07..cfb73ae65 100644 --- a/src/unit_tests/rules/subgraphisomorphism_ilp.rs +++ b/src/unit_tests/rules/subgraphisomorphism_ilp.rs @@ -84,7 +84,11 @@ fn test_subgraphisomorphism_to_ilp_infeasible() { ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp_solver = ILPSolver::new(); let result = ilp_solver.solve(reduction.target_problem()); - assert!(result.is_err(), "K3 in path should be infeasible"); + assert_eq!( + result, + Err(crate::solvers::ILPSolveError::Infeasible), + "K3 in path should be infeasible" + ); } #[test] diff --git a/src/unit_tests/rules/subsetsum_closestvectorproblem.rs b/src/unit_tests/rules/subsetsum_closestvectorproblem.rs index f6f4b2f9a..d60b64e14 100644 --- a/src/unit_tests/rules/subsetsum_closestvectorproblem.rs +++ b/src/unit_tests/rules/subsetsum_closestvectorproblem.rs @@ -18,7 +18,7 @@ fn test_subsetsum_to_closestvectorproblem_closed_loop() { .evaluate(&target_solution) .unwrap() .0, - Some(2.0) + Some(BigRational::from_integer(4.into())) ); } @@ -28,8 +28,9 @@ fn test_subsetsum_to_closestvectorproblem_structure() { let reduction = ReduceTo::>::reduce_to(&source).unwrap(); let target = reduction.target_problem(); - let expected: serde_json::Value = serde_json::json!({"basis": [[1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1], [0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1], [0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1], [0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 1, -2, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, -2, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, -2]], "target": [0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 1, 1]}); - assert_eq!(serde_json::to_value(target).unwrap(), expected); + assert_eq!(target.num_basis_vectors(), 7); + assert_eq!(target.ambient_dimension(), 12); + assert_eq!(&target.target()[..8], &[0, 0, 0, 0, 1, 1, 1, 1]); assert_eq!( ClosestVectorProblem::::variant(), vec![("target", "i64")] @@ -43,7 +44,10 @@ fn test_subsetsum_to_closestvectorproblem_binary_minimizers() { let target = reduction.target_problem(); for solution in [vec![1, 0, 0, 1, 0, 0, 0], vec![1, 1, 1, 0, 1, 1, 1]] { - assert_eq!(target.evaluate(&solution).unwrap().0, Some(2.0)); + assert_eq!( + target.evaluate(&solution).unwrap().0, + Some(BigRational::from_integer(4.into())) + ); assert!( source .evaluate(&reduction.extract_solution(&solution).unwrap()) @@ -66,12 +70,12 @@ fn test_subsetsum_to_closestvectorproblem_unsatisfiable_instance() { .evaluate(&solution) .unwrap() .unwrap() - > (source.num_elements() as f64).sqrt() + > BigRational::from_integer(source.num_elements().into()) ); } #[test] -fn test_subsetsum_to_closestvectorproblem_large_integers_and_unit_pivots() { +fn test_subsetsum_to_closestvectorproblem_binary_carries_preserve_large_inputs() { use num_bigint::BigUint; let size = BigUint::from(1u32) << 70usize; let source = SubsetSum::new(vec![size.clone()], size); @@ -80,7 +84,7 @@ fn test_subsetsum_to_closestvectorproblem_large_integers_and_unit_pivots() { witness[0] = 1; assert_eq!( result.target_problem().evaluate(&witness).unwrap(), - Min(Some(1.0)) + Min(Some(BigRational::from_integer(1.into()))) ); assert_eq!(result.extract_solution(&witness).unwrap(), vec![true]); assert!(result @@ -132,18 +136,18 @@ fn test_subsetsum_to_closestvectorproblem_all_small_coefficients() { }) .collect(); let value = target.evaluate(&config).unwrap(); - let certificate = value == Min(Some(result.target_distance)); + let certificate = value + == Min(Some(BigRational::from_integer( + source.num_elements().into(), + ))); assert_eq!( crate::rules::AggregateReductionResult::extract_value(&result, value), Or(certificate) ); - match result.extract_solution(&config) { - Ok(x) => { - assert!(certificate); - assert!(source.evaluate(&x).unwrap().0); - accepted = true; - } - Err(_) => assert!(!certificate), + if certificate { + let x = result.extract_solution(&config).unwrap(); + assert!(source.evaluate(&x).unwrap().0); + accepted = true; } } assert_eq!( @@ -153,7 +157,9 @@ fn test_subsetsum_to_closestvectorproblem_all_small_coefficients() { .unwrap() .is_some() ); - assert!(result.extract_solution(&vec![0; dimensions + 1]).is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&result), &vec![0; dimensions + 1]), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&result, value.clone()); value.is_valid() }) + ); assert_eq!( crate::rules::AggregateReductionResult::extract_value(&result, Min(None)), Or(false) diff --git a/src/unit_tests/rules/subsetsum_integerexpressionmembership.rs b/src/unit_tests/rules/subsetsum_integerexpressionmembership.rs index 9caba9ab6..fbb1b4363 100644 --- a/src/unit_tests/rules/subsetsum_integerexpressionmembership.rs +++ b/src/unit_tests/rules/subsetsum_integerexpressionmembership.rs @@ -52,11 +52,9 @@ fn test_subsetsum_to_integerexpressionmembership_extract_solution_matches_choice .unwrap(), issue_example_source_config() ); - assert_eq!( - reduction - .extract_solution(&vec![true, false, false, true]) - .unwrap(), - vec![true, false, false, true] + // Selecting 1 and 8 does not reach the source target 11. + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![true, false, false, true]), Ok(value) if { value.is_valid() }) ); } diff --git a/src/unit_tests/rules/sumofsquarespartition_ilp.rs b/src/unit_tests/rules/sumofsquarespartition_ilp.rs index 5827569bb..862423737 100644 --- a/src/unit_tests/rules/sumofsquarespartition_ilp.rs +++ b/src/unit_tests/rules/sumofsquarespartition_ilp.rs @@ -58,12 +58,19 @@ fn test_solution_extraction() { // element 0→g0, element 1→g1, element 2→g1, element 3→g0 // x_{0,0}=1,x_{0,1}=0, x_{1,0}=0,x_{1,1}=1, x_{2,0}=0,x_{2,1}=1, x_{3,0}=1,x_{3,1}=0 - // Set x vars, leave z vars as 0 for extraction test + // Set assignment variables and their within-group products. let mut ilp_solution = vec![0_i64; 4 * 2 + 4 * 4 * 2]; ilp_solution[0] = 1; // x_{0,0} ilp_solution[3] = 1; // x_{1,1} ilp_solution[5] = 1; // x_{2,1} ilp_solution[6] = 1; // x_{3,0} + for (group, members) in [(0, [0, 3]), (1, [1, 2])] { + for i in members { + for j in members { + ilp_solution[8 + (i * 4 + j) * 2 + group] = 1; + } + } + } let extracted = reduction.extract_solution(&ilp_solution).unwrap(); assert_eq!(extracted, vec![0, 1, 1, 0]); } diff --git a/src/unit_tests/rules/threedimensionalmatching_ilp.rs b/src/unit_tests/rules/threedimensionalmatching_ilp.rs index aaec26d1e..30eebc232 100644 --- a/src/unit_tests/rules/threedimensionalmatching_ilp.rs +++ b/src/unit_tests/rules/threedimensionalmatching_ilp.rs @@ -3,7 +3,7 @@ use crate::models::algebraic::{Comparison, ObjectiveSense, ILP}; use crate::models::misc::{ResourceConstrainedScheduling, ThreePartition}; use crate::models::set::ThreeDimensionalMatching; use crate::rules::{ReduceTo, ReductionGraph, ReductionResult}; -use crate::solvers::{BruteForce, ILPSolveError, ILPSolver}; +use crate::solvers::{BruteForce, ILPSolver}; use crate::traits::Problem; use crate::types::Or; @@ -112,8 +112,9 @@ fn test_threedimensionalmatching_to_ilp_infeasible_instance() { BruteForce::new().solve(&problem).unwrap().is_none(), "source instance should be infeasible" ); - assert!( - ILPSolver::new().solve(reduction.target_problem()).is_err(), + assert_eq!( + ILPSolver::new().solve(reduction.target_problem()), + Err(crate::solvers::ILPSolveError::Infeasible), "reduced ILP should be infeasible" ); } @@ -138,11 +139,6 @@ fn test_threedimensionalmatching_to_ilp_direct_path_beats_indirect_chain() { let direct_source = direct.extract_solution(&direct_solution).unwrap(); assert_eq!(problem.evaluate(&direct_source).unwrap(), Or(true)); - let indirect_solution = solver.solve(indirect.target_problem()); - assert!( - matches!(indirect_solution, Err(ILPSolveError::Extraction(_))), - "the numerically unstable indirect ILP should be rejected: {indirect_solution:?}" - ); assert!(direct.target_problem().num_vars() < indirect.target_problem().num_vars()); assert!( direct.target_problem().constraints().len() < indirect.target_problem().constraints().len() diff --git a/src/unit_tests/rules/threedimensionalmatching_minimumweightdecoding.rs b/src/unit_tests/rules/threedimensionalmatching_minimumweightdecoding.rs index 9b3edb61b..3ce6a03dd 100644 --- a/src/unit_tests/rules/threedimensionalmatching_minimumweightdecoding.rs +++ b/src/unit_tests/rules/threedimensionalmatching_minimumweightdecoding.rs @@ -175,7 +175,7 @@ fn test_threedimensionalmatching_to_minimumweightdecoding_solution_extraction_id ); } - assert!(reduction - .extract_solution(&vec![false, true, false]) - .is_err()); + assert!( + !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![false, true, false]), Ok(value) if { value.is_valid() }) + ); } diff --git a/src/unit_tests/rules/threepartition_sequencingwithreleasetimesanddeadlines.rs b/src/unit_tests/rules/threepartition_sequencingwithreleasetimesanddeadlines.rs index c7b85f5c7..015232d58 100644 --- a/src/unit_tests/rules/threepartition_sequencingwithreleasetimesanddeadlines.rs +++ b/src/unit_tests/rules/threepartition_sequencingwithreleasetimesanddeadlines.rs @@ -2,7 +2,6 @@ use super::*; use crate::models::misc::{SequencingWithReleaseTimesAndDeadlines, ThreePartition}; use crate::rules::test_helpers::assert_satisfaction_round_trip_from_satisfaction_target; use crate::solvers::BruteForce; -use crate::solvers::BruteForceProblem as _; use crate::traits::Problem; fn reduce(sizes: Vec, bound: i64) -> (ThreePartition, ReductionThreePartitionToSRTD) { @@ -91,6 +90,6 @@ fn test_threepartition_to_sequencingwithreleasetimesanddeadlines_dims() { let target = reduction.target_problem(); // 7 tasks -> Lehmer dims [7,6,5,4,3,2,1] - let dims = target.dimensions(); + let dims = crate::solvers::cartesian_dimensions(target).unwrap(); assert_eq!(dims, vec![7, 6, 5, 4, 3, 2, 1]); } diff --git a/src/unit_tests/rules/timetabledesign_ilp.rs b/src/unit_tests/rules/timetabledesign_ilp.rs index 14fd854ac..eac518e1c 100644 --- a/src/unit_tests/rules/timetabledesign_ilp.rs +++ b/src/unit_tests/rules/timetabledesign_ilp.rs @@ -51,8 +51,9 @@ fn test_timetabledesign_to_ilp_infeasible() { // Craftsman 0 available only in period 0, but needs 2 periods of work with task 0 let problem = TimetableDesign::new(1, 1, 1, vec![vec![true]], vec![vec![true]], vec![vec![2]]); let reduction = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); - assert!( - ILPSolver::new().solve(reduction.target_problem()).is_err(), + assert_eq!( + ILPSolver::new().solve(reduction.target_problem()), + Err(crate::solvers::ILPSolveError::Infeasible), "infeasible TD should produce infeasible ILP" ); } diff --git a/src/unit_tests/rules/traits.rs b/src/unit_tests/rules/traits.rs index bfc64c583..df2815fd0 100644 --- a/src/unit_tests/rules/traits.rs +++ b/src/unit_tests/rules/traits.rs @@ -1,11 +1,6 @@ -#[test] -fn test_traits_compile() { - // Traits should compile - actual tests in reduction implementations -} - use crate::rules::traits::{ - validate_target_solution, AggregateReductionResult, DynAggregateReductionResult, ReduceTo, - ReduceToAggregate, ReductionResult, + AggregateReductionResult, DynAggregateReductionResult, ReduceTo, ReduceToAggregate, + ReductionResult, }; use crate::traits::Problem; use crate::types::Sum; @@ -47,37 +42,28 @@ impl Problem for SourceProblem { } } -impl crate::solvers::BruteForceProblem for SourceProblem { - fn dimensions(&self) -> Vec { - vec![2, 2] - } -} - impl Problem for TargetProblem { const NAME: &'static str = "Target"; type Solution = Vec; - type Value = i64; + type Value = crate::types::Max; crate::problem_parameters![("num_variables", num_variables)]; - fn evaluate(&self, config: &Self::Solution) -> Result { + fn evaluate( + &self, + config: &Self::Solution, + ) -> Result { if config.len() != 2 || config.iter().any(|&value| value >= 2) { return Err(crate::traits::EvaluationError::InvalidConfiguration( "expected two binary target values".to_string(), )); } - Ok((config[0] + config[1]) as i64) + Ok(crate::types::Max(Some((config[0] + config[1]) as i64))) } fn variant() -> Vec<(&'static str, &'static str)> { vec![("graph", "SimpleGraph"), ("weight", "i64")] } } -impl crate::solvers::BruteForceProblem for TargetProblem { - fn dimensions(&self) -> Vec { - vec![2, 2] - } -} - #[derive(Clone)] struct TestReduction { target: TargetProblem, @@ -112,43 +98,43 @@ fn test_reduction() { let result = >::reduce_to(&source) .expect("reduction should succeed"); let target = result.target_problem(); - assert_eq!(target.evaluate(&vec![1, 1]).unwrap(), 2); + assert_eq!( + target.evaluate(&vec![1, 1]).unwrap(), + crate::types::Max(Some(2)) + ); assert_eq!(result.extract_solution(&vec![1, 0]).unwrap(), vec![1, 0]); } -#[test] -fn target_solution_validation_rejects_shape_and_domain_errors() { - let target = TargetProblem; - - assert_eq!(validate_target_solution(&target, &vec![1, 0]).unwrap(), 1); - assert!(validate_target_solution(&target, &vec![1]).is_err()); - assert!(validate_target_solution(&target, &vec![1, 0, 0]).is_err()); - assert!(validate_target_solution(&target, &vec![1, 2]).is_err()); -} - #[test] fn aggregate_value_from_solution_keeps_evaluation_errors_distinct_from_false() { use crate::models::decision::Decision; use crate::models::graph::MinimumVertexCover; use crate::rules::ExtractionError; use crate::topology::SimpleGraph; - use crate::types::Or; let source = Decision::new( MinimumVertexCover::new(SimpleGraph::new(2, vec![(0, 1)]), vec![1i64; 2]), 0, ); - let reduction = source.reduce_to_aggregate().unwrap(); - let value = reduction - .extract_value_from_solution_dyn(&vec![true, false]) + let edge = crate::rules::registry::reduction_entries() + .into_iter() + .find(|edge| { + edge.source_name == "DecisionMinimumVertexCover" + && edge.target_name == "MinimumVertexCover" + && (edge.source_variant_fn)() + == > as Problem>::variant() + }) .unwrap(); - assert_eq!(value.downcast_ref::(), Some(&Or(false))); + let step = (edge.reduce_fn.unwrap())(&source).unwrap(); + let interpret = step.interpret_optimum.as_ref().unwrap(); + let value = interpret(&vec![true, false]).unwrap(); + assert!(!value); assert!(matches!( - reduction.extract_value_from_solution_dyn(&vec![true]), + interpret(&vec![true]), Err(ExtractionError::Evaluation(_)) )); assert!(matches!( - reduction.extract_value_from_solution_dyn(&vec![1i64, 0]), + interpret(&vec![1i64, 0]), Err(ExtractionError::InvalidTargetSolution(_)) )); } @@ -190,12 +176,6 @@ impl Problem for AggregateSourceProblem { } } -impl crate::solvers::BruteForceProblem for AggregateSourceProblem { - fn dimensions(&self) -> Vec { - vec![2] - } -} - impl Problem for AggregateTargetProblem { const NAME: &'static str = "AggregateTarget"; type Solution = Vec; @@ -215,12 +195,6 @@ impl Problem for AggregateTargetProblem { } } -impl crate::solvers::BruteForceProblem for AggregateTargetProblem { - fn dimensions(&self) -> Vec { - vec![2] - } -} - struct TestAggregateReduction { target: AggregateTargetProblem, offset: u64, diff --git a/src/unit_tests/rules/travelingsalesman_ilp.rs b/src/unit_tests/rules/travelingsalesman_ilp.rs index d7c630715..394a33dea 100644 --- a/src/unit_tests/rules/travelingsalesman_ilp.rs +++ b/src/unit_tests/rules/travelingsalesman_ilp.rs @@ -108,8 +108,9 @@ fn test_no_hamiltonian_cycle_infeasible() { let ilp_solver = ILPSolver::new(); let result = ilp_solver.solve(ilp); - assert!( - result.is_err(), + assert_eq!( + result, + Err(crate::solvers::ILPSolveError::Infeasible), "Path graph should have no Hamiltonian cycle (infeasible ILP)" ); } diff --git a/src/unit_tests/rules/travelingsalesman_qubo.rs b/src/unit_tests/rules/travelingsalesman_qubo.rs index 77199d7e3..25fc70a65 100644 --- a/src/unit_tests/rules/travelingsalesman_qubo.rs +++ b/src/unit_tests/rules/travelingsalesman_qubo.rs @@ -66,13 +66,13 @@ fn test_travelingsalesman_to_qubo_sizes() { let graph3 = SimpleGraph::new(3, vec![(0, 1), (0, 2), (1, 2)]); let tsp3 = TravelingSalesman::new(graph3, vec![1i64; 3]); let reduction3 = ReduceTo::>::reduce_to(&tsp3).expect("reduction should succeed"); - assert_eq!(reduction3.target_problem().num_variables(), 9); + assert_eq!(reduction3.target_problem().num_variables().unwrap(), 9); // K4: n=4, QUBO should have n^2 = 16 variables let graph4 = SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]); let tsp4 = TravelingSalesman::new(graph4, vec![1i64; 6]); let reduction4 = ReduceTo::>::reduce_to(&tsp4).expect("reduction should succeed"); - assert_eq!(reduction4.target_problem().num_variables(), 16); + assert_eq!(reduction4.target_problem().num_variables().unwrap(), 16); } #[test] @@ -86,3 +86,84 @@ fn test_travelingsalesman_to_qubo_weighted_corpus_regression() { "weighted TSP position encoding", ); } + +#[test] +fn signed_and_small_tours_recover_all_optima_or_infeasibility() { + let cases = [ + (0, vec![], vec![]), + (1, vec![], vec![]), + (1, vec![(0, 0), (0, 0)], vec![4, -2]), + (2, vec![(0, 1)], vec![1]), + (2, vec![(0, 1), (0, 1), (0, 1)], vec![4, -2, 1]), + (3, vec![(0, 1), (1, 2)], vec![-5, 2]), + (3, vec![(0, 1), (1, 2), (0, 2)], vec![-5, 2, 1]), + ( + 3, + vec![(0, 1), (0, 1), (1, 2), (0, 2), (1, 1)], + vec![4, -5, 2, 1, -100], + ), + ( + 4, + vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)], + vec![-9, 1, 2, 3, 4, -8], + ), + ]; + for (n, edges, weights) in cases { + let m = edges.len(); + let source = TravelingSalesman::new(SimpleGraph::new(n, edges), weights); + let expected = (0..1usize << m) + .filter_map(|bits| { + source + .evaluate(&(0..m).map(|i| bits & (1 << i) != 0).collect()) + .unwrap() + .0 + }) + .min(); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + let entry = inventory::iter:: + .into_iter() + .find(|entry| entry.source_name == "TravelingSalesman" && entry.target_name == "QUBO") + .unwrap(); + let chain = + crate::rules::ReductionChain::execute(&source, &[entry.reduce_fn.unwrap()]).unwrap(); + + let solutions = BruteForce::new() + .find_all_witnesses(reduction.target_problem()) + .unwrap(); + assert!(!solutions.is_empty()); + for solution in solutions { + let completed = crate::solvers::complete_reduction( + &source, + &chain, + &crate::solvers::SolveOutcome::Optimal { + solution: serde_json::to_value(&solution).unwrap(), + evaluation: String::new(), + }, + ) + .unwrap(); + assert_eq!( + matches!(completed, crate::solvers::SolveOutcome::Optimal { .. }), + expected.is_some() + ); + assert_eq!( + crate::rules::AggregateReductionResult::extract_value( + &reduction, + reduction.target_problem().evaluate(&solution).unwrap() + ), + Min(expected) + ); + if expected.is_some() { + assert_eq!( + source + .evaluate(&reduction.extract_solution(&solution).unwrap()) + .unwrap(), + Min(expected) + ); + } + } + assert_eq!( + crate::rules::AggregateReductionResult::extract_value(&reduction, Min(None)), + Min(None) + ); + } +} diff --git a/src/unit_tests/rules/undirectedflowlowerbounds_ilp.rs b/src/unit_tests/rules/undirectedflowlowerbounds_ilp.rs index feb60cb9b..98ee7436d 100644 --- a/src/unit_tests/rules/undirectedflowlowerbounds_ilp.rs +++ b/src/unit_tests/rules/undirectedflowlowerbounds_ilp.rs @@ -76,8 +76,9 @@ fn test_undirectedflowlowerbounds_to_ilp_infeasible() { let problem = infeasible_instance(); let reduction: ReductionUFLBToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); - assert!( - ILPSolver::new().solve(reduction.target_problem()).is_err(), + assert_eq!( + ILPSolver::new().solve(reduction.target_problem()), + Err(crate::solvers::ILPSolveError::Infeasible), "infeasible instance should produce infeasible ILP" ); } diff --git a/src/unit_tests/rules/undirectedtwocommodityintegralflow_ilp.rs b/src/unit_tests/rules/undirectedtwocommodityintegralflow_ilp.rs index 1d8d087ed..9fa8be746 100644 --- a/src/unit_tests/rules/undirectedtwocommodityintegralflow_ilp.rs +++ b/src/unit_tests/rules/undirectedtwocommodityintegralflow_ilp.rs @@ -116,8 +116,9 @@ fn test_undirectedtwocommodityintegralflow_to_ilp_infeasible() { let problem = infeasible_instance(); let reduction: ReductionU2CIFToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); - assert!( - ILPSolver::new().solve(reduction.target_problem()).is_err(), + assert_eq!( + ILPSolver::new().solve(reduction.target_problem()), + Err(crate::solvers::ILPSolveError::Infeasible), "infeasible flow instance should yield infeasible ILP" ); } @@ -136,7 +137,10 @@ fn test_other_commodity_source_cannot_create_flow() { ); let reduction: ReductionU2CIFToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); - assert!(ILPSolver::new().solve(reduction.target_problem()).is_err()); + assert_eq!( + ILPSolver::new().solve(reduction.target_problem()), + Err(crate::solvers::ILPSolveError::Infeasible) + ); } #[test] diff --git a/src/unit_tests/solvers/brute_force.rs b/src/unit_tests/solvers/brute_force.rs index f2b6547d7..8c653314c 100644 --- a/src/unit_tests/solvers/brute_force.rs +++ b/src/unit_tests/solvers/brute_force.rs @@ -14,7 +14,12 @@ impl Problem for MaxSumProblem { type Solution = Vec; type Value = Max; - crate::problem_parameters![("num_variables", num_variables)]; + fn parameter_names() -> &'static [&'static str] { + &["num_variables"] + } + fn parameters(&self) -> crate::types::ProblemParameters { + crate::types::ProblemParameters::new(vec![("num_variables", self.weights.len() as u64)]) + } fn evaluate( &self, @@ -37,8 +42,12 @@ impl Problem for MaxSumProblem { } impl crate::solvers::BruteForceProblem for MaxSumProblem { - fn dimensions(&self) -> Vec { - vec![2; self.weights.len()] + fn num_variables(&self) -> Result { + Ok(self.weights.len()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } @@ -52,7 +61,12 @@ impl Problem for MinSumProblem { type Solution = Vec; type Value = Min; - crate::problem_parameters![("num_variables", num_variables)]; + fn parameter_names() -> &'static [&'static str] { + &["num_variables"] + } + fn parameters(&self) -> crate::types::ProblemParameters { + crate::types::ProblemParameters::new(vec![("num_variables", self.weights.len() as u64)]) + } fn evaluate( &self, @@ -75,8 +89,12 @@ impl Problem for MinSumProblem { } impl crate::solvers::BruteForceProblem for MinSumProblem { - fn dimensions(&self) -> Vec { - vec![2; self.weights.len()] + fn num_variables(&self) -> Result { + Ok(self.weights.len()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } @@ -91,7 +109,12 @@ impl Problem for SatProblem { type Solution = Vec; type Value = Or; - crate::problem_parameters![("num_variables", num_variables)]; + fn parameter_names() -> &'static [&'static str] { + &["num_variables"] + } + fn parameters(&self) -> crate::types::ProblemParameters { + crate::types::ProblemParameters::new(vec![("num_variables", self.num_vars as u64)]) + } fn evaluate( &self, @@ -106,8 +129,12 @@ impl Problem for SatProblem { } impl crate::solvers::BruteForceProblem for SatProblem { - fn dimensions(&self) -> Vec { - vec![2; self.num_vars] + fn num_variables(&self) -> Result { + Ok(self.num_vars) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } @@ -119,7 +146,12 @@ impl Problem for EvaluationFailureProblem { type Solution = Vec; type Value = Or; - crate::problem_parameters![("num_variables", num_variables)]; + fn parameter_names() -> &'static [&'static str] { + &["num_variables"] + } + fn parameters(&self) -> crate::types::ProblemParameters { + crate::types::ProblemParameters::new(vec![("num_variables", 1usize as u64)]) + } fn evaluate(&self, config: &Self::Solution) -> Result { if config.as_slice() == [1] { @@ -137,8 +169,12 @@ impl Problem for EvaluationFailureProblem { } impl crate::solvers::BruteForceProblem for EvaluationFailureProblem { - fn dimensions(&self) -> Vec { - vec![2] + fn num_variables(&self) -> Result { + Ok(1usize) + } + + fn dimension(&self, variable: usize) -> Result { + Ok([2][variable]) } } @@ -150,7 +186,12 @@ impl Problem for AggregationFailureProblem { type Solution = Vec; type Value = Max; - crate::problem_parameters![("num_variables", num_variables)]; + fn parameter_names() -> &'static [&'static str] { + &["num_variables"] + } + fn parameters(&self) -> crate::types::ProblemParameters { + crate::types::ProblemParameters::new(vec![("num_variables", 1usize as u64)]) + } fn evaluate(&self, _: &Self::Solution) -> Result, crate::traits::EvaluationError> { Ok(Max(Some(f64::NAN))) @@ -162,8 +203,12 @@ impl Problem for AggregationFailureProblem { } impl crate::solvers::BruteForceProblem for AggregationFailureProblem { - fn dimensions(&self) -> Vec { - vec![2] + fn num_variables(&self) -> Result { + Ok(1usize) + } + + fn dimension(&self, variable: usize) -> Result { + Ok([2][variable]) } } @@ -178,7 +223,12 @@ impl Problem for CountingSatProblem { type Solution = Vec; type Value = Or; - crate::problem_parameters![("num_variables", num_variables)]; + fn parameter_names() -> &'static [&'static str] { + &["num_variables"] + } + fn parameters(&self) -> crate::types::ProblemParameters { + crate::types::ProblemParameters::new(vec![("num_variables", 2usize as u64)]) + } fn evaluate( &self, @@ -196,8 +246,12 @@ impl Problem for CountingSatProblem { } impl crate::solvers::BruteForceProblem for CountingSatProblem { - fn dimensions(&self) -> Vec { - vec![2, 2] + fn num_variables(&self) -> Result { + Ok(2usize) + } + + fn dimension(&self, variable: usize) -> Result { + Ok([2, 2][variable]) } } @@ -563,18 +617,284 @@ fn cartesian_indices_zero_dimension_has_no_candidates() { } #[test] -fn cartesian_indices_is_exact_size() { +fn cartesian_indices_reports_exhaustion_without_a_total_count() { let mut indices = CartesianIndices::new(vec![2, 3]).unwrap(); - assert_eq!(indices.len(), 6); - indices.next(); - assert_eq!(indices.len(), 5); + assert_eq!(indices.size_hint(), (1, None)); + assert_eq!(indices.by_ref().count(), 6); + assert_eq!(indices.size_hint(), (0, Some(0))); + assert_eq!(indices.next(), None); } #[test] -fn cartesian_indices_reports_cardinality_overflow() { +fn cartesian_indices_visits_a_prefix_when_the_total_exceeds_usize() { + let prefix = CartesianIndices::new(vec![usize::MAX, 2]) + .unwrap() + .take(4) + .collect::>(); + assert_eq!(prefix, vec![vec![0, 0], vec![0, 1], vec![1, 0], vec![1, 1]]); +} + +#[test] +fn enumeration_reports_coordinate_count_and_storage_errors() { + use crate::models::set::SetBasis; + let count_overflow = SetBasis::new(2, vec![], usize::MAX); assert!(matches!( - CartesianIndices::new(vec![usize::MAX, 2]), - Err(crate::solvers::SolveError::SearchSpaceOverflow(dimensions)) - if dimensions == vec![usize::MAX, 2] + BruteForceProblem::num_variables(&count_overflow), + Err(SolveError::IntegerOverflow(_)) )); + assert!(matches!( + BruteForce::new().solve(&count_overflow), + Err(SolveError::IntegerOverflow(_)) + )); + let allocation_overflow = SetBasis::new(1, vec![], usize::MAX); + assert!(matches!( + cartesian_dimensions(&allocation_overflow), + Err(SolveError::Allocation(_)) + )); +} + +#[test] +fn window_product_does_not_restrict_construction_or_evaluation() { + use crate::models::misc::ClosestSubstring; + let problem = ClosestSubstring::new(1, vec![vec![0, 0]; 64], 1).unwrap(); + let restored: ClosestSubstring = + serde_json::from_value(serde_json::to_value(&problem).unwrap()).unwrap(); + assert_eq!(restored.evaluate(&vec![0; 65]).unwrap(), Min(Some(0))); + assert_eq!(restored.parameters(), problem.parameters()); + let dimensions = crate::solvers::cartesian_dimensions(&restored).unwrap(); + assert_eq!(dimensions[0], 1); + assert_eq!(&dimensions[1..], &[2; 64]); + let prefix: Vec<_> = CartesianIndices::new(dimensions).unwrap().take(2).collect(); + assert_eq!(prefix.len(), 2); + for witness in prefix { + assert_eq!(restored.evaluate(&witness).unwrap(), Min(Some(0))); + } +} + +#[test] +fn scalar_counts_report_unrepresentable_search_coordinates() { + use crate::models::algebraic::BMF; + use crate::models::misc::{ConsistencyOfDatabaseFrequencyTables, EnsembleComputation}; + let cases = [ + ( + "biclique slots", + crate::models::graph::BicliqueCover::new( + crate::topology::BipartiteGraph::new(1, 1, vec![(0, 0)]), + usize::MAX, + ) + .num_variables(), + ), + ( + "tree slots", + crate::models::graph::KthBestSpanningTree::new( + crate::topology::SimpleGraph::new(3, vec![(0, 1), (1, 2)]), + vec![1i64, 1], + usize::MAX, + 2, + ) + .num_variables(), + ), + ( + "tile slots", + crate::models::misc::SquareTiling::new(1, vec![(0, 0, 0, 0)], usize::MAX) + .num_variables(), + ), + ( + "factor rows", + BMF::new(vec![vec![true]; 2], usize::MAX).num_variables(), + ), + ( + "factor columns", + BMF::new(vec![vec![true; 2]], usize::MAX).num_variables(), + ), + ( + "factor sum", + BMF::new(vec![vec![true]], usize::MAX).num_variables(), + ), + ( + "operation operands", + EnsembleComputation::new(1, vec![], usize::MAX).num_variables(), + ), + ( + "database entries", + ConsistencyOfDatabaseFrequencyTables::new(usize::MAX, vec![1, 1], vec![], vec![]) + .num_variables(), + ), + ]; + for (context, result) in cases { + assert!( + matches!(result, Err(SolveError::IntegerOverflow(_))), + "{context}: {result:?}" + ); + } +} + +#[test] +fn scalar_domains_report_unrepresentable_coordinate_cardinalities() { + use crate::models::misc::{ + ConjunctiveQueryFoldability, EnsembleComputation, MinimumExternalMacroDataCompression, + MinimumInternalMacroDataCompression, + }; + let external = MinimumExternalMacroDataCompression::new(usize::MAX, vec![0], 1); + let cases = [ + ("external symbol", external.dimension(0)), + ("external pointer", external.dimension(1)), + ( + "internal alphabet", + MinimumInternalMacroDataCompression::new(usize::MAX, vec![0], 1).dimension(0), + ), + ( + "internal sentinel", + MinimumInternalMacroDataCompression::new(usize::MAX - 1, vec![0], 1).dimension(0), + ), + ( + "operand labels", + EnsembleComputation::new(usize::MAX, vec![], 1).dimension(0), + ), + ( + "distinguished labels", + ConjunctiveQueryFoldability::new(usize::MAX, 1, 1, vec![], vec![], vec![]).dimension(0), + ), + ( + "undistinguished labels", + ConjunctiveQueryFoldability::new(usize::MAX, 0, 1, vec![], vec![], vec![]).dimension(0), + ), + ]; + for (context, result) in cases { + assert!( + matches!(result, Err(SolveError::IntegerOverflow(_))), + "{context}: {result:?}" + ); + } +} + +#[test] +fn string_domains_reserve_a_representable_sentinel() { + use crate::models::misc::{ + LongestCommonSubsequence, ShortestCommonSupersequence, ShortestCommonSuperstring, + }; + use crate::models::set::ConsecutiveSets; + let cases = [ + ( + "subsequence", + LongestCommonSubsequence::new(usize::MAX, vec![vec![0]]).dimension(0), + ), + ( + "supersequence", + ShortestCommonSupersequence::new(usize::MAX, vec![vec![0]]).dimension(0), + ), + ( + "superstring", + ShortestCommonSuperstring::new(usize::MAX, vec![vec![0]]).dimension(0), + ), + ( + "consecutive sets", + ConsecutiveSets::new(usize::MAX, vec![vec![0]], 1).dimension(0), + ), + ]; + for (context, result) in cases { + assert!( + matches!(result, Err(SolveError::IntegerOverflow(_))), + "{context}: {result:?}" + ); + } +} + +#[test] +fn decision_tree_slots_fail_before_enumeration_storage_is_allocated() { + use crate::models::misc::MinimumDecisionTree; + let objects = usize::BITS as usize + 1; + let tests = objects.ilog2() as usize + 1; + let matrix = (0..tests) + .map(|bit| { + (0..objects) + .map(|object| object & (1 << bit) != 0) + .collect() + }) + .collect(); + let problem = MinimumDecisionTree::new(matrix, objects, tests); + assert!(matches!( + cartesian_dimensions(&problem), + Err(SolveError::Evaluation( + crate::traits::EvaluationError::IntegerOverflow(_) + )) + )); +} + +#[test] +fn large_products_remain_symbolic_in_model_parameters() { + use crate::models::misc::{ + ConsistencyOfDatabaseFrequencyTables, MinimumDiscretePlanarInverseKinematics, + }; + let arm = MinimumDiscretePlanarInverseKinematics::new( + vec![1.0; 64], + (64.0, 0.0), + vec![vec![0.0, 1.0]; 64], + vec![vec![(0, 0), (0, 1), (1, 0), (1, 1)]; 63], + ) + .unwrap(); + let restored: MinimumDiscretePlanarInverseKinematics = + serde_json::from_value(serde_json::to_value(&arm).unwrap()).unwrap(); + assert_eq!(arm.parameters(), restored.parameters()); + assert_eq!(arm.evaluate(&vec![0; 64]).unwrap(), Min(Some(0.0))); + assert_eq!( + CartesianIndices::new(cartesian_dimensions(&arm).unwrap()) + .unwrap() + .take(2) + .count(), + 2 + ); + let database = ConsistencyOfDatabaseFrequencyTables::new(1, vec![2; 64], vec![], vec![]); + let restored: ConsistencyOfDatabaseFrequencyTables = + serde_json::from_value(serde_json::to_value(&database).unwrap()).unwrap(); + assert_eq!(database.parameters(), restored.parameters()); + assert_eq!(database.evaluate(&vec![0; 64]).unwrap(), Or(true)); +} + +#[test] +fn test_max_solution_selection() { + assert!(Max::contributes_to_solution(&Max(Some(7)), &Max(Some(7)))); + assert!(!Max::contributes_to_solution(&Max(Some(3)), &Max(Some(7)))); + assert!(!Max::contributes_to_solution(&Max(None), &Max(Some(7)))); +} + +#[test] +fn test_min_solution_selection() { + assert!(Min::contributes_to_solution(&Min(Some(3)), &Min(Some(3)))); + assert!(!Min::contributes_to_solution(&Min(Some(7)), &Min(Some(3)))); + assert!(!Min::contributes_to_solution(&Min(None), &Min(Some(3)))); +} + +#[test] +fn test_or_solution_selection() { + assert!(Or::contributes_to_solution(&Or(true), &Or(true))); + assert!(!Or::contributes_to_solution(&Or(false), &Or(true))); + assert!(!Or::contributes_to_solution(&Or(true), &Or(false))); +} + +#[test] +fn test_extremum_solution_selection() { + // Matching value and sense -> contributes + assert!(Extremum::contributes_to_solution( + &Extremum::maximize(Some(10)), + &Extremum::maximize(Some(10)), + )); + + // Different value -> does not contribute + assert!(!Extremum::contributes_to_solution( + &Extremum::maximize(Some(5)), + &Extremum::maximize(Some(10)), + )); + + // None config -> does not contribute + assert!(!Extremum::contributes_to_solution( + &Extremum::::maximize(None), + &Extremum::maximize(Some(10)), + )); +} + +#[test] +fn test_minimumcutintoboundedsets_selects_optimal_solutions() { + type Value = as Problem>::Value; + assert!(Value::contributes_to_solution(&Min(Some(3)), &Min(Some(3)))); } diff --git a/src/unit_tests/solvers/customized/closest_vector_problem.rs b/src/unit_tests/solvers/customized/closest_vector_problem.rs index fe570f3f2..240a43792 100644 --- a/src/unit_tests/solvers/customized/closest_vector_problem.rs +++ b/src/unit_tests/solvers/customized/closest_vector_problem.rs @@ -31,31 +31,12 @@ fn test_cvp_solver_keeps_zero_on_tie_and_handles_empty_basis() { } #[test] -fn test_cvp_solver_reports_inexact_integer_conversion() { - let problem = ClosestVectorProblem::new( - vec![vec![crate::types::MAX_EXACT_F64_INTEGER + 1]], - vec![0_i64], - ) - .unwrap(); - assert!(matches!( - solve(&problem), - Err(crate::solvers::SolveError::InexactFloatConversion(_)) - )); - +fn test_cvp_solver_reports_search_representation_overflow() { let out_of_range = ClosestVectorProblem::new(vec![vec![1]], vec![1e20]).unwrap(); assert!(matches!( solve(&out_of_range), Err(SolveError::IntegerOverflow(_)) )); - let inexact = ClosestVectorProblem::new( - vec![vec![1]], - vec![crate::types::MAX_EXACT_F64_INTEGER as f64 + 2.0], - ) - .unwrap(); - assert!(matches!( - solve(&inexact), - Err(SolveError::InexactFloatConversion(_)) - )); } #[test] @@ -141,7 +122,10 @@ fn test_cvp_pruning_preserves_exact_large_translation_optimum() { let expected = vec![coefficient, coefficient]; assert_eq!(solve(&integer).unwrap(), expected); assert_eq!(solve(&real).unwrap(), expected); - assert_eq!(integer.evaluate(&expected).unwrap().0, Some(0.0)); + assert_eq!( + integer.evaluate(&expected).unwrap().0, + Some(BigRational::zero()) + ); } } diff --git a/src/unit_tests/solvers/customized/minimum_decision_tree.rs b/src/unit_tests/solvers/customized/minimum_decision_tree.rs index 6905c543e..45ebd53be 100644 --- a/src/unit_tests/solvers/customized/minimum_decision_tree.rs +++ b/src/unit_tests/solvers/customized/minimum_decision_tree.rs @@ -1,5 +1,6 @@ use super::*; -use crate::solvers::BruteForce; +use crate::registry::load_dyn; +use crate::solvers::{BruteForce, SolveOutcome, SolverExecution, SolverRequest}; use crate::traits::Problem; #[test] @@ -43,6 +44,50 @@ fn test_subset_dp_minimum_decision_tree_handles_eight_objects() { .map(|bit| (0..8).map(|object| object & (1 << bit) != 0).collect()) .collect(); let problem = MinimumDecisionTree::new(matrix, 8, 3); - let solution = solve(&problem).unwrap(); + let loaded = load_dyn( + MinimumDecisionTree::NAME, + &Default::default(), + serde_json::to_value(&problem).unwrap(), + ) + .unwrap(); + let result = crate::solvers::solve(&loaded, SolverRequest::Default).unwrap(); + assert!(matches!( + result.solver, + SolverExecution::Customized { + implementation: "subset-dp" + } + )); + let SolveOutcome::Optimal { + solution, + evaluation, + } = result.outcome + else { + panic!("the instance has a solution"); + }; + assert_eq!(evaluation, "Min(24)"); + let solution = serde_json::from_value(solution).unwrap(); assert_eq!(problem.evaluate(&solution).unwrap().0, Some(24)); } + +#[test] +fn subset_dp_reports_mask_and_table_representation_errors() { + for n in [usize::BITS as usize, usize::BITS as usize - 1] { + let tests = (n.ilog2() + 1) as usize; + let matrix = (0..tests) + .map(|bit| (0..n).map(|object| object & (1 << bit) != 0).collect()) + .collect(); + let problem = MinimumDecisionTree::new(matrix, n, tests); + let loaded = load_dyn( + MinimumDecisionTree::NAME, + &Default::default(), + serde_json::to_value(&problem).unwrap(), + ) + .unwrap(); + let error = crate::solvers::solve(&loaded, SolverRequest::Default).unwrap_err(); + if n == usize::BITS as usize { + assert!(matches!(error, SolveError::IntegerOverflow(_))); + } else { + assert!(matches!(error, SolveError::Allocation(_))); + } + } +} diff --git a/src/unit_tests/solvers/customized/shortest_common_superstring.rs b/src/unit_tests/solvers/customized/shortest_common_superstring.rs index a331c02f7..79565095b 100644 --- a/src/unit_tests/solvers/customized/shortest_common_superstring.rs +++ b/src/unit_tests/solvers/customized/shortest_common_superstring.rs @@ -1,5 +1,6 @@ use super::*; -use crate::solvers::BruteForce; +use crate::registry::load_dyn; +use crate::solvers::{BruteForce, SolveOutcome, SolverExecution, SolverRequest}; use crate::traits::Problem; #[test] @@ -35,6 +36,45 @@ fn test_subset_dp_shortest_common_superstring_handles_containment_and_scale() { vec![0, 1], ], ); - let solution = solve(&problem).unwrap(); + let loaded = load_dyn( + ShortestCommonSuperstring::NAME, + &Default::default(), + serde_json::to_value(&problem).unwrap(), + ) + .unwrap(); + let result = crate::solvers::solve(&loaded, SolverRequest::Default).unwrap(); + assert!(matches!( + result.solver, + SolverExecution::Customized { + implementation: "subset-dp" + } + )); + let SolveOutcome::Optimal { + solution, + evaluation, + } = result.outcome + else { + panic!("the instance has a solution"); + }; + assert_eq!(evaluation, "Min(6)"); + let solution = serde_json::from_value(solution).unwrap(); assert_eq!(problem.evaluate(&solution).unwrap().0, Some(6)); } + +#[test] +fn subset_dp_reports_mask_and_table_size_overflow() { + for count in [usize::BITS as usize, usize::BITS as usize - 1] { + let problem = + ShortestCommonSuperstring::new(count, (0..count).map(|symbol| vec![symbol]).collect()); + let loaded = load_dyn( + ShortestCommonSuperstring::NAME, + &Default::default(), + serde_json::to_value(&problem).unwrap(), + ) + .unwrap(); + assert!(matches!( + crate::solvers::solve(&loaded, SolverRequest::Default), + Err(SolveError::IntegerOverflow(_)) + )); + } +} diff --git a/src/unit_tests/solvers/customized/solver.rs b/src/unit_tests/solvers/customized/solver.rs index 85294c3e6..dcc2729c3 100644 --- a/src/unit_tests/solvers/customized/solver.rs +++ b/src/unit_tests/solvers/customized/solver.rs @@ -1,7 +1,6 @@ use crate::models::graph::{PartialFeedbackEdgeSet, RootedTreeArrangement}; use crate::solvers::brute_force::CartesianIndices; use crate::solvers::registry::solver_capability_registry; -use crate::solvers::BruteForceProblem as _; use crate::solvers::ExactProblemKey; use crate::topology::{Graph, SimpleGraph}; use crate::traits::Problem; @@ -59,7 +58,7 @@ fn exact_partial_feedback_edge_set_feasible( max_cycle_length: usize, ) -> bool { let problem = PartialFeedbackEdgeSet::new(graph.clone(), budget, max_cycle_length); - CartesianIndices::new(problem.dimensions()) + CartesianIndices::new(crate::solvers::cartesian_dimensions(&problem).unwrap()) .unwrap() .any(|config| { let solution = crate::config::config_to_bits(&config); @@ -69,7 +68,7 @@ fn exact_partial_feedback_edge_set_feasible( fn exact_rooted_tree_arrangement_min_stretch(graph: &SimpleGraph) -> Option { let problem = RootedTreeArrangement::new(graph.clone(), i64::MAX); - CartesianIndices::new(problem.dimensions()) + CartesianIndices::new(crate::solvers::cartesian_dimensions(&problem).unwrap()) .unwrap() .filter_map(|config| problem.total_edge_stretch(&config).unwrap()) .min() diff --git a/src/unit_tests/solvers/ilp/adapter.rs b/src/unit_tests/solvers/ilp/adapter.rs new file mode 100644 index 000000000..f78545a0a --- /dev/null +++ b/src/unit_tests/solvers/ilp/adapter.rs @@ -0,0 +1,235 @@ +use super::*; +use crate::models::algebraic::{IntegerVariable, LinearConstraint}; + +#[test] +fn backend_statuses_preserve_termination_causes() { + assert_eq!(accept_backend_status(HighsModelStatus::Optimal), Ok(())); + assert_eq!( + accept_backend_status(HighsModelStatus::Infeasible), + Err(IlpBackendError::Infeasible) + ); + assert_eq!( + accept_backend_status(HighsModelStatus::Unbounded), + Err(IlpBackendError::Unbounded) + ); + assert_eq!( + accept_backend_status(HighsModelStatus::ReachedTimeLimit), + Err(IlpBackendError::Timeout) + ); + for status in [ + HighsModelStatus::UnboundedOrInfeasible, + HighsModelStatus::SolveError, + HighsModelStatus::ObjectiveBound, + HighsModelStatus::ObjectiveTarget, + HighsModelStatus::ReachedIterationLimit, + HighsModelStatus::ReachedMemoryLimit, + HighsModelStatus::ReachedSolutionLimit, + HighsModelStatus::ReachedInterrupt, + ] { + assert!(matches!(accept_backend_status(status), + Err(IlpBackendError::BackendFailure(message)) if message.contains(&format!("{status:?}")))); + } +} + +#[test] +fn native_terminals_return_the_input_ilp_solution_format() { + let adapter = HighsAdapter::new(None); + let boolean_integer = ILP::::new( + 2, + vec![LinearConstraint::le(vec![(0, 1), (1, 1)], 1)], + vec![(0, 1), (1, 2)], + ObjectiveSense::Maximize, + ) + .unwrap(); + let boolean_float = ILP::::new( + 2, + vec![LinearConstraint::le(vec![(0, 1.0), (1, 1.0)], 1.0)], + vec![(0, 1.0), (1, 2.0)], + ObjectiveSense::Maximize, + ) + .unwrap(); + let integer_integer = ILP::::with_variables( + vec![IntegerVariable::new(Some(-2), Some(3)).unwrap()], + vec![], + vec![(0, 1)], + ObjectiveSense::Minimize, + ) + .unwrap(); + let integer_float = ILP::::with_variables( + vec![IntegerVariable::new(Some(-2), Some(3)).unwrap()], + vec![], + vec![(0, 1.0)], + ObjectiveSense::Minimize, + ) + .unwrap(); + let values: [Vec; 4] = [ + adapter.solve(&boolean_integer).unwrap(), + adapter.solve(&boolean_float).unwrap(), + adapter.solve(&integer_integer).unwrap(), + adapter.solve(&integer_float).unwrap(), + ]; + assert_eq!(values, [vec![0, 1], vec![0, 1], vec![-2], vec![-2]]); +} + +#[test] +fn decoding_checks_shape_integrality_range_and_original_constraints() { + let ilp = ILP::::new( + 2, + vec![LinearConstraint::eq(vec![(0, 1), (1, 1)], 1)], + vec![(0, 1)], + ObjectiveSense::Maximize, + ) + .unwrap(); + assert_eq!( + decode_and_validate(&ilp, [1.00000001, 0.0]).unwrap(), + vec![1, 0] + ); + for raw in [ + vec![], + vec![1.0], + vec![1.0, 0.0, 0.0], + vec![1.0, 1.0], + vec![2.0, -1.0], + vec![0.5, 0.5], + vec![f64::NAN, 0.0], + vec![f64::INFINITY, 0.0], + vec![f64::NEG_INFINITY, 0.0], + vec![i64::MAX as f64, 0.0], + vec![i64::MIN as f64, 0.0], + ] { + assert!(matches!( + decode_and_validate(&ilp, raw), + Err(IlpBackendError::InvalidSolution(_)) + )); + } +} + +#[test] +fn validation_rejects_constraint_violations_in_both_coefficient_domains() { + let integer = ILP::::new( + 1, + vec![LinearConstraint::le(vec![(0, 2)], 1)], + vec![], + ObjectiveSense::Minimize, + ) + .unwrap(); + assert!(matches!( + decode_and_validate(&integer, [1.0]), + Err(IlpBackendError::InvalidSolution(_)) + )); + let float = ILP::::new( + 1, + vec![LinearConstraint::le(vec![(0, 1.0)], 1.0 - 5e-10)], + vec![], + ObjectiveSense::Minimize, + ) + .unwrap(); + assert!(!float.is_feasible(&[1]).unwrap()); + assert!(matches!( + decode_and_validate(&float, [1.0]), + Err(IlpBackendError::InvalidSolution(_)) + )); +} + +#[test] +fn validation_propagates_constraint_and_objective_overflow() { + let objective = ILP::::new( + 2, + vec![], + vec![(0, i64::MAX), (1, 1)], + ObjectiveSense::Maximize, + ) + .unwrap(); + let constraint = ILP::::new( + 2, + vec![LinearConstraint::le(vec![(0, i64::MAX), (1, 1)], 0)], + vec![], + ObjectiveSense::Maximize, + ) + .unwrap(); + for ilp in [objective, constraint] { + assert!(matches!( + decode_and_validate(&ilp, [1.0, 1.0]), + Err(IlpBackendError::InvalidSolution(_)) + )); + } +} + +#[test] +fn coefficient_encoding_enforces_supported_transport_range() { + assert_eq!(BackendCoefficient::to_backend_number(17_i64).unwrap(), 17.0); + assert_eq!(BackendCoefficient::to_backend_number(0.5_f64).unwrap(), 0.5); + let value = MAX_EXACT_F64_INTEGER + 1; + for ilp in [ + ILP::::new(1, vec![], vec![(0, value)], ObjectiveSense::Maximize).unwrap(), + ILP::::new( + 1, + vec![LinearConstraint::le(vec![(0, value)], 1)], + vec![], + ObjectiveSense::Maximize, + ) + .unwrap(), + ILP::::new( + 1, + vec![LinearConstraint::le(vec![(0, 1)], value)], + vec![], + ObjectiveSense::Maximize, + ) + .unwrap(), + ] { + assert!(matches!( + HighsAdapter::new(None).solve(&ilp), + Err(IlpBackendError::InexactTransport(_)) + )); + } +} + +#[test] +fn invalid_time_limits_are_errors_instead_of_backend_panics() { + for time in [-1.0, f64::NAN, f64::INFINITY] { + assert!(matches!( + HighsAdapter::new(Some(time)).solve(&ILP::::empty()), + Err(IlpBackendError::BackendFailure(_)) + )); + } +} + +#[test] +fn adapter_accepts_an_ilp_domain_without_any_registry_entry() { + #[derive(Clone, Debug)] + struct UnregisteredDomain; + impl VariableDomain for UnregisteredDomain { + const NAME: &'static str = "UnregisteredDomain"; + fn default_variable() -> IntegerVariable { + ::default_variable() + } + fn validate_variables( + variables: &[IntegerVariable], + ) -> Result<(), crate::registry::ConstructionError> { + ::validate_variables(variables) + } + } + let ilp = ILP::::with_variables( + vec![IntegerVariable::new(Some(0), Some(2)).unwrap()], + vec![], + vec![(0, 1)], + ObjectiveSense::Maximize, + ) + .unwrap(); + assert_eq!(HighsAdapter::new(None).solve(&ilp).unwrap(), vec![2]); +} + +#[test] +fn backend_model_loading_failure_is_an_explicit_error() { + let ilp = ILP::::new( + 1, + vec![LinearConstraint::le(vec![(0, 1e30)], 1.0)], + vec![], + ObjectiveSense::Minimize, + ) + .unwrap(); + assert!(matches!( + HighsAdapter::new(None).solve(&ilp), + Err(IlpBackendError::BackendFailure(message)) if message.contains("loading HiGHS model") + )); +} diff --git a/src/unit_tests/solvers/ilp/solver.rs b/src/unit_tests/solvers/ilp/solver.rs index 5c193553e..c4e07649e 100644 --- a/src/unit_tests/solvers/ilp/solver.rs +++ b/src/unit_tests/solvers/ilp/solver.rs @@ -1,5 +1,5 @@ use super::*; -use crate::models::algebraic::{IntegerVariable, LinearConstraint}; +use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::traits::Problem; fn binary_ilp( @@ -113,26 +113,6 @@ fn test_ilp_solver_rejects_inexact_integer_transport() { )); } -#[test] -fn test_backend_errors_are_classified_without_losing_the_cause() { - assert_eq!( - classify_backend_error(ResolutionError::Infeasible, None), - ILPSolveError::Infeasible, - ); - assert_eq!( - classify_backend_error(ResolutionError::Unbounded, None), - ILPSolveError::Unbounded, - ); - assert_eq!( - classify_backend_error(ResolutionError::Other("NoSolutionFound"), Some(0.1)), - ILPSolveError::Timeout, - ); - assert!(matches!( - classify_backend_error(ResolutionError::Other("SolveError"), None), - ILPSolveError::BackendFailure(message) if message.contains("SolveError") - )); -} - #[test] fn test_ilp_rejects_solution_that_is_infeasible_after_rounding() { let ilp = binary_ilp( @@ -259,69 +239,43 @@ fn test_registered_ilp_pipeline_success() { } #[test] -fn test_ilp_solve_dyn_bool() { - let ilp = ILP::::new(1, vec![], vec![(0, 1.0)], ObjectiveSense::Maximize).unwrap(); - assert!(ILPSolver::new() - .solve_dyn(&ilp as &dyn std::any::Any) - .is_ok()); +fn test_float_qubo_objective_matches_reference() { + use crate::models::algebraic::QUBO; + use crate::solvers::BruteForce; + + let source = QUBO::::from_matrix(vec![ + vec![0.5, -2.5, 1.5, -4.0], + vec![0.0, -3.5, 4.0, -3.0], + vec![0.0, 0.0, 1.0, 4.5], + vec![0.0, 0.0, 0.0, -4.0], + ]) + .unwrap(); + let actual = ILPSolver::new().solve(&source).unwrap(); + let reference = BruteForce::new().solve(&source).unwrap().unwrap(); + let actual_value = source.evaluate(&actual).unwrap(); + assert!(actual_value.is_valid()); + assert_eq!(actual_value, source.evaluate(&reference).unwrap()); } #[test] -fn test_ilp_solve_dyn_i64() { - let ilp = ILP::::with_variables( - vec![ - IntegerVariable::new(Some(0), Some(3)).unwrap(), - IntegerVariable::new(Some(0), Some(3)).unwrap(), - ], - vec![], +fn test_ilp_solver_rejects_objective_overflow_after_backend_success() { + let ilp = ILP::::with_variables( + vec![IntegerVariable::new(Some(1025), Some(1025)).unwrap()], vec![], - ObjectiveSense::Minimize, + vec![(0, crate::types::MAX_EXACT_F64_INTEGER)], + ObjectiveSense::Maximize, ) .unwrap(); - assert!(ILPSolver::new() - .solve_dyn(&ilp as &dyn std::any::Any) - .is_ok()); -} - -#[test] -fn test_ilp_solve_dyn_unknown_type_returns_unsupported_problem_type() { - let result = ILPSolver::new().solve_dyn(&42_i64 as &dyn std::any::Any); - assert_eq!(result, Err(ILPSolveError::UnsupportedProblemType)); -} - -// Test acceptance policy in source-objective units, separate from variable rounding. -// This allows small absolute numerical differences near zero; it is not a -// guaranteed objective-error bound derived from HiGHS feasibility tolerances. -fn objective_close(a: f64, b: f64) -> bool { - let abs_tol = 1e-7; - let rel_tol = 1e-7; - (a - b).abs() <= abs_tol + rel_tol * a.abs().max(b.abs()) + assert!(matches!( + ILPSolver::new().solve(&ilp), + Err(ILPSolveError::InvalidSolution(_)) + )); } #[test] -fn test_float_qubo_objective_matches_reference_within_tolerance() { - use crate::models::algebraic::QUBO; - use crate::solvers::BruteForce; - - for scale in [1e-9, 1.0] { - let matrix = vec![ - vec![1.0, -5.0, 3.0, -8.0], - vec![0.0, -7.0, 8.0, -6.0], - vec![0.0, 0.0, 2.0, 9.0], - vec![0.0, 0.0, 0.0, -8.0], - ] - .into_iter() - .map(|row| row.into_iter().map(|v| v * scale).collect()) - .collect(); - let source = QUBO::::from_matrix(matrix).unwrap(); - let actual = ILPSolver::new().solve(&source).unwrap(); - let reference = BruteForce::new().solve(&source).unwrap().unwrap(); - let actual_value = source.evaluate(&actual).unwrap(); - let reference_value = source.evaluate(&reference).unwrap(); - assert!(actual_value.is_valid()); - assert!(objective_close( - actual_value.0.unwrap(), - reference_value.0.unwrap() - )); - } +fn test_invalid_public_time_limit_returns_existing_backend_error() { + assert!(matches!( + ILPSolver::with_time_limit(-1.0).solve(&ILP::::empty()), + Err(ILPSolveError::BackendFailure(_)) + )); } diff --git a/src/unit_tests/solvers/registry.rs b/src/unit_tests/solvers/registry.rs index 97c55d085..734706e6c 100644 --- a/src/unit_tests/solvers/registry.rs +++ b/src/unit_tests/solvers/registry.rs @@ -32,10 +32,6 @@ fn generic_decision_ilp_respects_maximization_bounds() { name: "ILP", variant: BOOL_VARIANT, }, - StaticProblemStep { - name: "ILP", - variant: FLOAT_BOOL_VARIANT, - }, ], }; let registry = build_registry( @@ -54,11 +50,11 @@ fn generic_decision_ilp_respects_maximization_bounds() { ); for bound in [0, 1, 2] { let decision = Decision::new(inner.clone(), bound); - let result = pipeline.solve(&decision, &crate::solvers::ILPSolver::new()); + let result = pipeline.solve(&decision, &HighsAdapter::new(None)); if bound > 1 { assert!(matches!( result, - Err(crate::solvers::ILPSolveError::UnresolvedDecision(_)) + Err(crate::solvers::ILPSolveError::Infeasible) )); assert!(BruteForce::new().solve(&decision).unwrap().is_none()); continue; @@ -72,22 +68,22 @@ fn generic_decision_ilp_respects_maximization_bounds() { } #[test] -fn generic_decision_ilp_reports_unresolved_but_preserves_extraction_errors() { +fn generic_decision_ilp_reports_no_but_preserves_extraction_errors() { use crate::models::decision::Decision; use crate::models::graph::MinimumVertexCover; use crate::rules::{ExtractionError, ReductionResult}; - use crate::solvers::{ILPSolveError, ILPSolver}; + use crate::solvers::ILPSolveError; use crate::topology::SimpleGraph; use crate::traits::Problem; type Inner = MinimumVertexCover; - struct BrokenExtractor(Inner); + struct BrokenExtractor(Decision); impl ReductionResult for BrokenExtractor { type Source = Decision; type Target = Inner; fn target_problem(&self) -> &Inner { - &self.0 + self.0.inner() } fn extract_solution(&self, _: &Vec) -> crate::rules::ExtractionResult> { @@ -95,6 +91,20 @@ fn generic_decision_ilp_reports_unresolved_but_preserves_extraction_errors() { } } + impl crate::rules::AggregateReductionResult for BrokenExtractor { + type Source = Decision; + type Target = Inner; + fn target_problem(&self) -> &Inner { + self.0.inner() + } + fn extract_value(&self, value: crate::types::Min) -> crate::types::Or { + crate::types::Or(crate::types::OptimizationValue::meets_bound( + &value, + self.0.bound(), + )) + } + } + let source = ExactProblemKey::new( Decision::::NAME, Decision::::variant() @@ -108,17 +118,33 @@ fn generic_decision_ilp_reports_unresolved_but_preserves_extraction_errors() { path: original.path.clone(), reducers: original.reducers.clone(), }; - pipeline.reducers[0].0 = |source| { + pipeline.reducers[0] = |source| { let source = source.downcast_ref::>().unwrap(); - Ok(Box::new(BrokenExtractor(source.inner().clone()))) + let result = std::rc::Rc::new(BrokenExtractor(source.clone())); + Ok(crate::rules::registry::ExecutedStep { + aggregate: Some(result.clone()), + interpret_optimum: Some({ + let result = result.clone(); + std::rc::Rc::new(move |solution: &dyn std::any::Any| { + let solution = solution.downcast_ref::>().unwrap(); + let value = result.0.inner().evaluate(solution)?; + Ok(crate::rules::AggregateReductionResult::extract_value( + result.as_ref(), + value, + ) + .is_valid()) + }) + }), + witness: result, + }) }; let inner = Inner::new(SimpleGraph::new(2, vec![(0, 1)]), vec![1i64; 2]); assert!(matches!( - pipeline.solve(&Decision::new(inner.clone(), 0), &ILPSolver::new()), - Err(ILPSolveError::UnresolvedDecision(_)) + pipeline.solve(&Decision::new(inner.clone(), 0), &HighsAdapter::new(None)), + Err(ILPSolveError::Infeasible) )); assert!(matches!( - pipeline.solve(&Decision::new(inner, 1), &ILPSolver::new()), + pipeline.solve(&Decision::new(inner, 1), &HighsAdapter::new(None)), Err(ILPSolveError::Extraction(ExtractionError::Reduction { message, .. })) if message == "broken witness decoder" )); @@ -406,11 +432,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", - "ILP" - ] + ["MaximumClique", "ILP"] ); let multihop_ilp = solver_capabilities(&key( @@ -435,10 +457,7 @@ fn solver_capability_registry_exposes_representative_capability_classes() { let ilp_itself = solver_capabilities(&key("ILP", &[("variable", "bool"), ("coefficient", "i64")])).unwrap(); - assert_eq!( - ilp_itself.ilp.unwrap().path_labels(), - ["ILP", "ILP"] - ); + assert_eq!(ilp_itself.ilp.unwrap().path_labels(), ["ILP"]); } #[test] @@ -538,18 +557,12 @@ fn solver_capability_registry_ignores_unrelated_reduction_edges() { minimal_pipeline .reducers .iter() - .map(|(reducer, aggregate)| ( - *reducer as usize, - aggregate.map(|reduce| reduce as usize) - )) + .map(|reducer| *reducer as usize) .collect::>(), expanded_pipeline .reducers .iter() - .map(|(reducer, aggregate)| ( - *reducer as usize, - aggregate.map(|reduce| reduce as usize) - )) + .map(|reducer| *reducer as usize) .collect::>() ); } @@ -588,3 +601,34 @@ fn solver_capability_registry_ambiguous_exact_edge_is_rejected() { RegistryBuildError::InvalidEdge { matches: 2, .. } )); } + +#[test] +fn native_terminal_dispatch_rejects_non_ilp_values() { + assert_eq!( + solve_ilp_terminal(&42_i64, &HighsAdapter::new(None)), + Err(crate::solvers::ILPSolveError::UnsupportedProblemType) + ); +} + +#[test] +fn registered_pipelines_stop_at_the_first_native_ilp() { + let registry = solver_capability_registry().unwrap(); + for pipeline in registry.ilp.values() { + assert!(pipeline.path.last().unwrap().is_supported_ilp()); + assert!(pipeline.path[..pipeline.path.len() - 1] + .iter() + .all(|step| !step.is_supported_ilp())); + } + for variable in ["bool", "i64"] { + for coefficient in ["i64", "f64"] { + let key = ExactProblemKey::new( + "ILP", + BTreeMap::from([ + ("variable".into(), variable.into()), + ("coefficient".into(), coefficient.into()), + ]), + ); + assert_eq!(registry.lookup(&key).ilp.unwrap().path(), &[key]); + } + } +} diff --git a/src/unit_tests/solvers/resolver.rs b/src/unit_tests/solvers/resolver.rs index 8cc05f909..5d10db5c3 100644 --- a/src/unit_tests/solvers/resolver.rs +++ b/src/unit_tests/solvers/resolver.rs @@ -42,16 +42,6 @@ fn decision_reductions_check_target_optimum_before_extracting_witness() { SolverRequest::Default, ] { let result = solve(&problem, backend); - if matches!( - &result, - Err(crate::solvers::SolveError::IlpSolve { - source: crate::solvers::ILPSolveError::UnresolvedDecision(_), - .. - }) - ) { - assert!(!expected, "{name}, {backend:?}"); - continue; - } match result.unwrap().outcome { SolveOutcome::Optimal { solution, @@ -59,7 +49,10 @@ fn decision_reductions_check_target_optimum_before_extracting_witness() { } => { assert!(expected, "{name}, {backend:?}"); assert_eq!(evaluation, "Or(true)"); - assert_eq!(problem.evaluate_dyn(&solution).unwrap(), "Or(true)"); + assert_eq!( + problem.evaluate_dyn(&solution).unwrap(), + ("Or(true)".into(), true) + ); } SolveOutcome::Infeasible => assert!(!expected, "{name}, {backend:?}"), } @@ -85,16 +78,6 @@ fn hamiltonian_ilp_matches_exhaustive_search_on_small_graphs() { .unwrap(); let reference = solve(&problem, SolverRequest::BruteForce).unwrap(); let actual = solve(&problem, SolverRequest::Ilp); - if matches!( - &actual, - Err(crate::solvers::SolveError::IlpSolve { - source: crate::solvers::ILPSolveError::UnresolvedDecision(_), - .. - }) - ) { - assert!(matches!(reference.outcome, SolveOutcome::Infeasible)); - continue; - } let actual = actual.unwrap(); assert_eq!( matches!(actual.outcome, SolveOutcome::Infeasible), @@ -102,7 +85,10 @@ fn hamiltonian_ilp_matches_exhaustive_search_on_small_graphs() { "graph {mask}" ); if let SolveOutcome::Optimal { solution, .. } = actual.outcome { - assert_eq!(problem.evaluate_dyn(&solution).unwrap(), "Or(true)"); + assert_eq!( + problem.evaluate_dyn(&solution).unwrap(), + ("Or(true)".into(), true) + ); } } } @@ -153,19 +139,6 @@ fn generic_decision_ilp_compares_inner_optimum_with_bound() { SolverRequest::Default, ] { let result = solve(&loaded, backend); - if bound < optimum && backend != SolverRequest::BruteForce { - assert!( - matches!( - result, - Err(crate::solvers::SolveError::IlpSolve { - source: crate::solvers::ILPSolveError::UnresolvedDecision(_), - .. - }) - ), - "{name}, {bound}, {backend:?}" - ); - continue; - } let result = result.unwrap(); if bound < optimum { assert_eq!( @@ -182,7 +155,10 @@ fn generic_decision_ilp_compares_inner_optimum_with_bound() { panic!("expected a witness for {name}, {bound}, {backend:?}"); }; assert_eq!(evaluation, "Or(true)"); - assert_eq!(loaded.evaluate_dyn(&solution).unwrap(), "Or(true)"); + assert_eq!( + loaded.evaluate_dyn(&solution).unwrap(), + ("Or(true)".into(), true) + ); } } } @@ -239,19 +215,6 @@ fn generic_decision_ilp_matches_exhaustive_search_on_small_graphs() { .unwrap(); let reference = solve(&loaded, SolverRequest::BruteForce).unwrap(); let actual = solve(&loaded, SolverRequest::Ilp); - if matches!(reference.outcome, SolveOutcome::Infeasible) { - assert!( - matches!( - actual, - Err(crate::solvers::SolveError::IlpSolve { - source: crate::solvers::ILPSolveError::UnresolvedDecision(_), - .. - }) - ), - "{name}, graph {mask}, bound {bound}" - ); - continue; - } let actual = actual.unwrap(); assert_eq!( matches!(actual.outcome, SolveOutcome::Infeasible), @@ -259,7 +222,10 @@ fn generic_decision_ilp_matches_exhaustive_search_on_small_graphs() { "{name}, graph {mask}, bound {bound}" ); if let SolveOutcome::Optimal { solution, .. } = actual.outcome { - assert_eq!(loaded.evaluate_dyn(&solution).unwrap(), "Or(true)"); + assert_eq!( + loaded.evaluate_dyn(&solution).unwrap(), + ("Or(true)".into(), true) + ); } } } @@ -365,7 +331,7 @@ fn deterministic_solver_dispatch_customized_infeasibility_does_not_fall_back() { } #[test] -fn deterministic_solver_dispatch_integer_ilp_uses_registered_cast_pipeline() { +fn deterministic_solver_dispatch_integer_ilp_uses_native_terminal() { let problem = ILP::::new(0, vec![], vec![], ObjectiveSense::Minimize).unwrap(); let loaded = load_dyn( ILP::::NAME, @@ -381,7 +347,7 @@ fn deterministic_solver_dispatch_integer_ilp_uses_registered_cast_pipeline() { assert_eq!( result.solver, SolverExecution::Ilp { - reduction_path: vec!["ILP".to_string(), "ILP".to_string()] + reduction_path: vec!["ILP".to_string()] } ); assert!(matches!( @@ -498,7 +464,6 @@ fn deterministic_solver_dispatch_fixed_multihop_pipeline_is_repeatable() { "MaximumIndependentSet", "MaximumSetPacking", "ILP", - "ILP", ] ); } @@ -589,22 +554,6 @@ fn check_unit_dominating_decision(num_vertices: usize, edges: &[(usize, usize)], let reference = solve(&problem, SolverRequest::BruteForce).unwrap(); for backend in [SolverRequest::Ilp, SolverRequest::Default] { let actual = solve(&problem, backend); - if matches!(reference.outcome, SolveOutcome::Infeasible) { - assert!( - matches!( - actual, - Err(crate::solvers::SolveError::IlpSolve { - source: crate::solvers::ILPSolveError::UnresolvedDecision(_), - .. - }) | Ok(crate::solvers::SolveResult { - outcome: SolveOutcome::Infeasible, - .. - }) - ), - "n={num_vertices}, edges={edges:?}, bound={bound}" - ); - continue; - } let actual = actual.unwrap(); let SolverExecution::Ilp { reduction_path } = &actual.solver else { panic!("expected the registered ILP pipeline"); @@ -626,7 +575,10 @@ fn check_unit_dominating_decision(num_vertices: usize, edges: &[(usize, usize)], } = actual.outcome { assert_eq!(evaluation, "Or(true)"); - assert_eq!(problem.evaluate_dyn(&solution).unwrap(), "Or(true)"); + assert_eq!( + problem.evaluate_dyn(&solution).unwrap(), + ("Or(true)".into(), true) + ); } } } diff --git a/src/unit_tests/trait_consistency.rs b/src/unit_tests/trait_consistency.rs index 4f766da84..8518a207e 100644 --- a/src/unit_tests/trait_consistency.rs +++ b/src/unit_tests/trait_consistency.rs @@ -8,7 +8,7 @@ use crate::topology::{BipartiteGraph, DirectedGraph, SimpleGraph}; use crate::variant::K3; fn check_brute_force_problem(problem: &P, name: &str) { - let dims = problem.dimensions(); + let dims = crate::solvers::cartesian_dimensions(&problem).unwrap(); assert!( !dims.is_empty() || name.contains("empty"), "{} should have dimensions", diff --git a/src/unit_tests/traits.rs b/src/unit_tests/traits.rs index 2c6cec82d..2fb2e851c 100644 --- a/src/unit_tests/traits.rs +++ b/src/unit_tests/traits.rs @@ -13,7 +13,12 @@ impl Problem for TestSatProblem { type Solution = Vec; type Value = Or; - crate::problem_parameters![("num_variables", num_variables)]; + fn parameter_names() -> &'static [&'static str] { + &["num_variables"] + } + fn parameters(&self) -> crate::types::ProblemParameters { + crate::types::ProblemParameters::new(vec![("num_variables", self.num_vars as u64)]) + } fn evaluate( &self, @@ -28,8 +33,12 @@ impl Problem for TestSatProblem { } impl crate::solvers::BruteForceProblem for TestSatProblem { - fn dimensions(&self) -> Vec { - vec![2; self.num_vars] + fn num_variables(&self) -> Result { + Ok(self.num_vars) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } @@ -40,7 +49,10 @@ fn test_problem_sat() { satisfying: vec![vec![1, 0], vec![0, 1]], }; - assert_eq!(p.dimensions(), vec![2, 2]); + assert_eq!( + crate::solvers::cartesian_dimensions(&p).unwrap(), + vec![2, 2] + ); assert_eq!(p.evaluate(&vec![1, 0]).unwrap(), Or(true)); assert_eq!(p.evaluate(&vec![0, 0]).unwrap(), Or(false)); } @@ -52,8 +64,8 @@ fn test_problem_num_variables() { satisfying: vec![], }; - assert_eq!(p.num_variables(), 5); - assert_eq!(p.dimensions().len(), 5); + assert_eq!(p.num_variables().unwrap(), 5); + assert_eq!(crate::solvers::cartesian_dimensions(&p).unwrap().len(), 5); } #[test] @@ -63,8 +75,8 @@ fn test_problem_empty() { satisfying: vec![], }; - assert_eq!(p.num_variables(), 0); - assert!(p.dimensions().is_empty()); + assert_eq!(p.num_variables().unwrap(), 0); + assert!(crate::solvers::cartesian_dimensions(&p).unwrap().is_empty()); } #[derive(Clone)] @@ -77,7 +89,12 @@ impl Problem for TestMaxProblem { type Solution = Vec; type Value = Max; - crate::problem_parameters![("num_variables", num_variables)]; + fn parameter_names() -> &'static [&'static str] { + &["num_variables"] + } + fn parameters(&self) -> crate::types::ProblemParameters { + crate::types::ProblemParameters::new(vec![("num_variables", self.weights.len() as u64)]) + } fn evaluate( &self, @@ -99,12 +116,6 @@ impl Problem for TestMaxProblem { } } -impl crate::solvers::BruteForceProblem for TestMaxProblem { - fn dimensions(&self) -> Vec { - vec![2; self.weights.len()] - } -} - #[derive(Clone)] struct TestMinProblem { costs: Vec, @@ -115,7 +126,12 @@ impl Problem for TestMinProblem { type Solution = Vec; type Value = Min; - crate::problem_parameters![("num_variables", num_variables)]; + fn parameter_names() -> &'static [&'static str] { + &["num_variables"] + } + fn parameters(&self) -> crate::types::ProblemParameters { + crate::types::ProblemParameters::new(vec![("num_variables", self.costs.len() as u64)]) + } fn evaluate( &self, @@ -137,12 +153,6 @@ impl Problem for TestMinProblem { } } -impl crate::solvers::BruteForceProblem for TestMinProblem { - fn dimensions(&self) -> Vec { - vec![2; self.costs.len()] - } -} - #[test] fn test_problem_max_value() { let p = TestMaxProblem { @@ -175,7 +185,12 @@ impl Problem for MultiDimProblem { type Solution = Vec; type Value = Sum; - crate::problem_parameters![("num_variables", num_variables)]; + fn parameter_names() -> &'static [&'static str] { + &["num_variables"] + } + fn parameters(&self) -> crate::types::ProblemParameters { + crate::types::ProblemParameters::new(vec![("num_variables", self.dims.len() as u64)]) + } fn evaluate( &self, @@ -190,8 +205,12 @@ impl Problem for MultiDimProblem { } impl crate::solvers::BruteForceProblem for MultiDimProblem { - fn dimensions(&self) -> Vec { - self.dims.clone() + fn num_variables(&self) -> Result { + Ok(self.dims.len()) + } + + fn dimension(&self, variable: usize) -> Result { + Ok(self.dims[variable]) } } @@ -201,8 +220,11 @@ fn test_multi_dim_problem() { dims: vec![2, 3, 4], }; - assert_eq!(p.dimensions(), vec![2, 3, 4]); - assert_eq!(p.num_variables(), 3); + assert_eq!( + crate::solvers::cartesian_dimensions(&p).unwrap(), + vec![2, 3, 4] + ); + assert_eq!(p.num_variables().unwrap(), 3); assert_eq!(p.evaluate(&vec![0, 0, 0]).unwrap(), Sum(0)); assert_eq!(p.evaluate(&vec![1, 2, 3]).unwrap(), Sum(6)); } @@ -225,7 +247,12 @@ impl Problem for FloatProblem { type Solution = Vec; type Value = Max; - crate::problem_parameters![("num_variables", num_variables)]; + fn parameter_names() -> &'static [&'static str] { + &["num_variables"] + } + fn parameters(&self) -> crate::types::ProblemParameters { + crate::types::ProblemParameters::new(vec![("num_variables", self.weights.len() as u64)]) + } fn evaluate( &self, @@ -248,8 +275,12 @@ impl Problem for FloatProblem { } impl crate::solvers::BruteForceProblem for FloatProblem { - fn dimensions(&self) -> Vec { - vec![2; self.weights.len()] + fn num_variables(&self) -> Result { + Ok(self.weights.len()) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2usize) } } @@ -259,7 +290,10 @@ fn test_float_value_problem() { weights: vec![1.5, 2.5, 3.0], }; - assert_eq!(p.dimensions(), vec![2, 2, 2]); + assert_eq!( + crate::solvers::cartesian_dimensions(&p).unwrap(), + vec![2, 2, 2] + ); assert!((p.evaluate(&vec![1, 1, 0]).unwrap().0.unwrap() - 4.0).abs() < 1e-10); assert!((p.evaluate(&vec![1, 1, 1]).unwrap().0.unwrap() - 7.0).abs() < 1e-10); } @@ -283,6 +317,9 @@ fn test_problem_is_clone() { }; let p2 = p1.clone(); - assert_eq!(p2.dimensions(), vec![2, 2]); + assert_eq!( + crate::solvers::cartesian_dimensions(&p2).unwrap(), + vec![2, 2] + ); assert_eq!(p2.evaluate(&vec![1, 0]).unwrap(), Or(true)); } diff --git a/src/unit_tests/truth_table.rs b/src/unit_tests/truth_table.rs index 1685079ae..509305a1f 100644 --- a/src/unit_tests/truth_table.rs +++ b/src/unit_tests/truth_table.rs @@ -2,7 +2,7 @@ use super::*; #[test] fn test_and_gate() { - let and = TruthTable::and(2); + let and = TruthTable::and(2).unwrap(); assert!(!and.evaluate(&[false, false])); assert!(!and.evaluate(&[true, false])); assert!(!and.evaluate(&[false, true])); @@ -11,7 +11,7 @@ fn test_and_gate() { #[test] fn test_or_gate() { - let or = TruthTable::or(2); + let or = TruthTable::or(2).unwrap(); assert!(!or.evaluate(&[false, false])); assert!(or.evaluate(&[true, false])); assert!(or.evaluate(&[false, true])); @@ -27,7 +27,7 @@ fn test_not_gate() { #[test] fn test_xor_gate() { - let xor = TruthTable::xor(2); + let xor = TruthTable::xor(2).unwrap(); assert!(!xor.evaluate(&[false, false])); assert!(xor.evaluate(&[true, false])); assert!(xor.evaluate(&[false, true])); @@ -36,7 +36,7 @@ fn test_xor_gate() { #[test] fn test_nand_gate() { - let nand = TruthTable::nand(2); + let nand = TruthTable::nand(2).unwrap(); assert!(nand.evaluate(&[false, false])); assert!(nand.evaluate(&[true, false])); assert!(nand.evaluate(&[false, true])); @@ -54,7 +54,8 @@ fn test_implies() { #[test] fn test_from_function() { - let majority = TruthTable::from_function(3, |input| input.iter().filter(|&&b| b).count() >= 2); + let majority = + TruthTable::from_function(3, |input| input.iter().filter(|&&b| b).count() >= 2).unwrap(); assert!(!majority.evaluate(&[false, false, false])); assert!(!majority.evaluate(&[true, false, false])); assert!(majority.evaluate(&[true, true, false])); @@ -63,7 +64,7 @@ fn test_from_function() { #[test] fn test_evaluate_config() { - let and = TruthTable::and(2); + let and = TruthTable::and(2).unwrap(); assert!(!and.evaluate_config(&[0, 0])); assert!(!and.evaluate_config(&[1, 0])); assert!(and.evaluate_config(&[1, 1])); @@ -71,26 +72,26 @@ fn test_evaluate_config() { #[test] fn test_satisfiable() { - let or = TruthTable::or(2); + let or = TruthTable::or(2).unwrap(); assert!(or.is_satisfiable()); - let contradiction = TruthTable::from_outputs(2, vec![false, false, false, false]); + let contradiction = TruthTable::from_outputs(2, vec![false, false, false, false]).unwrap(); assert!(!contradiction.is_satisfiable()); assert!(contradiction.is_contradiction()); } #[test] fn test_tautology() { - let tautology = TruthTable::from_outputs(2, vec![true, true, true, true]); + let tautology = TruthTable::from_outputs(2, vec![true, true, true, true]).unwrap(); assert!(tautology.is_tautology()); - let or = TruthTable::or(2); + let or = TruthTable::or(2).unwrap(); assert!(!or.is_tautology()); } #[test] fn test_satisfying_assignments() { - let xor = TruthTable::xor(2); + let xor = TruthTable::xor(2).unwrap(); let sat = xor.satisfying_assignments(); assert_eq!(sat.len(), 2); assert!(sat.contains(&vec![true, false])); @@ -99,14 +100,14 @@ fn test_satisfying_assignments() { #[test] fn test_count() { - let and = TruthTable::and(2); + let and = TruthTable::and(2).unwrap(); assert_eq!(and.count_ones(), 1); assert_eq!(and.count_zeros(), 3); } #[test] fn test_index_to_input() { - let tt = TruthTable::and(3); + let tt = TruthTable::and(3).unwrap(); assert_eq!(tt.index_to_input(0), vec![false, false, false]); assert_eq!(tt.index_to_input(1), vec![true, false, false]); assert_eq!(tt.index_to_input(7), vec![true, true, true]); @@ -114,49 +115,49 @@ fn test_index_to_input() { #[test] fn test_outputs_vec() { - let and = TruthTable::and(2); + let and = TruthTable::and(2).unwrap(); assert_eq!(and.outputs_vec(), vec![false, false, false, true]); } #[test] fn test_and_with() { - let a = TruthTable::from_outputs(1, vec![false, true]); - let b = TruthTable::from_outputs(1, vec![true, false]); + let a = TruthTable::from_outputs(1, vec![false, true]).unwrap(); + let b = TruthTable::from_outputs(1, vec![true, false]).unwrap(); let result = a.and_with(&b); assert_eq!(result.outputs_vec(), vec![false, false]); } #[test] fn test_or_with() { - let a = TruthTable::from_outputs(1, vec![false, true]); - let b = TruthTable::from_outputs(1, vec![true, false]); + let a = TruthTable::from_outputs(1, vec![false, true]).unwrap(); + let b = TruthTable::from_outputs(1, vec![true, false]).unwrap(); let result = a.or_with(&b); assert_eq!(result.outputs_vec(), vec![true, true]); } #[test] fn test_negate() { - let and = TruthTable::and(2); + let and = TruthTable::and(2).unwrap(); let nand = and.negate(); assert_eq!(nand.outputs_vec(), vec![true, true, true, false]); } #[test] fn test_num_rows() { - let tt = TruthTable::and(3); + let tt = TruthTable::and(3).unwrap(); assert_eq!(tt.num_rows(), 8); } #[test] fn test_3_input_and() { - let and3 = TruthTable::and(3); + let and3 = TruthTable::and(3).unwrap(); assert!(!and3.evaluate(&[true, true, false])); assert!(and3.evaluate(&[true, true, true])); } #[test] fn test_xnor() { - let xnor = TruthTable::xnor(2); + let xnor = TruthTable::xnor(2).unwrap(); assert!(xnor.evaluate(&[false, false])); assert!(!xnor.evaluate(&[true, false])); assert!(!xnor.evaluate(&[false, true])); @@ -165,7 +166,7 @@ fn test_xnor() { #[test] fn test_nor() { - let nor = TruthTable::nor(2); + let nor = TruthTable::nor(2).unwrap(); assert!(nor.evaluate(&[false, false])); assert!(!nor.evaluate(&[true, false])); assert!(!nor.evaluate(&[false, true])); @@ -174,7 +175,7 @@ fn test_nor() { #[test] fn test_serialization() { - let and = TruthTable::and(2); + let and = TruthTable::and(2).unwrap(); let json = serde_json::to_string(&and).unwrap(); let deserialized: TruthTable = serde_json::from_str(&json).unwrap(); assert_eq!(and, deserialized); @@ -182,13 +183,37 @@ fn test_serialization() { #[test] fn test_outputs() { - let and = TruthTable::and(2); + let and = TruthTable::and(2).unwrap(); let outputs = and.outputs(); assert_eq!(outputs.len(), 4); } #[test] fn test_num_inputs() { - let and = TruthTable::and(3); + let and = TruthTable::and(3).unwrap(); assert_eq!(and.num_inputs(), 3); } + +#[test] +fn construction_and_deserialization_enforce_row_shape() { + for outputs in [vec![true], vec![false; 5]] { + assert!(TruthTable::from_outputs(2, outputs.clone()).is_err()); + assert!(serde_json::from_value::(serde_json::json!({ + "num_inputs": 2, "outputs": outputs + })) + .is_err()); + } + let inputs = usize::BITS as usize; + assert!(matches!( + TruthTable::from_outputs(inputs, vec![]), + Err(ConstructionError::IntegerOverflow(_)) + )); + assert!(TruthTable::from_function(inputs, |_| true).is_err()); + assert!(serde_json::from_value::(serde_json::json!({ + "num_inputs": inputs, "outputs": [] + })) + .is_err()); + let empty = TruthTable::from_outputs(0, vec![true]).unwrap(); + assert_eq!(empty.num_rows(), 1); + assert!(empty.evaluate(&[])); +} diff --git a/src/unit_tests/types.rs b/src/unit_tests/types.rs index ac53023b0..de88e9934 100644 --- a/src/unit_tests/types.rs +++ b/src/unit_tests/types.rs @@ -1,6 +1,6 @@ use super::*; use crate::traits::EvaluationError; -use crate::types::{Aggregate, SolutionAggregate}; +use crate::types::Aggregate; #[test] fn test_max_identity_and_combine() { @@ -96,27 +96,6 @@ fn test_and_absorbing_value_is_false() { assert!(And(false).is_absorbing()); } -#[test] -fn test_max_solution_selection() { - assert!(Max::contributes_to_solution(&Max(Some(7)), &Max(Some(7)))); - assert!(!Max::contributes_to_solution(&Max(Some(3)), &Max(Some(7)))); - assert!(!Max::contributes_to_solution(&Max(None), &Max(Some(7)))); -} - -#[test] -fn test_min_solution_selection() { - assert!(Min::contributes_to_solution(&Min(Some(3)), &Min(Some(3)))); - assert!(!Min::contributes_to_solution(&Min(Some(7)), &Min(Some(3)))); - assert!(!Min::contributes_to_solution(&Min(None), &Min(Some(3)))); -} - -#[test] -fn test_or_solution_selection() { - assert!(Or::contributes_to_solution(&Or(true), &Or(true))); - assert!(!Or::contributes_to_solution(&Or(false), &Or(true))); - assert!(!Or::contributes_to_solution(&Or(true), &Or(false))); -} - #[test] fn test_max_helpers() { let size = Max(Some(42)); @@ -332,27 +311,6 @@ fn test_extremum_aggregate_identity_and_combine() { assert_eq!(combined, Extremum::minimize(Some(3))); } -#[test] -fn test_extremum_solution_selection() { - // Matching value and sense -> contributes - assert!(Extremum::contributes_to_solution( - &Extremum::maximize(Some(10)), - &Extremum::maximize(Some(10)), - )); - - // Different value -> does not contribute - assert!(!Extremum::contributes_to_solution( - &Extremum::maximize(Some(5)), - &Extremum::maximize(Some(10)), - )); - - // None config -> does not contribute - assert!(!Extremum::contributes_to_solution( - &Extremum::::maximize(None), - &Extremum::maximize(Some(10)), - )); -} - #[test] fn test_extremum_display() { assert_eq!(format!("{}", Extremum::maximize(Some(42))), "Max(42)"); diff --git a/tests/suites/integration.rs b/tests/suites/integration.rs index db976e33f..748140f1c 100644 --- a/tests/suites/integration.rs +++ b/tests/suites/integration.rs @@ -532,3 +532,52 @@ mod weighted_problems { assert!(satisfying.is_empty()); } } + +/// Exercise the solver as a downstream crate: struct construction, generic +/// bounds, solution type, and exhaustive error matching must keep compiling. +#[test] +fn ilp_public_api_supports_generic_witness_solving() { + use problemreductions::solvers::{ILPSolveError, ILPSolver}; + fn solve_generic

(problem: &P) -> std::result::Result + where + P: Problem + 'static, + P::Solution: 'static, + P::Value: problemreductions::solvers::SolutionAggregate, + { + ILPSolver::new().solve(problem) + } + fn classify_error(error: ILPSolveError) -> &'static str { + match error { + ILPSolveError::Infeasible => "infeasible", + ILPSolveError::Unbounded => "unbounded", + ILPSolveError::Timeout => "timeout", + ILPSolveError::BackendFailure(_) => "backend", + ILPSolveError::UnsupportedProblemType => "unsupported", + ILPSolveError::MissingPipeline(_) => "missing pipeline", + ILPSolveError::InvalidRegistry(_) => "registry", + ILPSolveError::PipelineTypeMismatch(_) => "type mismatch", + ILPSolveError::InvalidSolution(_) => "invalid solution", + ILPSolveError::Evaluation(_) => "evaluation", + ILPSolveError::InexactTransport(_) => "transport", + ILPSolveError::Extraction(_) => "extraction", + ILPSolveError::Reduction(_) => "reduction", + } + } + let solver = ILPSolver { time_limit: None }; + let ILPSolver { time_limit } = solver.clone(); + assert_eq!(time_limit, None); + let ilp = ILP::::new(1, vec![], vec![(0, 1)], ObjectiveSense::Maximize).unwrap(); + let solution: Vec = solve_generic(&ilp).unwrap(); + assert_eq!(solution, vec![1]); + let infeasible = ILP::::new( + 0, + vec![LinearConstraint::ge(vec![], 1)], + vec![], + ObjectiveSense::Minimize, + ) + .unwrap(); + assert_eq!( + classify_error(solver.solve(&infeasible).unwrap_err()), + "infeasible" + ); +} diff --git a/tests/suites/ksatisfiability_simultaneous_incongruences.rs b/tests/suites/ksatisfiability_simultaneous_incongruences.rs index 947f81dab..09cdbeb98 100644 --- a/tests/suites/ksatisfiability_simultaneous_incongruences.rs +++ b/tests/suites/ksatisfiability_simultaneous_incongruences.rs @@ -19,7 +19,7 @@ fn test_ksatisfiability_to_simultaneous_incongruences_closed_loop() { .expect("reduction should succeed"); let target = reduction.target_problem(); - assert_eq!(target.lcm_moduli(), 105); + assert_eq!(target.lcm_moduli().unwrap(), 105); assert_eq!(target.num_pairs(), 11); let solver = BruteForce::new(); diff --git a/tests/suites/reductions.rs b/tests/suites/reductions.rs index 51f4746e4..2fba18d41 100644 --- a/tests/suites/reductions.rs +++ b/tests/suites/reductions.rs @@ -246,7 +246,7 @@ mod sg_qubo_reductions { let result = ReduceTo::>::reduce_to(&sg).expect("reduction should succeed"); let qubo = result.target_problem(); - assert_eq!(qubo.num_variables(), 2); + assert_eq!(qubo.num_variables().unwrap(), 2); // Solve QUBO let solver = BruteForce::new(); @@ -574,7 +574,7 @@ mod qubo_reductions { .expect("Should reduce MaximumIndependentSet to QUBO"); let qubo: &QUBO = chain.target_problem(); - assert_eq!(qubo.num_variables(), data.qubo_num_vars); + assert_eq!(qubo.num_variables().unwrap(), data.qubo_num_vars); let solver = BruteForce::new(); let solutions = solver.find_all_witnesses(qubo).unwrap(); @@ -620,7 +620,7 @@ mod qubo_reductions { let reduction = ReduceTo::::reduce_to(&kc).expect("reduction should succeed"); let qubo = reduction.target_problem(); - assert_eq!(qubo.num_variables(), data.qubo_num_vars); + assert_eq!(qubo.num_variables().unwrap(), data.qubo_num_vars); let solver = BruteForce::new(); let solutions = solver.find_all_witnesses(qubo).unwrap(); @@ -657,7 +657,7 @@ mod qubo_reductions { let reduction = ReduceTo::>::reduce_to(&sp).expect("reduction should succeed"); let qubo = reduction.target_problem(); - assert_eq!(qubo.num_variables(), data.qubo_num_vars); + assert_eq!(qubo.num_variables().unwrap(), data.qubo_num_vars); let solver = BruteForce::new(); let solutions = solver.find_all_witnesses(qubo).unwrap(); @@ -727,7 +727,7 @@ mod qubo_reductions { let reduction = ReduceTo::::reduce_to(&ksat).expect("reduction should succeed"); let qubo = reduction.target_problem(); - assert_eq!(qubo.num_variables(), data.qubo_num_vars); + assert_eq!(qubo.num_variables().unwrap(), data.qubo_num_vars); let solver = BruteForce::new(); let solutions = solver.find_all_witnesses(qubo).unwrap(); @@ -815,7 +815,7 @@ mod qubo_reductions { let qubo = reduction.target_problem(); // QUBO may have more variables (slack), but original count matches - assert!(qubo.num_variables() >= data.qubo_num_vars); + assert!(qubo.num_variables().unwrap() >= data.qubo_num_vars); let solver = BruteForce::new(); let solutions = solver.find_all_witnesses(qubo).unwrap(); diff --git a/tests/suites/simultaneous_incongruences.rs b/tests/suites/simultaneous_incongruences.rs index 6c4aa63c0..7306e27e5 100644 --- a/tests/suites/simultaneous_incongruences.rs +++ b/tests/suites/simultaneous_incongruences.rs @@ -1,6 +1,5 @@ use problemreductions::models::algebraic::SimultaneousIncongruences; use problemreductions::solvers::BruteForce; -use problemreductions::solvers::BruteForceProblem as _; use problemreductions::traits::Problem; #[test] @@ -10,8 +9,11 @@ fn test_simultaneous_incongruences_issue_example() { assert_eq!(problem.num_pairs(), 4); assert_eq!(problem.pairs(), &[(2, 2), (1, 3), (2, 5), (3, 7)]); - assert_eq!(problem.lcm_moduli(), 210); - assert_eq!(problem.dimensions(), vec![210]); + assert_eq!(problem.lcm_moduli().unwrap(), 210); + assert_eq!( + problemreductions::solvers::cartesian_dimensions(&problem).unwrap(), + vec![210] + ); // x=5: 5%2=1!=0(=2%2), 5%3=2!=1, 5%5=0!=2, 5%7=5!=3 => valid assert!(problem.evaluate(&5).unwrap()); // x=2: 2%2=0=2%2 => invalid (first incongruence violated) From bdcc308ddd9716433a681ebd2d362fee043e224f Mon Sep 17 00:00:00 2001 From: Xiwei Pan Date: Mon, 14 Sep 2026 12:59:24 +0800 Subject: [PATCH 03/42] perf: reuse ILP buffers and petgraph graph algorithms Reuse the ILP row buffer and combine repeated Steiner extraction scans. Use petgraph union-find, connectivity, and articulation-point implementations in the existing graph checks. --- .../graph/biconnectivity_augmentation.rs | 80 ++++--------------- .../graph/minimum_dummy_activities_pert.rs | 32 +------- src/models/graph/partition_into_forests.rs | 15 +--- ...rizecollectingsteinerforest_steinertree.rs | 17 +--- src/solvers/ilp/adapter.rs | 12 +-- .../graph/biconnectivity_augmentation.rs | 18 ++++- 6 files changed, 43 insertions(+), 131 deletions(-) diff --git a/src/models/graph/biconnectivity_augmentation.rs b/src/models/graph/biconnectivity_augmentation.rs index f3682cc8a..578c1fca3 100644 --- a/src/models/graph/biconnectivity_augmentation.rs +++ b/src/models/graph/biconnectivity_augmentation.rs @@ -9,6 +9,8 @@ use crate::topology::{Graph, SimpleGraph}; use crate::traits::Problem; use crate::types::WeightElement; use num_traits::Zero; +use petgraph::algo::{articulation_points::articulation_points, connected_components}; +use petgraph::graph::{NodeIndex, UnGraph}; use serde::{Deserialize, Serialize}; use std::collections::BTreeSet; @@ -189,7 +191,7 @@ impl BiconnectivityAugmentation { fn augmented_graph( &self, config: &[bool], - ) -> Result, crate::traits::EvaluationError> { + ) -> Result>, crate::traits::EvaluationError> { if config.len() != self.num_potential_edges() { return Ok(None); } @@ -215,10 +217,14 @@ impl BiconnectivityAugmentation { return Ok(None); } - Ok(Some(SimpleGraph::new( - self.num_vertices(), - edges.into_iter().collect(), - ))) + let mut graph = UnGraph::new_undirected(); + for _ in 0..self.num_vertices() { + graph.add_node(()); + } + for (u, v) in edges { + graph.add_edge(NodeIndex::new(u), NodeIndex::new(v), ()); + } + Ok(Some(graph)) } } @@ -281,67 +287,9 @@ fn normalize_edge(u: usize, v: usize) -> (usize, usize) { } } -struct DfsState { - visited: Vec, - discovery_time: Vec, - low: Vec, - parent: Vec>, - time: usize, - has_articulation_point: bool, -} - -fn dfs_articulation_points(graph: &G, vertex: usize, state: &mut DfsState) { - if state.has_articulation_point { - return; - } - - state.visited[vertex] = true; - state.time += 1; - state.discovery_time[vertex] = state.time; - state.low[vertex] = state.time; - - let mut child_count = 0; - for neighbor in graph.neighbors(vertex) { - if !state.visited[neighbor] { - child_count += 1; - state.parent[neighbor] = Some(vertex); - dfs_articulation_points(graph, neighbor, state); - state.low[vertex] = state.low[vertex].min(state.low[neighbor]); - - if state.parent[vertex].is_none() && child_count > 1 { - state.has_articulation_point = true; - return; - } - - if state.parent[vertex].is_some() && state.low[neighbor] >= state.discovery_time[vertex] - { - state.has_articulation_point = true; - return; - } - } else if state.parent[vertex] != Some(neighbor) { - state.low[vertex] = state.low[vertex].min(state.discovery_time[neighbor]); - } - } -} - -fn is_biconnected(graph: &G) -> bool { - let num_vertices = graph.num_vertices(); - if num_vertices <= 1 { - return true; - } - - let mut state = DfsState { - visited: vec![false; num_vertices], - discovery_time: vec![0; num_vertices], - low: vec![0; num_vertices], - parent: vec![None; num_vertices], - time: 0, - has_articulation_point: false, - }; - - dfs_articulation_points(graph, 0, &mut state); - - !state.has_articulation_point && state.visited.into_iter().all(|seen| seen) +fn is_biconnected(graph: &UnGraph<(), ()>) -> bool { + graph.node_count() <= 1 + || (connected_components(graph) == 1 && articulation_points(graph).is_empty()) } crate::declare_variants! { diff --git a/src/models/graph/minimum_dummy_activities_pert.rs b/src/models/graph/minimum_dummy_activities_pert.rs index 18bef5734..277ffa5b7 100644 --- a/src/models/graph/minimum_dummy_activities_pert.rs +++ b/src/models/graph/minimum_dummy_activities_pert.rs @@ -11,6 +11,7 @@ use crate::registry::{CreateSpec, ProblemSchemaEntry}; use crate::topology::DirectedGraph; use crate::traits::Problem; use crate::types::Min; +use petgraph::unionfind::UnionFind; use serde::{Deserialize, Deserializer, Serialize}; use std::collections::{BTreeMap, BTreeSet}; @@ -161,7 +162,7 @@ impl MinimumDummyActivitiesPert { } let roots: Vec = (0..2 * num_tasks) - .map(|endpoint| uf.find(endpoint)) + .map(|endpoint| uf.find_mut(endpoint)) .collect(); let mut root_to_dense = BTreeMap::new(); for &root in &roots { @@ -292,35 +293,6 @@ struct CandidatePertNetwork { num_dummy_arcs: usize, } -#[derive(Debug)] -struct UnionFind { - parent: Vec, -} - -impl UnionFind { - fn new(size: usize) -> Self { - Self { - parent: (0..size).collect(), - } - } - - fn find(&mut self, x: usize) -> usize { - if self.parent[x] != x { - let root = self.find(self.parent[x]); - self.parent[x] = root; - } - self.parent[x] - } - - fn union(&mut self, a: usize, b: usize) { - let root_a = self.find(a); - let root_b = self.find(b); - if root_a != root_b { - self.parent[root_b] = root_a; - } - } -} - fn start_endpoint(task: usize) -> usize { 2 * task } diff --git a/src/models/graph/partition_into_forests.rs b/src/models/graph/partition_into_forests.rs index a7c7356a9..37e30fdff 100644 --- a/src/models/graph/partition_into_forests.rs +++ b/src/models/graph/partition_into_forests.rs @@ -8,6 +8,7 @@ use crate::registry::{FieldInfo, ProblemSchemaEntry, VariantDimension}; use crate::topology::{Graph, SimpleGraph}; use crate::traits::Problem; use crate::variant::VariantParam; +use petgraph::unionfind::UnionFind; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -163,14 +164,7 @@ fn is_valid_forest_partition(graph: &G, num_forests: usize, config: &[ // For each forest class, verify the induced subgraph is acyclic using union-find. // An undirected graph is acyclic iff union-find never sees an edge (u, v) where // u and v already share a component. - let mut parent: Vec = (0..n).collect(); - - fn find(parent: &mut Vec, x: usize) -> usize { - if parent[x] != x { - parent[x] = find(parent, parent[x]); - } - parent[x] - } + let mut components = UnionFind::::new(n); for (u, v) in graph.edges() { if config[u] != config[v] { @@ -178,12 +172,9 @@ fn is_valid_forest_partition(graph: &G, num_forests: usize, config: &[ continue; } // Both u and v are in the same class; check for cycle - let ru = find(&mut parent, u); - let rv = find(&mut parent, v); - if ru == rv { + if !components.union(u, v) { return false; // Cycle detected } - parent[ru] = rv; // Union } true diff --git a/src/rules/prizecollectingsteinerforest_steinertree.rs b/src/rules/prizecollectingsteinerforest_steinertree.rs index 41a1befc7..8447b4fd6 100644 --- a/src/rules/prizecollectingsteinerforest_steinertree.rs +++ b/src/rules/prizecollectingsteinerforest_steinertree.rs @@ -80,6 +80,7 @@ impl ReductionResult for ReductionPCSFToSteinerTree { let m = self.num_source_edges; let mut selected_vertices = vec![false; n]; let mut selected_edges = vec![false; m]; + let edges = self.target.graph().edges(); // Mark vertices included via their gadget include-edge `(v, t_v)`, // and edges via the matching original edge. @@ -91,19 +92,9 @@ impl ReductionResult for ReductionPCSFToSteinerTree { selected_vertices[v] = true; } else if let Some(src_edge) = self.target_to_source_edge[target_idx] { selected_edges[src_edge] = true; - } - } - - // Any original edge selected in `T*` forces both endpoints into - // `V_F`. The PCSF model rejects configurations where a selected - // edge has an unselected endpoint, so we mark endpoints explicitly - // (this also covers prize-zero endpoints, which have no gadget). - let edges = self.target.graph().edges(); - for (target_idx, &(u, v)) in edges.iter().enumerate() { - if !target_solution[target_idx] { - continue; - } - if self.target_to_source_edge[target_idx].is_some() { + // Include both endpoints, including prize-zero vertices + // that have no inclusion gadget. + let (u, v) = edges[target_idx]; selected_vertices[u] = true; selected_vertices[v] = true; } diff --git a/src/solvers/ilp/adapter.rs b/src/solvers/ilp/adapter.rs index a766a69d1..98930f0f2 100644 --- a/src/solvers/ilp/adapter.rs +++ b/src/solvers/ilp/adapter.rs @@ -124,19 +124,19 @@ impl HighsAdapter { Ok(backend.add_integer_column(costs[index], lower..=upper)) }) .collect::, IlpBackendError>>()?; + let mut terms = Vec::new(); for constraint in problem.constraints() { - let terms = constraint - .terms() - .iter() - .map(|&(index, coefficient)| Ok((columns[index], coefficient.to_backend_number()?))) - .collect::, IlpBackendError>>()?; + terms.clear(); + for &(index, coefficient) in constraint.terms() { + terms.push((columns[index], coefficient.to_backend_number()?)); + } let rhs = constraint.rhs().to_backend_number()?; let (lower, upper) = match constraint.comparison() { Comparison::Le => (f64::NEG_INFINITY, rhs), Comparison::Ge => (rhs, f64::INFINITY), Comparison::Eq => (rhs, rhs), }; - backend.add_row(lower..=upper, terms); + backend.add_row(lower..=upper, &terms); } let sense = match problem.sense() { ObjectiveSense::Minimize => Sense::Minimise, diff --git a/src/unit_tests/models/graph/biconnectivity_augmentation.rs b/src/unit_tests/models/graph/biconnectivity_augmentation.rs index 0d9bf5f6d..a8045edb8 100644 --- a/src/unit_tests/models/graph/biconnectivity_augmentation.rs +++ b/src/unit_tests/models/graph/biconnectivity_augmentation.rs @@ -162,10 +162,20 @@ fn test_biconnectivity_augmentation_paper_example() { #[test] fn test_is_biconnected() { - assert!(is_biconnected(&SimpleGraph::cycle(4))); - assert!(is_biconnected(&SimpleGraph::complete(3))); - assert!(!is_biconnected(&SimpleGraph::path(4))); - assert!(!is_biconnected(&SimpleGraph::new(4, vec![(0, 1), (2, 3)]))); + for (graph, expected) in [ + (SimpleGraph::empty(0), true), + (SimpleGraph::empty(1), true), + (SimpleGraph::empty(2), false), + (SimpleGraph::path(2), true), + (SimpleGraph::cycle(4), true), + (SimpleGraph::complete(3), true), + (SimpleGraph::path(4), false), + (SimpleGraph::new(4, vec![(0, 1), (2, 3)]), false), + (SimpleGraph::new(2, vec![(0, 0), (0, 1), (0, 1)]), true), + ] { + let problem = BiconnectivityAugmentation::<_, i64>::new(graph, vec![], 0); + assert_eq!(problem.evaluate(&vec![]).unwrap().0, expected); + } } #[test] From c7def3d36e7ba90cedfa993ca9dfb87e77c8f5ca Mon Sep 17 00:00:00 2001 From: Xiwei Pan Date: Mon, 14 Sep 2026 12:59:24 +0800 Subject: [PATCH 04/42] fix: validate deserialized models and rebuild derived state Apply model construction checks to persisted input and reconstruct derived caches from source fields. Preserve public constructor and setter signatures, return deserialization errors for invalid input, and cover creation and loading boundaries with regression tests. --- problemreductions-cli/src/dispatch.rs | 2 +- problemreductions-cli/tests/cli_tests.rs | 2 +- src/models/algebraic/bmf.rs | 33 +++- .../algebraic/consecutive_ones_submatrix.rs | 41 +++-- .../algebraic/feasible_basis_extension.rs | 64 +++---- .../algebraic/minimum_matrix_domination.rs | 28 ++- .../algebraic/minimum_weight_decoding.rs | 31 ++-- ...mum_weight_solution_to_linear_equations.rs | 31 ++-- src/models/algebraic/quadratic_assignment.rs | 41 +++-- .../algebraic/sparse_matrix_compression.rs | 24 ++- src/models/graph/acyclic_partition.rs | 81 +++++++-- .../graph/biconnectivity_augmentation.rs | 79 ++++++--- .../graph/bottleneck_traveling_salesman.rs | 42 ++++- .../bounded_component_spanning_forest.rs | 75 ++++++-- .../graph/bounded_diameter_spanning_tree.rs | 83 ++++++--- .../graph/degree_constrained_spanning_tree.rs | 31 +++- .../directed_two_commodity_integral_flow.rs | 95 ++++++++--- src/models/graph/disjoint_connecting_paths.rs | 63 +++++-- src/models/graph/generalized_hex.rs | 46 ++++- .../hamiltonian_path_between_two_vertices.rs | 60 +++++-- src/models/graph/integral_flow_bundles.rs | 104 +++++++---- .../graph/integral_flow_homologous_arcs.rs | 90 +++++++--- .../graph/integral_flow_with_multipliers.rs | 91 +++++++--- src/models/graph/kclique.rs | 33 +++- src/models/graph/kth_best_spanning_tree.rs | 52 ++++-- .../graph/length_bounded_disjoint_paths.rs | 57 +++++-- src/models/graph/longest_circuit.rs | 56 ++++-- src/models/graph/longest_path.rs | 110 ++++++++---- src/models/graph/max_cut.rs | 37 +++- src/models/graph/maximal_is.rs | 35 +++- src/models/graph/maximum_clique.rs | 37 +++- src/models/graph/maximum_co_k_plex.rs | 65 +++++-- .../graph/maximum_common_edge_subgraph.rs | 51 ++++-- src/models/graph/maximum_independent_set.rs | 33 +++- .../graph/maximum_leaf_spanning_tree.rs | 31 +++- src/models/graph/maximum_matching.rs | 45 ++++- src/models/graph/min_max_multicenter.rs | 87 +++++++--- .../minimum_capacitated_spanning_tree.rs | 92 +++++++--- src/models/graph/minimum_cost_circulation.rs | 53 ++++-- .../graph/minimum_cut_into_bounded_sets.rs | 69 ++++++-- src/models/graph/minimum_dominating_set.rs | 37 +++- src/models/graph/minimum_edge_cost_flow.rs | 81 +++++++-- src/models/graph/minimum_feedback_arc_set.rs | 45 ++++- .../graph/minimum_feedback_vertex_set.rs | 47 ++++- src/models/graph/minimum_multiway_cut.rs | 56 ++++-- src/models/graph/minimum_sum_multicenter.rs | 63 +++++-- src/models/graph/minimum_vertex_cover.rs | 37 +++- .../graph/multiple_copy_file_allocation.rs | 43 +++-- src/models/graph/partition_into_cliques.rs | 36 +++- src/models/graph/partition_into_forests.rs | 29 +++- .../graph/partition_into_paths_of_length_2.rs | 37 +++- .../graph/partition_into_perfect_matchings.rs | 38 ++++- src/models/graph/partition_into_triangles.rs | 36 +++- src/models/graph/rural_postman.rs | 63 +++++-- .../graph/shortest_weight_constrained_path.rs | 135 ++++++++++----- src/models/graph/traveling_salesman.rs | 45 ++++- .../graph/undirected_flow_lower_bounds.rs | 69 +++++--- .../undirected_two_commodity_integral_flow.rs | 81 +++++++-- src/models/misc/additional_key.rs | 83 ++++++--- .../misc/boyce_codd_normal_form_violation.rs | 65 ++++--- src/models/misc/capacity_assignment.rs | 71 ++++---- src/models/misc/closest_string.rs | 61 ++++--- src/models/misc/clustering.rs | 65 ++++--- src/models/misc/conjunctive_boolean_query.rs | 99 +++++++---- .../misc/conjunctive_query_foldability.rs | 116 +++++++++---- ...onsistency_of_database_frequency_tables.rs | 43 ++++- src/models/misc/cosine_product_integration.rs | 27 ++- .../misc/feasible_register_assignment.rs | 84 ++++----- src/models/misc/flow_shop_scheduling.rs | 57 +++++-- src/models/misc/grouping_by_swapping.rs | 49 ++++-- .../misc/integer_expression_membership.rs | 35 +++- src/models/misc/job_shop_scheduling.rs | 61 ++++--- src/models/misc/knapsack.rs | 95 +++++------ src/models/misc/longest_common_subsequence.rs | 47 +++-- src/models/misc/maximum_likelihood_ranking.rs | 56 ++++-- src/models/misc/minimum_axiom_set.rs | 61 +++++-- .../minimum_code_generation_one_register.rs | 67 +++++--- ...um_code_generation_parallel_assignments.rs | 47 +++-- ...mum_code_generation_unlimited_registers.rs | 82 ++++++--- src/models/misc/minimum_decision_tree.rs | 46 +++-- .../misc/minimum_disjunctive_normal_form.rs | 50 ++++-- .../misc/minimum_fault_detection_test_set.rs | 73 ++++---- .../minimum_register_sufficiency_for_loops.rs | 57 +++++-- .../misc/minimum_tardiness_sequencing.rs | 161 +++++++++--------- .../misc/minimum_weight_and_or_graph.rs | 101 ++++++----- src/models/misc/multiprocessor_scheduling.rs | 47 +++-- .../optimum_communication_spanning_tree.rs | 137 +++++++++------ src/models/misc/paintshop.rs | 70 ++++++-- src/models/misc/partially_ordered_knapsack.rs | 64 ++++--- .../misc/precedence_constrained_scheduling.rs | 68 ++++++-- src/models/misc/production_planning.rs | 84 ++++----- .../misc/rectilinear_picture_compression.rs | 26 ++- src/models/misc/register_sufficiency.rs | 46 +++-- .../scheduling_with_individual_deadlines.rs | 80 ++++++--- ...quencing_to_minimize_weighted_tardiness.rs | 55 +++--- ...encing_with_release_times_and_deadlines.rs | 58 +++++-- .../misc/shortest_common_supersequence.rs | 43 ++++- .../misc/shortest_common_superstring.rs | 43 ++++- src/models/misc/staff_scheduling.rs | 76 ++++++--- .../misc/string_to_string_correction.rs | 52 ++++-- src/models/misc/subset_product.rs | 48 ++++-- src/models/misc/subset_sum.rs | 44 ++++- src/models/misc/timetable_design.rs | 122 +++++++------ src/models/set/consecutive_sets.rs | 55 ++++-- src/models/set/exact_cover_by_3_sets.rs | 42 ++--- src/models/set/minimum_cardinality_key.rs | 44 +++-- src/models/set/minimum_hitting_set.rs | 39 +++-- src/models/set/minimum_set_covering.rs | 69 ++++++-- src/models/set/prime_attribute_name.rs | 64 ++++--- src/models/set/set_basis.rs | 44 +++-- src/models/set/three_dimensional_matching.rs | 55 +++--- src/topology/bipartite_graph.rs | 54 ++++-- src/topology/directed_graph.rs | 25 ++- src/topology/graph.rs | 25 ++- src/topology/mixed_graph.rs | 55 ++++-- src/topology/planar_graph.rs | 47 +++-- src/unit_tests/models/algebraic/bmf.rs | 16 ++ .../algebraic/consecutive_ones_submatrix.rs | 8 + .../algebraic/feasible_basis_extension.rs | 8 + .../algebraic/minimum_matrix_domination.rs | 17 ++ .../algebraic/minimum_weight_decoding.rs | 8 + ...mum_weight_solution_to_linear_equations.rs | 10 ++ .../models/algebraic/quadratic_assignment.rs | 8 + .../algebraic/sparse_matrix_compression.rs | 8 + src/unit_tests/models/graph/max_cut.rs | 8 + src/unit_tests/models/graph/maximal_is.rs | 13 ++ src/unit_tests/models/graph/maximum_clique.rs | 13 ++ .../models/graph/maximum_co_k_plex.rs | 11 ++ .../graph/maximum_common_edge_subgraph.rs | 17 ++ .../models/graph/maximum_independent_set.rs | 15 ++ .../models/graph/maximum_matching.rs | 8 + .../models/graph/minimum_dominating_set.rs | 15 ++ .../models/graph/minimum_feedback_arc_set.rs | 8 + .../graph/minimum_feedback_vertex_set.rs | 8 + .../models/graph/minimum_vertex_cover.rs | 13 ++ .../models/graph/traveling_salesman.rs | 8 + .../misc/feasible_register_assignment.rs | 12 ++ .../misc/minimum_disjunctive_normal_form.rs | 11 ++ .../misc/minimum_fault_detection_test_set.rs | 12 ++ .../misc/minimum_weight_and_or_graph.rs | 13 ++ .../models/misc/multiprocessor_scheduling.rs | 2 +- src/unit_tests/models/misc/paintshop.rs | 23 +++ .../misc/rectilinear_picture_compression.rs | 12 ++ src/unit_tests/models/set/consecutive_sets.rs | 7 + .../models/set/exact_cover_by_3_sets.rs | 25 +++ .../models/set/minimum_cardinality_key.rs | 7 + .../models/set/minimum_hitting_set.rs | 7 + .../models/set/minimum_set_covering.rs | 12 ++ .../models/set/prime_attribute_name.rs | 8 + src/unit_tests/models/set/set_basis.rs | 21 +-- .../models/set/three_dimensional_matching.rs | 9 + src/unit_tests/registry/dispatch.rs | 95 +++++++++++ .../rules/bottlenecktravelingsalesman_ilp.rs | 11 +- ...ionintocliques_minimumcoveringbycliques.rs | 17 +- src/unit_tests/topology/bipartite_graph.rs | 16 ++ src/unit_tests/topology/directed_graph.rs | 8 + src/unit_tests/topology/graph.rs | 8 + src/unit_tests/topology/mixed_graph.rs | 10 ++ src/unit_tests/topology/planar_graph.rs | 16 ++ tests/suites/reductions.rs | 5 +- 160 files changed, 5334 insertions(+), 2051 deletions(-) diff --git a/problemreductions-cli/src/dispatch.rs b/problemreductions-cli/src/dispatch.rs index 7ec0719a2..62c946295 100644 --- a/problemreductions-cli/src/dispatch.rs +++ b/problemreductions-cli/src/dispatch.rs @@ -679,7 +679,7 @@ mod tests { ); let err = loaded.err().unwrap(); assert!( - err.to_string().contains("expected positive integer, got 0"), + err.to_string().contains("num_processors must be positive"), "unexpected error: {err}" ); } diff --git a/problemreductions-cli/tests/cli_tests.rs b/problemreductions-cli/tests/cli_tests.rs index 28a9432ed..fd7fa51be 100644 --- a/problemreductions-cli/tests/cli_tests.rs +++ b/problemreductions-cli/tests/cli_tests.rs @@ -7845,7 +7845,7 @@ fn test_evaluate_multiprocessor_scheduling_rejects_zero_processors_json() { ); let stderr = String::from_utf8_lossy(&output.stderr); assert!( - stderr.contains("expected positive integer, got 0"), + stderr.contains("num_processors must be positive"), "stderr: {stderr}" ); diff --git a/src/models/algebraic/bmf.rs b/src/models/algebraic/bmf.rs index a5971dcac..c089e640f 100644 --- a/src/models/algebraic/bmf.rs +++ b/src/models/algebraic/bmf.rs @@ -54,6 +54,7 @@ inventory::submit! { /// assert!(problem.is_exact(&witness).unwrap()); /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "BMFData")] pub struct BMF { /// The target matrix A (m x n). matrix: Vec>, @@ -65,6 +66,20 @@ pub struct BMF { k: usize, } +#[derive(Deserialize)] +struct BMFData { + matrix: Vec>, + k: usize, +} + +impl TryFrom for BMF { + type Error = crate::registry::ConstructionError; + + fn try_from(data: BMFData) -> Result { + Self::try_new(data.matrix, data.k) + } +} + impl BMF { /// Create a new BMF problem. /// @@ -72,15 +87,19 @@ impl BMF { /// * `matrix` - The target m x n boolean matrix /// * `k` - The factorization rank pub fn new(matrix: Vec>, k: usize) -> Self { - let m = matrix.len(); - let n = if m > 0 { matrix[0].len() } else { 0 }; + Self::try_new(matrix, k).unwrap_or_else(|error| panic!("{error}")) + } - // Validate matrix dimensions - for row in &matrix { - assert_eq!(row.len(), n, "All rows must have the same length"); + fn try_new( + matrix: Vec>, + k: usize, + ) -> Result { + let m = matrix.len(); + let n = matrix.first().map_or(0, Vec::len); + if matrix.iter().any(|row| row.len() != n) { + return Err("all matrix rows must have the same length".into()); } - - Self { matrix, m, n, k } + Ok(Self { matrix, m, n, k }) } /// Get the number of rows. diff --git a/src/models/algebraic/consecutive_ones_submatrix.rs b/src/models/algebraic/consecutive_ones_submatrix.rs index 94cf051cc..eece16118 100644 --- a/src/models/algebraic/consecutive_ones_submatrix.rs +++ b/src/models/algebraic/consecutive_ones_submatrix.rs @@ -59,11 +59,26 @@ inventory::submit! { /// assert!(solution.is_some()); /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "ConsecutiveOnesSubmatrixData")] pub struct ConsecutiveOnesSubmatrix { matrix: Vec>, bound: i64, } +#[derive(Deserialize)] +struct ConsecutiveOnesSubmatrixData { + matrix: Vec>, + bound: i64, +} + +impl TryFrom for ConsecutiveOnesSubmatrix { + type Error = crate::registry::ConstructionError; + + fn try_from(data: ConsecutiveOnesSubmatrixData) -> Result { + Self::try_new(data.matrix, data.bound) + } +} + impl ConsecutiveOnesSubmatrix { /// Create a new ConsecutiveOnesSubmatrix instance. /// @@ -71,19 +86,21 @@ impl ConsecutiveOnesSubmatrix { /// /// Panics if `bound > n`, or if rows have inconsistent lengths. pub fn new(matrix: Vec>, bound: i64) -> Self { - let n = if matrix.is_empty() { - 0 - } else { - matrix[0].len() - }; - for row in &matrix { - assert_eq!(row.len(), n, "All rows must have the same length"); + Self::try_new(matrix, bound).unwrap_or_else(|error| panic!("{error}")) + } + + fn try_new( + matrix: Vec>, + bound: i64, + ) -> Result { + let n = matrix.first().map_or(0, Vec::len); + if matrix.iter().any(|row| row.len() != n) { + return Err("all matrix rows must have the same length".into()); + } + if !(bound < 0 || usize::try_from(bound).is_ok_and(|bound| bound <= n)) { + return Err(format!("bound ({bound}) must be <= number of columns ({n})").into()); } - assert!( - bound < 0 || usize::try_from(bound).is_ok_and(|bound| bound <= n), - "bound ({bound}) must be <= number of columns ({n})" - ); - Self { matrix, bound } + Ok(Self { matrix, bound }) } /// Returns the binary matrix. diff --git a/src/models/algebraic/feasible_basis_extension.rs b/src/models/algebraic/feasible_basis_extension.rs index 5f55d335a..c9c336db6 100644 --- a/src/models/algebraic/feasible_basis_extension.rs +++ b/src/models/algebraic/feasible_basis_extension.rs @@ -57,6 +57,7 @@ inventory::submit! { /// assert!(solution.is_some()); /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "FeasibleBasisExtensionCreateSpec")] pub struct FeasibleBasisExtension { matrix: Vec>, rhs: Vec, @@ -127,46 +128,45 @@ impl FeasibleBasisExtension { /// - Any required column index is out of bounds /// - Required columns contain duplicates pub fn new(matrix: Vec>, rhs: Vec, required_columns: Vec) -> Self { + Self::try_new(matrix, rhs, required_columns).unwrap_or_else(|error| panic!("{error}")) + } + + fn try_new( + matrix: Vec>, + rhs: Vec, + required_columns: Vec, + ) -> Result { let m = matrix.len(); - assert!(m > 0, "Matrix must have at least one row"); - let n = matrix[0].len(); - for row in &matrix { - assert_eq!(row.len(), n, "All rows must have the same length"); + let first = matrix.first().ok_or("matrix must have at least one row")?; + let n = first.len(); + if matrix.iter().any(|row| row.len() != n) { + return Err("all matrix rows must have the same length".into()); } - assert!( - m < n, - "Number of rows ({m}) must be less than number of columns ({n})" - ); - assert_eq!( - rhs.len(), - m, - "rhs length ({}) must equal number of rows ({m})", - rhs.len() - ); - assert!( - required_columns.len() < m, - "|S| ({}) must be less than m ({m})", - required_columns.len() - ); - for &col in &required_columns { - assert!(col < n, "Required column index {col} out of bounds (n={n})"); + if m >= n { + return Err("number of rows must be less than number of columns".into()); + } + if rhs.len() != m { + return Err("rhs length must equal number of rows".into()); } - // Check for duplicates - let mut sorted = required_columns.clone(); - sorted.sort_unstable(); - for i in 1..sorted.len() { - assert_ne!( - sorted[i - 1], - sorted[i], - "Duplicate required column index {}", - sorted[i] + if required_columns.len() >= m { + return Err( + format!("|S| ({}) must be less than m ({m})", required_columns.len()).into(), ); } - Self { + let mut seen = std::collections::HashSet::new(); + for &column in &required_columns { + if column >= n { + return Err(format!("required column {column} is out of bounds").into()); + } + if !seen.insert(column) { + return Err(format!("Duplicate required column index {column}").into()); + } + } + Ok(Self { matrix, rhs, required_columns, - } + }) } /// Returns the matrix A. diff --git a/src/models/algebraic/minimum_matrix_domination.rs b/src/models/algebraic/minimum_matrix_domination.rs index 8cafbc706..843fd5f28 100644 --- a/src/models/algebraic/minimum_matrix_domination.rs +++ b/src/models/algebraic/minimum_matrix_domination.rs @@ -54,6 +54,7 @@ inventory::submit! { /// assert_eq!(witness, Some(vec![true, true, true])); /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "MinimumMatrixDominationData")] pub struct MinimumMatrixDomination { /// The binary matrix. matrix: Vec>, @@ -61,6 +62,19 @@ pub struct MinimumMatrixDomination { ones: Vec<(usize, usize)>, } +#[derive(Deserialize)] +struct MinimumMatrixDominationData { + matrix: Vec>, +} + +impl TryFrom for MinimumMatrixDomination { + type Error = crate::registry::ConstructionError; + + fn try_from(data: MinimumMatrixDominationData) -> Result { + Self::try_new(data.matrix) + } +} + impl MinimumMatrixDomination { /// Create a new MinimumMatrixDomination instance. /// @@ -68,21 +82,25 @@ impl MinimumMatrixDomination { /// /// Panics if the matrix rows have inconsistent lengths. pub fn new(matrix: Vec>) -> Self { + Self::try_new(matrix).unwrap_or_else(|error| panic!("{error}")) + } + + fn try_new(matrix: Vec>) -> Result { let num_cols = matrix.first().map_or(0, Vec::len); - for row in &matrix { - assert_eq!(row.len(), num_cols, "All rows must have the same length"); + if matrix.iter().any(|row| row.len() != num_cols) { + return Err("all matrix rows must have the same length".into()); } - let ones: Vec<(usize, usize)> = matrix + let ones = matrix .iter() .enumerate() .flat_map(|(i, row)| { row.iter() .enumerate() - .filter(|(_, &v)| v) + .filter(|(_, &value)| value) .map(move |(j, _)| (i, j)) }) .collect(); - Self { matrix, ones } + Ok(Self { matrix, ones }) } /// Returns a reference to the binary matrix. diff --git a/src/models/algebraic/minimum_weight_decoding.rs b/src/models/algebraic/minimum_weight_decoding.rs index 6f92fae39..8521362ac 100644 --- a/src/models/algebraic/minimum_weight_decoding.rs +++ b/src/models/algebraic/minimum_weight_decoding.rs @@ -52,6 +52,7 @@ inventory::submit! { /// assert!(witness.is_some()); /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "MinimumWeightDecodingCreateSpec")] pub struct MinimumWeightDecoding { /// The n×m binary parity-check matrix H. matrix: Vec>, @@ -83,7 +84,7 @@ impl TryFrom for MinimumWeightDecoding { return Err("all matrix rows must have the same length".into()); } if spec.target.len() != spec.matrix.len() { - return Err("rhs length must equal number of rows".into()); + return Err("Target length must equal number of rows".into()); } Ok(Self { matrix: spec.matrix, @@ -100,18 +101,24 @@ impl MinimumWeightDecoding { /// Panics if the matrix is empty, rows have inconsistent lengths, /// target length does not match the number of rows, or there are no columns. pub fn new(matrix: Vec>, target: Vec) -> Self { - assert!(!matrix.is_empty(), "Matrix must have at least one row"); - let num_cols = matrix[0].len(); - assert!(num_cols > 0, "Matrix must have at least one column"); - for row in &matrix { - assert_eq!(row.len(), num_cols, "All rows must have the same length"); + Self::try_new(matrix, target).unwrap_or_else(|error| panic!("{error}")) + } + + fn try_new( + matrix: Vec>, + target: Vec, + ) -> Result { + let first = matrix.first().ok_or("matrix must have at least one row")?; + if first.is_empty() { + return Err("matrix must have at least one column".into()); + } + if matrix.iter().any(|row| row.len() != first.len()) { + return Err("all matrix rows must have the same length".into()); + } + if target.len() != matrix.len() { + return Err("Target length must equal number of rows".into()); } - assert_eq!( - target.len(), - matrix.len(), - "Target length must equal number of rows" - ); - Self { matrix, target } + Ok(Self { matrix, target }) } /// Returns a reference to the parity-check matrix H. diff --git a/src/models/algebraic/minimum_weight_solution_to_linear_equations.rs b/src/models/algebraic/minimum_weight_solution_to_linear_equations.rs index 988f7eb62..c90f66f83 100644 --- a/src/models/algebraic/minimum_weight_solution_to_linear_equations.rs +++ b/src/models/algebraic/minimum_weight_solution_to_linear_equations.rs @@ -51,6 +51,7 @@ inventory::submit! { /// assert!(witness.is_some()); /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "MinimumWeightSolutionCreateSpec")] pub struct MinimumWeightSolutionToLinearEquations { /// The n×m integer matrix A. matrix: Vec>, @@ -82,7 +83,7 @@ impl TryFrom for MinimumWeightSolutionToLinearE return Err("all matrix rows must have the same length".into()); } if spec.rhs.len() != spec.matrix.len() { - return Err("rhs length must equal number of rows".into()); + return Err("RHS length must equal number of rows".into()); } Ok(Self { matrix: spec.matrix, @@ -99,18 +100,24 @@ impl MinimumWeightSolutionToLinearEquations { /// Panics if the matrix is empty, rows have inconsistent lengths, /// rhs length does not match the number of rows, or there are no columns. pub fn new(matrix: Vec>, rhs: Vec) -> Self { - assert!(!matrix.is_empty(), "Matrix must have at least one row"); - let num_cols = matrix[0].len(); - assert!(num_cols > 0, "Matrix must have at least one column"); - for row in &matrix { - assert_eq!(row.len(), num_cols, "All rows must have the same length"); + Self::try_new(matrix, rhs).unwrap_or_else(|error| panic!("{error}")) + } + + fn try_new( + matrix: Vec>, + rhs: Vec, + ) -> Result { + let first = matrix.first().ok_or("matrix must have at least one row")?; + if first.is_empty() { + return Err("matrix must have at least one column".into()); + } + if matrix.iter().any(|row| row.len() != first.len()) { + return Err("all matrix rows must have the same length".into()); + } + if rhs.len() != matrix.len() { + return Err("RHS length must equal number of rows".into()); } - assert_eq!( - rhs.len(), - matrix.len(), - "RHS length must equal number of rows" - ); - Self { matrix, rhs } + Ok(Self { matrix, rhs }) } /// Returns a reference to the matrix A. diff --git a/src/models/algebraic/quadratic_assignment.rs b/src/models/algebraic/quadratic_assignment.rs index e1974fb92..5df31475a 100644 --- a/src/models/algebraic/quadratic_assignment.rs +++ b/src/models/algebraic/quadratic_assignment.rs @@ -58,6 +58,7 @@ inventory::submit! { /// assert!(best.is_some()); /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "QuadraticAssignmentData")] pub struct QuadraticAssignment { /// Cost/flow matrix between facilities (n x n). cost_matrix: Vec>, @@ -65,6 +66,20 @@ pub struct QuadraticAssignment { distance_matrix: Vec>, } +#[derive(Deserialize)] +struct QuadraticAssignmentData { + cost_matrix: Vec>, + distance_matrix: Vec>, +} + +impl TryFrom for QuadraticAssignment { + type Error = crate::registry::ConstructionError; + + fn try_from(data: QuadraticAssignmentData) -> Result { + Self::try_new(data.cost_matrix, data.distance_matrix) + } +} + impl QuadraticAssignment { /// Create a new Quadratic Assignment Problem. /// @@ -75,22 +90,28 @@ impl QuadraticAssignment { /// # Panics /// Panics if either matrix is not square, or if num_facilities > num_locations. pub fn new(cost_matrix: Vec>, distance_matrix: Vec>) -> Self { + Self::try_new(cost_matrix, distance_matrix).unwrap_or_else(|error| panic!("{error}")) + } + + fn try_new( + cost_matrix: Vec>, + distance_matrix: Vec>, + ) -> Result { let n = cost_matrix.len(); - for row in &cost_matrix { - assert_eq!(row.len(), n, "cost_matrix must be square"); + if cost_matrix.iter().any(|row| row.len() != n) { + return Err("cost_matrix must be square".into()); } let m = distance_matrix.len(); - for row in &distance_matrix { - assert_eq!(row.len(), m, "distance_matrix must be square"); + if distance_matrix.iter().any(|row| row.len() != m) { + return Err("distance_matrix must be square".into()); } - assert!( - n <= m, - "num_facilities ({n}) must be <= num_locations ({m})" - ); - Self { + if n > m { + return Err(format!("num_facilities ({n}) must be <= num_locations ({m})").into()); + } + Ok(Self { cost_matrix, distance_matrix, - } + }) } /// Get the cost/flow matrix. diff --git a/src/models/algebraic/sparse_matrix_compression.rs b/src/models/algebraic/sparse_matrix_compression.rs index fd4dcfcb6..d9d60380e 100644 --- a/src/models/algebraic/sparse_matrix_compression.rs +++ b/src/models/algebraic/sparse_matrix_compression.rs @@ -28,6 +28,7 @@ inventory::submit! { /// enumerating storage-vector entries directly, so brute-force search runs over /// `bound_k ^ num_rows` shift assignments. #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "SparseMatrixCompressionCreateSpec")] pub struct SparseMatrixCompression { matrix: Vec>, bound_k: usize, @@ -53,7 +54,7 @@ impl TryFrom for SparseMatrixCompression { .to_string() .into()); } - Ok(Self::new(spec.matrix, spec.bound_k)) + Self::try_new(spec.matrix, spec.bound_k) } } @@ -64,14 +65,23 @@ impl SparseMatrixCompression { /// /// Panics if `bound_k == 0` or if the matrix rows are ragged. pub fn new(matrix: Vec>, bound_k: usize) -> Self { - assert!(bound_k > 0, "bound_k must be positive"); + Self::try_new(matrix, bound_k).unwrap_or_else(|error| panic!("{error}")) + } - let num_cols = matrix.first().map_or(0, Vec::len); - for row in &matrix { - assert_eq!(row.len(), num_cols, "All rows must have the same length"); + fn try_new( + matrix: Vec>, + bound_k: usize, + ) -> Result { + if bound_k == 0 { + return Err("bound_k must be positive".to_string().into()); } - - Self { matrix, bound_k } + let columns = matrix.first().map_or(0, Vec::len); + if matrix.iter().any(|row| row.len() != columns) { + return Err("all matrix rows must have the same length" + .to_string() + .into()); + } + Ok(Self { matrix, bound_k }) } /// Return the binary matrix. diff --git a/src/models/graph/acyclic_partition.rs b/src/models/graph/acyclic_partition.rs index 0c89fbdde..10efa8c41 100644 --- a/src/models/graph/acyclic_partition.rs +++ b/src/models/graph/acyclic_partition.rs @@ -29,7 +29,7 @@ inventory::submit! { } /// Acyclic Partition (Garey & Johnson ND15). -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize)] pub struct AcyclicPartition { graph: DirectedGraph, vertex_weights: Vec, @@ -38,6 +38,34 @@ pub struct AcyclicPartition { cost_bound: W::Sum, } +#[derive(Deserialize)] +#[serde(bound(deserialize = "W: WeightElement + Deserialize<'de>, W::Sum: Deserialize<'de>"))] +struct AcyclicPartitionData { + graph: DirectedGraph, + vertex_weights: Vec, + arc_costs: Vec, + weight_bound: W::Sum, + cost_bound: W::Sum, +} + +impl<'de, W> Deserialize<'de> for AcyclicPartition +where + W: WeightElement + Deserialize<'de>, + W::Sum: Deserialize<'de>, +{ + fn deserialize>(deserializer: D) -> Result { + let data = AcyclicPartitionData::::deserialize(deserializer)?; + Self::try_new( + data.graph, + data.vertex_weights, + data.arc_costs, + data.weight_bound, + data.cost_bound, + ) + .map_err(serde::de::Error::custom) + } +} + #[derive(Debug, Deserialize, crate::CreateSpec)] struct AcyclicPartitionCreateSpec { #[create(codec = "arc-list")] @@ -94,13 +122,13 @@ impl TryFrom for AcyclicPartition { ) .into()); } - Ok(Self::new( + Self::try_new( graph, vertex_weights, arc_costs, spec.weight_bound, spec.cost_bound, - )) + ) } } @@ -113,23 +141,26 @@ impl AcyclicPartition { weight_bound: W::Sum, cost_bound: W::Sum, ) -> Self { - assert_eq!( - vertex_weights.len(), - graph.num_vertices(), - "vertex_weights length must match graph num_vertices" - ); - assert_eq!( - arc_costs.len(), - graph.num_arcs(), - "arc_costs length must match graph num_arcs" - ); - Self { + Self::try_new(graph, vertex_weights, arc_costs, weight_bound, cost_bound) + .unwrap_or_else(|error| panic!("{error}")) + } + + fn try_new( + graph: DirectedGraph, + vertex_weights: Vec, + arc_costs: Vec, + weight_bound: W::Sum, + cost_bound: W::Sum, + ) -> Result { + Self::check_vertex_weights(&graph, &vertex_weights)?; + Self::check_arc_costs(&graph, &arc_costs)?; + Ok(Self { graph, vertex_weights, arc_costs, weight_bound, cost_bound, - } + }) } /// Get the underlying graph. @@ -157,6 +188,16 @@ impl AcyclicPartition { self.vertex_weights = vertex_weights; } + fn check_vertex_weights( + graph: &DirectedGraph, + vertex_weights: &[W], + ) -> Result<(), crate::registry::ConstructionError> { + if vertex_weights.len() != graph.num_vertices() { + return Err("vertex_weights length must match graph num_vertices".into()); + } + Ok(()) + } + /// Replace the arc costs. pub fn set_arc_costs(&mut self, arc_costs: Vec) { assert_eq!( @@ -167,6 +208,16 @@ impl AcyclicPartition { self.arc_costs = arc_costs; } + fn check_arc_costs( + graph: &DirectedGraph, + arc_costs: &[W], + ) -> Result<(), crate::registry::ConstructionError> { + if arc_costs.len() != graph.num_arcs() { + return Err("arc_costs length must match graph num_arcs".into()); + } + Ok(()) + } + /// Get the per-part weight bound. pub fn weight_bound(&self) -> &W::Sum { &self.weight_bound diff --git a/src/models/graph/biconnectivity_augmentation.rs b/src/models/graph/biconnectivity_augmentation.rs index 578c1fca3..db6e04f0b 100644 --- a/src/models/graph/biconnectivity_augmentation.rs +++ b/src/models/graph/biconnectivity_augmentation.rs @@ -36,7 +36,7 @@ inventory::submit! { /// determine whether there exists a subset of potential edges `E'` such that: /// - `sum_{e in E'} w(e) <= B` /// - `(V, E union E')` is biconnected -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize)] #[serde(bound( serialize = "G: serde::Serialize, W: serde::Serialize, W::Sum: serde::Serialize", deserialize = "G: serde::Deserialize<'de>, W: serde::Deserialize<'de>, W::Sum: serde::Deserialize<'de>" @@ -53,6 +53,29 @@ where budget: W::Sum, } +#[derive(Deserialize)] +#[serde(bound( + deserialize = "G: Graph + Deserialize<'de>, W: WeightElement + Deserialize<'de>, W::Sum: Deserialize<'de>" +))] +struct BiconnectivityAugmentationData { + graph: G, + potential_weights: Vec<(usize, usize, W)>, + budget: W::Sum, +} + +impl<'de, G, W> Deserialize<'de> for BiconnectivityAugmentation +where + G: Graph + Deserialize<'de>, + W: WeightElement + Deserialize<'de>, + W::Sum: Deserialize<'de>, +{ + fn deserialize>(deserializer: D) -> Result { + let data = BiconnectivityAugmentationData::::deserialize(deserializer)?; + Self::try_new(data.graph, data.potential_weights, data.budget) + .map_err(serde::de::Error::custom) + } +} + #[derive(Debug, Deserialize, crate::CreateSpec)] struct BiconnectivityAugmentationCreateSpec { #[create(codec = "edge-list")] @@ -120,37 +143,47 @@ impl BiconnectivityAugmentation { /// is a self-loop, duplicates another candidate edge, or already exists in /// the input graph. pub fn new(graph: G, potential_weights: Vec<(usize, usize, W)>, budget: W::Sum) -> Self { + Self::try_new(graph, potential_weights, budget).unwrap_or_else(|error| panic!("{error}")) + } + + fn try_new( + graph: G, + potential_weights: Vec<(usize, usize, W)>, + budget: W::Sum, + ) -> Result { let num_vertices = graph.num_vertices(); let mut seen_potential_edges = BTreeSet::new(); for &(u, v, _) in &potential_weights { - assert!( - u < num_vertices && v < num_vertices, - "potential edge ({}, {}) references vertex >= num_vertices ({})", - u, - v, - num_vertices - ); - assert!(u != v, "potential edge ({}, {}) is a self-loop", u, v); + if !(u < num_vertices && v < num_vertices) { + return Err(format!( + "potential edge ({}, {}) references vertex >= num_vertices ({})", + u, v, num_vertices + ) + .into()); + } + if u == v { + return Err(format!("potential edge ({}, {}) is a self-loop", u, v).into()); + } let edge = normalize_edge(u, v); - assert!( - !graph.has_edge(edge.0, edge.1), - "potential edge ({}, {}) already exists in the graph", - edge.0, - edge.1 - ); - assert!( - seen_potential_edges.insert(edge), - "potential edge ({}, {}) is duplicated", - edge.0, - edge.1 - ); + if !(!graph.has_edge(edge.0, edge.1)) { + return Err(format!( + "potential edge ({}, {}) already exists in the graph", + edge.0, edge.1 + ) + .into()); + } + if !(seen_potential_edges.insert(edge)) { + return Err( + format!("potential edge ({}, {}) is duplicated", edge.0, edge.1).into(), + ); + } } - Self { + Ok(Self { graph, potential_weights, budget, - } + }) } /// Get a reference to the underlying graph. diff --git a/src/models/graph/bottleneck_traveling_salesman.rs b/src/models/graph/bottleneck_traveling_salesman.rs index f3561b53e..5005e52ce 100644 --- a/src/models/graph/bottleneck_traveling_salesman.rs +++ b/src/models/graph/bottleneck_traveling_salesman.rs @@ -24,11 +24,25 @@ inventory::submit! { /// The Bottleneck Traveling Salesman problem on a simple weighted graph. #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "BottleneckTravelingSalesmanData")] pub struct BottleneckTravelingSalesman { graph: SimpleGraph, edge_weights: Vec, } +#[derive(Deserialize)] +struct BottleneckTravelingSalesmanData { + graph: SimpleGraph, + edge_weights: Vec, +} + +impl TryFrom for BottleneckTravelingSalesman { + type Error = crate::registry::ConstructionError; + fn try_from(data: BottleneckTravelingSalesmanData) -> Result { + Self::try_new(data.graph, data.edge_weights) + } +} + #[derive(Debug, Deserialize, crate::CreateSpec)] struct BottleneckTravelingSalesmanCreateSpec { #[create(codec = "edge-list")] @@ -54,7 +68,7 @@ impl TryFrom for BottleneckTravelingSales ) .into()); } - Ok(Self::new(graph, edge_weights)) + Self::try_new(graph, edge_weights) } } @@ -92,15 +106,18 @@ fn simple_graph_from_create( impl BottleneckTravelingSalesman { /// Create a BottleneckTravelingSalesman problem from a graph with edge weights. pub fn new(graph: SimpleGraph, edge_weights: Vec) -> Self { - assert_eq!( - edge_weights.len(), - graph.num_edges(), - "edge_weights length must match num_edges" - ); - Self { + Self::try_new(graph, edge_weights).unwrap_or_else(|error| panic!("{error}")) + } + + fn try_new( + graph: SimpleGraph, + edge_weights: Vec, + ) -> Result { + Self::check_weights(&graph, &edge_weights)?; + Ok(Self { graph, edge_weights, - } + }) } /// Get a reference to the underlying graph. @@ -118,6 +135,15 @@ impl BottleneckTravelingSalesman { assert_eq!(weights.len(), self.graph.num_edges()); self.edge_weights = weights; } + fn check_weights( + graph: &SimpleGraph, + weights: &[i64], + ) -> Result<(), crate::registry::ConstructionError> { + if weights.len() != graph.num_edges() { + return Err("edge_weights length must match num_edges".into()); + } + Ok(()) + } /// Get all edges with their weights. pub fn edges(&self) -> Vec<(usize, usize, i64)> { diff --git a/src/models/graph/bounded_component_spanning_forest.rs b/src/models/graph/bounded_component_spanning_forest.rs index c1b60de21..302e6c5c8 100644 --- a/src/models/graph/bounded_component_spanning_forest.rs +++ b/src/models/graph/bounded_component_spanning_forest.rs @@ -34,7 +34,7 @@ inventory::submit! { /// integer `K`, and a bound `B`, determine whether the vertices can be /// partitioned into at most `K` non-empty sets such that every set induces a /// connected subgraph and the total weight of each set is at most `B`. -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize)] pub struct BoundedComponentSpanningForest { /// The underlying graph. graph: G, @@ -46,6 +46,35 @@ pub struct BoundedComponentSpanningForest { max_weight: W::Sum, } +#[derive(Deserialize)] +#[serde(bound( + deserialize = "G: Graph + Deserialize<'de>, W: WeightElement + Deserialize<'de>, W::Sum: Deserialize<'de>" +))] +struct BoundedComponentSpanningForestData { + graph: G, + weights: Vec, + max_components: usize, + max_weight: W::Sum, +} + +impl<'de, G, W> Deserialize<'de> for BoundedComponentSpanningForest +where + G: Graph + Deserialize<'de>, + W: WeightElement + Deserialize<'de>, + W::Sum: Deserialize<'de>, +{ + fn deserialize>(deserializer: D) -> Result { + let data = BoundedComponentSpanningForestData::::deserialize(deserializer)?; + Self::try_new( + data.graph, + data.weights, + data.max_components, + data.max_weight, + ) + .map_err(serde::de::Error::custom) + } +} + #[derive(Debug, Deserialize, crate::CreateSpec)] struct BoundedComponentSpanningForestCreateSpec { /// The underlying graph G=(V,E). @@ -81,32 +110,44 @@ impl TryFrom if spec.max_weight <= 0 { return Err("max_weight must be positive".to_string().into()); } - Ok(Self::new(spec.graph, spec.weights, spec.k, spec.max_weight)) + Self::try_new(spec.graph, spec.weights, spec.k, spec.max_weight) } } impl BoundedComponentSpanningForest { /// Create a new bounded-component spanning forest instance. pub fn new(graph: G, weights: Vec, max_components: usize, max_weight: W::Sum) -> Self { - assert_eq!( - weights.len(), - graph.num_vertices(), - "weights length must match graph num_vertices" - ); - assert!( - weights - .iter() - .all(|weight| weight.to_sum() >= W::Sum::zero()), - "weights must be nonnegative" - ); - assert!(max_components >= 1, "max_components must be at least 1"); - assert!(max_weight > W::Sum::zero(), "max_weight must be positive"); - Self { + Self::try_new(graph, weights, max_components, max_weight) + .unwrap_or_else(|error| panic!("{error}")) + } + + fn try_new( + graph: G, + weights: Vec, + max_components: usize, + max_weight: W::Sum, + ) -> Result { + if weights.len() != graph.num_vertices() { + return Err("weights length must match graph num_vertices".into()); + } + if !(weights + .iter() + .all(|weight| weight.to_sum() >= W::Sum::zero())) + { + return Err("weights must be nonnegative".into()); + } + if max_components == 0 { + return Err("max_components must be at least 1".into()); + } + if max_weight.partial_cmp(&W::Sum::zero()) != Some(std::cmp::Ordering::Greater) { + return Err("max_weight must be positive".into()); + } + Ok(Self { graph, weights, max_components, max_weight, - } + }) } /// Get a reference to the underlying graph. diff --git a/src/models/graph/bounded_diameter_spanning_tree.rs b/src/models/graph/bounded_diameter_spanning_tree.rs index 3c645a783..449ea2df0 100644 --- a/src/models/graph/bounded_diameter_spanning_tree.rs +++ b/src/models/graph/bounded_diameter_spanning_tree.rs @@ -59,7 +59,7 @@ inventory::submit! { /// let solution = solver.solve(&problem).unwrap(); /// assert!(solution.is_some()); /// ``` -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize)] #[serde(bound( deserialize = "G: serde::Deserialize<'de>, W: serde::Deserialize<'de>, W::Sum: serde::Deserialize<'de>" ))] @@ -76,6 +76,35 @@ pub struct BoundedDiameterSpanningTree { edge_list: Vec<(usize, usize)>, } +#[derive(Deserialize)] +#[serde(bound( + deserialize = "G: Graph + Deserialize<'de>, W: WeightElement + Deserialize<'de>, W::Sum: Deserialize<'de>" +))] +struct BoundedDiameterSpanningTreeData { + graph: G, + edge_weights: Vec, + weight_bound: W::Sum, + diameter_bound: usize, +} + +impl<'de, G, W> Deserialize<'de> for BoundedDiameterSpanningTree +where + G: Graph + Deserialize<'de>, + W: WeightElement + Deserialize<'de>, + W::Sum: Deserialize<'de>, +{ + fn deserialize>(deserializer: D) -> Result { + let data = BoundedDiameterSpanningTreeData::::deserialize(deserializer)?; + Self::try_new( + data.graph, + data.edge_weights, + data.weight_bound, + data.diameter_bound, + ) + .map_err(serde::de::Error::custom) + } +} + #[derive(Debug, Deserialize, crate::CreateSpec)] struct BoundedDiameterSpanningTreeCreateSpec { #[create(codec = "edge-list")] @@ -114,12 +143,7 @@ impl TryFrom if spec.diameter_bound == 0 { return Err("diameter_bound must be at least 1".to_string().into()); } - Ok(Self::new( - graph, - edge_weights, - spec.weight_bound, - spec.diameter_bound, - )) + Self::try_new(graph, edge_weights, spec.weight_bound, spec.diameter_bound) } } @@ -163,26 +187,32 @@ impl BoundedDiameterSpanningTree { weight_bound: W::Sum, diameter_bound: usize, ) -> Self { - assert_eq!( - edge_weights.len(), - graph.num_edges(), - "edge_weights length must match num_edges" - ); + Self::try_new(graph, edge_weights, weight_bound, diameter_bound) + .unwrap_or_else(|error| panic!("{error}")) + } + + fn try_new( + graph: G, + edge_weights: Vec, + weight_bound: W::Sum, + diameter_bound: usize, + ) -> Result { + Self::check_weights(&graph, &edge_weights)?; let zero = W::Sum::zero(); - assert!( - edge_weights.iter().all(|w| w.to_sum() > zero.clone()), - "All edge weights must be positive (> 0)" - ); - assert!(weight_bound > zero, "weight_bound must be positive (> 0)"); - assert!(diameter_bound >= 1, "diameter_bound must be at least 1"); + if weight_bound.partial_cmp(&zero) != Some(std::cmp::Ordering::Greater) { + return Err("weight_bound must be positive (> 0)".into()); + } + if diameter_bound == 0 { + return Err("diameter_bound must be at least 1".into()); + } let edge_list = graph.edges(); - Self { + Ok(Self { graph, edge_weights, weight_bound, diameter_bound, edge_list, - } + }) } /// Get a reference to the underlying graph. @@ -210,6 +240,19 @@ impl BoundedDiameterSpanningTree { self.edge_weights = edge_weights; } + fn check_weights(graph: &G, weights: &[W]) -> Result<(), crate::registry::ConstructionError> { + if weights.len() != graph.num_edges() { + return Err("edge_weights length must match num_edges".into()); + } + if !weights + .iter() + .all(|weight| weight.to_sum() > W::Sum::zero()) + { + return Err("edge_weights must be positive (> 0)".into()); + } + Ok(()) + } + /// Get the weight bound B. pub fn weight_bound(&self) -> &W::Sum { &self.weight_bound diff --git a/src/models/graph/degree_constrained_spanning_tree.rs b/src/models/graph/degree_constrained_spanning_tree.rs index 52c03b053..145143ce9 100644 --- a/src/models/graph/degree_constrained_spanning_tree.rs +++ b/src/models/graph/degree_constrained_spanning_tree.rs @@ -55,7 +55,7 @@ inventory::submit! { /// let solution = solver.solve(&problem).unwrap(); /// assert!(solution.is_some()); /// ``` -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize)] #[serde(bound(deserialize = "G: serde::Deserialize<'de>"))] pub struct DegreeConstrainedSpanningTree { /// The underlying graph. @@ -66,19 +66,42 @@ pub struct DegreeConstrainedSpanningTree { edge_list: Vec<(usize, usize)>, } +#[derive(Deserialize)] +#[serde(bound(deserialize = "G: Graph + Deserialize<'de>"))] +struct DegreeConstrainedSpanningTreeData { + graph: G, + max_degree: usize, +} + +impl<'de, G> Deserialize<'de> for DegreeConstrainedSpanningTree +where + G: Graph + Deserialize<'de>, +{ + fn deserialize>(deserializer: D) -> Result { + let data = DegreeConstrainedSpanningTreeData::::deserialize(deserializer)?; + Self::try_new(data.graph, data.max_degree).map_err(serde::de::Error::custom) + } +} + impl DegreeConstrainedSpanningTree { /// Create a new Degree-Constrained Spanning Tree instance. /// /// # Panics /// Panics if `max_degree` is zero. pub fn new(graph: G, max_degree: usize) -> Self { - assert!(max_degree >= 1, "max_degree must be at least 1"); + Self::try_new(graph, max_degree).unwrap_or_else(|error| panic!("{error}")) + } + + fn try_new(graph: G, max_degree: usize) -> Result { + if max_degree == 0 { + return Err("max_degree must be at least 1".into()); + } let edge_list = graph.edges(); - Self { + Ok(Self { graph, max_degree, edge_list, - } + }) } /// Get a reference to the underlying graph. diff --git a/src/models/graph/directed_two_commodity_integral_flow.rs b/src/models/graph/directed_two_commodity_integral_flow.rs index 5974e2986..192124b1d 100644 --- a/src/models/graph/directed_two_commodity_integral_flow.rs +++ b/src/models/graph/directed_two_commodity_integral_flow.rs @@ -68,6 +68,7 @@ inventory::submit! { /// assert!(solver.solve(&problem).unwrap().is_some()); /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "DirectedTwoCommodityIntegralFlowData")] pub struct DirectedTwoCommodityIntegralFlow { /// The directed graph G = (V, A). graph: DirectedGraph, @@ -87,6 +88,34 @@ pub struct DirectedTwoCommodityIntegralFlow { requirement_2: i64, } +#[derive(Deserialize)] +struct DirectedTwoCommodityIntegralFlowData { + graph: DirectedGraph, + capacities: Vec, + source_1: usize, + sink_1: usize, + source_2: usize, + sink_2: usize, + requirement_1: i64, + requirement_2: i64, +} + +impl TryFrom for DirectedTwoCommodityIntegralFlow { + type Error = crate::registry::ConstructionError; + fn try_from(data: DirectedTwoCommodityIntegralFlowData) -> Result { + Self::try_new( + data.graph, + data.capacities, + data.source_1, + data.sink_1, + data.source_2, + data.sink_2, + data.requirement_1, + data.requirement_2, + ) + } +} + impl DirectedTwoCommodityIntegralFlow { /// Create a new Directed Two-Commodity Integral Flow problem. /// @@ -106,25 +135,7 @@ impl DirectedTwoCommodityIntegralFlow { requirement_1: i64, requirement_2: i64, ) -> Self { - let n = graph.num_vertices(); - assert_eq!( - capacities.len(), - graph.num_arcs(), - "capacities length must match graph num_arcs" - ); - assert!( - capacities.iter().all(|&capacity| capacity >= 0), - "capacities must be nonnegative" - ); - assert!( - requirement_1 >= 0 && requirement_2 >= 0, - "flow requirements must be nonnegative" - ); - assert!(source_1 < n, "source_1 ({source_1}) >= num_vertices ({n})"); - assert!(sink_1 < n, "sink_1 ({sink_1}) >= num_vertices ({n})"); - assert!(source_2 < n, "source_2 ({source_2}) >= num_vertices ({n})"); - assert!(sink_2 < n, "sink_2 ({sink_2}) >= num_vertices ({n})"); - Self { + Self::try_new( graph, capacities, source_1, @@ -133,7 +144,53 @@ impl DirectedTwoCommodityIntegralFlow { sink_2, requirement_1, requirement_2, + ) + .unwrap_or_else(|error| panic!("{error}")) + } + + #[allow(clippy::too_many_arguments)] + fn try_new( + graph: DirectedGraph, + capacities: Vec, + source_1: usize, + sink_1: usize, + source_2: usize, + sink_2: usize, + requirement_1: i64, + requirement_2: i64, + ) -> Result { + let n = graph.num_vertices(); + if capacities.len() != graph.num_arcs() { + return Err("capacities length must match graph num_arcs".into()); + } + if !(capacities.iter().all(|&capacity| capacity >= 0)) { + return Err("capacities must be nonnegative".into()); + } + if !(requirement_1 >= 0 && requirement_2 >= 0) { + return Err("flow requirements must be nonnegative".into()); + } + if !(source_1 < n) { + return Err(format!("source_1 ({source_1}) >= num_vertices ({n})").into()); + } + if !(sink_1 < n) { + return Err(format!("sink_1 ({sink_1}) >= num_vertices ({n})").into()); } + if !(source_2 < n) { + return Err(format!("source_2 ({source_2}) >= num_vertices ({n})").into()); + } + if !(sink_2 < n) { + return Err(format!("sink_2 ({sink_2}) >= num_vertices ({n})").into()); + } + Ok(Self { + graph, + capacities, + source_1, + sink_1, + source_2, + sink_2, + requirement_1, + requirement_2, + }) } /// Get a reference to the underlying directed graph. diff --git a/src/models/graph/disjoint_connecting_paths.rs b/src/models/graph/disjoint_connecting_paths.rs index ba694765d..049aeeb2b 100644 --- a/src/models/graph/disjoint_connecting_paths.rs +++ b/src/models/graph/disjoint_connecting_paths.rs @@ -30,13 +30,30 @@ inventory::submit! { /// A configuration uses one binary variable per edge in the graph's canonical /// sorted edge list. A valid solution selects exactly the edges of one simple /// path for each terminal pair, with all such paths pairwise vertex-disjoint. -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize)] #[serde(bound(deserialize = "G: serde::Deserialize<'de>"))] pub struct DisjointConnectingPaths { graph: G, terminal_pairs: Vec<(usize, usize)>, } +#[derive(Deserialize)] +#[serde(bound(deserialize = "G: Graph + Deserialize<'de>"))] +struct DisjointConnectingPathsData { + graph: G, + terminal_pairs: Vec<(usize, usize)>, +} + +impl<'de, G> Deserialize<'de> for DisjointConnectingPaths +where + G: Graph + Deserialize<'de>, +{ + fn deserialize>(deserializer: D) -> Result { + let data = DisjointConnectingPathsData::::deserialize(deserializer)?; + Self::try_new(data.graph, data.terminal_pairs).map_err(serde::de::Error::custom) + } +} + #[derive(Debug, Deserialize, crate::CreateSpec)] struct DisjointConnectingPathsCreateSpec { #[create(codec = "edge-list")] @@ -101,33 +118,43 @@ impl DisjointConnectingPaths { /// Panics if no terminal pairs are provided, if a pair uses invalid or /// repeated endpoints, or if any terminal appears in more than one pair. pub fn new(graph: G, terminal_pairs: Vec<(usize, usize)>) -> Self { - assert!( - !terminal_pairs.is_empty(), - "terminal_pairs must contain at least one pair" - ); + Self::try_new(graph, terminal_pairs).unwrap_or_else(|error| panic!("{error}")) + } + + fn try_new( + graph: G, + terminal_pairs: Vec<(usize, usize)>, + ) -> Result { + if terminal_pairs.is_empty() { + return Err("terminal_pairs must contain at least one pair".into()); + } let num_vertices = graph.num_vertices(); let mut used = vec![false; num_vertices]; for &(source, sink) in &terminal_pairs { - assert!(source < num_vertices, "terminal pair source out of bounds"); - assert!(sink < num_vertices, "terminal pair sink out of bounds"); - assert_ne!(source, sink, "terminal pair endpoints must be distinct"); - assert!( - !used[source], - "terminal vertices must be pairwise disjoint across pairs" - ); - assert!( - !used[sink], - "terminal vertices must be pairwise disjoint across pairs" - ); + if !(source < num_vertices) { + return Err("terminal pair source out of bounds".into()); + } + if !(sink < num_vertices) { + return Err("terminal pair sink out of bounds".into()); + } + if source == sink { + return Err("terminal pair endpoints must be distinct".into()); + } + if !(!used[source]) { + return Err("terminal vertices must be pairwise disjoint across pairs".into()); + } + if !(!used[sink]) { + return Err("terminal vertices must be pairwise disjoint across pairs".into()); + } used[source] = true; used[sink] = true; } - Self { + Ok(Self { graph, terminal_pairs, - } + }) } /// Get a reference to the underlying graph. diff --git a/src/models/graph/generalized_hex.rs b/src/models/graph/generalized_hex.rs index fe3cf54cc..ac383d7d4 100644 --- a/src/models/graph/generalized_hex.rs +++ b/src/models/graph/generalized_hex.rs @@ -32,7 +32,7 @@ inventory::submit! { /// The problem is represented as a zero-variable decision problem: the graph /// instance fully determines the question, so `evaluate([])` runs a memoized /// game-tree search from the initial empty board. -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize)] #[serde(bound(deserialize = "G: serde::Deserialize<'de>"))] pub struct GeneralizedHex { graph: G, @@ -40,6 +40,24 @@ pub struct GeneralizedHex { target: usize, } +#[derive(Deserialize)] +#[serde(bound(deserialize = "G: Graph + Deserialize<'de>"))] +struct GeneralizedHexData { + graph: G, + source: usize, + target: usize, +} + +impl<'de, G> Deserialize<'de> for GeneralizedHex +where + G: Graph + Deserialize<'de>, +{ + fn deserialize>(deserializer: D) -> Result { + let data = GeneralizedHexData::::deserialize(deserializer)?; + Self::try_new(data.graph, data.source, data.target).map_err(serde::de::Error::custom) + } +} + #[derive(Debug, Deserialize, crate::CreateSpec)] struct GeneralizedHexCreateSpec { /// The underlying graph G=(V,E). @@ -72,7 +90,7 @@ impl TryFrom for GeneralizedHex { if spec.source == spec.sink { return Err("source and sink must be distinct".to_string().into()); } - Ok(Self::new(spec.graph, spec.source, spec.sink)) + Self::try_new(spec.graph, spec.source, spec.sink) } } @@ -86,15 +104,29 @@ enum ClaimState { impl GeneralizedHex { /// Create a new Generalized Hex instance. pub fn new(graph: G, source: usize, target: usize) -> Self { + Self::try_new(graph, source, target).unwrap_or_else(|error| panic!("{error}")) + } + + fn try_new( + graph: G, + source: usize, + target: usize, + ) -> Result { let num_vertices = graph.num_vertices(); - assert!(source < num_vertices, "source must be a valid graph vertex"); - assert!(target < num_vertices, "target must be a valid graph vertex"); - assert_ne!(source, target, "source and target must be distinct"); - Self { + if !(source < num_vertices) { + return Err("source must be a valid graph vertex".into()); + } + if !(target < num_vertices) { + return Err("target must be a valid graph vertex".into()); + } + if source == target { + return Err("source and target must be distinct".into()); + } + Ok(Self { graph, source, target, - } + }) } /// Get a reference to the underlying graph. diff --git a/src/models/graph/hamiltonian_path_between_two_vertices.rs b/src/models/graph/hamiltonian_path_between_two_vertices.rs index 624c4a445..cec1931d3 100644 --- a/src/models/graph/hamiltonian_path_between_two_vertices.rs +++ b/src/models/graph/hamiltonian_path_between_two_vertices.rs @@ -68,7 +68,7 @@ inventory::submit! { /// let solution = solver.solve(&problem).unwrap(); /// assert!(solution.is_some()); /// ``` -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize)] #[serde(bound(deserialize = "G: serde::Deserialize<'de>"))] pub struct HamiltonianPathBetweenTwoVertices { graph: G, @@ -76,6 +76,25 @@ pub struct HamiltonianPathBetweenTwoVertices { target_vertex: usize, } +#[derive(Deserialize)] +#[serde(bound(deserialize = "G: Graph + Deserialize<'de>"))] +struct HamiltonianPathBetweenTwoVerticesData { + graph: G, + source_vertex: usize, + target_vertex: usize, +} + +impl<'de, G> Deserialize<'de> for HamiltonianPathBetweenTwoVertices +where + G: Graph + Deserialize<'de>, +{ + fn deserialize>(deserializer: D) -> Result { + let data = HamiltonianPathBetweenTwoVerticesData::::deserialize(deserializer)?; + Self::try_new(data.graph, data.source_vertex, data.target_vertex) + .map_err(serde::de::Error::custom) + } +} + #[derive(Debug, Deserialize, crate::CreateSpec)] struct HamiltonianPathBetweenTwoVerticesRandomSpec { /// Number of graph vertices. @@ -97,24 +116,35 @@ impl HamiltonianPathBetweenTwoVertices { /// /// Panics if `source_vertex` or `target_vertex` is out of range, or if they are equal. pub fn new(graph: G, source_vertex: usize, target_vertex: usize) -> Self { + Self::try_new(graph, source_vertex, target_vertex).unwrap_or_else(|error| panic!("{error}")) + } + + fn try_new( + graph: G, + source_vertex: usize, + target_vertex: usize, + ) -> Result { let n = graph.num_vertices(); - assert!( - source_vertex < n, - "source_vertex {source_vertex} out of range for graph with {n} vertices" - ); - assert!( - target_vertex < n, - "target_vertex {target_vertex} out of range for graph with {n} vertices" - ); - assert_ne!( - source_vertex, target_vertex, - "source_vertex and target_vertex must be distinct" - ); - Self { + if !(source_vertex < n) { + return Err(format!( + "source_vertex {source_vertex} out of range for graph with {n} vertices" + ) + .into()); + } + if !(target_vertex < n) { + return Err(format!( + "target_vertex {target_vertex} out of range for graph with {n} vertices" + ) + .into()); + } + if source_vertex == target_vertex { + return Err("source_vertex and target_vertex must be distinct".into()); + } + Ok(Self { graph, source_vertex, target_vertex, - } + }) } /// Get a reference to the underlying graph. diff --git a/src/models/graph/integral_flow_bundles.rs b/src/models/graph/integral_flow_bundles.rs index 2de08530d..f7f05f792 100644 --- a/src/models/graph/integral_flow_bundles.rs +++ b/src/models/graph/integral_flow_bundles.rs @@ -24,6 +24,7 @@ inventory::submit! { /// Integral Flow with Bundles (Garey & Johnson ND36). #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "IntegralFlowBundlesData")] pub struct IntegralFlowBundles { graph: DirectedGraph, source: usize, @@ -33,6 +34,30 @@ pub struct IntegralFlowBundles { requirement: i64, } +#[derive(Deserialize)] +struct IntegralFlowBundlesData { + graph: DirectedGraph, + source: usize, + sink: usize, + bundles: Vec>, + bundle_capacities: Vec, + requirement: i64, +} + +impl TryFrom for IntegralFlowBundles { + type Error = crate::registry::ConstructionError; + fn try_from(data: IntegralFlowBundlesData) -> Result { + Self::try_new( + data.graph, + data.source, + data.sink, + data.bundles, + data.bundle_capacities, + data.requirement, + ) + } +} + #[derive(Debug, Deserialize, crate::CreateSpec)] struct IntegralFlowBundlesCreateSpec { #[create(codec = "arc-list")] @@ -131,64 +156,77 @@ impl IntegralFlowBundles { bundle_capacities: Vec, requirement: i64, ) -> Self { + Self::try_new(graph, source, sink, bundles, bundle_capacities, requirement) + .unwrap_or_else(|error| panic!("{error}")) + } + + fn try_new( + graph: DirectedGraph, + source: usize, + sink: usize, + bundles: Vec>, + bundle_capacities: Vec, + requirement: i64, + ) -> Result { let num_vertices = graph.num_vertices(); let num_arcs = graph.num_arcs(); - assert!( - source < num_vertices, - "source ({source}) >= num_vertices ({num_vertices})" - ); - assert!( - sink < num_vertices, - "sink ({sink}) >= num_vertices ({num_vertices})" - ); - assert!(source != sink, "source and sink must be distinct"); - assert_eq!( - bundles.len(), - bundle_capacities.len(), - "bundles length must match bundle_capacities length" - ); - assert!(requirement > 0, "requirement must be positive"); + if !(source < num_vertices) { + return Err(format!("source ({source}) >= num_vertices ({num_vertices})").into()); + } + if !(sink < num_vertices) { + return Err(format!("sink ({sink}) >= num_vertices ({num_vertices})").into()); + } + if source == sink { + return Err("source and sink must be distinct".into()); + } + if bundles.len() != bundle_capacities.len() { + return Err("bundles length must match bundle_capacities length".into()); + } + if requirement <= 0 { + return Err("requirement must be positive".into()); + } let mut arc_covered = vec![false; num_arcs]; for (bundle_index, (bundle, &capacity)) in bundles.iter().zip(&bundle_capacities).enumerate() { - assert!( - capacity > 0, - "bundle capacity at index {bundle_index} must be positive" - ); + if !(capacity > 0) { + return Err( + format!("bundle capacity at index {bundle_index} must be positive").into(), + ); + } let mut seen = BTreeSet::new(); for &arc_index in bundle { - assert!( - arc_index < num_arcs, - "bundle {bundle_index} references arc {arc_index}, but num_arcs is {num_arcs}" - ); - assert!( - seen.insert(arc_index), - "bundle {bundle_index} contains duplicate arc index {arc_index}" - ); + if !(arc_index < num_arcs) { + return Err(format!("bundle {bundle_index} references arc {arc_index}, but num_arcs is {num_arcs}").into()); + } + if !(seen.insert(arc_index)) { + return Err(format!( + "bundle {bundle_index} contains duplicate arc index {arc_index}" + ) + .into()); + } arc_covered[arc_index] = true; } } for (arc_index, covered) in arc_covered.iter().copied().enumerate() { - assert!( - covered, - "arc {arc_index} must belong to at least one bundle" - ); + if !(covered) { + return Err(format!("arc {arc_index} must belong to at least one bundle").into()); + } } - Self { + Ok(Self { graph, source, sink, bundles, bundle_capacities, requirement, - } + }) } /// Get the underlying directed graph. diff --git a/src/models/graph/integral_flow_homologous_arcs.rs b/src/models/graph/integral_flow_homologous_arcs.rs index c60a7fb89..d17a92824 100644 --- a/src/models/graph/integral_flow_homologous_arcs.rs +++ b/src/models/graph/integral_flow_homologous_arcs.rs @@ -29,6 +29,7 @@ inventory::submit! { /// capacities, flow conservation at non-terminal vertices, every homologous-pair /// equality constraint, and the required net inflow at the sink. #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "IntegralFlowHomologousArcsData")] pub struct IntegralFlowHomologousArcs { graph: DirectedGraph, capacities: Vec, @@ -38,6 +39,30 @@ pub struct IntegralFlowHomologousArcs { homologous_pairs: Vec<(usize, usize)>, } +#[derive(Deserialize)] +struct IntegralFlowHomologousArcsData { + graph: DirectedGraph, + capacities: Vec, + source: usize, + sink: usize, + requirement: i64, + homologous_pairs: Vec<(usize, usize)>, +} + +impl TryFrom for IntegralFlowHomologousArcs { + type Error = crate::registry::ConstructionError; + fn try_from(data: IntegralFlowHomologousArcsData) -> Result { + Self::try_new( + data.graph, + data.capacities, + data.source, + data.sink, + data.requirement, + data.homologous_pairs, + ) + } +} + #[derive(Debug, Deserialize, crate::CreateSpec)] struct IntegralFlowHomologousArcsCreateSpec { #[create(codec = "arc-list")] @@ -107,41 +132,64 @@ impl IntegralFlowHomologousArcs { requirement: i64, homologous_pairs: Vec<(usize, usize)>, ) -> Self { + Self::try_new( + graph, + capacities, + source, + sink, + requirement, + homologous_pairs, + ) + .unwrap_or_else(|error| panic!("{error}")) + } + + fn try_new( + graph: DirectedGraph, + capacities: Vec, + source: usize, + sink: usize, + requirement: i64, + homologous_pairs: Vec<(usize, usize)>, + ) -> Result { let num_vertices = graph.num_vertices(); let num_arcs = graph.num_arcs(); - assert_eq!( - capacities.len(), - num_arcs, - "capacities length must match graph.num_arcs()" - ); - assert!( - source < num_vertices, - "source ({source}) must be less than num_vertices ({num_vertices})" - ); - assert!( - sink < num_vertices, - "sink ({sink}) must be less than num_vertices ({num_vertices})" - ); + if capacities.len() != num_arcs { + return Err("capacities length must match graph.num_arcs()".into()); + } + if !(source < num_vertices) { + return Err(format!( + "source ({source}) must be less than num_vertices ({num_vertices})" + ) + .into()); + } + if !(sink < num_vertices) { + return Err( + format!("sink ({sink}) must be less than num_vertices ({num_vertices})").into(), + ); + } for &(a, b) in &homologous_pairs { - assert!(a < num_arcs, "homologous arc index {a} out of range"); - assert!(b < num_arcs, "homologous arc index {b} out of range"); + if !(a < num_arcs) { + return Err(format!("homologous arc index {a} out of range").into()); + } + if !(b < num_arcs) { + return Err(format!("homologous arc index {b} out of range").into()); + } } - assert!( - capacities.iter().all(|&capacity| capacity >= 0), - "capacities must be nonnegative" - ); + if !(capacities.iter().all(|&capacity| capacity >= 0)) { + return Err("capacities must be nonnegative".into()); + } - Self { + Ok(Self { graph, capacities, source, sink, requirement, homologous_pairs, - } + }) } pub fn graph(&self) -> &DirectedGraph { diff --git a/src/models/graph/integral_flow_with_multipliers.rs b/src/models/graph/integral_flow_with_multipliers.rs index c3ea1215d..e06ec3da1 100644 --- a/src/models/graph/integral_flow_with_multipliers.rs +++ b/src/models/graph/integral_flow_with_multipliers.rs @@ -23,6 +23,7 @@ inventory::submit! { } #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "IntegralFlowWithMultipliersData")] pub struct IntegralFlowWithMultipliers { graph: DirectedGraph, source: usize, @@ -32,6 +33,30 @@ pub struct IntegralFlowWithMultipliers { requirement: i64, } +#[derive(Deserialize)] +struct IntegralFlowWithMultipliersData { + graph: DirectedGraph, + source: usize, + sink: usize, + multipliers: Vec, + capacities: Vec, + requirement: i64, +} + +impl TryFrom for IntegralFlowWithMultipliers { + type Error = crate::registry::ConstructionError; + fn try_from(data: IntegralFlowWithMultipliersData) -> Result { + Self::try_new( + data.graph, + data.source, + data.sink, + data.multipliers, + data.capacities, + data.requirement, + ) + } +} + #[derive(Debug, Deserialize, crate::CreateSpec)] struct IntegralFlowWithMultipliersCreateSpec { #[create(codec = "arc-list")] @@ -106,47 +131,59 @@ impl IntegralFlowWithMultipliers { capacities: Vec, requirement: i64, ) -> Self { - assert_eq!( - capacities.len(), - graph.num_arcs(), - "capacities length must match graph num_arcs" - ); - assert_eq!( - multipliers.len(), - graph.num_vertices(), - "multipliers length must match graph num_vertices" - ); + Self::try_new(graph, source, sink, multipliers, capacities, requirement) + .unwrap_or_else(|error| panic!("{error}")) + } + + fn try_new( + graph: DirectedGraph, + source: usize, + sink: usize, + multipliers: Vec, + capacities: Vec, + requirement: i64, + ) -> Result { + if capacities.len() != graph.num_arcs() { + return Err("capacities length must match graph num_arcs".into()); + } + if multipliers.len() != graph.num_vertices() { + return Err("multipliers length must match graph num_vertices".into()); + } let num_vertices = graph.num_vertices(); - assert!( - source < num_vertices, - "source ({source}) must be less than num_vertices ({num_vertices})" - ); - assert!( - sink < num_vertices, - "sink ({sink}) must be less than num_vertices ({num_vertices})" - ); - assert_ne!(source, sink, "source and sink must be distinct"); + if !(source < num_vertices) { + return Err(format!( + "source ({source}) must be less than num_vertices ({num_vertices})" + ) + .into()); + } + if !(sink < num_vertices) { + return Err( + format!("sink ({sink}) must be less than num_vertices ({num_vertices})").into(), + ); + } + if source == sink { + return Err("source and sink must be distinct".into()); + } for (vertex, &multiplier) in multipliers.iter().enumerate() { - if vertex != source && vertex != sink { - assert!(multiplier > 0, "non-terminal multipliers must be positive"); + if vertex != source && vertex != sink && !(multiplier > 0) { + return Err("non-terminal multipliers must be positive".into()); } } - assert!( - capacities.iter().all(|&capacity| capacity >= 0), - "capacities must be nonnegative" - ); + if !(capacities.iter().all(|&capacity| capacity >= 0)) { + return Err("capacities must be nonnegative".into()); + } - Self { + Ok(Self { graph, source, sink, multipliers, capacities, requirement, - } + }) } pub fn graph(&self) -> &DirectedGraph { diff --git a/src/models/graph/kclique.rs b/src/models/graph/kclique.rs index 3cd2b6f1a..336900979 100644 --- a/src/models/graph/kclique.rs +++ b/src/models/graph/kclique.rs @@ -26,12 +26,29 @@ inventory::submit! { /// Given a graph `G = (V, E)` and a positive integer `k`, determine whether /// there exists a subset `K ⊆ V` of size at least `k` such that every pair of /// distinct vertices in `K` is adjacent. -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize)] pub struct KClique { graph: G, k: usize, } +#[derive(Deserialize)] +#[serde(bound(deserialize = "G: Graph + Deserialize<'de>"))] +struct KCliqueData { + graph: G, + k: usize, +} + +impl<'de, G> Deserialize<'de> for KClique +where + G: Graph + Deserialize<'de>, +{ + fn deserialize>(deserializer: D) -> Result { + let data = KCliqueData::::deserialize(deserializer)?; + Self::try_new(data.graph, data.k).map_err(serde::de::Error::custom) + } +} + #[derive(Debug, Deserialize, crate::CreateSpec)] struct KCliqueCreateSpec { #[create(codec = "edge-list")] @@ -79,9 +96,17 @@ impl TryFrom for KClique { impl KClique { /// Create a new k-Clique problem instance. pub fn new(graph: G, k: usize) -> Self { - assert!(k > 0, "k must be positive"); - assert!(k <= graph.num_vertices(), "k must be <= graph num_vertices"); - Self { graph, k } + Self::try_new(graph, k).unwrap_or_else(|error| panic!("{error}")) + } + + fn try_new(graph: G, k: usize) -> Result { + if k == 0 { + return Err("k must be positive".into()); + } + if !(k <= graph.num_vertices()) { + return Err("k must be <= graph num_vertices".into()); + } + Ok(Self { graph, k }) } /// Get a reference to the underlying graph. diff --git a/src/models/graph/kth_best_spanning_tree.rs b/src/models/graph/kth_best_spanning_tree.rs index 6ee5d8f88..af7ac0597 100644 --- a/src/models/graph/kth_best_spanning_tree.rs +++ b/src/models/graph/kth_best_spanning_tree.rs @@ -34,7 +34,7 @@ inventory::submit! { /// /// A configuration is `k` consecutive binary blocks of length `|E|`. /// Each block selects the edges of one candidate spanning tree. -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize)] pub struct KthBestSpanningTree { graph: SimpleGraph, weights: Vec, @@ -42,6 +42,27 @@ pub struct KthBestSpanningTree { bound: W::Sum, } +#[derive(Deserialize)] +#[serde(bound(deserialize = "W: WeightElement + Deserialize<'de>, W::Sum: Deserialize<'de>"))] +struct KthBestSpanningTreeData { + graph: SimpleGraph, + weights: Vec, + k: usize, + bound: W::Sum, +} + +impl<'de, W> Deserialize<'de> for KthBestSpanningTree +where + W: WeightElement + Deserialize<'de>, + W::Sum: Deserialize<'de>, +{ + fn deserialize>(deserializer: D) -> Result { + let data = KthBestSpanningTreeData::::deserialize(deserializer)?; + Self::try_new(data.graph, data.weights, data.k, data.bound) + .map_err(serde::de::Error::custom) + } +} + #[derive(Debug, Deserialize, crate::CreateSpec)] struct KthBestSpanningTreeCreateSpec { #[create(codec = "edge-list")] @@ -72,7 +93,7 @@ impl TryFrom for KthBestSpanningTree { if spec.k == 0 { return Err("k must be positive".to_string().into()); } - Ok(Self::new(graph, weights, spec.k, spec.bound)) + Self::try_new(graph, weights, spec.k, spec.bound) } } @@ -112,19 +133,28 @@ impl KthBestSpanningTree { /// Panics if the number of weights does not match the number of edges, or /// if `k` is zero. pub fn new(graph: SimpleGraph, weights: Vec, k: usize, bound: W::Sum) -> Self { - assert_eq!( - weights.len(), - graph.num_edges(), - "weights length must match graph num_edges" - ); - assert!(k > 0, "k must be positive"); - - Self { + Self::try_new(graph, weights, k, bound).unwrap_or_else(|error| panic!("{error}")) + } + + fn try_new( + graph: SimpleGraph, + weights: Vec, + k: usize, + bound: W::Sum, + ) -> Result { + if weights.len() != graph.num_edges() { + return Err("weights length must match graph num_edges".into()); + } + if k == 0 { + return Err("k must be positive".into()); + } + + Ok(Self { graph, weights, k, bound, - } + }) } /// Get the underlying graph. diff --git a/src/models/graph/length_bounded_disjoint_paths.rs b/src/models/graph/length_bounded_disjoint_paths.rs index 78be3c4a8..ae93de0c2 100644 --- a/src/models/graph/length_bounded_disjoint_paths.rs +++ b/src/models/graph/length_bounded_disjoint_paths.rs @@ -33,7 +33,7 @@ inventory::submit! { /// vertices of different slots must be disjoint. Empty slots (all zeros) are /// unused and do not count toward the objective. The objective is to maximize /// the number of non-empty valid path slots. -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize)] #[serde(bound(deserialize = "G: serde::Deserialize<'de>"))] pub struct LengthBoundedDisjointPaths { graph: G, @@ -43,6 +43,26 @@ pub struct LengthBoundedDisjointPaths { max_length: usize, } +#[derive(Deserialize)] +#[serde(bound(deserialize = "G: Graph + Deserialize<'de>"))] +struct LengthBoundedDisjointPathsData { + graph: G, + source: usize, + sink: usize, + max_length: usize, +} + +impl<'de, G> Deserialize<'de> for LengthBoundedDisjointPaths +where + G: Graph + Deserialize<'de>, +{ + fn deserialize>(deserializer: D) -> Result { + let data = LengthBoundedDisjointPathsData::::deserialize(deserializer)?; + Self::try_new(data.graph, data.source, data.sink, data.max_length) + .map_err(serde::de::Error::custom) + } +} + #[derive(Debug, Deserialize, crate::CreateSpec)] struct LengthBoundedDisjointPathsCreateSpec { /// Undirected graph edges. @@ -140,26 +160,37 @@ impl LengthBoundedDisjointPaths { /// Panics if `source` or `sink` is not a valid graph vertex, if `source == /// sink`, or if `max_length == 0`. pub fn new(graph: G, source: usize, sink: usize, max_length: usize) -> Self { - assert!( - source < graph.num_vertices(), - "source must be a valid graph vertex" - ); - assert!( - sink < graph.num_vertices(), - "sink must be a valid graph vertex" - ); - assert_ne!(source, sink, "source and sink must be distinct"); - assert!(max_length > 0, "max_length must be positive"); + Self::try_new(graph, source, sink, max_length).unwrap_or_else(|error| panic!("{error}")) + } + + fn try_new( + graph: G, + source: usize, + sink: usize, + max_length: usize, + ) -> Result { + if !(source < graph.num_vertices()) { + return Err("source must be a valid graph vertex".into()); + } + if !(sink < graph.num_vertices()) { + return Err("sink must be a valid graph vertex".into()); + } + if source == sink { + return Err("source and sink must be distinct".into()); + } + if max_length == 0 { + return Err("max_length must be positive".into()); + } let deg_s = graph.neighbors(source).len(); let deg_t = graph.neighbors(sink).len(); let max_paths = deg_s.min(deg_t); - Self { + Ok(Self { graph, source, sink, max_paths, max_length, - } + }) } /// Get a reference to the underlying graph. diff --git a/src/models/graph/longest_circuit.rs b/src/models/graph/longest_circuit.rs index 9c01712d7..808a17e29 100644 --- a/src/models/graph/longest_circuit.rs +++ b/src/models/graph/longest_circuit.rs @@ -40,12 +40,30 @@ inventory::submit! { /// /// A valid configuration must select edges that form exactly one connected /// simple circuit using only edges from `graph`. -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize)] pub struct LongestCircuit { graph: G, edge_lengths: Vec, } +#[derive(Deserialize)] +#[serde(bound(deserialize = "G: Graph + Deserialize<'de>, W: WeightElement + Deserialize<'de>"))] +struct LongestCircuitData { + graph: G, + edge_lengths: Vec, +} + +impl<'de, G, W> Deserialize<'de> for LongestCircuit +where + G: Graph + Deserialize<'de>, + W: WeightElement + Deserialize<'de>, +{ + fn deserialize>(deserializer: D) -> Result { + let data = LongestCircuitData::::deserialize(deserializer)?; + Self::try_new(data.graph, data.edge_lengths).map_err(serde::de::Error::custom) + } +} + #[derive(Debug, Deserialize, crate::CreateSpec)] struct LongestCircuitCreateSpec { #[create(codec = "edge-list")] @@ -74,7 +92,7 @@ impl TryFrom for LongestCircuit { if edge_lengths.iter().any(|&length| length <= 0) { return Err("edge_weights must be positive".to_string().into()); } - Ok(Self::new(graph, edge_lengths)) + Self::try_new(graph, edge_lengths) } } @@ -117,22 +135,15 @@ impl LongestCircuit { /// Panics if the number of edge lengths does not match the graph's edge /// count, or if any edge length is non-positive. pub fn new(graph: G, edge_lengths: Vec) -> Self { - assert_eq!( - edge_lengths.len(), - graph.num_edges(), - "edge_lengths length must match num_edges" - ); - let zero = W::Sum::zero(); - assert!( - edge_lengths - .iter() - .all(|length| length.to_sum() > zero.clone()), - "All edge lengths must be positive (> 0)" - ); - Self { + Self::try_new(graph, edge_lengths).unwrap_or_else(|error| panic!("{error}")) + } + + fn try_new(graph: G, edge_lengths: Vec) -> Result { + Self::check_weights(&graph, &edge_lengths)?; + Ok(Self { graph, edge_lengths, - } + }) } /// Get a reference to the underlying graph. @@ -162,6 +173,19 @@ impl LongestCircuit { self.edge_lengths = edge_lengths; } + fn check_weights(graph: &G, weights: &[W]) -> Result<(), crate::registry::ConstructionError> { + if weights.len() != graph.num_edges() { + return Err("edge_lengths length must match num_edges".into()); + } + if !weights + .iter() + .all(|weight| weight.to_sum() > W::Sum::zero()) + { + return Err("All edge lengths must be positive (> 0)".into()); + } + Ok(()) + } + /// Replace the edge lengths via the generic weight-management naming. pub fn set_weights(&mut self, weights: Vec) { self.set_lengths(weights); diff --git a/src/models/graph/longest_path.rs b/src/models/graph/longest_path.rs index f6d5d189d..029e06af5 100644 --- a/src/models/graph/longest_path.rs +++ b/src/models/graph/longest_path.rs @@ -40,7 +40,7 @@ inventory::submit! { /// /// A valid configuration must select exactly the edges of one simple /// undirected path from `source_vertex` to `target_vertex`. -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize)] pub struct LongestPath { graph: G, edge_lengths: Vec, @@ -48,6 +48,32 @@ pub struct LongestPath { target_vertex: usize, } +#[derive(Deserialize)] +#[serde(bound(deserialize = "G: Graph + Deserialize<'de>, W: WeightElement + Deserialize<'de>"))] +struct LongestPathData { + graph: G, + edge_lengths: Vec, + source_vertex: usize, + target_vertex: usize, +} + +impl<'de, G, W> Deserialize<'de> for LongestPath +where + G: Graph + Deserialize<'de>, + W: WeightElement + Deserialize<'de>, +{ + fn deserialize>(deserializer: D) -> Result { + let data = LongestPathData::::deserialize(deserializer)?; + Self::try_new( + data.graph, + data.edge_lengths, + data.source_vertex, + data.target_vertex, + ) + .map_err(serde::de::Error::custom) + } +} + macro_rules! longest_path_create_spec { (@lengths $spec:ident, $lengths:ident) => { $spec.$lengths }; (@lengths $spec:ident) => { vec![One; $spec.graph.len()] }; @@ -109,42 +135,41 @@ longest_path_create_spec!(LongestPathI64CreateSpec, i64, edge_lengths); longest_path_create_spec!(LongestPathOneCreateSpec, One); impl LongestPath { - fn assert_positive_edge_lengths(edge_lengths: &[W]) { - let zero = W::Sum::zero(); - assert!( - edge_lengths - .iter() - .all(|length| length.to_sum() > zero.clone()), - "All edge lengths must be positive (> 0)" - ); - } - /// Create a new LongestPath instance. pub fn new(graph: G, edge_lengths: Vec, source_vertex: usize, target_vertex: usize) -> Self { - assert_eq!( - edge_lengths.len(), - graph.num_edges(), - "edge_lengths length must match num_edges" - ); - Self::assert_positive_edge_lengths(&edge_lengths); - assert!( - source_vertex < graph.num_vertices(), - "source_vertex {} out of bounds (graph has {} vertices)", - source_vertex, - graph.num_vertices() - ); - assert!( - target_vertex < graph.num_vertices(), - "target_vertex {} out of bounds (graph has {} vertices)", - target_vertex, - graph.num_vertices() - ); - Self { + Self::try_new(graph, edge_lengths, source_vertex, target_vertex) + .unwrap_or_else(|error| panic!("{error}")) + } + + fn try_new( + graph: G, + edge_lengths: Vec, + source_vertex: usize, + target_vertex: usize, + ) -> Result { + Self::check_weights(&graph, &edge_lengths)?; + if !(source_vertex < graph.num_vertices()) { + return Err(format!( + "source_vertex {} out of bounds (graph has {} vertices)", + source_vertex, + graph.num_vertices() + ) + .into()); + } + if !(target_vertex < graph.num_vertices()) { + return Err(format!( + "target_vertex {} out of bounds (graph has {} vertices)", + target_vertex, + graph.num_vertices() + ) + .into()); + } + Ok(Self { graph, edge_lengths, source_vertex, target_vertex, - } + }) } /// Get a reference to the underlying graph. @@ -168,6 +193,19 @@ impl LongestPath { self.edge_lengths = edge_lengths; } + fn check_weights(graph: &G, weights: &[W]) -> Result<(), crate::registry::ConstructionError> { + if weights.len() != graph.num_edges() { + return Err("edge_lengths length must match num_edges".into()); + } + if !weights + .iter() + .all(|weight| weight.to_sum() > W::Sum::zero()) + { + return Err("All edge lengths must be positive (> 0)".into()); + } + Ok(()) + } + /// Get the source vertex. pub fn source_vertex(&self) -> usize { self.source_vertex @@ -203,6 +241,16 @@ impl LongestPath { config, ) } + + fn assert_positive_edge_lengths(edge_lengths: &[W]) { + let zero = W::Sum::zero(); + assert!( + edge_lengths + .iter() + .all(|length| length.to_sum() > zero.clone()), + "All edge lengths must be positive (> 0)" + ); + } } impl Problem for LongestPath diff --git a/src/models/graph/max_cut.rs b/src/models/graph/max_cut.rs index a10666c97..9a0a9a645 100644 --- a/src/models/graph/max_cut.rs +++ b/src/models/graph/max_cut.rs @@ -67,7 +67,7 @@ inventory::submit! { /// assert_eq!(size, Max(Some(2))); /// } /// ``` -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize)] pub struct MaxCut { /// The underlying graph structure. graph: G, @@ -75,6 +75,23 @@ pub struct MaxCut { edge_weights: Vec, } +#[derive(Deserialize)] +struct MaxCutData { + graph: G, + edge_weights: Vec, +} + +impl<'de, G, W> Deserialize<'de> for MaxCut +where + G: Graph + Deserialize<'de>, + W: Clone + Default + Deserialize<'de>, +{ + fn deserialize>(deserializer: D) -> Result { + let data = MaxCutData::deserialize(deserializer)?; + Self::try_new(data.graph, data.edge_weights).map_err(serde::de::Error::custom) + } +} + macro_rules! max_cut_create_spec { ($name:ident, $weight:ty, $one:expr $(, $edge_weights:ident)?) => { #[derive(Debug, Deserialize, crate::CreateSpec)] @@ -102,7 +119,7 @@ macro_rules! max_cut_create_spec { ) .into()); } - Ok(Self::new(graph, edge_weights)) + Self::try_new(graph, edge_weights) } } }; @@ -146,15 +163,17 @@ impl MaxCut { /// * `graph` - The underlying graph /// * `edge_weights` - Weights for each edge (must match graph.num_edges()) pub fn new(graph: G, edge_weights: Vec) -> Self { - assert_eq!( - edge_weights.len(), - graph.num_edges(), - "edge_weights length must match num_edges" - ); - Self { + Self::try_new(graph, edge_weights).unwrap_or_else(|error| panic!("{error}")) + } + + fn try_new(graph: G, edge_weights: Vec) -> Result { + if edge_weights.len() != graph.num_edges() { + return Err("edge_weights length must match graph num_edges".into()); + } + Ok(Self { graph, edge_weights, - } + }) } /// Create a MaxCut problem with unit weights. diff --git a/src/models/graph/maximal_is.rs b/src/models/graph/maximal_is.rs index 249ad2e80..8813d884c 100644 --- a/src/models/graph/maximal_is.rs +++ b/src/models/graph/maximal_is.rs @@ -53,7 +53,7 @@ inventory::submit! { /// assert!(problem.evaluate(sol).unwrap().is_valid()); /// } /// ``` -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize)] pub struct MaximalIS { /// The underlying graph. graph: G, @@ -61,6 +61,23 @@ pub struct MaximalIS { weights: Vec, } +#[derive(Deserialize)] +struct MaximalISData { + graph: G, + weights: Vec, +} + +impl<'de, G, W> Deserialize<'de> for MaximalIS +where + G: Graph + Deserialize<'de>, + W: Clone + Default + Deserialize<'de>, +{ + fn deserialize>(deserializer: D) -> Result { + let data = MaximalISData::deserialize(deserializer)?; + Self::try_new(data.graph, data.weights).map_err(serde::de::Error::custom) + } +} + #[derive(Debug, Deserialize, crate::CreateSpec)] struct MaximalISCreateSpec { /// The underlying graph G=(V,E). @@ -80,19 +97,21 @@ impl TryFrom for MaximalIS { ) .into()); } - Ok(Self::new(spec.graph, spec.weights)) + Self::try_new(spec.graph, spec.weights) } } impl MaximalIS { /// Create a Maximal Independent Set problem from a graph with given weights. pub fn new(graph: G, weights: Vec) -> Self { - assert_eq!( - weights.len(), - graph.num_vertices(), - "weights length must match graph num_vertices" - ); - Self { graph, weights } + Self::try_new(graph, weights).unwrap_or_else(|error| panic!("{error}")) + } + + fn try_new(graph: G, weights: Vec) -> Result { + if weights.len() != graph.num_vertices() { + return Err("weights length must match graph num_vertices".into()); + } + Ok(Self { graph, weights }) } /// Get a reference to the underlying graph. diff --git a/src/models/graph/maximum_clique.rs b/src/models/graph/maximum_clique.rs index 6c3baf47d..01b8021aa 100644 --- a/src/models/graph/maximum_clique.rs +++ b/src/models/graph/maximum_clique.rs @@ -56,7 +56,7 @@ inventory::submit! { /// // Maximum clique in a triangle (K3) is size 3 /// assert!(solutions.iter().all(|s| s.iter().filter(|&&selected| selected).count() == 3)); /// ``` -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize)] pub struct MaximumClique { /// The underlying graph. graph: G, @@ -64,6 +64,23 @@ pub struct MaximumClique { weights: Vec, } +#[derive(Deserialize)] +struct MaximumCliqueData { + graph: G, + weights: Vec, +} + +impl<'de, G, W> Deserialize<'de> for MaximumClique +where + G: Graph + Deserialize<'de>, + W: Clone + Default + Deserialize<'de>, +{ + fn deserialize>(deserializer: D) -> Result { + let data = MaximumCliqueData::deserialize(deserializer)?; + Self::try_new(data.graph, data.weights).map_err(serde::de::Error::custom) + } +} + #[derive(Debug, Deserialize, crate::CreateSpec)] struct MaximumCliqueCreateSpec { /// The underlying graph G=(V,E). @@ -83,19 +100,21 @@ impl TryFrom> for MaximumClique MaximumClique { /// Create a MaximumClique problem from a graph with given weights. pub fn new(graph: G, weights: Vec) -> Self { - assert_eq!( - weights.len(), - graph.num_vertices(), - "weights length must match graph num_vertices" - ); - Self { graph, weights } + Self::try_new(graph, weights).unwrap_or_else(|error| panic!("{error}")) + } + + fn try_new(graph: G, weights: Vec) -> Result { + if weights.len() != graph.num_vertices() { + return Err("weights length must match graph num_vertices".into()); + } + Ok(Self { graph, weights }) } /// Get a reference to the underlying graph. @@ -229,7 +248,7 @@ impl TryFrom for MaximumClique { type Error = crate::registry::ConstructionError; fn try_from(spec: MaximumCliqueOneCreateSpec) -> Result { let weights = vec![One; spec.graph.num_vertices()]; - Ok(Self::new(spec.graph, weights)) + Self::try_new(spec.graph, weights) } } diff --git a/src/models/graph/maximum_co_k_plex.rs b/src/models/graph/maximum_co_k_plex.rs index a1f654a0b..73d401d89 100644 --- a/src/models/graph/maximum_co_k_plex.rs +++ b/src/models/graph/maximum_co_k_plex.rs @@ -63,8 +63,7 @@ inventory::submit! { /// MaximumCoKPlex::<_, One, KN>::with_k(graph, vec![One; 5], 2); /// assert_eq!(problem.bound_k(), 2); /// ``` -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(bound(deserialize = "G: serde::Deserialize<'de>, W: serde::Deserialize<'de>"))] +#[derive(Debug, Clone, Serialize)] pub struct MaximumCoKPlex { /// The underlying graph. graph: G, @@ -81,6 +80,25 @@ pub struct MaximumCoKPlex { _phantom: std::marker::PhantomData, } +#[derive(Deserialize)] +struct MaximumCoKPlexData { + graph: G, + weights: Vec, + bound_k: usize, +} + +impl<'de, G, W, K> Deserialize<'de> for MaximumCoKPlex +where + G: Graph + Deserialize<'de>, + W: Clone + Default + Deserialize<'de>, + K: KValue, +{ + fn deserialize>(deserializer: D) -> Result { + let data = MaximumCoKPlexData::deserialize(deserializer)?; + Self::try_with_k(data.graph, data.weights, data.bound_k).map_err(serde::de::Error::custom) + } +} + #[derive(Debug, Deserialize, crate::CreateSpec)] struct MaximumCoKPlexCreateSpec { /// The underlying graph G=(V,E). @@ -108,7 +126,7 @@ impl TryFrom> if spec.k == 0 { return Err("k must be at least 1".to_string().into()); } - Ok(Self::with_k(spec.graph, spec.weights, spec.k)) + Self::try_with_k(spec.graph, spec.weights, spec.k) } } @@ -120,24 +138,31 @@ impl MaximumCoKPlex { /// `bound_k == 0`, or if `K` declares a fixed value that disagrees with /// `bound_k`. pub fn with_k(graph: G, weights: Vec, bound_k: usize) -> Self { - assert_eq!( - weights.len(), - graph.num_vertices(), - "weights length must match graph num_vertices" - ); - assert!(bound_k >= 1, "co-k-plex parameter k must be at least 1"); + Self::try_with_k(graph, weights, bound_k).unwrap_or_else(|error| panic!("{error}")) + } + + fn try_with_k( + graph: G, + weights: Vec, + bound_k: usize, + ) -> Result { + if weights.len() != graph.num_vertices() { + return Err("weights length must match graph num_vertices".into()); + } + if bound_k == 0 { + return Err("co-k-plex parameter k must be at least 1".into()); + } if let Some(fixed) = K::K { - assert_eq!( - fixed, bound_k, - "fixed K type disagrees with runtime bound_k" - ); + if fixed != bound_k { + return Err("fixed K type disagrees with runtime bound_k".into()); + } } - Self { + Ok(Self { graph, weights, bound_k, _phantom: std::marker::PhantomData, - } + }) } /// Create a new instance using the compile-time `K`. @@ -146,8 +171,12 @@ impl MaximumCoKPlex { /// Panics if `K` is [`KN`] (use [`MaximumCoKPlex::with_k`] instead) or if /// `weights.len()` does not match `graph.num_vertices()`. pub fn new(graph: G, weights: Vec) -> Self { - let k = K::K.expect("KN requires with_k"); - Self::with_k(graph, weights, k) + Self::try_new(graph, weights).unwrap_or_else(|error| panic!("{error}")) + } + + fn try_new(graph: G, weights: Vec) -> Result { + let k = K::K.ok_or("KN requires with_k")?; + Self::try_with_k(graph, weights, k) } /// Get a reference to the underlying graph. @@ -286,7 +315,7 @@ impl TryFrom for MaximumCoKPlex, } +#[derive(Deserialize)] +struct LabelledDigraphData { + num_vertices: usize, + arcs: Vec, +} + +impl TryFrom for LabelledDigraph { + type Error = crate::registry::ConstructionError; + fn try_from(data: LabelledDigraphData) -> Result { + Self::try_new(data.num_vertices, data.arcs) + } +} + impl LabelledDigraph { /// Construct a new labelled digraph. /// /// # Panics /// Panics if any arc references a vertex index outside `0..num_vertices`. pub fn new(num_vertices: usize, arcs: Vec) -> Self { + Self::try_new(num_vertices, arcs).unwrap_or_else(|error| panic!("{error}")) + } + + fn try_new( + num_vertices: usize, + arcs: Vec, + ) -> Result { for arc in &arcs { - assert!( - arc.src < num_vertices, - "labelled arc source {} out of range for num_vertices = {}", - arc.src, - num_vertices - ); - assert!( - arc.dst < num_vertices, - "labelled arc destination {} out of range for num_vertices = {}", - arc.dst, - num_vertices - ); + if !(arc.src < num_vertices) { + return Err(format!( + "labelled arc source {} out of range for num_vertices = {}", + arc.src, num_vertices + ) + .into()); + }; + if !(arc.dst < num_vertices) { + return Err(format!( + "labelled arc destination {} out of range for num_vertices = {}", + arc.dst, num_vertices + ) + .into()); + }; } // Deduplicate while preserving order so set semantics hold. let mut seen = std::collections::HashSet::new(); @@ -104,10 +127,10 @@ impl LabelledDigraph { deduped.push(arc); } } - Self { + Ok(Self { num_vertices, arcs: deduped, - } + }) } /// Number of vertices `|V|`. diff --git a/src/models/graph/maximum_independent_set.rs b/src/models/graph/maximum_independent_set.rs index 4d5663cdc..533561c65 100644 --- a/src/models/graph/maximum_independent_set.rs +++ b/src/models/graph/maximum_independent_set.rs @@ -58,7 +58,7 @@ inventory::submit! { /// // Maximum independent set in a triangle has size 1 /// assert!(solutions.iter().all(|s| s.iter().filter(|&&selected| selected).count() == 1)); /// ``` -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize)] pub struct MaximumIndependentSet { /// The underlying graph. graph: G, @@ -66,6 +66,23 @@ pub struct MaximumIndependentSet { weights: Vec, } +#[derive(Deserialize)] +struct MaximumIndependentSetData { + graph: G, + weights: Vec, +} + +impl<'de, G, W> Deserialize<'de> for MaximumIndependentSet +where + G: Graph + Deserialize<'de>, + W: Clone + Default + Deserialize<'de>, +{ + fn deserialize>(deserializer: D) -> Result { + let data = MaximumIndependentSetData::deserialize(deserializer)?; + Self::try_new(data.graph, data.weights).map_err(serde::de::Error::custom) + } +} + macro_rules! simple_mis_spec { ($name:ident,$weight:ty,$one:expr $(, $weights:ident)?) => { #[derive(Debug, Deserialize, crate::CreateSpec)] @@ -213,12 +230,14 @@ unit_disk_mis_spec!( impl MaximumIndependentSet { /// Create an Independent Set problem from a graph with given weights. pub fn new(graph: G, weights: Vec) -> Self { - assert_eq!( - weights.len(), - graph.num_vertices(), - "weights length must match graph num_vertices" - ); - Self { graph, weights } + Self::try_new(graph, weights).unwrap_or_else(|error| panic!("{error}")) + } + + fn try_new(graph: G, weights: Vec) -> Result { + if weights.len() != graph.num_vertices() { + return Err("weights length must match graph num_vertices".into()); + } + Ok(Self { graph, weights }) } /// Get a reference to the underlying graph. diff --git a/src/models/graph/maximum_leaf_spanning_tree.rs b/src/models/graph/maximum_leaf_spanning_tree.rs index cc63bf7f5..7f792c2b5 100644 --- a/src/models/graph/maximum_leaf_spanning_tree.rs +++ b/src/models/graph/maximum_leaf_spanning_tree.rs @@ -43,22 +43,41 @@ inventory::submit! { /// # Type Parameters /// /// * `G` - The graph type (e.g., `SimpleGraph`) -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize)] pub struct MaximumLeafSpanningTree { /// The underlying graph. graph: G, } +#[derive(Deserialize)] +#[serde(bound(deserialize = "G: Graph + Deserialize<'de>"))] +struct MaximumLeafSpanningTreeData { + graph: G, +} + +impl<'de, G> Deserialize<'de> for MaximumLeafSpanningTree +where + G: Graph + Deserialize<'de>, +{ + fn deserialize>(deserializer: D) -> Result { + let data = MaximumLeafSpanningTreeData::::deserialize(deserializer)?; + Self::try_new(data.graph).map_err(serde::de::Error::custom) + } +} + impl MaximumLeafSpanningTree { /// Create a MaximumLeafSpanningTree problem from a graph. /// /// The graph must have at least 2 vertices. pub fn new(graph: G) -> Self { - assert!( - graph.num_vertices() >= 2, - "graph must have at least 2 vertices" - ); - Self { graph } + Self::try_new(graph).unwrap_or_else(|error| panic!("{error}")) + } + + fn try_new(graph: G) -> Result { + if !(graph.num_vertices() >= 2) { + return Err("graph must have at least 2 vertices".into()); + } + Ok(Self { graph }) } /// Get a reference to the underlying graph. diff --git a/src/models/graph/maximum_matching.rs b/src/models/graph/maximum_matching.rs index c031ab507..2a1403c5e 100644 --- a/src/models/graph/maximum_matching.rs +++ b/src/models/graph/maximum_matching.rs @@ -56,7 +56,7 @@ inventory::submit! { /// assert_eq!(sol.iter().filter(|&&selected| selected).count(), 1); /// } /// ``` -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize)] pub struct MaximumMatching { /// The underlying graph. graph: G, @@ -64,6 +64,23 @@ pub struct MaximumMatching { edge_weights: Vec, } +#[derive(Deserialize)] +struct MaximumMatchingData { + graph: G, + edge_weights: Vec, +} + +impl<'de, G, W> Deserialize<'de> for MaximumMatching +where + G: Graph + Deserialize<'de>, + W: Clone + Default + Deserialize<'de>, +{ + fn deserialize>(deserializer: D) -> Result { + let data = MaximumMatchingData::deserialize(deserializer)?; + Self::try_new(data.graph, data.edge_weights).map_err(serde::de::Error::custom) + } +} + #[derive(Debug, Deserialize, crate::CreateSpec)] struct MaximumMatchingCreateSpec { #[create(codec = "edge-list")] @@ -89,7 +106,7 @@ impl TryFrom for MaximumMatching { ) .into()); } - Ok(Self::new(graph, edge_weights)) + Self::try_new(graph, edge_weights) } } @@ -131,15 +148,15 @@ impl MaximumMatching { /// * `graph` - The graph /// * `edge_weights` - Weight for each edge (in graph.edges() order) pub fn new(graph: G, edge_weights: Vec) -> Self { - assert_eq!( - edge_weights.len(), - graph.num_edges(), - "edge_weights length must match num_edges" - ); - Self { + Self::try_new(graph, edge_weights).unwrap_or_else(|error| panic!("{error}")) + } + + fn try_new(graph: G, edge_weights: Vec) -> Result { + Self::check_weights(&graph, &edge_weights)?; + Ok(Self { graph, edge_weights, - } + }) } /// Create a MaximumMatching problem with unit weights. @@ -213,6 +230,16 @@ impl MaximumMatching { self.edge_weights = weights; } + fn check_weights( + graph: &G, + edge_weights: &[W], + ) -> Result<(), crate::registry::ConstructionError> { + if edge_weights.len() != graph.num_edges() { + return Err("edge_weights length must match graph num_edges".into()); + } + Ok(()) + } + /// Get the weights for the problem. pub fn weights(&self) -> Vec { self.edge_weights.clone() diff --git a/src/models/graph/min_max_multicenter.rs b/src/models/graph/min_max_multicenter.rs index 7af2ec0cd..da1b374ef 100644 --- a/src/models/graph/min_max_multicenter.rs +++ b/src/models/graph/min_max_multicenter.rs @@ -53,7 +53,7 @@ inventory::submit! { /// let solution = solver.solve(&problem).unwrap(); /// assert!(solution.is_some()); /// ``` -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize)] pub struct MinMaxMulticenter { /// The underlying graph. graph: G, @@ -65,6 +65,27 @@ pub struct MinMaxMulticenter { k: usize, } +#[derive(Deserialize)] +#[serde(bound(deserialize = "G: Graph + Deserialize<'de>, W: WeightElement + Deserialize<'de>"))] +struct MinMaxMulticenterData { + graph: G, + vertex_weights: Vec, + edge_lengths: Vec, + k: usize, +} + +impl<'de, G, W> Deserialize<'de> for MinMaxMulticenter +where + G: Graph + Deserialize<'de>, + W: WeightElement + Deserialize<'de>, +{ + fn deserialize>(deserializer: D) -> Result { + let data = MinMaxMulticenterData::::deserialize(deserializer)?; + Self::try_new(data.graph, data.vertex_weights, data.edge_lengths, data.k) + .map_err(serde::de::Error::custom) + } +} + macro_rules! min_max_multicenter_create_spec { ($name:ident, $weight:ty, $one:expr $(, $weights:ident, $edge_weights:ident)?) => { #[derive(Debug, Deserialize, crate::CreateSpec)] @@ -122,7 +143,7 @@ macro_rules! min_max_multicenter_create_spec { if spec.k == 0 || spec.k > graph.num_vertices() { return Err(format!("k must be between 1 and {}", graph.num_vertices()).into()); } - Ok(Self::new(graph, vertex_weights, edge_lengths, spec.k)) + Self::try_new(graph, vertex_weights, edge_lengths, spec.k) } } }; @@ -174,37 +195,47 @@ impl MinMaxMulticenter { /// - If any vertex weight or edge length is negative /// - If `k == 0` or `k > graph.num_vertices()` pub fn new(graph: G, vertex_weights: Vec, edge_lengths: Vec, k: usize) -> Self { - assert_eq!( - vertex_weights.len(), - graph.num_vertices(), - "vertex_weights length must match num_vertices" - ); - assert_eq!( - edge_lengths.len(), - graph.num_edges(), - "edge_lengths length must match num_edges" - ); + Self::try_new(graph, vertex_weights, edge_lengths, k) + .unwrap_or_else(|error| panic!("{error}")) + } + + fn try_new( + graph: G, + vertex_weights: Vec, + edge_lengths: Vec, + k: usize, + ) -> Result { + if vertex_weights.len() != graph.num_vertices() { + return Err("vertex_weights length must match num_vertices".into()); + } + if edge_lengths.len() != graph.num_edges() { + return Err("edge_lengths length must match num_edges".into()); + } let zero = W::Sum::zero(); - assert!( - vertex_weights - .iter() - .all(|weight| weight.to_sum() >= zero.clone()), - "vertex_weights must be non-negative" - ); - assert!( - edge_lengths - .iter() - .all(|length| length.to_sum() >= zero.clone()), - "edge_lengths must be non-negative" - ); - assert!(k > 0, "k must be positive"); - assert!(k <= graph.num_vertices(), "k must not exceed num_vertices"); - Self { + if !(vertex_weights + .iter() + .all(|weight| weight.to_sum() >= zero.clone())) + { + return Err("vertex_weights must be non-negative".into()); + } + if !(edge_lengths + .iter() + .all(|length| length.to_sum() >= zero.clone())) + { + return Err("edge_lengths must be non-negative".into()); + } + if k == 0 { + return Err("k must be positive".into()); + } + if !(k <= graph.num_vertices()) { + return Err("k must not exceed num_vertices".into()); + } + Ok(Self { graph, vertex_weights, edge_lengths, k, - } + }) } /// Get a reference to the underlying graph. diff --git a/src/models/graph/minimum_capacitated_spanning_tree.rs b/src/models/graph/minimum_capacitated_spanning_tree.rs index 97893e491..5a1bd8995 100644 --- a/src/models/graph/minimum_capacitated_spanning_tree.rs +++ b/src/models/graph/minimum_capacitated_spanning_tree.rs @@ -48,7 +48,7 @@ inventory::submit! { /// /// * `G` - The graph type (e.g., `SimpleGraph`) /// * `W` - The weight type for edges and requirements (e.g., `i64`) -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize)] pub struct MinimumCapacitatedSpanningTree { /// The underlying graph. graph: G, @@ -62,6 +62,37 @@ pub struct MinimumCapacitatedSpanningTree { capacity: W::Sum, } +#[derive(Deserialize)] +#[serde(bound( + deserialize = "G: Graph + Deserialize<'de>, W: WeightElement + Deserialize<'de>, W::Sum: Deserialize<'de>" +))] +struct MinimumCapacitatedSpanningTreeData { + graph: G, + weights: Vec, + root: usize, + requirements: Vec, + capacity: W::Sum, +} + +impl<'de, G, W> Deserialize<'de> for MinimumCapacitatedSpanningTree +where + G: Graph + Deserialize<'de>, + W: WeightElement + Deserialize<'de>, + W::Sum: Deserialize<'de>, +{ + fn deserialize>(deserializer: D) -> Result { + let data = MinimumCapacitatedSpanningTreeData::::deserialize(deserializer)?; + Self::try_new( + data.graph, + data.weights, + data.root, + data.requirements, + data.capacity, + ) + .map_err(serde::de::Error::custom) + } +} + #[derive(Debug, Deserialize, crate::CreateSpec)] struct MinimumCapacitatedSpanningTreeCreateSpec { /// The underlying graph. @@ -99,13 +130,13 @@ impl TryFrom if spec.root >= vertices { return Err("root is outside the graph".to_string().into()); } - Ok(Self::new( + Self::try_new( spec.graph, weights, spec.root, spec.requirements, spec.capacity, - )) + ) } } @@ -124,32 +155,38 @@ impl MinimumCapacitatedSpanningTree { requirements: Vec, capacity: W::Sum, ) -> Self { - assert_eq!( - weights.len(), - graph.num_edges(), - "weights length must match num_edges" - ); - assert_eq!( - requirements.len(), - graph.num_vertices(), - "requirements length must match num_vertices" - ); - assert!( - root < graph.num_vertices(), - "root {root} out of range (num_vertices = {})", - graph.num_vertices() - ); - assert!( - graph.num_vertices() >= 2, - "graph must have at least 2 vertices" - ); - Self { + Self::try_new(graph, weights, root, requirements, capacity) + .unwrap_or_else(|error| panic!("{error}")) + } + + fn try_new( + graph: G, + weights: Vec, + root: usize, + requirements: Vec, + capacity: W::Sum, + ) -> Result { + Self::check_weights(&graph, &weights)?; + if requirements.len() != graph.num_vertices() { + return Err("requirements length must match num_vertices".into()); + } + if !(root < graph.num_vertices()) { + return Err(format!( + "root {root} out of range (num_vertices = {})", + graph.num_vertices() + ) + .into()); + } + if !(graph.num_vertices() >= 2) { + return Err("graph must have at least 2 vertices".into()); + } + Ok(Self { graph, weights, root, requirements, capacity, - } + }) } /// Get a reference to the underlying graph. @@ -168,6 +205,13 @@ impl MinimumCapacitatedSpanningTree { self.weights = weights; } + fn check_weights(graph: &G, weights: &[W]) -> Result<(), crate::registry::ConstructionError> { + if weights.len() != graph.num_edges() { + return Err("weights length must match num_edges".into()); + } + Ok(()) + } + /// Check if the problem uses a non-unit weight type. pub fn is_weighted(&self) -> bool { !W::IS_UNIT diff --git a/src/models/graph/minimum_cost_circulation.rs b/src/models/graph/minimum_cost_circulation.rs index be297fbad..86fe2de01 100644 --- a/src/models/graph/minimum_cost_circulation.rs +++ b/src/models/graph/minimum_cost_circulation.rs @@ -85,6 +85,7 @@ inventory::submit! { /// assert_eq!(problem.total_cost(&witness).unwrap(), -5); /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "MinimumCostCirculationData")] pub struct MinimumCostCirculation { /// The directed multigraph G = (V, A). graph: DirectedGraph, @@ -94,6 +95,20 @@ pub struct MinimumCostCirculation { costs: Vec, } +#[derive(Deserialize)] +struct MinimumCostCirculationData { + graph: DirectedGraph, + capacities: Vec, + costs: Vec, +} + +impl TryFrom for MinimumCostCirculation { + type Error = crate::registry::ConstructionError; + fn try_from(data: MinimumCostCirculationData) -> Result { + Self::try_new(data.graph, data.capacities, data.costs) + } +} + impl MinimumCostCirculation { /// Create a new Minimum-Cost Circulation problem. /// @@ -106,27 +121,35 @@ impl MinimumCostCirculation { /// /// Note: costs are signed and **may be negative**. pub fn new(graph: DirectedGraph, capacities: Vec, costs: Vec) -> Self { + Self::try_new(graph, capacities, costs).unwrap_or_else(|error| panic!("{error}")) + } + + fn try_new( + graph: DirectedGraph, + capacities: Vec, + costs: Vec, + ) -> Result { let m = graph.num_arcs(); - assert_eq!( - capacities.len(), - m, - "capacities length ({}) must match num_arcs ({m})", - capacities.len() - ); - assert_eq!( - costs.len(), - m, - "costs length ({}) must match num_arcs ({m})", - costs.len() - ); + if capacities.len() != m { + return Err(format!( + "capacities length ({}) must match num_arcs ({m})", + capacities.len() + ) + .into()); + } + if costs.len() != m { + return Err(format!("costs length ({}) must match num_arcs ({m})", costs.len()).into()); + } for (i, &c) in capacities.iter().enumerate() { - assert!(c >= 0, "capacity[{i}] = {c} is negative"); + if !(c >= 0) { + return Err(format!("capacity[{i}] = {c} is negative").into()); + } } - Self { + Ok(Self { graph, capacities, costs, - } + }) } /// Get a reference to the underlying directed graph. diff --git a/src/models/graph/minimum_cut_into_bounded_sets.rs b/src/models/graph/minimum_cut_into_bounded_sets.rs index 2ea6548b3..ebae83c1b 100644 --- a/src/models/graph/minimum_cut_into_bounded_sets.rs +++ b/src/models/graph/minimum_cut_into_bounded_sets.rs @@ -56,7 +56,7 @@ inventory::submit! { /// let val = problem.evaluate(&vec![false, false, true, true]).unwrap(); /// assert_eq!(val, problemreductions::types::Min(Some(1))); /// ``` -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize)] pub struct MinimumCutIntoBoundedSets { /// The underlying graph structure. graph: G, @@ -70,6 +70,34 @@ pub struct MinimumCutIntoBoundedSets { size_bound: usize, } +#[derive(Deserialize)] +#[serde(bound(deserialize = "G: Graph + Deserialize<'de>, W: WeightElement + Deserialize<'de>"))] +struct MinimumCutIntoBoundedSetsData { + graph: G, + edge_weights: Vec, + source: usize, + sink: usize, + size_bound: usize, +} + +impl<'de, G, W> Deserialize<'de> for MinimumCutIntoBoundedSets +where + G: Graph + Deserialize<'de>, + W: WeightElement + Deserialize<'de>, +{ + fn deserialize>(deserializer: D) -> Result { + let data = MinimumCutIntoBoundedSetsData::::deserialize(deserializer)?; + Self::try_new( + data.graph, + data.edge_weights, + data.source, + data.sink, + data.size_bound, + ) + .map_err(serde::de::Error::custom) + } +} + #[derive(Debug, Deserialize, crate::CreateSpec)] struct MinimumCutIntoBoundedSetsCreateSpec { /// The undirected graph. @@ -101,13 +129,13 @@ impl TryFrom for MinimumCutIntoBoundedSets< .to_string() .into()); } - Ok(Self::new( + Self::try_new( spec.graph, edge_weights, spec.source, spec.sink, spec.size_bound, - )) + ) } } @@ -131,21 +159,36 @@ impl MinimumCutIntoBoundedSets { sink: usize, size_bound: usize, ) -> Self { - assert_eq!( - edge_weights.len(), - graph.num_edges(), - "edge_weights length must match num_edges" - ); - assert!(source < graph.num_vertices(), "source vertex out of bounds"); - assert!(sink < graph.num_vertices(), "sink vertex out of bounds"); - assert_ne!(source, sink, "source and sink must be different vertices"); - Self { + Self::try_new(graph, edge_weights, source, sink, size_bound) + .unwrap_or_else(|error| panic!("{error}")) + } + + fn try_new( + graph: G, + edge_weights: Vec, + source: usize, + sink: usize, + size_bound: usize, + ) -> Result { + if edge_weights.len() != graph.num_edges() { + return Err("edge_weights length must match num_edges".into()); + } + if !(source < graph.num_vertices()) { + return Err("source vertex out of bounds".into()); + } + if !(sink < graph.num_vertices()) { + return Err("sink vertex out of bounds".into()); + } + if source == sink { + return Err("source and sink must be different vertices".into()); + } + Ok(Self { graph, edge_weights, source, sink, size_bound, - } + }) } /// Get a reference to the underlying graph. diff --git a/src/models/graph/minimum_dominating_set.rs b/src/models/graph/minimum_dominating_set.rs index 500fa3b39..1fc1d6895 100644 --- a/src/models/graph/minimum_dominating_set.rs +++ b/src/models/graph/minimum_dominating_set.rs @@ -52,7 +52,7 @@ inventory::submit! { /// // Minimum dominating set is just the center vertex /// assert!(solutions.contains(&vec![true, false, false, false])); /// ``` -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize)] pub struct MinimumDominatingSet { /// The underlying graph. graph: G, @@ -60,6 +60,23 @@ pub struct MinimumDominatingSet { weights: Vec, } +#[derive(Deserialize)] +struct MinimumDominatingSetData { + graph: G, + weights: Vec, +} + +impl<'de, G, W> Deserialize<'de> for MinimumDominatingSet +where + G: Graph + Deserialize<'de>, + W: Clone + Default + Deserialize<'de>, +{ + fn deserialize>(deserializer: D) -> Result { + let data = MinimumDominatingSetData::deserialize(deserializer)?; + Self::try_new(data.graph, data.weights).map_err(serde::de::Error::custom) + } +} + #[derive(Debug, Deserialize, crate::CreateSpec)] struct MinimumDominatingSetCreateSpec { /// The underlying graph G=(V,E). @@ -81,19 +98,21 @@ impl TryFrom> ) .into()); } - Ok(Self::new(spec.graph, spec.weights)) + Self::try_new(spec.graph, spec.weights) } } impl MinimumDominatingSet { /// Create a Dominating Set problem from a graph with given weights. pub fn new(graph: G, weights: Vec) -> Self { - assert_eq!( - weights.len(), - graph.num_vertices(), - "weights length must match graph num_vertices" - ); - Self { graph, weights } + Self::try_new(graph, weights).unwrap_or_else(|error| panic!("{error}")) + } + + fn try_new(graph: G, weights: Vec) -> Result { + if weights.len() != graph.num_vertices() { + return Err("weights length must match graph num_vertices".into()); + } + Ok(Self { graph, weights }) } /// Get a reference to the underlying graph. @@ -231,7 +250,7 @@ impl TryFrom for MinimumDominatingSet Result { let weights = vec![One; spec.graph.num_vertices()]; - Ok(Self::new(spec.graph, weights)) + Self::try_new(spec.graph, weights) } } diff --git a/src/models/graph/minimum_edge_cost_flow.rs b/src/models/graph/minimum_edge_cost_flow.rs index ec861b944..5da5a783e 100644 --- a/src/models/graph/minimum_edge_cost_flow.rs +++ b/src/models/graph/minimum_edge_cost_flow.rs @@ -66,6 +66,7 @@ inventory::submit! { /// assert_eq!(problem.evaluate(&witness).unwrap(), problemreductions::types::Min(Some(3))); /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "MinimumEdgeCostFlowData")] pub struct MinimumEdgeCostFlow { /// The directed graph G = (V, A). graph: DirectedGraph, @@ -81,6 +82,30 @@ pub struct MinimumEdgeCostFlow { required_flow: i64, } +#[derive(Deserialize)] +struct MinimumEdgeCostFlowData { + graph: DirectedGraph, + prices: Vec, + capacities: Vec, + source: usize, + sink: usize, + required_flow: i64, +} + +impl TryFrom for MinimumEdgeCostFlow { + type Error = crate::registry::ConstructionError; + fn try_from(data: MinimumEdgeCostFlowData) -> Result { + Self::try_new( + data.graph, + data.prices, + data.capacities, + data.source, + data.sink, + data.required_flow, + ) + } +} + impl MinimumEdgeCostFlow { /// Create a new Minimum Edge-Cost Flow problem. /// @@ -110,34 +135,54 @@ impl MinimumEdgeCostFlow { sink: usize, required_flow: i64, ) -> Self { + Self::try_new(graph, prices, capacities, source, sink, required_flow) + .unwrap_or_else(|error| panic!("{error}")) + } + + fn try_new( + graph: DirectedGraph, + prices: Vec, + capacities: Vec, + source: usize, + sink: usize, + required_flow: i64, + ) -> Result { let n = graph.num_vertices(); let m = graph.num_arcs(); - assert_eq!( - prices.len(), - m, - "prices length ({}) must match num_arcs ({m})", - prices.len() - ); - assert_eq!( - capacities.len(), - m, - "capacities length ({}) must match num_arcs ({m})", - capacities.len() - ); - assert!(source < n, "source ({source}) >= num_vertices ({n})"); - assert!(sink < n, "sink ({sink}) >= num_vertices ({n})"); - assert_ne!(source, sink, "source and sink must be distinct"); + if prices.len() != m { + return Err( + format!("prices length ({}) must match num_arcs ({m})", prices.len()).into(), + ); + } + if capacities.len() != m { + return Err(format!( + "capacities length ({}) must match num_arcs ({m})", + capacities.len() + ) + .into()); + } + if !(source < n) { + return Err(format!("source ({source}) >= num_vertices ({n})").into()); + } + if !(sink < n) { + return Err(format!("sink ({sink}) >= num_vertices ({n})").into()); + } + if source == sink { + return Err("source and sink must be distinct".into()); + } for (i, &c) in capacities.iter().enumerate() { - assert!(c >= 0, "capacity[{i}] = {c} is negative"); + if !(c >= 0) { + return Err(format!("capacity[{i}] = {c} is negative").into()); + } } - Self { + Ok(Self { graph, prices, capacities, source, sink, required_flow, - } + }) } /// Get a reference to the underlying directed graph. diff --git a/src/models/graph/minimum_feedback_arc_set.rs b/src/models/graph/minimum_feedback_arc_set.rs index a37211c8e..c188159ca 100644 --- a/src/models/graph/minimum_feedback_arc_set.rs +++ b/src/models/graph/minimum_feedback_arc_set.rs @@ -55,7 +55,7 @@ inventory::submit! { /// // Minimum FAS has size 1 (remove any single arc to break the cycle) /// assert_eq!(solution.iter().filter(|&&selected| selected).count(), 1); /// ``` -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize)] pub struct MinimumFeedbackArcSet { /// The directed graph. graph: DirectedGraph, @@ -63,6 +63,22 @@ pub struct MinimumFeedbackArcSet { weights: Vec, } +#[derive(Deserialize)] +struct MinimumFeedbackArcSetData { + graph: DirectedGraph, + weights: Vec, +} + +impl<'de, W> Deserialize<'de> for MinimumFeedbackArcSet +where + W: Clone + Default + Deserialize<'de>, +{ + fn deserialize>(deserializer: D) -> Result { + let data = MinimumFeedbackArcSetData::deserialize(deserializer)?; + Self::try_new(data.graph, data.weights).map_err(serde::de::Error::custom) + } +} + #[derive(Debug, Deserialize, crate::CreateSpec)] struct MinimumFeedbackArcSetCreateSpec { /// The directed graph. @@ -78,19 +94,22 @@ impl TryFrom for MinimumFeedbackArcSet { if weights.len() != count { return Err(format!("weights has {} entries, expected {count}", weights.len()).into()); } - Ok(Self::new(spec.graph, weights)) + Self::try_new(spec.graph, weights) } } impl MinimumFeedbackArcSet { /// Create a Minimum Feedback Arc Set problem from a directed graph with given weights. pub fn new(graph: DirectedGraph, weights: Vec) -> Self { - assert_eq!( - weights.len(), - graph.num_arcs(), - "weights length must match graph num_arcs" - ); - Self { graph, weights } + Self::try_new(graph, weights).unwrap_or_else(|error| panic!("{error}")) + } + + fn try_new( + graph: DirectedGraph, + weights: Vec, + ) -> Result { + Self::check_weights(&graph, &weights)?; + Ok(Self { graph, weights }) } /// Get a reference to the underlying directed graph. @@ -113,6 +132,16 @@ impl MinimumFeedbackArcSet { self.weights = weights; } + fn check_weights( + graph: &DirectedGraph, + weights: &[W], + ) -> Result<(), crate::registry::ConstructionError> { + if weights.len() != graph.num_arcs() { + return Err("weights length must match graph num_arcs".into()); + } + Ok(()) + } + /// Check if a configuration is a valid feedback arc set. /// /// A configuration is valid if removing the selected arcs makes the graph acyclic. diff --git a/src/models/graph/minimum_feedback_vertex_set.rs b/src/models/graph/minimum_feedback_vertex_set.rs index 06853fd1f..59478685d 100644 --- a/src/models/graph/minimum_feedback_vertex_set.rs +++ b/src/models/graph/minimum_feedback_vertex_set.rs @@ -49,7 +49,7 @@ inventory::submit! { /// // Any single vertex breaks the cycle /// assert_eq!(solutions.len(), 3); /// ``` -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize)] pub struct MinimumFeedbackVertexSet { /// The underlying directed graph. graph: DirectedGraph, @@ -57,6 +57,22 @@ pub struct MinimumFeedbackVertexSet { weights: Vec, } +#[derive(Deserialize)] +struct MinimumFeedbackVertexSetData { + graph: DirectedGraph, + weights: Vec, +} + +impl<'de, W> Deserialize<'de> for MinimumFeedbackVertexSet +where + W: Clone + Default + Deserialize<'de>, +{ + fn deserialize>(deserializer: D) -> Result { + let data = MinimumFeedbackVertexSetData::deserialize(deserializer)?; + Self::try_new(data.graph, data.weights).map_err(serde::de::Error::custom) + } +} + #[derive(Debug, Deserialize, crate::CreateSpec)] struct MinimumFeedbackVertexSetCreateSpec { /// The directed graph. @@ -74,19 +90,22 @@ impl TryFrom> if weights.len() != count { return Err(format!("weights has {} entries, expected {count}", weights.len()).into()); } - Ok(Self::new(spec.graph, weights)) + Self::try_new(spec.graph, weights) } } impl MinimumFeedbackVertexSet { /// Create a Feedback Vertex Set problem from a directed graph with given weights. pub fn new(graph: DirectedGraph, weights: Vec) -> Self { - assert_eq!( - weights.len(), - graph.num_vertices(), - "weights length must match graph num_vertices" - ); - Self { graph, weights } + Self::try_new(graph, weights).unwrap_or_else(|error| panic!("{error}")) + } + + fn try_new( + graph: DirectedGraph, + weights: Vec, + ) -> Result { + Self::check_weights(&graph, &weights)?; + Ok(Self { graph, weights }) } /// Get a reference to the underlying directed graph. @@ -109,6 +128,16 @@ impl MinimumFeedbackVertexSet { self.weights = weights; } + fn check_weights( + graph: &DirectedGraph, + weights: &[W], + ) -> Result<(), crate::registry::ConstructionError> { + if weights.len() != graph.num_vertices() { + return Err("weights length must match graph num_vertices".into()); + } + Ok(()) + } + /// Check if a configuration is a valid feedback vertex set. pub fn is_valid_solution(&self, config: &[usize]) -> bool { if config.len() != self.graph.num_vertices() { @@ -204,7 +233,7 @@ impl TryFrom for MinimumFeedbackVertexSet type Error = crate::registry::ConstructionError; fn try_from(spec: MinimumFeedbackVertexSetOneCreateSpec) -> Result { let weights = vec![One; spec.graph.num_vertices()]; - Ok(Self::new(spec.graph, weights)) + Self::try_new(spec.graph, weights) } } diff --git a/src/models/graph/minimum_multiway_cut.rs b/src/models/graph/minimum_multiway_cut.rs index 6223b7e43..ef9692216 100644 --- a/src/models/graph/minimum_multiway_cut.rs +++ b/src/models/graph/minimum_multiway_cut.rs @@ -42,13 +42,33 @@ inventory::submit! { /// /// A configuration is feasible if removing the cut edges disconnects all /// terminal pairs. -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize)] pub struct MinimumMultiwayCut { graph: G, terminals: Vec, edge_weights: Vec, } +#[derive(Deserialize)] +#[serde(bound(deserialize = "G: Graph + Deserialize<'de>, W: WeightElement + Deserialize<'de>"))] +struct MinimumMultiwayCutData { + graph: G, + terminals: Vec, + edge_weights: Vec, +} + +impl<'de, G, W> Deserialize<'de> for MinimumMultiwayCut +where + G: Graph + Deserialize<'de>, + W: WeightElement + Deserialize<'de>, +{ + fn deserialize>(deserializer: D) -> Result { + let data = MinimumMultiwayCutData::::deserialize(deserializer)?; + Self::try_new(data.graph, data.terminals, data.edge_weights) + .map_err(serde::de::Error::custom) + } +} + #[derive(Debug, Deserialize, crate::CreateSpec)] struct MinimumMultiwayCutCreateSpec { /// The undirected graph G=(V,E). @@ -90,7 +110,7 @@ impl TryFrom for MinimumMultiwayCut MinimumMultiwayCut { /// - If any terminal index is out of bounds /// - If there are duplicate terminal indices pub fn new(graph: G, terminals: Vec, edge_weights: Vec) -> Self { - assert_eq!( - edge_weights.len(), - graph.num_edges(), - "edge_weights length must match num_edges" - ); - assert!(terminals.len() >= 2, "need at least 2 terminals"); + Self::try_new(graph, terminals, edge_weights).unwrap_or_else(|error| panic!("{error}")) + } + + fn try_new( + graph: G, + terminals: Vec, + edge_weights: Vec, + ) -> Result { + if edge_weights.len() != graph.num_edges() { + return Err("edge_weights length must match num_edges".into()); + } + if !(terminals.len() >= 2) { + return Err("need at least 2 terminals".into()); + } let mut sorted = terminals.clone(); sorted.sort(); sorted.dedup(); - assert_eq!(sorted.len(), terminals.len(), "duplicate terminal indices"); + if sorted.len() != terminals.len() { + return Err("duplicate terminal indices".into()); + } for &t in &terminals { - assert!(t < graph.num_vertices(), "terminal index out of bounds"); + if !(t < graph.num_vertices()) { + return Err("terminal index out of bounds".into()); + } } - Self { + Ok(Self { graph, terminals, edge_weights, - } + }) } /// Get a reference to the underlying graph. diff --git a/src/models/graph/minimum_sum_multicenter.rs b/src/models/graph/minimum_sum_multicenter.rs index 25a925762..e1f6cbe72 100644 --- a/src/models/graph/minimum_sum_multicenter.rs +++ b/src/models/graph/minimum_sum_multicenter.rs @@ -54,7 +54,7 @@ inventory::submit! { /// // Center at vertex 1 gives total distance 0+1+1 = 2 (optimal) /// assert_eq!(solution, vec![false, true, false]); /// ``` -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize)] pub struct MinimumSumMulticenter { /// The underlying graph. graph: G, @@ -66,6 +66,27 @@ pub struct MinimumSumMulticenter { k: usize, } +#[derive(Deserialize)] +#[serde(bound(deserialize = "G: Graph + Deserialize<'de>, W: WeightElement + Deserialize<'de>"))] +struct MinimumSumMulticenterData { + graph: G, + vertex_weights: Vec, + edge_lengths: Vec, + k: usize, +} + +impl<'de, G, W> Deserialize<'de> for MinimumSumMulticenter +where + G: Graph + Deserialize<'de>, + W: WeightElement + Deserialize<'de>, +{ + fn deserialize>(deserializer: D) -> Result { + let data = MinimumSumMulticenterData::::deserialize(deserializer)?; + Self::try_new(data.graph, data.vertex_weights, data.edge_lengths, data.k) + .map_err(serde::de::Error::custom) + } +} + #[derive(Debug, Deserialize, crate::CreateSpec)] struct MinimumSumMulticenterCreateSpec { #[create(codec = "edge-list")] @@ -120,7 +141,7 @@ impl TryFrom for MinimumSumMulticenter graph.num_vertices() { return Err(format!("k must be between 1 and {}", graph.num_vertices()).into()); } - Ok(Self::new(graph, vertex_weights, edge_lengths, spec.k)) + Self::try_new(graph, vertex_weights, edge_lengths, spec.k) } } @@ -160,24 +181,34 @@ impl MinimumSumMulticenter { /// - If `edge_lengths.len() != graph.num_edges()` /// - If `k == 0` or `k > graph.num_vertices()` pub fn new(graph: G, vertex_weights: Vec, edge_lengths: Vec, k: usize) -> Self { - assert_eq!( - vertex_weights.len(), - graph.num_vertices(), - "vertex_weights length must match num_vertices" - ); - assert_eq!( - edge_lengths.len(), - graph.num_edges(), - "edge_lengths length must match num_edges" - ); - assert!(k > 0, "k must be positive"); - assert!(k <= graph.num_vertices(), "k must not exceed num_vertices"); - Self { + Self::try_new(graph, vertex_weights, edge_lengths, k) + .unwrap_or_else(|error| panic!("{error}")) + } + + fn try_new( + graph: G, + vertex_weights: Vec, + edge_lengths: Vec, + k: usize, + ) -> Result { + if vertex_weights.len() != graph.num_vertices() { + return Err("vertex_weights length must match num_vertices".into()); + } + if edge_lengths.len() != graph.num_edges() { + return Err("edge_lengths length must match num_edges".into()); + } + if k == 0 { + return Err("k must be positive".into()); + } + if !(k <= graph.num_vertices()) { + return Err("k must not exceed num_vertices".into()); + } + Ok(Self { graph, vertex_weights, edge_lengths, k, - } + }) } /// Get a reference to the underlying graph. diff --git a/src/models/graph/minimum_vertex_cover.rs b/src/models/graph/minimum_vertex_cover.rs index 33af2207d..cd9df87f6 100644 --- a/src/models/graph/minimum_vertex_cover.rs +++ b/src/models/graph/minimum_vertex_cover.rs @@ -52,7 +52,7 @@ inventory::submit! { /// // Minimum vertex cover is just vertex 1 /// assert!(solutions.contains(&vec![false, true, false])); /// ``` -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize)] pub struct MinimumVertexCover { /// The underlying graph. graph: G, @@ -60,6 +60,23 @@ pub struct MinimumVertexCover { weights: Vec, } +#[derive(Deserialize)] +struct MinimumVertexCoverData { + graph: G, + weights: Vec, +} + +impl<'de, G, W> Deserialize<'de> for MinimumVertexCover +where + G: Graph + Deserialize<'de>, + W: Clone + Default + Deserialize<'de>, +{ + fn deserialize>(deserializer: D) -> Result { + let data = MinimumVertexCoverData::deserialize(deserializer)?; + Self::try_new(data.graph, data.weights).map_err(serde::de::Error::custom) + } +} + #[derive(Debug, Deserialize, crate::CreateSpec)] struct MinimumVertexCoverCreateSpec { /// The underlying graph G=(V,E). @@ -84,19 +101,21 @@ impl TryFrom> ) .into()); } - Ok(Self::new(spec.graph, weights)) + Self::try_new(spec.graph, weights) } } impl MinimumVertexCover { /// Create a Vertex Covering problem from a graph with given weights. pub fn new(graph: G, weights: Vec) -> Self { - assert_eq!( - weights.len(), - graph.num_vertices(), - "weights length must match graph num_vertices" - ); - Self { graph, weights } + Self::try_new(graph, weights).unwrap_or_else(|error| panic!("{error}")) + } + + fn try_new(graph: G, weights: Vec) -> Result { + if weights.len() != graph.num_vertices() { + return Err("weights length must match graph num_vertices".into()); + } + Ok(Self { graph, weights }) } /// Get a reference to the underlying graph. @@ -221,7 +240,7 @@ impl TryFrom for MinimumVertexCover Result { let weights = vec![One; spec.graph.num_vertices()]; - Ok(Self::new(spec.graph, weights)) + Self::try_new(spec.graph, weights) } } diff --git a/src/models/graph/multiple_copy_file_allocation.rs b/src/models/graph/multiple_copy_file_allocation.rs index 2c73b07e5..2475804e1 100644 --- a/src/models/graph/multiple_copy_file_allocation.rs +++ b/src/models/graph/multiple_copy_file_allocation.rs @@ -33,12 +33,27 @@ inventory::submit! { /// /// where d(v, V') is the shortest-path distance from v to the nearest copy in V'. #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "MultipleCopyFileAllocationData")] pub struct MultipleCopyFileAllocation { graph: SimpleGraph, usage: Vec, storage: Vec, } +#[derive(Deserialize)] +struct MultipleCopyFileAllocationData { + graph: SimpleGraph, + usage: Vec, + storage: Vec, +} + +impl TryFrom for MultipleCopyFileAllocation { + type Error = crate::registry::ConstructionError; + fn try_from(data: MultipleCopyFileAllocationData) -> Result { + Self::try_new(data.graph, data.usage, data.storage) + } +} + #[derive(Debug, Deserialize, crate::CreateSpec)] struct MultipleCopyFileAllocationCreateSpec { /// Network graph edges. @@ -94,21 +109,25 @@ impl TryFrom for MultipleCopyFileAllocatio impl MultipleCopyFileAllocation { /// Create a new Multiple Copy File Allocation instance. pub fn new(graph: SimpleGraph, usage: Vec, storage: Vec) -> Self { - assert_eq!( - usage.len(), - graph.num_vertices(), - "usage length must match graph num_vertices" - ); - assert_eq!( - storage.len(), - graph.num_vertices(), - "storage length must match graph num_vertices" - ); - Self { + Self::try_new(graph, usage, storage).unwrap_or_else(|error| panic!("{error}")) + } + + fn try_new( + graph: SimpleGraph, + usage: Vec, + storage: Vec, + ) -> Result { + if usage.len() != graph.num_vertices() { + return Err("usage length must match graph num_vertices".into()); + } + if storage.len() != graph.num_vertices() { + return Err("storage length must match graph num_vertices".into()); + } + Ok(Self { graph, usage, storage, - } + }) } /// Get a reference to the underlying graph. diff --git a/src/models/graph/partition_into_cliques.rs b/src/models/graph/partition_into_cliques.rs index c1767b6cc..fbb7545b3 100644 --- a/src/models/graph/partition_into_cliques.rs +++ b/src/models/graph/partition_into_cliques.rs @@ -53,7 +53,7 @@ inventory::submit! { /// let solution = solver.solve(&problem).unwrap(); /// assert!(solution.is_some()); /// ``` -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize)] #[serde(bound(deserialize = "G: serde::Deserialize<'de>"))] pub struct PartitionIntoCliques { /// The underlying graph. @@ -62,18 +62,40 @@ pub struct PartitionIntoCliques { num_cliques: usize, } +#[derive(Deserialize)] +#[serde(bound(deserialize = "G: Graph + Deserialize<'de>"))] +struct PartitionIntoCliquesData { + graph: G, + num_cliques: usize, +} + +impl<'de, G> Deserialize<'de> for PartitionIntoCliques +where + G: Graph + Deserialize<'de>, +{ + fn deserialize>(deserializer: D) -> Result { + let data = PartitionIntoCliquesData::::deserialize(deserializer)?; + Self::try_new(data.graph, data.num_cliques).map_err(serde::de::Error::custom) + } +} + impl PartitionIntoCliques { /// Create a new Partition Into Cliques instance. /// /// # Panics /// Panics if `num_cliques` is zero or greater than `graph.num_vertices()`. pub fn new(graph: G, num_cliques: usize) -> Self { - assert!(num_cliques >= 1, "num_cliques must be at least 1"); - assert!( - num_cliques <= graph.num_vertices(), - "num_cliques must be at most num_vertices" - ); - Self { graph, num_cliques } + Self::try_new(graph, num_cliques).unwrap_or_else(|error| panic!("{error}")) + } + + fn try_new(graph: G, num_cliques: usize) -> Result { + if num_cliques == 0 { + return Err("num_cliques must be at least 1".into()); + } + if !(num_cliques <= graph.num_vertices()) { + return Err("num_cliques must be at most num_vertices".into()); + } + Ok(Self { graph, num_cliques }) } /// Get a reference to the underlying graph. diff --git a/src/models/graph/partition_into_forests.rs b/src/models/graph/partition_into_forests.rs index 37e30fdff..92f116f9d 100644 --- a/src/models/graph/partition_into_forests.rs +++ b/src/models/graph/partition_into_forests.rs @@ -54,7 +54,7 @@ inventory::submit! { /// let solution = solver.solve(&problem).unwrap(); /// assert!(solution.is_some()); /// ``` -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize)] #[serde(bound(deserialize = "G: serde::Deserialize<'de>"))] pub struct PartitionIntoForests { /// The underlying graph. @@ -63,14 +63,37 @@ pub struct PartitionIntoForests { num_forests: usize, } +#[derive(Deserialize)] +#[serde(bound(deserialize = "G: Graph + Deserialize<'de>"))] +struct PartitionIntoForestsData { + graph: G, + num_forests: usize, +} + +impl<'de, G> Deserialize<'de> for PartitionIntoForests +where + G: Graph + Deserialize<'de>, +{ + fn deserialize>(deserializer: D) -> Result { + let data = PartitionIntoForestsData::::deserialize(deserializer)?; + Self::try_new(data.graph, data.num_forests).map_err(serde::de::Error::custom) + } +} + impl PartitionIntoForests { /// Create a new Partition Into Forests instance. /// /// # Panics /// Panics if `num_forests` is zero. pub fn new(graph: G, num_forests: usize) -> Self { - assert!(num_forests >= 1, "num_forests must be at least 1"); - Self { graph, num_forests } + Self::try_new(graph, num_forests).unwrap_or_else(|error| panic!("{error}")) + } + + fn try_new(graph: G, num_forests: usize) -> Result { + if num_forests == 0 { + return Err("num_forests must be at least 1".into()); + } + Ok(Self { graph, num_forests }) } /// Get a reference to the underlying graph. diff --git a/src/models/graph/partition_into_paths_of_length_2.rs b/src/models/graph/partition_into_paths_of_length_2.rs index 807040694..a00375c87 100644 --- a/src/models/graph/partition_into_paths_of_length_2.rs +++ b/src/models/graph/partition_into_paths_of_length_2.rs @@ -58,26 +58,47 @@ inventory::submit! { /// let solution = solver.solve(&problem).unwrap(); /// assert!(solution.is_some()); /// ``` -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize)] #[serde(bound(deserialize = "G: serde::Deserialize<'de>"))] pub struct PartitionIntoPathsOfLength2 { /// The underlying graph. graph: G, } +#[derive(Deserialize)] +#[serde(bound(deserialize = "G: Graph + Deserialize<'de>"))] +struct PartitionIntoPathsOfLength2Data { + graph: G, +} + +impl<'de, G> Deserialize<'de> for PartitionIntoPathsOfLength2 +where + G: Graph + Deserialize<'de>, +{ + fn deserialize>(deserializer: D) -> Result { + let data = PartitionIntoPathsOfLength2Data::::deserialize(deserializer)?; + Self::try_new(data.graph).map_err(serde::de::Error::custom) + } +} + impl PartitionIntoPathsOfLength2 { /// Create a new PartitionIntoPathsOfLength2 problem from a graph. /// /// # Panics /// Panics if `graph.num_vertices()` is not divisible by 3. pub fn new(graph: G) -> Self { - assert_eq!( - graph.num_vertices() % 3, - 0, - "Number of vertices ({}) must be divisible by 3", - graph.num_vertices() - ); - Self { graph } + Self::try_new(graph).unwrap_or_else(|error| panic!("{error}")) + } + + fn try_new(graph: G) -> Result { + if !graph.num_vertices().is_multiple_of(3) { + return Err(format!( + "Number of vertices ({}) must be divisible by 3", + graph.num_vertices() + ) + .into()); + } + Ok(Self { graph }) } /// Get a reference to the underlying graph. diff --git a/src/models/graph/partition_into_perfect_matchings.rs b/src/models/graph/partition_into_perfect_matchings.rs index 8fd16d380..29f1debb1 100644 --- a/src/models/graph/partition_into_perfect_matchings.rs +++ b/src/models/graph/partition_into_perfect_matchings.rs @@ -55,7 +55,7 @@ inventory::submit! { /// let solution = solver.solve(&problem).unwrap(); /// assert!(solution.is_some()); /// ``` -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize)] #[serde(bound(deserialize = "G: serde::Deserialize<'de>"))] pub struct PartitionIntoPerfectMatchings { /// The underlying graph. @@ -64,21 +64,43 @@ pub struct PartitionIntoPerfectMatchings { num_matchings: usize, } +#[derive(Deserialize)] +#[serde(bound(deserialize = "G: Graph + Deserialize<'de>"))] +struct PartitionIntoPerfectMatchingsData { + graph: G, + num_matchings: usize, +} + +impl<'de, G> Deserialize<'de> for PartitionIntoPerfectMatchings +where + G: Graph + Deserialize<'de>, +{ + fn deserialize>(deserializer: D) -> Result { + let data = PartitionIntoPerfectMatchingsData::::deserialize(deserializer)?; + Self::try_new(data.graph, data.num_matchings).map_err(serde::de::Error::custom) + } +} + impl PartitionIntoPerfectMatchings { /// Create a new Partition Into Perfect Matchings instance. /// /// # Panics /// Panics if `num_matchings` is zero or greater than `graph.num_vertices()`. pub fn new(graph: G, num_matchings: usize) -> Self { - assert!(num_matchings >= 1, "num_matchings must be at least 1"); - assert!( - num_matchings <= graph.num_vertices(), - "num_matchings must be at most num_vertices" - ); - Self { + Self::try_new(graph, num_matchings).unwrap_or_else(|error| panic!("{error}")) + } + + fn try_new(graph: G, num_matchings: usize) -> Result { + if num_matchings == 0 { + return Err("num_matchings must be at least 1".into()); + } + if !(num_matchings <= graph.num_vertices()) { + return Err("num_matchings must be at most num_vertices".into()); + } + Ok(Self { graph, num_matchings, - } + }) } /// Get a reference to the underlying graph. diff --git a/src/models/graph/partition_into_triangles.rs b/src/models/graph/partition_into_triangles.rs index 816009a41..5e6f9e8a8 100644 --- a/src/models/graph/partition_into_triangles.rs +++ b/src/models/graph/partition_into_triangles.rs @@ -50,25 +50,47 @@ inventory::submit! { /// let solution = solver.solve(&problem).unwrap(); /// assert!(solution.is_some()); /// ``` -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize)] #[serde(bound(deserialize = "G: serde::Deserialize<'de>"))] pub struct PartitionIntoTriangles { /// The underlying graph. graph: G, } +#[derive(Deserialize)] +#[serde(bound(deserialize = "G: Graph + Deserialize<'de>"))] +struct PartitionIntoTrianglesData { + graph: G, +} + +impl<'de, G> Deserialize<'de> for PartitionIntoTriangles +where + G: Graph + Deserialize<'de>, +{ + fn deserialize>(deserializer: D) -> Result { + let data = PartitionIntoTrianglesData::::deserialize(deserializer)?; + Self::try_new(data.graph).map_err(serde::de::Error::custom) + } +} + impl PartitionIntoTriangles { /// Create a new Partition Into Triangles problem from a graph. /// /// # Panics /// Panics if the number of vertices is not divisible by 3. pub fn new(graph: G) -> Self { - assert!( - graph.num_vertices().is_multiple_of(3), - "Number of vertices ({}) must be divisible by 3", - graph.num_vertices() - ); - Self { graph } + Self::try_new(graph).unwrap_or_else(|error| panic!("{error}")) + } + + fn try_new(graph: G) -> Result { + if !(graph.num_vertices().is_multiple_of(3)) { + return Err(format!( + "Number of vertices ({}) must be divisible by 3", + graph.num_vertices() + ) + .into()); + } + Ok(Self { graph }) } /// Get a reference to the underlying graph. diff --git a/src/models/graph/rural_postman.rs b/src/models/graph/rural_postman.rs index 7a098c101..5265da012 100644 --- a/src/models/graph/rural_postman.rs +++ b/src/models/graph/rural_postman.rs @@ -52,7 +52,7 @@ inventory::submit! { /// /// * `G` - The graph type (e.g., `SimpleGraph`) /// * `W` - The weight type for edge lengths (e.g., `i64`, `f64`) -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize)] pub struct RuralPostman { /// The underlying graph. graph: G, @@ -62,6 +62,26 @@ pub struct RuralPostman { required_edges: Vec, } +#[derive(Deserialize)] +#[serde(bound(deserialize = "G: Graph + Deserialize<'de>, W: WeightElement + Deserialize<'de>"))] +struct RuralPostmanData { + graph: G, + edge_lengths: Vec, + required_edges: Vec, +} + +impl<'de, G, W> Deserialize<'de> for RuralPostman +where + G: Graph + Deserialize<'de>, + W: WeightElement + Deserialize<'de>, +{ + fn deserialize>(deserializer: D) -> Result { + let data = RuralPostmanData::::deserialize(deserializer)?; + Self::try_new(data.graph, data.edge_lengths, data.required_edges) + .map_err(serde::de::Error::custom) + } +} + #[derive(Debug, Deserialize, crate::CreateSpec)] struct RuralPostmanCreateSpec { #[create(codec = "edge-list")] @@ -96,7 +116,7 @@ impl TryFrom for RuralPostman { { return Err(format!("required edge index {edge} is out of bounds").into()); } - Ok(Self::new(graph, edge_lengths, spec.required_edges)) + Self::try_new(graph, edge_lengths, spec.required_edges) } } @@ -138,24 +158,30 @@ impl RuralPostman { /// Panics if edge_lengths length does not match graph edges, /// or if any required edge index is out of bounds. pub fn new(graph: G, edge_lengths: Vec, required_edges: Vec) -> Self { - assert_eq!( - edge_lengths.len(), - graph.num_edges(), - "edge_lengths length must match num_edges" - ); + Self::try_new(graph, edge_lengths, required_edges).unwrap_or_else(|error| panic!("{error}")) + } + + fn try_new( + graph: G, + edge_lengths: Vec, + required_edges: Vec, + ) -> Result { + Self::check_weights(&graph, &edge_lengths)?; for &idx in &required_edges { - assert!( - idx < graph.num_edges(), - "required edge index {} out of bounds (graph has {} edges)", - idx, - graph.num_edges() - ); + if !(idx < graph.num_edges()) { + return Err(format!( + "required edge index {} out of bounds (graph has {} edges)", + idx, + graph.num_edges() + ) + .into()); + } } - Self { + Ok(Self { graph, edge_lengths, required_edges, - } + }) } /// Get a reference to the underlying graph. @@ -194,6 +220,13 @@ impl RuralPostman { self.edge_lengths = weights; } + fn check_weights(graph: &G, weights: &[W]) -> Result<(), crate::registry::ConstructionError> { + if weights.len() != graph.num_edges() { + return Err("edge_lengths length must match num_edges".into()); + } + Ok(()) + } + /// Get the edge lengths as a Vec. pub fn weights(&self) -> Vec { self.edge_lengths.clone() diff --git a/src/models/graph/shortest_weight_constrained_path.rs b/src/models/graph/shortest_weight_constrained_path.rs index 5b9aa0eee..0f455b0c2 100644 --- a/src/models/graph/shortest_weight_constrained_path.rs +++ b/src/models/graph/shortest_weight_constrained_path.rs @@ -51,7 +51,7 @@ inventory::submit! { /// /// * `G` - The graph type (e.g., `SimpleGraph`) /// * `N` - The edge length / weight type (e.g., `i64`, `f64`) -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize)] pub struct ShortestWeightConstrainedPath { /// The underlying graph. graph: G, @@ -67,6 +67,39 @@ pub struct ShortestWeightConstrainedPath { weight_bound: N::Sum, } +#[derive(Deserialize)] +#[serde(bound( + deserialize = "G: Graph + Deserialize<'de>, N: WeightElement + Deserialize<'de>, N::Sum: Deserialize<'de>" +))] +struct ShortestWeightConstrainedPathData { + graph: G, + edge_lengths: Vec, + edge_weights: Vec, + source_vertex: usize, + target_vertex: usize, + weight_bound: N::Sum, +} + +impl<'de, G, N> Deserialize<'de> for ShortestWeightConstrainedPath +where + G: Graph + Deserialize<'de>, + N: WeightElement + Deserialize<'de>, + N::Sum: Deserialize<'de>, +{ + fn deserialize>(deserializer: D) -> Result { + let data = ShortestWeightConstrainedPathData::::deserialize(deserializer)?; + Self::try_new( + data.graph, + data.edge_lengths, + data.edge_weights, + data.source_vertex, + data.target_vertex, + data.weight_bound, + ) + .map_err(serde::de::Error::custom) + } +} + #[derive(Debug, Deserialize, crate::CreateSpec)] struct ShortestWeightConstrainedPathCreateSpec { /// The underlying graph G=(V,E). @@ -127,29 +160,30 @@ impl TryFrom if spec.weight_bound <= 0 { return Err("weight_bound must be positive".to_string().into()); } - Ok(Self::new( + Self::try_new( spec.graph, spec.edge_lengths, spec.edge_weights, spec.source_vertex, spec.target_vertex, spec.weight_bound, - )) + ) } } impl ShortestWeightConstrainedPath { - fn assert_positive_edge_values(values: &[N], label: &str) { - let zero = N::Sum::zero(); - assert!( - values.iter().all(|value| value.to_sum() > zero.clone()), - "All {label} must be positive (> 0)" - ); - } - - fn assert_positive_bound(bound: &N::Sum, label: &str) { - let zero = N::Sum::zero(); - assert!(bound > &zero, "{label} must be positive (> 0)"); + fn check_edge_values( + graph: &G, + values: &[N], + label: &str, + ) -> Result<(), crate::registry::ConstructionError> { + if values.len() != graph.num_edges() { + return Err(format!("{label} length must match num_edges").into()); + } + if !values.iter().all(|value| value.to_sum() > N::Sum::zero()) { + return Err(format!("All {label} must be positive (> 0)").into()); + } + Ok(()) } /// Create a new ShortestWeightConstrainedPath instance. @@ -166,39 +200,54 @@ impl ShortestWeightConstrainedPath { target_vertex: usize, weight_bound: N::Sum, ) -> Self { - assert_eq!( - edge_lengths.len(), - graph.num_edges(), - "edge_lengths length must match num_edges" - ); - assert_eq!( - edge_weights.len(), - graph.num_edges(), - "edge_weights length must match num_edges" - ); - Self::assert_positive_edge_values(&edge_lengths, "edge lengths"); - Self::assert_positive_edge_values(&edge_weights, "edge weights"); - assert!( - source_vertex < graph.num_vertices(), - "source_vertex {} out of bounds (graph has {} vertices)", + Self::try_new( + graph, + edge_lengths, + edge_weights, source_vertex, - graph.num_vertices() - ); - assert!( - target_vertex < graph.num_vertices(), - "target_vertex {} out of bounds (graph has {} vertices)", target_vertex, - graph.num_vertices() - ); - Self::assert_positive_bound(&weight_bound, "weight_bound"); - Self { + weight_bound, + ) + .unwrap_or_else(|error| panic!("{error}")) + } + + fn try_new( + graph: G, + edge_lengths: Vec, + edge_weights: Vec, + source_vertex: usize, + target_vertex: usize, + weight_bound: N::Sum, + ) -> Result { + Self::check_edge_values(&graph, &edge_lengths, "edge lengths")?; + Self::check_edge_values(&graph, &edge_weights, "edge weights")?; + if !(source_vertex < graph.num_vertices()) { + return Err(format!( + "source_vertex {} out of bounds (graph has {} vertices)", + source_vertex, + graph.num_vertices() + ) + .into()); + } + if !(target_vertex < graph.num_vertices()) { + return Err(format!( + "target_vertex {} out of bounds (graph has {} vertices)", + target_vertex, + graph.num_vertices() + ) + .into()); + } + if weight_bound.partial_cmp(&N::Sum::zero()) != Some(std::cmp::Ordering::Greater) { + return Err("weight_bound must be positive (> 0)".into()); + } + Ok(Self { graph, edge_lengths, edge_weights, source_vertex, target_vertex, weight_bound, - } + }) } /// Get a reference to the underlying graph. @@ -321,6 +370,14 @@ impl ShortestWeightConstrainedPath { Ok(Some(total_length)) } } + + fn assert_positive_edge_values(values: &[N], label: &str) { + let zero = N::Sum::zero(); + assert!( + values.iter().all(|value| value.to_sum() > zero.clone()), + "All {label} must be positive (> 0)" + ); + } } impl Problem for ShortestWeightConstrainedPath diff --git a/src/models/graph/traveling_salesman.rs b/src/models/graph/traveling_salesman.rs index 6d38980d4..aaba87d17 100644 --- a/src/models/graph/traveling_salesman.rs +++ b/src/models/graph/traveling_salesman.rs @@ -47,7 +47,7 @@ inventory::submit! { /// /// * `G` - The graph type (e.g., `SimpleGraph`, `KingsSubgraph`) /// * `W` - The weight type for edges (e.g., `i64`, `f64`) -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize)] pub struct TravelingSalesman { /// The underlying graph. graph: G, @@ -55,6 +55,23 @@ pub struct TravelingSalesman { edge_weights: Vec, } +#[derive(Deserialize)] +struct TravelingSalesmanData { + graph: G, + edge_weights: Vec, +} + +impl<'de, G, W> Deserialize<'de> for TravelingSalesman +where + G: Graph + Deserialize<'de>, + W: Clone + Default + Deserialize<'de>, +{ + fn deserialize>(deserializer: D) -> Result { + let data = TravelingSalesmanData::deserialize(deserializer)?; + Self::try_new(data.graph, data.edge_weights).map_err(serde::de::Error::custom) + } +} + #[derive(Debug, Deserialize, crate::CreateSpec)] struct TravelingSalesmanCreateSpec { #[create(codec = "edge-list")] @@ -80,7 +97,7 @@ impl TryFrom for TravelingSalesman TravelingSalesman { /// Create a TravelingSalesman problem from a graph with given edge weights. pub fn new(graph: G, edge_weights: Vec) -> Self { - assert_eq!( - edge_weights.len(), - graph.num_edges(), - "edge_weights length must match num_edges" - ); - Self { + Self::try_new(graph, edge_weights).unwrap_or_else(|error| panic!("{error}")) + } + + fn try_new(graph: G, edge_weights: Vec) -> Result { + Self::check_weights(&graph, &edge_weights)?; + Ok(Self { graph, edge_weights, - } + }) } /// Create a TravelingSalesman problem with unit weights. @@ -162,6 +179,16 @@ impl TravelingSalesman { self.edge_weights = weights; } + fn check_weights( + graph: &G, + edge_weights: &[W], + ) -> Result<(), crate::registry::ConstructionError> { + if edge_weights.len() != graph.num_edges() { + return Err("edge_weights length must match graph num_edges".into()); + } + Ok(()) + } + /// Get the weights for the problem. pub fn weights(&self) -> Vec { self.edge_weights.clone() diff --git a/src/models/graph/undirected_flow_lower_bounds.rs b/src/models/graph/undirected_flow_lower_bounds.rs index e2bb0f979..1144eb34f 100644 --- a/src/models/graph/undirected_flow_lower_bounds.rs +++ b/src/models/graph/undirected_flow_lower_bounds.rs @@ -33,6 +33,7 @@ inventory::submit! { } #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "UndirectedFlowLowerBoundsCreateSpec")] pub struct UndirectedFlowLowerBounds { graph: SimpleGraph, capacities: Vec, @@ -96,14 +97,14 @@ impl TryFrom for UndirectedFlowLowerBounds { return Err(format!("lower bound at edge {index} exceeds its capacity").into()); } - Ok(Self::new( + Self::try_new( spec.graph, spec.capacities, spec.lower_bounds, spec.source, spec.sink, spec.requirement, - )) + ) } } @@ -116,44 +117,56 @@ impl UndirectedFlowLowerBounds { sink: usize, requirement: i64, ) -> Self { - assert_eq!( - capacities.len(), - graph.num_edges(), - "capacities length must match graph num_edges" - ); - assert_eq!( - lower_bounds.len(), - graph.num_edges(), - "lower_bounds length must match graph num_edges" - ); + Self::try_new(graph, capacities, lower_bounds, source, sink, requirement) + .unwrap_or_else(|error| panic!("{error}")) + } + + fn try_new( + graph: SimpleGraph, + capacities: Vec, + lower_bounds: Vec, + source: usize, + sink: usize, + requirement: i64, + ) -> Result { + if capacities.len() != graph.num_edges() { + return Err("capacities length must match graph num_edges".into()); + } + if lower_bounds.len() != graph.num_edges() { + return Err("lower_bounds length must match graph num_edges".into()); + } let num_vertices = graph.num_vertices(); - assert!( - source < num_vertices, - "source must be less than num_vertices ({num_vertices})" - ); - assert!( - sink < num_vertices, - "sink must be less than num_vertices ({num_vertices})" - ); - assert!(source != sink, "source and sink must be distinct"); - assert!(requirement >= 1, "requirement must be at least 1"); + if !(source < num_vertices) { + return Err(format!("source must be less than num_vertices ({num_vertices})").into()); + } + if !(sink < num_vertices) { + return Err(format!("sink must be less than num_vertices ({num_vertices})").into()); + } + if source == sink { + return Err("source and sink must be distinct".into()); + } + if requirement == 0 { + return Err("requirement must be at least 1".into()); + } for (edge_index, (&lower, &upper)) in lower_bounds.iter().zip(&capacities).enumerate() { - assert!( - lower <= upper, - "lower bound at edge {edge_index} must be at most its capacity" - ); + if !(lower <= upper) { + return Err(format!( + "lower bound at edge {edge_index} must be at most its capacity" + ) + .into()); + } } - Self { + Ok(Self { graph, capacities, lower_bounds, source, sink, requirement, - } + }) } pub fn graph(&self) -> &SimpleGraph { diff --git a/src/models/graph/undirected_two_commodity_integral_flow.rs b/src/models/graph/undirected_two_commodity_integral_flow.rs index 975155bc6..63183e80d 100644 --- a/src/models/graph/undirected_two_commodity_integral_flow.rs +++ b/src/models/graph/undirected_two_commodity_integral_flow.rs @@ -30,6 +30,7 @@ inventory::submit! { /// - `f2(u, v)` /// - `f2(v, u)` #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "UndirectedTwoCommodityIntegralFlowData")] pub struct UndirectedTwoCommodityIntegralFlow { graph: SimpleGraph, capacities: Vec, @@ -41,6 +42,34 @@ pub struct UndirectedTwoCommodityIntegralFlow { requirement_2: i64, } +#[derive(Deserialize)] +struct UndirectedTwoCommodityIntegralFlowData { + graph: SimpleGraph, + capacities: Vec, + source_1: usize, + sink_1: usize, + source_2: usize, + sink_2: usize, + requirement_1: i64, + requirement_2: i64, +} + +impl TryFrom for UndirectedTwoCommodityIntegralFlow { + type Error = crate::registry::ConstructionError; + fn try_from(data: UndirectedTwoCommodityIntegralFlowData) -> Result { + Self::try_new( + data.graph, + data.capacities, + data.source_1, + data.sink_1, + data.source_2, + data.sink_2, + data.requirement_1, + data.requirement_2, + ) + } +} + #[derive(Debug, Deserialize, crate::CreateSpec)] struct UndirectedTwoCommodityIntegralFlowCreateSpec { /// Undirected graph edges. @@ -129,11 +158,33 @@ impl UndirectedTwoCommodityIntegralFlow { requirement_1: i64, requirement_2: i64, ) -> Self { - assert_eq!( - capacities.len(), - graph.num_edges(), - "capacities length must match graph num_edges" - ); + Self::try_new( + graph, + capacities, + source_1, + sink_1, + source_2, + sink_2, + requirement_1, + requirement_2, + ) + .unwrap_or_else(|error| panic!("{error}")) + } + + #[allow(clippy::too_many_arguments)] + fn try_new( + graph: SimpleGraph, + capacities: Vec, + source_1: usize, + sink_1: usize, + source_2: usize, + sink_2: usize, + requirement_1: i64, + requirement_2: i64, + ) -> Result { + if capacities.len() != graph.num_edges() { + return Err("capacities length must match graph num_edges".into()); + } let num_vertices = graph.num_vertices(); for (label, vertex) in [ @@ -142,18 +193,18 @@ impl UndirectedTwoCommodityIntegralFlow { ("source_2", source_2), ("sink_2", sink_2), ] { - assert!( - vertex < num_vertices, - "{label} must be less than num_vertices ({num_vertices})" - ); + if !(vertex < num_vertices) { + return Err( + format!("{label} must be less than num_vertices ({num_vertices})").into(), + ); + } } - assert!( - capacities.iter().all(|&capacity| capacity >= 0), - "capacities must be nonnegative" - ); + if !(capacities.iter().all(|&capacity| capacity >= 0)) { + return Err("capacities must be nonnegative".into()); + } - Self { + Ok(Self { graph, capacities, source_1, @@ -162,7 +213,7 @@ impl UndirectedTwoCommodityIntegralFlow { sink_2, requirement_1, requirement_2, - } + }) } pub fn graph(&self) -> &SimpleGraph { diff --git a/src/models/misc/additional_key.rs b/src/models/misc/additional_key.rs index dc48e4a05..abe66066c 100644 --- a/src/models/misc/additional_key.rs +++ b/src/models/misc/additional_key.rs @@ -61,6 +61,7 @@ inventory::submit! { /// assert!(solution.is_some()); /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "AdditionalKeyData")] pub struct AdditionalKey { num_attributes: usize, dependencies: Vec<(Vec, Vec)>, @@ -68,6 +69,26 @@ pub struct AdditionalKey { known_keys: Vec>, } +#[derive(Deserialize)] +struct AdditionalKeyData { + num_attributes: usize, + dependencies: Vec<(Vec, Vec)>, + relation_attrs: Vec, + known_keys: Vec>, +} + +impl TryFrom for AdditionalKey { + type Error = crate::registry::ConstructionError; + fn try_from(data: AdditionalKeyData) -> Result { + Self::try_new( + data.num_attributes, + data.dependencies, + data.relation_attrs, + data.known_keys, + ) + } +} + impl AdditionalKey { /// Create a new AdditionalKey instance. /// @@ -81,42 +102,58 @@ impl AdditionalKey { relation_attrs: Vec, known_keys: Vec>, ) -> Self { + Self::try_new(num_attributes, dependencies, relation_attrs, known_keys) + .unwrap_or_else(|error| panic!("{error}")) + } + + fn try_new( + num_attributes: usize, + dependencies: Vec<(Vec, Vec)>, + relation_attrs: Vec, + known_keys: Vec>, + ) -> Result { // Validate all attribute indices for &a in &relation_attrs { - assert!( - a < num_attributes, - "relation_attrs element {a} >= num_attributes {num_attributes}" - ); + if !(a < num_attributes) { + return Err(format!( + "relation_attrs element {a} >= num_attributes {num_attributes}" + ) + .into()); + } } // Validate relation_attrs uniqueness let mut sorted_ra = relation_attrs.clone(); sorted_ra.sort_unstable(); sorted_ra.dedup(); - assert_eq!( - sorted_ra.len(), - relation_attrs.len(), - "relation_attrs contains duplicates" - ); + if sorted_ra.len() != relation_attrs.len() { + return Err("relation_attrs contains duplicates".into()); + } for (lhs, rhs) in &dependencies { for &a in lhs { - assert!( - a < num_attributes, - "dependency lhs attribute {a} >= num_attributes {num_attributes}" - ); + if !(a < num_attributes) { + return Err(format!( + "dependency lhs attribute {a} >= num_attributes {num_attributes}" + ) + .into()); + } } for &a in rhs { - assert!( - a < num_attributes, - "dependency rhs attribute {a} >= num_attributes {num_attributes}" - ); + if !(a < num_attributes) { + return Err(format!( + "dependency rhs attribute {a} >= num_attributes {num_attributes}" + ) + .into()); + } } } for key in &known_keys { for &a in key { - assert!( - a < num_attributes, - "known_keys attribute {a} >= num_attributes {num_attributes}" - ); + if !(a < num_attributes) { + return Err(format!( + "known_keys attribute {a} >= num_attributes {num_attributes}" + ) + .into()); + } } } // Sort known_keys entries internally for consistent comparison @@ -127,12 +164,12 @@ impl AdditionalKey { k }) .collect(); - Self { + Ok(Self { num_attributes, dependencies, relation_attrs, known_keys, - } + }) } /// Returns the number of attributes in the universal set A. diff --git a/src/models/misc/boyce_codd_normal_form_violation.rs b/src/models/misc/boyce_codd_normal_form_violation.rs index a6e2ddeb7..c693988c7 100644 --- a/src/models/misc/boyce_codd_normal_form_violation.rs +++ b/src/models/misc/boyce_codd_normal_form_violation.rs @@ -58,6 +58,7 @@ inventory::submit! { /// .unwrap()); /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "BoyceCoddNormalFormViolationData")] pub struct BoyceCoddNormalFormViolation { /// Total number of attributes (elements are `0..num_attributes`). num_attributes: usize, @@ -67,6 +68,24 @@ pub struct BoyceCoddNormalFormViolation { target_subset: Vec, } +#[derive(Deserialize)] +struct BoyceCoddNormalFormViolationData { + num_attributes: usize, + functional_deps: Vec<(Vec, Vec)>, + target_subset: Vec, +} + +impl TryFrom for BoyceCoddNormalFormViolation { + type Error = crate::registry::ConstructionError; + fn try_from(data: BoyceCoddNormalFormViolationData) -> Result { + Self::try_new( + data.num_attributes, + data.functional_deps, + data.target_subset, + ) + } +} + #[derive(Debug, Deserialize, crate::CreateSpec)] struct BoyceCoddNormalFormViolationCreateSpec { /// Total number of attributes in A. @@ -107,7 +126,7 @@ impl TryFrom for BoyceCoddNormalFormViol ) .into()); } - Ok(Self::new(spec.n, spec.subsets, spec.target)) + Self::try_new(spec.n, spec.subsets, spec.target) } } @@ -129,27 +148,32 @@ impl BoyceCoddNormalFormViolation { functional_deps: Vec<(Vec, Vec)>, target_subset: Vec, ) -> Self { - assert!(!target_subset.is_empty(), "target_subset must be non-empty"); + Self::try_new(num_attributes, functional_deps, target_subset) + .unwrap_or_else(|error| panic!("{error}")) + } + + fn try_new( + num_attributes: usize, + functional_deps: Vec<(Vec, Vec)>, + target_subset: Vec, + ) -> Result { + if target_subset.is_empty() { + return Err("target_subset must be non-empty".into()); + } let mut functional_deps = functional_deps; for (fd_index, (lhs, rhs)) in functional_deps.iter_mut().enumerate() { - assert!( - !lhs.is_empty(), - "Functional dependency {} has an empty LHS", - fd_index - ); + if lhs.is_empty() { + return Err(format!("Functional dependency {} has an empty LHS", fd_index).into()); + } lhs.sort_unstable(); lhs.dedup(); rhs.sort_unstable(); rhs.dedup(); for &attr in lhs.iter().chain(rhs.iter()) { - assert!( - attr < num_attributes, - "Functional dependency {} contains attribute {} which is out of range (num_attributes = {})", - fd_index, - attr, - num_attributes - ); + if !(attr < num_attributes) { + return Err(format!("Functional dependency {} contains attribute {} which is out of range (num_attributes = {})", fd_index, attr, num_attributes).into()); + } } } @@ -157,19 +181,16 @@ impl BoyceCoddNormalFormViolation { target_subset.sort_unstable(); target_subset.dedup(); for &attr in &target_subset { - assert!( - attr < num_attributes, - "target_subset contains attribute {} which is out of range (num_attributes = {})", - attr, - num_attributes - ); + if !(attr < num_attributes) { + return Err(format!("target_subset contains attribute {} which is out of range (num_attributes = {})", attr, num_attributes).into()); + } } - Self { + Ok(Self { num_attributes, functional_deps, target_subset, - } + }) } /// Return the total number of attributes. diff --git a/src/models/misc/capacity_assignment.rs b/src/models/misc/capacity_assignment.rs index 061e5dbb8..7ca9f765f 100644 --- a/src/models/misc/capacity_assignment.rs +++ b/src/models/misc/capacity_assignment.rs @@ -27,6 +27,7 @@ inventory::submit! { /// with respect to the ordered capacity list. The objective is to minimize /// total cost subject to a delay budget constraint. #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "CapacityAssignmentCreateSpec")] pub struct CapacityAssignment { capacities: Vec, cost: Vec>, @@ -93,51 +94,53 @@ impl CapacityAssignment { delay: Vec>, delay_budget: i64, ) -> Self { - assert!(!capacities.is_empty(), "capacities must be non-empty"); - assert!( - capacities.iter().all(|&capacity| capacity > 0), - "capacities must be positive" - ); - assert!( - capacities.windows(2).all(|w| w[0] < w[1]), - "capacities must be strictly increasing" - ); - assert_eq!( - cost.len(), - delay.len(), - "cost and delay must have the same number of links" - ); + Self::try_new(capacities, cost, delay, delay_budget) + .unwrap_or_else(|error| panic!("{error}")) + } + + fn try_new( + capacities: Vec, + cost: Vec>, + delay: Vec>, + delay_budget: i64, + ) -> Result { + if capacities.is_empty() { + return Err("capacities must be non-empty".into()); + } + if !(capacities.iter().all(|&capacity| capacity > 0)) { + return Err("capacities must be positive".into()); + } + if !(capacities.windows(2).all(|w| w[0] < w[1])) { + return Err("capacities must be strictly increasing".into()); + } + if cost.len() != delay.len() { + return Err("cost and delay must have the same number of links".into()); + } let num_capacities = capacities.len(); for (link, row) in cost.iter().enumerate() { - assert_eq!( - row.len(), - num_capacities, - "cost row {link} length must match capacities length" - ); - assert!( - row.windows(2).all(|w| w[0] <= w[1]), - "cost row {link} must be non-decreasing" - ); + if row.len() != num_capacities { + return Err(format!("cost row {link} length must match capacities length").into()); + } + if !(row.windows(2).all(|w| w[0] <= w[1])) { + return Err(format!("cost row {link} must be non-decreasing").into()); + } } for (link, row) in delay.iter().enumerate() { - assert_eq!( - row.len(), - num_capacities, - "delay row {link} length must match capacities length" - ); - assert!( - row.windows(2).all(|w| w[0] >= w[1]), - "delay row {link} must be non-increasing" - ); + if row.len() != num_capacities { + return Err(format!("delay row {link} length must match capacities length").into()); + } + if !(row.windows(2).all(|w| w[0] >= w[1])) { + return Err(format!("delay row {link} must be non-increasing").into()); + } } - Self { + Ok(Self { capacities, cost, delay, delay_budget, - } + }) } /// Number of communication links. diff --git a/src/models/misc/closest_string.rs b/src/models/misc/closest_string.rs index b0d7cf8b0..2f556d627 100644 --- a/src/models/misc/closest_string.rs +++ b/src/models/misc/closest_string.rs @@ -46,11 +46,26 @@ inventory::submit! { /// syntactically feasible; the objective is its worst-case Hamming distance /// to the input strings. #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "ClosestStringData")] pub struct ClosestString { alphabet_size: usize, strings: Vec>, } +#[derive(Deserialize)] +struct ClosestStringData { + alphabet_size: usize, + strings: Vec>, +} + +impl TryFrom for ClosestString { + type Error = crate::registry::ConstructionError; + + fn try_from(data: ClosestStringData) -> Result { + Self::try_new(data.alphabet_size, data.strings) + } +} + impl ClosestString { /// Create a new `ClosestString` instance. /// @@ -62,30 +77,34 @@ impl ClosestString { /// - `alphabet_size == 0` while any input string is non-empty, /// - any symbol in any input string is `>= alphabet_size`. pub fn new(alphabet_size: usize, strings: Vec>) -> Self { - assert!( - !strings.is_empty(), - "ClosestString requires at least one input string" - ); + Self::try_new(alphabet_size, strings).unwrap_or_else(|error| panic!("{error}")) + } + + fn try_new( + alphabet_size: usize, + strings: Vec>, + ) -> Result { + if strings.is_empty() { + return Err("ClosestString requires at least one input string".into()); + } let string_length = strings[0].len(); - assert!( - strings.iter().all(|s| s.len() == string_length), - "all input strings must have the same length" - ); - assert!( - alphabet_size > 0 || string_length == 0, - "alphabet_size must be > 0 when input strings are non-empty" - ); - assert!( - strings - .iter() - .flat_map(|s| s.iter()) - .all(|&symbol| symbol < alphabet_size), - "input symbols must be less than alphabet_size" - ); - Self { + if !(strings.iter().all(|s| s.len() == string_length)) { + return Err("all input strings must have the same length".into()); + } + if !(alphabet_size > 0 || string_length == 0) { + return Err("alphabet_size must be > 0 when input strings are non-empty".into()); + } + if !(strings + .iter() + .flat_map(|s| s.iter()) + .all(|&symbol| symbol < alphabet_size)) + { + return Err("input symbols must be less than alphabet_size".into()); + } + Ok(Self { alphabet_size, strings, - } + }) } /// Returns the alphabet size `q`. diff --git a/src/models/misc/clustering.rs b/src/models/misc/clustering.rs index 691ba00be..ebb692dfa 100644 --- a/src/models/misc/clustering.rs +++ b/src/models/misc/clustering.rs @@ -59,6 +59,7 @@ inventory::submit! { /// assert!(solution.is_some()); /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "ClusteringData")] pub struct Clustering { /// Symmetric distance matrix with zero diagonal. distances: Vec>, @@ -68,6 +69,21 @@ pub struct Clustering { diameter_bound: i64, } +#[derive(Deserialize)] +struct ClusteringData { + distances: Vec>, + num_clusters: usize, + diameter_bound: i64, +} + +impl TryFrom for Clustering { + type Error = crate::registry::ConstructionError; + + fn try_from(data: ClusteringData) -> Result { + Self::try_new(data.distances, data.num_clusters, data.diameter_bound) + } +} + impl Clustering { /// Create a new Clustering instance. /// @@ -80,35 +96,46 @@ impl Clustering { /// - diagonal entries are not zero /// - `num_clusters` is zero pub fn new(distances: Vec>, num_clusters: usize, diameter_bound: i64) -> Self { + Self::try_new(distances, num_clusters, diameter_bound) + .unwrap_or_else(|error| panic!("{error}")) + } + + fn try_new( + distances: Vec>, + num_clusters: usize, + diameter_bound: i64, + ) -> Result { let n = distances.len(); - assert!(n > 0, "Clustering requires at least one element"); - assert!(num_clusters > 0, "num_clusters must be at least 1"); + if !(n > 0) { + return Err("Clustering requires at least one element".into()); + } + if num_clusters == 0 { + return Err("num_clusters must be at least 1".into()); + } for (i, row) in distances.iter().enumerate() { - assert_eq!( - row.len(), - n, - "Distance matrix must be square: row {i} has {} columns, expected {n}", - row.len() - ); - assert_eq!( - distances[i][i], 0, - "Diagonal entry distances[{i}][{i}] must be 0" - ); + if row.len() != n { + return Err(format!( + "Distance matrix must be square: row {i} has {} columns, expected {n}", + row.len() + ) + .into()); + } + if distances[i][i] != 0 { + return Err(format!("Diagonal entry distances[{i}][{i}] must be 0").into()); + } } for (i, row_i) in distances.iter().enumerate() { for j in (i + 1)..n { - assert_eq!( - row_i[j], distances[j][i], - "Distance matrix must be symmetric: distances[{i}][{j}] = {} != distances[{j}][{i}] = {}", - row_i[j], distances[j][i] - ); + if row_i[j] != distances[j][i] { + return Err(format!("Distance matrix must be symmetric: distances[{i}][{j}] = {} != distances[{j}][{i}] = {}", row_i[j], distances[j][i]).into()); + } } } - Self { + Ok(Self { distances, num_clusters, diameter_bound, - } + }) } /// Returns the distance matrix. diff --git a/src/models/misc/conjunctive_boolean_query.rs b/src/models/misc/conjunctive_boolean_query.rs index 26004ce7a..7d62fdd2d 100644 --- a/src/models/misc/conjunctive_boolean_query.rs +++ b/src/models/misc/conjunctive_boolean_query.rs @@ -76,6 +76,7 @@ pub enum QueryArg { /// assert!(solution.is_some()); /// ``` #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(try_from = "ConjunctiveBooleanQueryData")] pub struct ConjunctiveBooleanQuery { domain_size: usize, relations: Vec, @@ -83,6 +84,26 @@ pub struct ConjunctiveBooleanQuery { conjuncts: Vec<(usize, Vec)>, } +#[derive(Deserialize)] +struct ConjunctiveBooleanQueryData { + domain_size: usize, + relations: Vec, + num_variables: usize, + conjuncts: Vec<(usize, Vec)>, +} + +impl TryFrom for ConjunctiveBooleanQuery { + type Error = crate::registry::ConstructionError; + fn try_from(data: ConjunctiveBooleanQueryData) -> Result { + Self::try_new( + data.domain_size, + data.relations, + data.num_variables, + data.conjuncts, + ) + } +} + #[derive(Debug, Deserialize, crate::CreateSpec)] struct ConjunctiveBooleanQueryCreateSpec { /// Size of the finite domain. @@ -185,57 +206,73 @@ impl ConjunctiveBooleanQuery { num_variables: usize, conjuncts: Vec<(usize, Vec)>, ) -> Self { + Self::try_new(domain_size, relations, num_variables, conjuncts) + .unwrap_or_else(|error| panic!("{error}")) + } + + fn try_new( + domain_size: usize, + relations: Vec, + num_variables: usize, + conjuncts: Vec<(usize, Vec)>, + ) -> Result { for (i, rel) in relations.iter().enumerate() { for (j, tuple) in rel.tuples.iter().enumerate() { - assert!( - tuple.len() == rel.arity, - "Relation {i}: tuple {j} has length {}, expected arity {}", - tuple.len(), - rel.arity - ); + if !(tuple.len() == rel.arity) { + return Err(format!( + "Relation {i}: tuple {j} has length {}, expected arity {}", + tuple.len(), + rel.arity + ) + .into()); + } for (k, &val) in tuple.iter().enumerate() { - assert!( - val < domain_size, - "Relation {i}: tuple {j}, entry {k} is {val}, must be < {domain_size}" - ); + if !(val < domain_size) { + return Err(format!( + "Relation {i}: tuple {j}, entry {k} is {val}, must be < {domain_size}" + ) + .into()); + } } } } for (i, (rel_idx, args)) in conjuncts.iter().enumerate() { - assert!( - *rel_idx < relations.len(), - "Conjunct {i}: relation index {rel_idx} out of range (have {} relations)", - relations.len() - ); - assert!( - args.len() == relations[*rel_idx].arity, - "Conjunct {i}: has {} args, expected arity {}", - args.len(), - relations[*rel_idx].arity - ); + if !(*rel_idx < relations.len()) { + return Err(format!( + "Conjunct {i}: relation index {rel_idx} out of range (have {} relations)", + relations.len() + ) + .into()); + } + if !(args.len() == relations[*rel_idx].arity) { + return Err(format!( + "Conjunct {i}: has {} args, expected arity {}", + args.len(), + relations[*rel_idx].arity + ) + .into()); + } for (k, arg) in args.iter().enumerate() { match arg { QueryArg::Variable(v) => { - assert!( - *v < num_variables, - "Conjunct {i}, arg {k}: Variable({v}) >= num_variables ({num_variables})" - ); + if !(*v < num_variables) { + return Err(format!("Conjunct {i}, arg {k}: Variable({v}) >= num_variables ({num_variables})").into()); + } } QueryArg::Constant(c) => { - assert!( - *c < domain_size, - "Conjunct {i}, arg {k}: Constant({c}) >= domain_size ({domain_size})" - ); + if !(*c < domain_size) { + return Err(format!("Conjunct {i}, arg {k}: Constant({c}) >= domain_size ({domain_size})").into()); + } } } } } - Self { + Ok(Self { domain_size, relations, num_variables, conjuncts, - } + }) } /// Returns the size of the finite domain. diff --git a/src/models/misc/conjunctive_query_foldability.rs b/src/models/misc/conjunctive_query_foldability.rs index 08721a619..c4ee0bd34 100644 --- a/src/models/misc/conjunctive_query_foldability.rs +++ b/src/models/misc/conjunctive_query_foldability.rs @@ -86,6 +86,7 @@ pub enum Term { /// assert!(solution.is_some()); /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "ConjunctiveQueryFoldabilityData")] pub struct ConjunctiveQueryFoldability { /// Size of the finite domain D. domain_size: usize, @@ -101,6 +102,30 @@ pub struct ConjunctiveQueryFoldability { query2_conjuncts: Vec<(usize, Vec)>, } +#[derive(Deserialize)] +struct ConjunctiveQueryFoldabilityData { + domain_size: usize, + num_distinguished: usize, + num_undistinguished: usize, + relation_arities: Vec, + query1_conjuncts: Vec<(usize, Vec)>, + query2_conjuncts: Vec<(usize, Vec)>, +} + +impl TryFrom for ConjunctiveQueryFoldability { + type Error = crate::registry::ConstructionError; + fn try_from(data: ConjunctiveQueryFoldabilityData) -> Result { + Self::try_new( + data.domain_size, + data.num_distinguished, + data.num_undistinguished, + data.relation_arities, + data.query1_conjuncts, + data.query2_conjuncts, + ) + } +} + impl ConjunctiveQueryFoldability { /// Create a new `ConjunctiveQueryFoldability` instance. /// @@ -129,6 +154,25 @@ impl ConjunctiveQueryFoldability { query1_conjuncts: Vec<(usize, Vec)>, query2_conjuncts: Vec<(usize, Vec)>, ) -> Self { + Self::try_new( + domain_size, + num_distinguished, + num_undistinguished, + relation_arities, + query1_conjuncts, + query2_conjuncts, + ) + .unwrap_or_else(|error| panic!("{error}")) + } + + fn try_new( + domain_size: usize, + num_distinguished: usize, + num_undistinguished: usize, + relation_arities: Vec, + query1_conjuncts: Vec<(usize, Vec)>, + query2_conjuncts: Vec<(usize, Vec)>, + ) -> Result { let instance = Self { domain_size, num_distinguished, @@ -137,55 +181,63 @@ impl ConjunctiveQueryFoldability { query1_conjuncts, query2_conjuncts, }; - instance.validate(); - instance + instance.validate()?; + Ok(instance) } - /// Validate the instance, panicking on any inconsistency. - fn validate(&self) { + /// Check relation arities and argument indices. + fn validate(&self) -> Result<(), crate::registry::ConstructionError> { for (query_name, conjuncts) in [ ("Q1", &self.query1_conjuncts), ("Q2", &self.query2_conjuncts), ] { for (atom_idx, (rel_idx, args)) in conjuncts.iter().enumerate() { - assert!( - *rel_idx < self.relation_arities.len(), - "Atom {atom_idx} of {query_name}: relation index {rel_idx} out of range \ + if !(*rel_idx < self.relation_arities.len()) { + return Err(format!( + "Atom {atom_idx} of {query_name}: relation index {rel_idx} out of range \ (num_relations = {})", - self.relation_arities.len() - ); + self.relation_arities.len() + ) + .into()); + }; let arity = self.relation_arities[*rel_idx]; - assert_eq!( - args.len(), - arity, - "Atom {atom_idx} of {query_name}: relation {rel_idx} has arity {arity} \ + if args.len() != arity { + return Err(format!( + "Atom {atom_idx} of {query_name}: relation {rel_idx} has arity {arity} \ but got {} arguments", - args.len() - ); + args.len() + ) + .into()); + }; for term in args { match term { - Term::Constant(i) => assert!( - *i < self.domain_size, - "Atom {atom_idx} of {query_name}: Constant({i}) out of range \ + Term::Constant(i) => { + if !(*i < self.domain_size) { + return Err(format!( + "Atom {atom_idx} of {query_name}: Constant({i}) out of range \ (domain_size = {})", - self.domain_size - ), - Term::Distinguished(i) => assert!( - *i < self.num_distinguished, - "Atom {atom_idx} of {query_name}: Distinguished({i}) out of range \ - (num_distinguished = {})", - self.num_distinguished - ), - Term::Undistinguished(i) => assert!( - *i < self.num_undistinguished, - "Atom {atom_idx} of {query_name}: Undistinguished({i}) out of range \ - (num_undistinguished = {})", - self.num_undistinguished - ), + self.domain_size + ) + .into()); + } + } + Term::Distinguished(i) => { + if !(*i < self.num_distinguished) { + return Err(format!("Atom {atom_idx} of {query_name}: Distinguished({i}) out of range \ + (num_distinguished = {})", self.num_distinguished).into()); + } + } + Term::Undistinguished(i) => { + if !(*i < self.num_undistinguished) { + return Err(format!("Atom {atom_idx} of {query_name}: Undistinguished({i}) out of range \ + (num_undistinguished = {})", self.num_undistinguished).into()); + } + } } } } } + Ok(()) } /// Returns the size of the finite domain D. diff --git a/src/models/misc/consistency_of_database_frequency_tables.rs b/src/models/misc/consistency_of_database_frequency_tables.rs index b4b0c73c5..9563a84cb 100644 --- a/src/models/misc/consistency_of_database_frequency_tables.rs +++ b/src/models/misc/consistency_of_database_frequency_tables.rs @@ -97,6 +97,7 @@ inventory::submit! { /// The Consistency of Database Frequency Tables decision problem. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(try_from = "ConsistencyOfDatabaseFrequencyTablesData")] pub struct ConsistencyOfDatabaseFrequencyTables { num_objects: usize, attribute_domains: Vec, @@ -104,6 +105,26 @@ pub struct ConsistencyOfDatabaseFrequencyTables { known_values: Vec, } +#[derive(Deserialize)] +struct ConsistencyOfDatabaseFrequencyTablesData { + num_objects: usize, + attribute_domains: Vec, + frequency_tables: Vec, + known_values: Vec, +} + +impl TryFrom for ConsistencyOfDatabaseFrequencyTables { + type Error = crate::registry::ConstructionError; + fn try_from(data: ConsistencyOfDatabaseFrequencyTablesData) -> Result { + Self::try_new( + data.num_objects, + data.attribute_domains, + data.frequency_tables, + data.known_values, + ) + } +} + #[derive(Debug, Deserialize, crate::CreateSpec)] struct ConsistencyOfDatabaseFrequencyTablesCreateSpec { /// Number of database objects. @@ -220,20 +241,34 @@ impl ConsistencyOfDatabaseFrequencyTables { frequency_tables: Vec, known_values: Vec, ) -> Self { + Self::try_new( + num_objects, + attribute_domains, + frequency_tables, + known_values, + ) + .unwrap_or_else(|error| panic!("{error}")) + } + + fn try_new( + num_objects: usize, + attribute_domains: Vec, + frequency_tables: Vec, + known_values: Vec, + ) -> Result { validate_cdft_create( num_objects, &attribute_domains, &frequency_tables, &known_values, - ) - .unwrap_or_else(|error| panic!("{error}")); + )?; - Self { + Ok(Self { num_objects, attribute_domains, frequency_tables, known_values, - } + }) } /// Returns the number of objects. diff --git a/src/models/misc/cosine_product_integration.rs b/src/models/misc/cosine_product_integration.rs index 30e4de5f7..bdfd218ff 100644 --- a/src/models/misc/cosine_product_integration.rs +++ b/src/models/misc/cosine_product_integration.rs @@ -55,10 +55,24 @@ inventory::submit! { /// assert!(solution.is_some()); /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "CosineProductIntegrationData")] pub struct CosineProductIntegration { coefficients: Vec, } +#[derive(Deserialize)] +struct CosineProductIntegrationData { + coefficients: Vec, +} + +impl TryFrom for CosineProductIntegration { + type Error = crate::registry::ConstructionError; + + fn try_from(data: CosineProductIntegrationData) -> Result { + Self::try_new(data.coefficients) + } +} + impl CosineProductIntegration { /// Create a new CosineProductIntegration instance. /// @@ -66,11 +80,14 @@ impl CosineProductIntegration { /// /// Panics if `coefficients` is empty. pub fn new(coefficients: Vec) -> Self { - assert!( - !coefficients.is_empty(), - "CosineProductIntegration requires at least one coefficient" - ); - Self { coefficients } + Self::try_new(coefficients).unwrap_or_else(|error| panic!("{error}")) + } + + fn try_new(coefficients: Vec) -> Result { + if coefficients.is_empty() { + return Err("CosineProductIntegration requires at least one coefficient".into()); + } + Ok(Self { coefficients }) } /// Returns the cosine coefficients. diff --git a/src/models/misc/feasible_register_assignment.rs b/src/models/misc/feasible_register_assignment.rs index d68da6521..f619e3174 100644 --- a/src/models/misc/feasible_register_assignment.rs +++ b/src/models/misc/feasible_register_assignment.rs @@ -90,15 +90,13 @@ impl<'de> Deserialize<'de> for FeasibleRegisterAssignment { D: Deserializer<'de>, { let data = FeasibleRegisterAssignmentData::deserialize(deserializer)?; - let (dependencies, dependents) = Self::build_adjacency(data.num_vertices, &data.arcs); - Ok(Self { - num_vertices: data.num_vertices, - arcs: data.arcs, - num_registers: data.num_registers, - assignment: data.assignment, - dependencies, - dependents, - }) + Self::try_new( + data.num_vertices, + data.arcs, + data.num_registers, + data.assignment, + ) + .map_err(serde::de::Error::custom) } } @@ -116,47 +114,57 @@ impl FeasibleRegisterAssignment { num_registers: usize, assignment: Vec, ) -> Self { + Self::try_new(num_vertices, arcs, num_registers, assignment) + .unwrap_or_else(|error| panic!("{error}")) + } + + fn try_new( + num_vertices: usize, + arcs: Vec<(usize, usize)>, + num_registers: usize, + assignment: Vec, + ) -> Result { for &(v, u) in &arcs { - assert!( - v < num_vertices && u < num_vertices, - "Arc ({}, {}) out of bounds for {} vertices", - v, - u, - num_vertices - ); - assert!(v != u, "Self-loop ({}, {}) not allowed in a DAG", v, u); + if !(v < num_vertices && u < num_vertices) { + return Err(format!( + "Arc ({}, {}) out of bounds for {} vertices", + v, u, num_vertices + ) + .into()); + }; + if v == u { + return Err(format!("Self-loop ({}, {}) not allowed in a DAG", v, u).into()); + }; } - assert_eq!( - assignment.len(), - num_vertices, - "Assignment length {} does not match num_vertices {}", - assignment.len(), - num_vertices - ); - if num_vertices > 0 { - assert!( - num_registers > 0, - "num_registers must be positive when there are vertices" - ); + if assignment.len() != num_vertices { + return Err(format!( + "Assignment length {} does not match num_vertices {}", + assignment.len(), + num_vertices + ) + .into()); + }; + if num_vertices > 0 && num_registers == 0 { + return Err("num_registers must be positive when there are vertices".into()); } for (v, &r) in assignment.iter().enumerate() { - assert!( - r < num_registers, - "Assignment[{}] = {} is out of bounds for {} registers", - v, - r, - num_registers - ); + if !(r < num_registers) { + return Err(format!( + "Assignment[{}] = {} is out of bounds for {} registers", + v, r, num_registers + ) + .into()); + }; } let (dependencies, dependents) = Self::build_adjacency(num_vertices, &arcs); - Self { + Ok(Self { num_vertices, arcs, num_registers, assignment, dependencies, dependents, - } + }) } /// Build dependency and dependent adjacency lists from arcs. diff --git a/src/models/misc/flow_shop_scheduling.rs b/src/models/misc/flow_shop_scheduling.rs index 20724b953..2dfbfb4e3 100644 --- a/src/models/misc/flow_shop_scheduling.rs +++ b/src/models/misc/flow_shop_scheduling.rs @@ -58,6 +58,7 @@ inventory::submit! { /// assert!(solution.is_some()); /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "FlowShopSchedulingData")] pub struct FlowShopScheduling { /// Number of processors (machines). num_processors: usize, @@ -67,6 +68,21 @@ pub struct FlowShopScheduling { deadline: i64, } +#[derive(Deserialize)] +struct FlowShopSchedulingData { + num_processors: usize, + task_lengths: Vec>, + deadline: i64, +} + +impl TryFrom for FlowShopScheduling { + type Error = crate::registry::ConstructionError; + + fn try_from(data: FlowShopSchedulingData) -> Result { + Self::try_new(data.num_processors, data.task_lengths, data.deadline) + } +} + impl FlowShopScheduling { /// Create a new Flow Shop Scheduling instance. /// @@ -79,26 +95,37 @@ impl FlowShopScheduling { /// # Panics /// Panics if any job does not have exactly `num_processors` tasks. pub fn new(num_processors: usize, task_lengths: Vec>, deadline: i64) -> Self { + Self::try_new(num_processors, task_lengths, deadline) + .unwrap_or_else(|error| panic!("{error}")) + } + + fn try_new( + num_processors: usize, + task_lengths: Vec>, + deadline: i64, + ) -> Result { for (j, tasks) in task_lengths.iter().enumerate() { - assert_eq!( - tasks.len(), - num_processors, - "Job {} has {} tasks, expected {}", - j, - tasks.len(), - num_processors - ); + if tasks.len() != num_processors { + return Err(format!( + "Job {} has {} tasks, expected {}", + j, + tasks.len(), + num_processors + ) + .into()); + } } - assert!( - task_lengths.iter().flatten().all(|&length| length >= 0), - "task lengths must be nonnegative" - ); - assert!(deadline >= 0, "deadline must be nonnegative"); - Self { + if !(task_lengths.iter().flatten().all(|&length| length >= 0)) { + return Err("task lengths must be nonnegative".into()); + } + if !(deadline >= 0) { + return Err("deadline must be nonnegative".into()); + } + Ok(Self { num_processors, task_lengths, deadline, - } + }) } /// Get the number of processors. diff --git a/src/models/misc/grouping_by_swapping.rs b/src/models/misc/grouping_by_swapping.rs index 87639b30e..7ad4a0ac7 100644 --- a/src/models/misc/grouping_by_swapping.rs +++ b/src/models/misc/grouping_by_swapping.rs @@ -27,12 +27,28 @@ inventory::submit! { /// adjacent swap position `i` (swap positions `i` and `i + 1`) or the special /// no-op value `string_len - 1`. #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "GroupingBySwappingData")] pub struct GroupingBySwapping { alphabet_size: usize, string: Vec, budget: usize, } +#[derive(Deserialize)] +struct GroupingBySwappingData { + alphabet_size: usize, + string: Vec, + budget: usize, +} + +impl TryFrom for GroupingBySwapping { + type Error = crate::registry::ConstructionError; + + fn try_from(data: GroupingBySwappingData) -> Result { + Self::try_new(data.alphabet_size, data.string, data.budget) + } +} + #[derive(Debug, Deserialize, crate::CreateSpec)] struct GroupingBySwappingCreateSpec { /// Optional alphabet size; omitted values are inferred from the string. @@ -93,23 +109,28 @@ impl GroupingBySwapping { /// Panics if the string contains a symbol outside the declared alphabet, /// or if the string is empty while the budget is positive. pub fn new(alphabet_size: usize, string: Vec, budget: usize) -> Self { - assert!( - alphabet_size > 0 || string.is_empty(), - "alphabet_size must be > 0 when string is non-empty" - ); - assert!( - string.iter().all(|&symbol| symbol < alphabet_size), - "input symbols must be less than alphabet_size" - ); - assert!( - !string.is_empty() || budget == 0, - "budget must be 0 when string is empty" - ); - Self { + Self::try_new(alphabet_size, string, budget).unwrap_or_else(|error| panic!("{error}")) + } + + fn try_new( + alphabet_size: usize, + string: Vec, + budget: usize, + ) -> Result { + if !(alphabet_size > 0 || string.is_empty()) { + return Err("alphabet_size must be > 0 when string is non-empty".into()); + } + if !(string.iter().all(|&symbol| symbol < alphabet_size)) { + return Err("input symbols must be less than alphabet_size".into()); + } + if !(!string.is_empty() || budget == 0) { + return Err("budget must be 0 when string is empty".into()); + } + Ok(Self { alphabet_size, string, budget, - } + }) } /// Returns the alphabet size. diff --git a/src/models/misc/integer_expression_membership.rs b/src/models/misc/integer_expression_membership.rs index 83ca4e717..e10b7af47 100644 --- a/src/models/misc/integer_expression_membership.rs +++ b/src/models/misc/integer_expression_membership.rs @@ -155,6 +155,7 @@ impl IntExpr { /// assert!(solution.is_some()); /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "IntegerExpressionMembershipData")] pub struct IntegerExpressionMembership { /// The recursive expression tree. expression: IntExpr, @@ -162,6 +163,20 @@ pub struct IntegerExpressionMembership { target: i64, } +#[derive(Deserialize)] +struct IntegerExpressionMembershipData { + expression: IntExpr, + target: i64, +} + +impl TryFrom for IntegerExpressionMembership { + type Error = crate::registry::ConstructionError; + + fn try_from(data: IntegerExpressionMembershipData) -> Result { + Self::try_new(data.expression, data.target) + } +} + impl IntegerExpressionMembership { /// Create a new IntegerExpressionMembership instance. /// @@ -169,12 +184,20 @@ impl IntegerExpressionMembership { /// * `expression` - The integer expression tree /// * `target` - The target integer K pub fn new(expression: IntExpr, target: i64) -> Self { - assert!(target > 0, "target must be a positive integer (got 0)"); - assert!( - expression.all_atoms_positive(), - "all Atom values must be positive (> 0)" - ); - Self { expression, target } + Self::try_new(expression, target).unwrap_or_else(|error| panic!("{error}")) + } + + fn try_new( + expression: IntExpr, + target: i64, + ) -> Result { + if target <= 0 { + return Err("target must be a positive integer (got 0)".into()); + } + if !(expression.all_atoms_positive()) { + return Err("all Atom values must be positive (> 0)".into()); + } + Ok(Self { expression, target }) } /// Returns a reference to the expression tree. diff --git a/src/models/misc/job_shop_scheduling.rs b/src/models/misc/job_shop_scheduling.rs index 911ca0eb0..4b576287d 100644 --- a/src/models/misc/job_shop_scheduling.rs +++ b/src/models/misc/job_shop_scheduling.rs @@ -25,11 +25,26 @@ inventory::submit! { } #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "JobShopSchedulingData")] pub struct JobShopScheduling { num_processors: usize, jobs: Vec>, } +#[derive(Deserialize)] +struct JobShopSchedulingData { + num_processors: usize, + jobs: Vec>, +} + +impl TryFrom for JobShopScheduling { + type Error = crate::registry::ConstructionError; + + fn try_from(data: JobShopSchedulingData) -> Result { + Self::try_new(data.num_processors, data.jobs) + } +} + #[derive(Debug, Deserialize, crate::CreateSpec)] struct JobShopSchedulingCreateSpec { /// Jobs expressed as ordered processor-duration operations. @@ -97,40 +112,42 @@ struct FlattenedTasks { impl JobShopScheduling { pub fn new(num_processors: usize, jobs: Vec>) -> Self { - let num_tasks: usize = jobs.iter().map(Vec::len).sum(); - if num_tasks > 0 { - assert!( - num_processors > 0, - "num_processors must be positive when tasks are present" - ); + Self::try_new(num_processors, jobs).unwrap_or_else(|error| panic!("{error}")) + } + + fn try_new( + num_processors: usize, + jobs: Vec>, + ) -> Result { + if jobs.iter().any(|job| !job.is_empty()) && !(num_processors > 0) { + return Err("num_processors must be positive when tasks are present".into()); + } + if !(jobs.iter().flatten().all(|&(_, length)| length >= 0)) { + return Err("operation lengths must be nonnegative".into()); } - assert!( - jobs.iter().flatten().all(|&(_, length)| length >= 0), - "operation lengths must be nonnegative" - ); for (job_index, job) in jobs.iter().enumerate() { for (task_index, &(processor, _length)) in job.iter().enumerate() { - assert!( - processor < num_processors, - "job {job_index} task {task_index} uses processor {processor}, but num_processors = {num_processors}" - ); + if !(processor < num_processors) { + return Err(format!("job {job_index} task {task_index} uses processor {processor}, but num_processors = {num_processors}").into()); + } } for (task_index, pair) in job.windows(2).enumerate() { - assert_ne!( - pair[0].0, - pair[1].0, - "job {job_index} tasks {task_index} and {} must use different processors", - task_index + 1 - ); + if pair[0].0 == pair[1].0 { + return Err(format!( + "job {job_index} tasks {task_index} and {} must use different processors", + task_index + 1 + ) + .into()); + } } } - Self { + Ok(Self { num_processors, jobs, - } + }) } pub fn num_processors(&self) -> usize { diff --git a/src/models/misc/knapsack.rs b/src/models/misc/knapsack.rs index 4678efa07..d52a5094b 100644 --- a/src/models/misc/knapsack.rs +++ b/src/models/misc/knapsack.rs @@ -44,15 +44,28 @@ inventory::submit! { /// assert!(solution.is_some()); /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "KnapsackData")] pub struct Knapsack { - #[serde(deserialize_with = "nonnegative_i64_vec::deserialize")] weights: Vec, - #[serde(deserialize_with = "nonnegative_i64_vec::deserialize")] values: Vec, - #[serde(deserialize_with = "nonnegative_i64::deserialize")] capacity: i64, } +#[derive(Deserialize)] +struct KnapsackData { + weights: Vec, + values: Vec, + capacity: i64, +} + +impl TryFrom for Knapsack { + type Error = crate::registry::ConstructionError; + + fn try_from(data: KnapsackData) -> Result { + Self::try_new(data.weights, data.values, data.capacity) + } +} + #[derive(Debug, Deserialize, crate::CreateSpec)] struct KnapsackCreateSpec { /// Nonnegative item weights; defaults to one per value. @@ -78,7 +91,7 @@ impl TryFrom for Knapsack { .to_string() .into()); } - Ok(Self::new(weights, spec.values, spec.capacity)) + Self::try_new(weights, spec.values, spec.capacity) } } @@ -89,25 +102,31 @@ impl Knapsack { /// Panics if `weights` and `values` have different lengths, or if any /// weight, value, or the capacity is negative. pub fn new(weights: Vec, values: Vec, capacity: i64) -> Self { - assert_eq!( - weights.len(), - values.len(), - "weights and values must have the same length" - ); - assert!( - weights.iter().all(|&weight| weight >= 0), - "Knapsack weights must be nonnegative" - ); - assert!( - values.iter().all(|&value| value >= 0), - "Knapsack values must be nonnegative" - ); - assert!(capacity >= 0, "Knapsack capacity must be nonnegative"); - Self { + Self::try_new(weights, values, capacity).unwrap_or_else(|error| panic!("{error}")) + } + + fn try_new( + weights: Vec, + values: Vec, + capacity: i64, + ) -> Result { + if weights.len() != values.len() { + return Err("weights and values must have the same length".into()); + } + if !(weights.iter().all(|&weight| weight >= 0)) { + return Err("Knapsack weights must be nonnegative".into()); + } + if !(values.iter().all(|&value| value >= 0)) { + return Err("Knapsack values must be nonnegative".into()); + } + if !(capacity >= 0) { + return Err("Knapsack capacity must be nonnegative".into()); + } + Ok(Self { weights, values, capacity, - } + }) } /// Returns the item weights. @@ -214,42 +233,6 @@ crate::register_brute_force! { Knapsack decode |_, indices: Vec| crate::config::config_to_bits(&indices), } -mod nonnegative_i64 { - use serde::de::Error; - use serde::{Deserialize, Deserializer}; - - pub fn deserialize<'de, D>(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - let value = i64::deserialize(deserializer)?; - if value < 0 { - return Err(D::Error::custom(format!( - "expected nonnegative integer, got {value}" - ))); - } - Ok(value) - } -} - -mod nonnegative_i64_vec { - use serde::de::Error; - use serde::{Deserialize, Deserializer}; - - pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> - where - D: Deserializer<'de>, - { - let values = Vec::::deserialize(deserializer)?; - if let Some(value) = values.iter().copied().find(|value| *value < 0) { - return Err(D::Error::custom(format!( - "expected nonnegative integers, got {value}" - ))); - } - Ok(values) - } -} - #[cfg(feature = "example-db")] pub(crate) fn canonical_model_example_specs() -> Vec { // 4 items: weights [2,3,4,5], values [3,4,5,7], capacity 7 diff --git a/src/models/misc/longest_common_subsequence.rs b/src/models/misc/longest_common_subsequence.rs index 802897bf7..19ba82846 100644 --- a/src/models/misc/longest_common_subsequence.rs +++ b/src/models/misc/longest_common_subsequence.rs @@ -36,12 +36,27 @@ inventory::submit! { /// subsequence consists of the symbols before padding starts. The objective is /// to maximize the effective length. #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "LongestCommonSubsequenceData")] pub struct LongestCommonSubsequence { alphabet_size: usize, strings: Vec>, max_length: usize, } +#[derive(Deserialize)] +struct LongestCommonSubsequenceData { + alphabet_size: usize, + strings: Vec>, +} + +impl TryFrom for LongestCommonSubsequence { + type Error = crate::registry::ConstructionError; + + fn try_from(data: LongestCommonSubsequenceData) -> Result { + Self::try_new(data.alphabet_size, data.strings) + } +} + #[derive(Debug, Deserialize, crate::CreateSpec)] struct LongestCommonSubsequenceCreateSpec { /// Optional alphabet size; omitted values are inferred from the strings. @@ -104,23 +119,29 @@ impl LongestCommonSubsequence { /// an input symbol is outside the declared alphabet, or if all strings are /// empty (max_length would be 0, requiring at least one non-empty string). pub fn new(alphabet_size: usize, strings: Vec>) -> Self { + Self::try_new(alphabet_size, strings).unwrap_or_else(|error| panic!("{error}")) + } + + fn try_new( + alphabet_size: usize, + strings: Vec>, + ) -> Result { let max_length = strings.iter().map(|s| s.len()).min().unwrap_or(0); - assert!( - alphabet_size > 0 || strings.iter().all(|s| s.is_empty()), - "alphabet_size must be > 0 when any input string is non-empty" - ); - assert!( - strings - .iter() - .flat_map(|s| s.iter()) - .all(|&symbol| symbol < alphabet_size), - "input symbols must be less than alphabet_size" - ); - Self { + if !(alphabet_size > 0 || strings.iter().all(|s| s.is_empty())) { + return Err("alphabet_size must be > 0 when any input string is non-empty".into()); + } + if !(strings + .iter() + .flat_map(|s| s.iter()) + .all(|&symbol| symbol < alphabet_size)) + { + return Err("input symbols must be less than alphabet_size".into()); + } + Ok(Self { alphabet_size, strings, max_length, - } + }) } /// Returns the alphabet size. diff --git a/src/models/misc/maximum_likelihood_ranking.rs b/src/models/misc/maximum_likelihood_ranking.rs index 687b3d776..30fa85d3f 100644 --- a/src/models/misc/maximum_likelihood_ranking.rs +++ b/src/models/misc/maximum_likelihood_ranking.rs @@ -55,10 +55,23 @@ inventory::submit! { /// assert!(solution.is_some()); /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "MaximumLikelihoodRankingData")] pub struct MaximumLikelihoodRanking { matrix: Vec>, } +#[derive(Deserialize)] +struct MaximumLikelihoodRankingData { + matrix: Vec>, +} + +impl TryFrom for MaximumLikelihoodRanking { + type Error = crate::registry::ConstructionError; + fn try_from(data: MaximumLikelihoodRankingData) -> Result { + Self::try_new(data.matrix) + } +} + impl MaximumLikelihoodRanking { /// Create a new MaximumLikelihoodRanking instance. /// @@ -67,37 +80,44 @@ impl MaximumLikelihoodRanking { /// or if the pairwise sums `a_ij + a_ji` are not the same constant for /// all `i != j`. pub fn new(matrix: Vec>) -> Self { + Self::try_new(matrix).unwrap_or_else(|error| panic!("{error}")) + } + + fn try_new(matrix: Vec>) -> Result { let n = matrix.len(); for (i, row) in matrix.iter().enumerate() { - assert_eq!( - row.len(), - n, - "matrix must be square: row {i} has length {} but expected {n}", - row.len() - ); - assert_eq!( - row[i], 0, - "diagonal entries must be zero: matrix[{i}][{i}] = {}", - row[i] - ); + if row.len() != n { + return Err(format!( + "matrix must be square: row {i} has length {} but expected {n}", + row.len() + ) + .into()); + } + if row[i] != 0 { + return Err(format!( + "diagonal entries must be zero: matrix[{i}][{i}] = {}", + row[i] + ) + .into()); + } } let mut comparison_count = None; for (i, row) in matrix.iter().enumerate() { for (j, &entry) in row.iter().enumerate().skip(i + 1) { - let pair_sum = entry + matrix[j][i]; + let pair_sum = i128::from(entry) + i128::from(matrix[j][i]); match comparison_count { None => comparison_count = Some(pair_sum), - Some(expected) => assert_eq!( - pair_sum, - expected, - "all off-diagonal pairs must have the same comparison count: matrix[{i}][{j}] + matrix[{j}][{i}] = {pair_sum}, expected {expected}" - ), + Some(expected) => { + if pair_sum != expected { + return Err(format!("all off-diagonal pairs must have the same comparison count: matrix[{i}][{j}] + matrix[{j}][{i}] = {pair_sum}, expected {expected}").into()); + } + } } } } - Self { matrix } + Ok(Self { matrix }) } /// Returns the comparison matrix. diff --git a/src/models/misc/minimum_axiom_set.rs b/src/models/misc/minimum_axiom_set.rs index 8ba882ff9..2492ba4e0 100644 --- a/src/models/misc/minimum_axiom_set.rs +++ b/src/models/misc/minimum_axiom_set.rs @@ -63,6 +63,7 @@ inventory::submit! { /// assert!(solution.is_some()); /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "MinimumAxiomSetData")] pub struct MinimumAxiomSet { /// Total number of sentences |S|. num_sentences: usize, @@ -72,6 +73,20 @@ pub struct MinimumAxiomSet { implications: Vec<(Vec, usize)>, } +#[derive(Deserialize)] +struct MinimumAxiomSetData { + num_sentences: usize, + true_sentences: Vec, + implications: Vec<(Vec, usize)>, +} + +impl TryFrom for MinimumAxiomSet { + type Error = crate::registry::ConstructionError; + fn try_from(data: MinimumAxiomSetData) -> Result { + Self::try_new(data.num_sentences, data.true_sentences, data.implications) + } +} + impl MinimumAxiomSet { /// Create a new Minimum Axiom Set instance. /// @@ -85,37 +100,53 @@ impl MinimumAxiomSet { true_sentences: Vec, implications: Vec<(Vec, usize)>, ) -> Self { + Self::try_new(num_sentences, true_sentences, implications) + .unwrap_or_else(|error| panic!("{error}")) + } + + fn try_new( + num_sentences: usize, + true_sentences: Vec, + implications: Vec<(Vec, usize)>, + ) -> Result { // Validate true sentences for &s in &true_sentences { - assert!( - s < num_sentences, - "True sentence index {s} out of range [0, {num_sentences})" - ); + if !(s < num_sentences) { + return Err( + format!("True sentence index {s} out of range [0, {num_sentences})").into(), + ); + } } // Check no duplicates let mut seen = vec![false; num_sentences]; for &s in &true_sentences { - assert!(!seen[s], "Duplicate true sentence index {s}"); + if !(!seen[s]) { + return Err(format!("Duplicate true sentence index {s}").into()); + } seen[s] = true; } // Validate implications for (antecedents, consequent) in &implications { for &a in antecedents { - assert!( - a < num_sentences, - "Implication antecedent {a} out of range [0, {num_sentences})" - ); + if !(a < num_sentences) { + return Err(format!( + "Implication antecedent {a} out of range [0, {num_sentences})" + ) + .into()); + } + } + if !(*consequent < num_sentences) { + return Err(format!( + "Implication consequent {consequent} out of range [0, {num_sentences})" + ) + .into()); } - assert!( - *consequent < num_sentences, - "Implication consequent {consequent} out of range [0, {num_sentences})" - ); } - Self { + Ok(Self { num_sentences, true_sentences, implications, - } + }) } /// Returns the total number of sentences |S|. diff --git a/src/models/misc/minimum_code_generation_one_register.rs b/src/models/misc/minimum_code_generation_one_register.rs index 4cfcc0d5d..00de85369 100644 --- a/src/models/misc/minimum_code_generation_one_register.rs +++ b/src/models/misc/minimum_code_generation_one_register.rs @@ -57,6 +57,7 @@ inventory::submit! { /// assert_eq!(problem.evaluate(&solution).unwrap(), Min(Some(8))); /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "MinimumCodeGenerationOneRegisterData")] pub struct MinimumCodeGenerationOneRegister { /// Number of vertices |V|. num_vertices: usize, @@ -66,6 +67,20 @@ pub struct MinimumCodeGenerationOneRegister { num_leaves: usize, } +#[derive(Deserialize)] +struct MinimumCodeGenerationOneRegisterData { + num_vertices: usize, + edges: Vec<(usize, usize)>, + num_leaves: usize, +} + +impl TryFrom for MinimumCodeGenerationOneRegister { + type Error = crate::registry::ConstructionError; + fn try_from(data: MinimumCodeGenerationOneRegisterData) -> Result { + Self::try_new(data.num_vertices, data.edges, data.num_leaves) + } +} + impl MinimumCodeGenerationOneRegister { /// Create a new instance. /// @@ -80,36 +95,50 @@ impl MinimumCodeGenerationOneRegister { /// Panics if any edge index is out of bounds, if any vertex has /// out-degree > 2, or if `num_leaves > num_vertices`. pub fn new(num_vertices: usize, edges: Vec<(usize, usize)>, num_leaves: usize) -> Self { - assert!( - num_leaves <= num_vertices, - "num_leaves ({num_leaves}) exceeds num_vertices ({num_vertices})" - ); + Self::try_new(num_vertices, edges, num_leaves).unwrap_or_else(|error| panic!("{error}")) + } + + fn try_new( + num_vertices: usize, + edges: Vec<(usize, usize)>, + num_leaves: usize, + ) -> Result { + if !(num_leaves <= num_vertices) { + return Err( + format!("num_leaves ({num_leaves}) exceeds num_vertices ({num_vertices})").into(), + ); + } let mut out_degree = vec![0usize; num_vertices]; for &(parent, child) in &edges { - assert!( - parent < num_vertices && child < num_vertices, - "Edge ({parent}, {child}) out of bounds for {num_vertices} vertices" - ); - assert!( - parent != child, - "Self-loop ({parent}, {parent}) not allowed" - ); + if !(parent < num_vertices && child < num_vertices) { + return Err(format!( + "Edge ({parent}, {child}) out of bounds for {num_vertices} vertices" + ) + .into()); + } + if parent == child { + return Err(format!("Self-loop ({parent}, {parent}) not allowed").into()); + } out_degree[parent] += 1; } for (v, °) in out_degree.iter().enumerate() { - assert!(deg <= 2, "Vertex {v} has out-degree {deg} > 2"); + if !(deg <= 2) { + return Err(format!("Vertex {v} has out-degree {deg} > 2").into()); + } } // Verify leaf count: leaves are vertices with out-degree 0 let actual_leaves = out_degree.iter().filter(|&&d| d == 0).count(); - assert_eq!( - actual_leaves, num_leaves, - "Declared num_leaves ({num_leaves}) != actual leaf count ({actual_leaves})" - ); - Self { + if actual_leaves != num_leaves { + return Err(format!( + "Declared num_leaves ({num_leaves}) != actual leaf count ({actual_leaves})" + ) + .into()); + } + Ok(Self { num_vertices, edges, num_leaves, - } + }) } /// Get the number of vertices. diff --git a/src/models/misc/minimum_code_generation_parallel_assignments.rs b/src/models/misc/minimum_code_generation_parallel_assignments.rs index 2e668c04c..074f1b788 100644 --- a/src/models/misc/minimum_code_generation_parallel_assignments.rs +++ b/src/models/misc/minimum_code_generation_parallel_assignments.rs @@ -56,33 +56,60 @@ inventory::submit! { /// assert!(solution.is_some()); /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "MinimumCodeGenerationParallelAssignmentsData")] pub struct MinimumCodeGenerationParallelAssignments { num_variables: usize, assignments: Vec<(usize, Vec)>, } +#[derive(Deserialize)] +struct MinimumCodeGenerationParallelAssignmentsData { + num_variables: usize, + assignments: Vec<(usize, Vec)>, +} + +impl TryFrom + for MinimumCodeGenerationParallelAssignments +{ + type Error = crate::registry::ConstructionError; + fn try_from(data: MinimumCodeGenerationParallelAssignmentsData) -> Result { + Self::try_new(data.num_variables, data.assignments) + } +} + impl MinimumCodeGenerationParallelAssignments { /// Create a new MinimumCodeGenerationParallelAssignments instance. /// /// # Panics /// Panics if any target variable or read variable index is >= num_variables. pub fn new(num_variables: usize, assignments: Vec<(usize, Vec)>) -> Self { + Self::try_new(num_variables, assignments).unwrap_or_else(|error| panic!("{error}")) + } + + fn try_new( + num_variables: usize, + assignments: Vec<(usize, Vec)>, + ) -> Result { for (i, (target, reads)) in assignments.iter().enumerate() { - assert!( - *target < num_variables, - "assignment {i}: target variable {target} >= num_variables {num_variables}" - ); + if !(*target < num_variables) { + return Err(format!( + "assignment {i}: target variable {target} >= num_variables {num_variables}" + ) + .into()); + } for &r in reads { - assert!( - r < num_variables, - "assignment {i}: read variable {r} >= num_variables {num_variables}" - ); + if !(r < num_variables) { + return Err(format!( + "assignment {i}: read variable {r} >= num_variables {num_variables}" + ) + .into()); + } } } - Self { + Ok(Self { num_variables, assignments, - } + }) } /// Returns the number of variables. diff --git a/src/models/misc/minimum_code_generation_unlimited_registers.rs b/src/models/misc/minimum_code_generation_unlimited_registers.rs index 4eda7c15a..c9851f76d 100644 --- a/src/models/misc/minimum_code_generation_unlimited_registers.rs +++ b/src/models/misc/minimum_code_generation_unlimited_registers.rs @@ -62,6 +62,7 @@ inventory::submit! { /// assert_eq!(problem.evaluate(&solution).unwrap(), Min(Some(4))); /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "MinimumCodeGenerationUnlimitedRegistersData")] pub struct MinimumCodeGenerationUnlimitedRegisters { /// Number of vertices |V|. num_vertices: usize, @@ -71,6 +72,22 @@ pub struct MinimumCodeGenerationUnlimitedRegisters { right_arcs: Vec<(usize, usize)>, } +#[derive(Deserialize)] +struct MinimumCodeGenerationUnlimitedRegistersData { + num_vertices: usize, + left_arcs: Vec<(usize, usize)>, + right_arcs: Vec<(usize, usize)>, +} + +impl TryFrom + for MinimumCodeGenerationUnlimitedRegisters +{ + type Error = crate::registry::ConstructionError; + fn try_from(data: MinimumCodeGenerationUnlimitedRegistersData) -> Result { + Self::try_new(data.num_vertices, data.left_arcs, data.right_arcs) + } +} + impl MinimumCodeGenerationUnlimitedRegisters { /// Create a new instance. /// @@ -90,56 +107,67 @@ impl MinimumCodeGenerationUnlimitedRegisters { left_arcs: Vec<(usize, usize)>, right_arcs: Vec<(usize, usize)>, ) -> Self { + Self::try_new(num_vertices, left_arcs, right_arcs).unwrap_or_else(|error| panic!("{error}")) + } + + fn try_new( + num_vertices: usize, + left_arcs: Vec<(usize, usize)>, + right_arcs: Vec<(usize, usize)>, + ) -> Result { let mut left_count = vec![0usize; num_vertices]; let mut right_count = vec![0usize; num_vertices]; for &(parent, child) in &left_arcs { - assert!( - parent < num_vertices && child < num_vertices, - "Left arc ({parent}, {child}) out of bounds for {num_vertices} vertices" - ); - assert!( - parent != child, - "Self-loop ({parent}, {parent}) not allowed" - ); + if !(parent < num_vertices && child < num_vertices) { + return Err(format!( + "Left arc ({parent}, {child}) out of bounds for {num_vertices} vertices" + ) + .into()); + } + if parent == child { + return Err(format!("Self-loop ({parent}, {parent}) not allowed").into()); + } left_count[parent] += 1; } for &(parent, child) in &right_arcs { - assert!( - parent < num_vertices && child < num_vertices, - "Right arc ({parent}, {child}) out of bounds for {num_vertices} vertices" - ); - assert!( - parent != child, - "Self-loop ({parent}, {parent}) not allowed" - ); + if !(parent < num_vertices && child < num_vertices) { + return Err(format!( + "Right arc ({parent}, {child}) out of bounds for {num_vertices} vertices" + ) + .into()); + } + if parent == child { + return Err(format!("Self-loop ({parent}, {parent}) not allowed").into()); + } right_count[parent] += 1; } for v in 0..num_vertices { let out = left_count[v] + right_count[v]; - assert!(out <= 2, "Vertex {v} has out-degree {out} > 2"); + if !(out <= 2) { + return Err(format!("Vertex {v} has out-degree {out} > 2").into()); + } // Binary vertex: exactly one left and one right - if out == 2 { - assert!( - left_count[v] == 1 && right_count[v] == 1, - "Binary vertex {v} must have exactly 1 left and 1 right arc" + if out == 2 && !(left_count[v] == 1 && right_count[v] == 1) { + return Err( + format!("Binary vertex {v} must have exactly 1 left and 1 right arc").into(), ); } // Unary vertex: one left arc (result overwrites operand register) - if out == 1 { - assert!( - left_count[v] == 1 && right_count[v] == 0, + if out == 1 && !(left_count[v] == 1 && right_count[v] == 0) { + return Err(format!( "Unary vertex {v} must have exactly 1 left arc and 0 right arcs" - ); + ) + .into()); } } - Self { + Ok(Self { num_vertices, left_arcs, right_arcs, - } + }) } /// Get the number of vertices. diff --git a/src/models/misc/minimum_decision_tree.rs b/src/models/misc/minimum_decision_tree.rs index f04cf05ec..c42680885 100644 --- a/src/models/misc/minimum_decision_tree.rs +++ b/src/models/misc/minimum_decision_tree.rs @@ -50,6 +50,7 @@ inventory::submit! { /// let value = solver.solve(&problem).unwrap(); /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "MinimumDecisionTreeCreateSpec")] pub struct MinimumDecisionTree { /// Binary matrix: test_matrix[j][i] = true iff object i passes test j. test_matrix: Vec>, @@ -116,35 +117,44 @@ impl MinimumDecisionTree { /// - If test_matrix dimensions don't match /// - If tests don't distinguish all object pairs pub fn new(test_matrix: Vec>, num_objects: usize, num_tests: usize) -> Self { - assert!(num_objects >= 2, "Need at least 2 objects"); - assert!(num_tests >= 1, "Need at least 1 test"); - assert_eq!( - test_matrix.len(), - num_tests, - "test_matrix must have num_tests rows" - ); + Self::try_new(test_matrix, num_objects, num_tests).unwrap_or_else(|error| panic!("{error}")) + } + + fn try_new( + test_matrix: Vec>, + num_objects: usize, + num_tests: usize, + ) -> Result { + if !(num_objects >= 2) { + return Err("Need at least 2 objects".into()); + } + if num_tests == 0 { + return Err("Need at least 1 test".into()); + } + if test_matrix.len() != num_tests { + return Err("test_matrix must have num_tests rows".into()); + } for (j, row) in test_matrix.iter().enumerate() { - assert_eq!( - row.len(), - num_objects, - "test_matrix[{j}] must have num_objects columns" - ); + if row.len() != num_objects { + return Err(format!("test_matrix[{j}] must have num_objects columns").into()); + } } // Check that every pair of objects is distinguished by at least one test for a in 0..num_objects { for b in (a + 1)..num_objects { let distinguished = (0..num_tests).any(|j| test_matrix[j][a] != test_matrix[j][b]); - assert!( - distinguished, - "Objects {a} and {b} are not distinguished by any test" - ); + if !(distinguished) { + return Err( + format!("Objects {a} and {b} are not distinguished by any test").into(), + ); + } } } - Self { + Ok(Self { test_matrix, num_objects, num_tests, - } + }) } /// Get the number of objects. diff --git a/src/models/misc/minimum_disjunctive_normal_form.rs b/src/models/misc/minimum_disjunctive_normal_form.rs index 7f1bc5ac4..4ca7e552d 100644 --- a/src/models/misc/minimum_disjunctive_normal_form.rs +++ b/src/models/misc/minimum_disjunctive_normal_form.rs @@ -70,6 +70,7 @@ impl PrimeImplicant { /// let value = solver.solve(&problem).unwrap(); /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "MinimumDisjunctiveNormalFormData")] pub struct MinimumDisjunctiveNormalForm { /// Number of Boolean variables. num_variables: usize, @@ -81,6 +82,19 @@ pub struct MinimumDisjunctiveNormalForm { minterms: Vec, } +#[derive(Deserialize)] +struct MinimumDisjunctiveNormalFormData { + num_variables: usize, + truth_table: Vec, +} + +impl TryFrom for MinimumDisjunctiveNormalForm { + type Error = crate::registry::ConstructionError; + fn try_from(data: MinimumDisjunctiveNormalFormData) -> Result { + Self::try_new(data.num_variables, data.truth_table) + } +} + impl MinimumDisjunctiveNormalForm { /// Create a new MinimumDisjunctiveNormalForm problem. /// @@ -88,31 +102,43 @@ impl MinimumDisjunctiveNormalForm { /// - If truth_table length != 2^num_variables /// - If the function is identically false (no minterms) pub fn new(num_variables: usize, truth_table: Vec) -> Self { - assert!(num_variables >= 1, "Need at least 1 variable"); - assert_eq!( - truth_table.len(), - 1 << num_variables, - "Truth table must have 2^n entries" - ); + Self::try_new(num_variables, truth_table).unwrap_or_else(|error| panic!("{error}")) + } + + fn try_new( + num_variables: usize, + truth_table: Vec, + ) -> Result { + if num_variables == 0 { + return Err("Need at least 1 variable".into()); + } + if truth_table.len() + != 1usize + .checked_shl( + u32::try_from(num_variables).map_err(|_| "truth table size overflows usize")?, + ) + .ok_or("truth table size overflows usize")? + { + return Err("Truth table must have 2^n entries".into()); + } let minterms: Vec = truth_table .iter() .enumerate() .filter_map(|(i, &v)| if v { Some(i) } else { None }) .collect(); - assert!( - !minterms.is_empty(), - "Function must have at least one minterm" - ); + if minterms.is_empty() { + return Err("Function must have at least one minterm".into()); + } let prime_implicants = compute_prime_implicants(num_variables, &minterms); - Self { + Ok(Self { num_variables, truth_table, prime_implicants, minterms, - } + }) } /// Get the number of variables. diff --git a/src/models/misc/minimum_fault_detection_test_set.rs b/src/models/misc/minimum_fault_detection_test_set.rs index 51ceacd14..23f3b014d 100644 --- a/src/models/misc/minimum_fault_detection_test_set.rs +++ b/src/models/misc/minimum_fault_detection_test_set.rs @@ -87,15 +87,8 @@ impl<'de> Deserialize<'de> for MinimumFaultDetectionTestSet { D: Deserializer<'de>, { let data = MinimumFaultDetectionTestSetData::deserialize(deserializer)?; - let coverage = - Self::build_coverage(data.num_vertices, &data.arcs, &data.inputs, &data.outputs); - Ok(Self { - num_vertices: data.num_vertices, - arcs: data.arcs, - inputs: data.inputs, - outputs: data.outputs, - coverage, - }) + Self::try_new(data.num_vertices, data.arcs, data.inputs, data.outputs) + .map_err(serde::de::Error::custom) } } @@ -112,42 +105,56 @@ impl MinimumFaultDetectionTestSet { inputs: Vec, outputs: Vec, ) -> Self { - assert!(!inputs.is_empty(), "Inputs must not be empty"); - assert!(!outputs.is_empty(), "Outputs must not be empty"); + Self::try_new(num_vertices, arcs, inputs, outputs).unwrap_or_else(|error| panic!("{error}")) + } + + fn try_new( + num_vertices: usize, + arcs: Vec<(usize, usize)>, + inputs: Vec, + outputs: Vec, + ) -> Result { + if inputs.is_empty() { + return Err("Inputs must not be empty".into()); + }; + if outputs.is_empty() { + return Err("Outputs must not be empty".into()); + }; for (i, &(u, v)) in arcs.iter().enumerate() { - assert!( - u < num_vertices && v < num_vertices, - "Arc {} ({}, {}) out of bounds for {} vertices", - i, - u, - v, - num_vertices - ); + if !(u < num_vertices && v < num_vertices) { + return Err(format!( + "Arc {} ({}, {}) out of bounds for {} vertices", + i, u, v, num_vertices + ) + .into()); + }; } for &inp in &inputs { - assert!( - inp < num_vertices, - "Input vertex {} out of bounds for {} vertices", - inp, - num_vertices - ); + if !(inp < num_vertices) { + return Err(format!( + "Input vertex {} out of bounds for {} vertices", + inp, num_vertices + ) + .into()); + }; } for &out in &outputs { - assert!( - out < num_vertices, - "Output vertex {} out of bounds for {} vertices", - out, - num_vertices - ); + if !(out < num_vertices) { + return Err(format!( + "Output vertex {} out of bounds for {} vertices", + out, num_vertices + ) + .into()); + }; } let coverage = Self::build_coverage(num_vertices, &arcs, &inputs, &outputs); - Self { + Ok(Self { num_vertices, arcs, inputs, outputs, coverage, - } + }) } /// Compute forward reachability from a given vertex using BFS on the DAG. diff --git a/src/models/misc/minimum_register_sufficiency_for_loops.rs b/src/models/misc/minimum_register_sufficiency_for_loops.rs index 081f0cd17..798386e27 100644 --- a/src/models/misc/minimum_register_sufficiency_for_loops.rs +++ b/src/models/misc/minimum_register_sufficiency_for_loops.rs @@ -62,6 +62,7 @@ inventory::submit! { /// assert_eq!(val, Min(Some(3))); /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "MinimumRegisterSufficiencyForLoopsData")] pub struct MinimumRegisterSufficiencyForLoops { /// Loop length N (number of timesteps in the circular loop). loop_length: usize, @@ -69,6 +70,19 @@ pub struct MinimumRegisterSufficiencyForLoops { variables: Vec<(usize, usize)>, } +#[derive(Deserialize)] +struct MinimumRegisterSufficiencyForLoopsData { + loop_length: usize, + variables: Vec<(usize, usize)>, +} + +impl TryFrom for MinimumRegisterSufficiencyForLoops { + type Error = crate::registry::ConstructionError; + fn try_from(data: MinimumRegisterSufficiencyForLoopsData) -> Result { + Self::try_new(data.loop_length, data.variables) + } +} + impl MinimumRegisterSufficiencyForLoops { /// Create a new Minimum Register Sufficiency for Loops instance. /// @@ -77,27 +91,36 @@ impl MinimumRegisterSufficiencyForLoops { /// Panics if `loop_length` is zero, if any duration is zero or exceeds /// `loop_length`, or if any `start_time >= loop_length`. pub fn new(loop_length: usize, variables: Vec<(usize, usize)>) -> Self { - assert!(loop_length > 0, "loop_length must be positive"); + Self::try_new(loop_length, variables).unwrap_or_else(|error| panic!("{error}")) + } + + fn try_new( + loop_length: usize, + variables: Vec<(usize, usize)>, + ) -> Result { + if loop_length == 0 { + return Err("loop_length must be positive".into()); + } for (i, &(start, dur)) in variables.iter().enumerate() { - assert!( - start < loop_length, - "Variable {} start_time {} >= loop_length {}", - i, - start, - loop_length - ); - assert!( - dur > 0 && dur <= loop_length, - "Variable {} duration {} must be in [1, {}]", - i, - dur, - loop_length - ); + if !(start < loop_length) { + return Err(format!( + "Variable {} start_time {} >= loop_length {}", + i, start, loop_length + ) + .into()); + } + if !(dur > 0 && dur <= loop_length) { + return Err(format!( + "Variable {} duration {} must be in [1, {}]", + i, dur, loop_length + ) + .into()); + } } - Self { + Ok(Self { loop_length, variables, - } + }) } /// Get the loop length N. diff --git a/src/models/misc/minimum_tardiness_sequencing.rs b/src/models/misc/minimum_tardiness_sequencing.rs index fe9ee747b..dbd995de8 100644 --- a/src/models/misc/minimum_tardiness_sequencing.rs +++ b/src/models/misc/minimum_tardiness_sequencing.rs @@ -54,46 +54,34 @@ inventory::submit! { /// let solution = solver.solve(&problem).unwrap(); /// assert!(solution.is_some()); /// ``` -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize)] pub struct MinimumTardinessSequencing { lengths: Vec, deadlines: Vec, precedences: Vec<(usize, usize)>, } -macro_rules! minimum_tardiness_create_spec { - ($name:ident, $weight:ty, $construct:expr) => { - #[derive(Debug, Deserialize, crate::CreateSpec)] - struct $name { - lengths: Vec<$weight>, - deadlines: Vec, - precedences: Option>, - } +#[derive(Deserialize)] +struct MinimumTardinessSequencingData { + lengths: Vec, + deadlines: Vec, + precedences: Vec<(usize, usize)>, +} - impl TryFrom<$name> for MinimumTardinessSequencing<$weight> { - type Error = crate::registry::ConstructionError; +impl<'de> Deserialize<'de> for MinimumTardinessSequencing { + fn deserialize>(deserializer: D) -> Result { + let data = MinimumTardinessSequencingData::::deserialize(deserializer)?; + Self::try_new(data.lengths.len(), data.deadlines, data.precedences) + .map_err(serde::de::Error::custom) + } +} - fn try_from(spec: $name) -> Result { - if spec.lengths.len() != spec.deadlines.len() { - return Err("lengths and deadlines must have the same length" - .to_string() - .into()); - } - let precedences = spec.precedences.unwrap_or_default(); - let num_tasks = spec.lengths.len(); - if let Some(&(pred, succ)) = precedences - .iter() - .find(|&&(pred, succ)| pred >= num_tasks || succ >= num_tasks) - { - return Err(format!( - "precedence ({pred}, {succ}) is out of range for {num_tasks} tasks" - ) - .into()); - } - $construct(spec.lengths, spec.deadlines, precedences) - } - } - }; +impl<'de> Deserialize<'de> for MinimumTardinessSequencing { + fn deserialize>(deserializer: D) -> Result { + let data = MinimumTardinessSequencingData::::deserialize(deserializer)?; + Self::try_with_lengths(data.lengths, data.deadlines, data.precedences) + .map_err(serde::de::Error::custom) + } } #[derive(Debug, Deserialize, crate::CreateSpec)] @@ -112,24 +100,26 @@ impl TryFrom for MinimumTardinessSequen { return Err("precedence indices must be within the task count".into()); } - Ok(Self::new(num_tasks, spec.deadlines, precedences)) + Self::try_new(num_tasks, spec.deadlines, precedences) } } -minimum_tardiness_create_spec!( - MinimumTardinessSequencingI64CreateSpec, - i64, - |lengths: Vec, deadlines, precedences| { - if lengths.iter().any(|&length| length <= 0) { - return Err("all task lengths must be positive".to_string().into()); - } - Ok(MinimumTardinessSequencing::with_lengths( - lengths, - deadlines, - precedences, - )) +#[derive(Debug, Deserialize, crate::CreateSpec)] +struct MinimumTardinessSequencingI64CreateSpec { + lengths: Vec, + deadlines: Vec, + precedences: Option>, +} +impl TryFrom for MinimumTardinessSequencing { + type Error = crate::registry::ConstructionError; + fn try_from(spec: MinimumTardinessSequencingI64CreateSpec) -> Result { + Self::try_with_lengths( + spec.lengths, + spec.deadlines, + spec.precedences.unwrap_or_default(), + ) } -); +} impl MinimumTardinessSequencing { /// Create a new unit-length MinimumTardinessSequencing instance. @@ -139,17 +129,20 @@ impl MinimumTardinessSequencing { /// Panics if `deadlines.len() != num_tasks` or if any task index in `precedences` /// is out of range. pub fn new(num_tasks: usize, deadlines: Vec, precedences: Vec<(usize, usize)>) -> Self { - assert_eq!( - deadlines.len(), - num_tasks, - "deadlines length must equal num_tasks" - ); - validate_precedences(num_tasks, &precedences); - Self { + Self::try_new(num_tasks, deadlines, precedences).unwrap_or_else(|error| panic!("{error}")) + } + + fn try_new( + num_tasks: usize, + deadlines: Vec, + precedences: Vec<(usize, usize)>, + ) -> Result { + validate_task_data(num_tasks, &deadlines, &precedences)?; + Ok(Self { lengths: vec![One; num_tasks], deadlines, precedences, - } + }) } } @@ -165,40 +158,48 @@ impl MinimumTardinessSequencing { deadlines: Vec, precedences: Vec<(usize, usize)>, ) -> Self { - assert_eq!( - lengths.len(), - deadlines.len(), - "lengths and deadlines must have the same length" - ); - assert!( - lengths.iter().all(|&l| l > 0), - "all task lengths must be positive" - ); - let num_tasks = lengths.len(); - validate_precedences(num_tasks, &precedences); - Self { + Self::try_with_lengths(lengths, deadlines, precedences) + .unwrap_or_else(|error| panic!("{error}")) + } + + fn try_with_lengths( + lengths: Vec, + deadlines: Vec, + precedences: Vec<(usize, usize)>, + ) -> Result { + validate_task_data(lengths.len(), &deadlines, &precedences)?; + if lengths.iter().any(|&length| length <= 0) { + return Err("all task lengths must be positive".into()); + } + Ok(Self { lengths, deadlines, precedences, - } + }) } } -fn validate_precedences(num_tasks: usize, precedences: &[(usize, usize)]) { +fn validate_task_data( + num_tasks: usize, + deadlines: &[i64], + precedences: &[(usize, usize)], +) -> Result<(), crate::registry::ConstructionError> { + if deadlines.len() != num_tasks { + return Err("deadlines length must equal num_tasks".into()); + } for &(pred, succ) in precedences { - assert!( - pred < num_tasks, - "predecessor index {} out of range (num_tasks = {})", - pred, - num_tasks - ); - assert!( - succ < num_tasks, - "successor index {} out of range (num_tasks = {})", - succ, - num_tasks - ); + if pred >= num_tasks { + return Err( + format!("predecessor index {pred} out of range (num_tasks = {num_tasks})").into(), + ); + } + if succ >= num_tasks { + return Err( + format!("successor index {succ} out of range (num_tasks = {num_tasks})").into(), + ); + } } + Ok(()) } impl MinimumTardinessSequencing { diff --git a/src/models/misc/minimum_weight_and_or_graph.rs b/src/models/misc/minimum_weight_and_or_graph.rs index 346bbe243..64856af28 100644 --- a/src/models/misc/minimum_weight_and_or_graph.rs +++ b/src/models/misc/minimum_weight_and_or_graph.rs @@ -115,13 +115,13 @@ impl TryFrom for MinimumWeightAndOrGraph { ) .into()); } - Ok(Self::new( + Self::try_new( spec.num_vertices, spec.arcs, spec.source, spec.gate_types, arc_weights, - )) + ) } } @@ -140,15 +140,14 @@ impl<'de> Deserialize<'de> for MinimumWeightAndOrGraph { D: Deserializer<'de>, { let data = MinimumWeightAndOrGraphData::deserialize(deserializer)?; - let outgoing = Self::build_outgoing(data.num_vertices, &data.arcs); - Ok(Self { - num_vertices: data.num_vertices, - arcs: data.arcs, - source: data.source, - gate_types: data.gate_types, - arc_weights: data.arc_weights, - outgoing, - }) + Self::try_new( + data.num_vertices, + data.arcs, + data.source, + data.gate_types, + data.arc_weights, + ) + .map_err(serde::de::Error::custom) } } @@ -167,49 +166,61 @@ impl MinimumWeightAndOrGraph { gate_types: Vec>, arc_weights: Vec, ) -> Self { - assert!( - source < num_vertices, - "Source vertex {} out of bounds for {} vertices", - source, - num_vertices - ); - assert_eq!( - gate_types.len(), - num_vertices, - "gate_types length {} does not match num_vertices {}", - gate_types.len(), - num_vertices - ); - assert_eq!( - arc_weights.len(), - arcs.len(), - "arc_weights length {} does not match number of arcs {}", - arc_weights.len(), - arcs.len() - ); - for (i, &(u, v)) in arcs.iter().enumerate() { - assert!( - u < num_vertices && v < num_vertices, - "Arc {} ({}, {}) out of bounds for {} vertices", - i, - u, - v, + Self::try_new(num_vertices, arcs, source, gate_types, arc_weights) + .unwrap_or_else(|error| panic!("{error}")) + } + + fn try_new( + num_vertices: usize, + arcs: Vec<(usize, usize)>, + source: usize, + gate_types: Vec>, + arc_weights: Vec, + ) -> Result { + if !(source < num_vertices) { + return Err(format!( + "Source vertex {} out of bounds for {} vertices", + source, num_vertices + ) + .into()); + }; + if gate_types.len() != num_vertices { + return Err(format!( + "gate_types length {} does not match num_vertices {}", + gate_types.len(), num_vertices - ); + ) + .into()); + }; + if arc_weights.len() != arcs.len() { + return Err(format!( + "arc_weights length {} does not match number of arcs {}", + arc_weights.len(), + arcs.len() + ) + .into()); + }; + for (i, &(u, v)) in arcs.iter().enumerate() { + if !(u < num_vertices && v < num_vertices) { + return Err(format!( + "Arc {} ({}, {}) out of bounds for {} vertices", + i, u, v, num_vertices + ) + .into()); + }; } - assert!( - gate_types[source].is_some(), - "Source vertex must be an AND or OR gate, not a leaf" - ); + if !(gate_types[source].is_some()) { + return Err("Source vertex must be an AND or OR gate, not a leaf".into()); + }; let outgoing = Self::build_outgoing(num_vertices, &arcs); - Self { + Ok(Self { num_vertices, arcs, source, gate_types, arc_weights, outgoing, - } + }) } /// Build outgoing arc index lists for each vertex. diff --git a/src/models/misc/multiprocessor_scheduling.rs b/src/models/misc/multiprocessor_scheduling.rs index c9271d6f6..733bb9486 100644 --- a/src/models/misc/multiprocessor_scheduling.rs +++ b/src/models/misc/multiprocessor_scheduling.rs @@ -50,11 +50,11 @@ inventory::submit! { /// assert!(solution.is_some()); /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "MultiprocessorSchedulingCreateSpec")] pub struct MultiprocessorScheduling { /// Processing time for each task. lengths: Vec, /// Number of identical processors. - #[serde(deserialize_with = "positive_usize::deserialize")] num_processors: usize, /// Global deadline. deadline: i64, @@ -75,7 +75,7 @@ impl TryFrom for MultiprocessorScheduling { if spec.num_processors == 0 { return Err("num_processors must be positive".to_string().into()); } - Ok(Self::new(spec.lengths, spec.num_processors, spec.deadline)) + Self::try_new(spec.lengths, spec.num_processors, spec.deadline) } } @@ -85,17 +85,28 @@ impl MultiprocessorScheduling { /// # Panics /// Panics if `num_processors` is zero. pub fn new(lengths: Vec, num_processors: usize, deadline: i64) -> Self { - assert!(num_processors > 0, "num_processors must be positive"); - assert!( - lengths.iter().all(|&length| length >= 0), - "task lengths must be nonnegative" - ); - assert!(deadline >= 0, "deadline must be nonnegative"); - Self { + Self::try_new(lengths, num_processors, deadline).unwrap_or_else(|error| panic!("{error}")) + } + + fn try_new( + lengths: Vec, + num_processors: usize, + deadline: i64, + ) -> Result { + if num_processors == 0 { + return Err("num_processors must be positive".into()); + } + if !(lengths.iter().all(|&length| length >= 0)) { + return Err("task lengths must be nonnegative".into()); + } + if !(deadline >= 0) { + return Err("deadline must be nonnegative".into()); + } + Ok(Self { lengths, num_processors, deadline, - } + }) } /// Returns the processing times for each task. @@ -197,22 +208,6 @@ pub(crate) fn canonical_model_example_specs() -> Vec(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - let value = usize::deserialize(deserializer)?; - if value == 0 { - return Err(D::Error::custom("expected positive integer, got 0")); - } - Ok(value) - } -} - #[cfg(test)] #[path = "../../unit_tests/models/misc/multiprocessor_scheduling.rs"] mod tests; diff --git a/src/models/misc/optimum_communication_spanning_tree.rs b/src/models/misc/optimum_communication_spanning_tree.rs index 1eb9ec3f0..ca183757f 100644 --- a/src/models/misc/optimum_communication_spanning_tree.rs +++ b/src/models/misc/optimum_communication_spanning_tree.rs @@ -63,12 +63,26 @@ inventory::submit! { /// assert!(solution.is_some()); /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "OptimumCommunicationSpanningTreeData")] pub struct OptimumCommunicationSpanningTree { num_vertices: usize, edge_weights: Vec>, requirements: Vec>, } +#[derive(Deserialize)] +struct OptimumCommunicationSpanningTreeData { + edge_weights: Vec>, + requirements: Vec>, +} + +impl TryFrom for OptimumCommunicationSpanningTree { + type Error = crate::registry::ConstructionError; + fn try_from(data: OptimumCommunicationSpanningTreeData) -> Result { + Self::try_new(data.edge_weights, data.requirements) + } +} + #[derive(Debug, Deserialize, crate::CreateSpec)] struct OptimumCommunicationSpanningTreeCreateSpec { /// Number of vertices. @@ -108,7 +122,7 @@ impl TryFrom for OptimumCommunicatio } } } - Ok(Self::new(edge_weights, spec.requirements)) + Self::try_new(edge_weights, spec.requirements) } } @@ -125,73 +139,94 @@ impl OptimumCommunicationSpanningTree { /// Panics if the matrices are not square, not the same size, have nonzero /// diagonals, are not symmetric, or contain negative entries. pub fn new(edge_weights: Vec>, requirements: Vec>) -> Self { + Self::try_new(edge_weights, requirements).unwrap_or_else(|error| panic!("{error}")) + } + + fn try_new( + edge_weights: Vec>, + requirements: Vec>, + ) -> Result { let n = edge_weights.len(); - assert!(n >= 2, "must have at least 2 vertices"); - assert_eq!( - requirements.len(), - n, - "requirements matrix must have same size as edge_weights" - ); + if !(n >= 2) { + return Err("must have at least 2 vertices".into()); + } + if requirements.len() != n { + return Err("requirements matrix must have same size as edge_weights".into()); + } for (i, row) in edge_weights.iter().enumerate() { - assert_eq!( - row.len(), - n, - "edge_weights must be square: row {i} has length {} but expected {n}", - row.len() - ); - assert_eq!( - row[i], 0, - "diagonal of edge_weights must be zero: edge_weights[{i}][{i}] = {}", - row[i] - ); + if row.len() != n { + return Err(format!( + "edge_weights must be square: row {i} has length {} but expected {n}", + row.len() + ) + .into()); + } + if row[i] != 0 { + return Err(format!( + "diagonal of edge_weights must be zero: edge_weights[{i}][{i}] = {}", + row[i] + ) + .into()); + } } for (i, row) in requirements.iter().enumerate() { - assert_eq!( - row.len(), - n, - "requirements must be square: row {i} has length {} but expected {n}", - row.len() - ); - assert_eq!( - row[i], 0, - "diagonal of requirements must be zero: requirements[{i}][{i}] = {}", - row[i] - ); + if row.len() != n { + return Err(format!( + "requirements must be square: row {i} has length {} but expected {n}", + row.len() + ) + .into()); + } + if row[i] != 0 { + return Err(format!( + "diagonal of requirements must be zero: requirements[{i}][{i}] = {}", + row[i] + ) + .into()); + } } // Check symmetry and non-negativity for i in 0..n { for j in (i + 1)..n { - assert_eq!( - edge_weights[i][j], edge_weights[j][i], - "edge_weights must be symmetric: w[{i}][{j}]={} != w[{j}][{i}]={}", - edge_weights[i][j], edge_weights[j][i] - ); - assert!( - edge_weights[i][j] >= 0, - "edge_weights must be non-negative: w[{i}][{j}]={}", - edge_weights[i][j] - ); - assert_eq!( - requirements[i][j], requirements[j][i], - "requirements must be symmetric: r[{i}][{j}]={} != r[{j}][{i}]={}", - requirements[i][j], requirements[j][i] - ); - assert!( - requirements[i][j] >= 0, - "requirements must be non-negative: r[{i}][{j}]={}", - requirements[i][j] - ); + if edge_weights[i][j] != edge_weights[j][i] { + return Err(format!( + "edge_weights must be symmetric: w[{i}][{j}]={} != w[{j}][{i}]={}", + edge_weights[i][j], edge_weights[j][i] + ) + .into()); + } + if !(edge_weights[i][j] >= 0) { + return Err(format!( + "edge_weights must be non-negative: w[{i}][{j}]={}", + edge_weights[i][j] + ) + .into()); + } + if requirements[i][j] != requirements[j][i] { + return Err(format!( + "requirements must be symmetric: r[{i}][{j}]={} != r[{j}][{i}]={}", + requirements[i][j], requirements[j][i] + ) + .into()); + } + if !(requirements[i][j] >= 0) { + return Err(format!( + "requirements must be non-negative: r[{i}][{j}]={}", + requirements[i][j] + ) + .into()); + } } } - Self { + Ok(Self { num_vertices: n, edge_weights, requirements, - } + }) } /// Returns the number of vertices. diff --git a/src/models/misc/paintshop.rs b/src/models/misc/paintshop.rs index 3524e7bb1..470e15ba3 100644 --- a/src/models/misc/paintshop.rs +++ b/src/models/misc/paintshop.rs @@ -50,6 +50,7 @@ inventory::submit! { /// } /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "PaintShopData")] pub struct PaintShop { /// The sequence of car labels (as indices into unique cars). sequence_indices: Vec, @@ -61,42 +62,72 @@ pub struct PaintShop { num_cars: usize, } +#[derive(Deserialize)] +struct PaintShopData { + sequence_indices: Vec, + car_labels: Vec, +} + +impl TryFrom for PaintShop { + type Error = crate::registry::ConstructionError; + + fn try_from(data: PaintShopData) -> Result { + let sequence = data + .sequence_indices + .into_iter() + .map(|index| { + data.car_labels.get(index).ok_or_else(|| { + crate::registry::ConstructionError::from(format!( + "car index {index} is outside car_labels" + )) + }) + }) + .collect::, _>>()?; + Self::try_new(sequence) + } +} + impl PaintShop { /// Create a new Paint Shop problem from string labels. /// /// Each element in the sequence must appear exactly twice. pub fn new>(sequence: Vec) -> Self { - let sequence: Vec = sequence.iter().map(|s| s.as_ref().to_string()).collect(); - Self::from_strings(sequence) + Self::try_new(sequence).unwrap_or_else(|error| panic!("{error}")) } - /// Create from a vector of strings. - pub fn from_strings(sequence: Vec) -> Self { + fn try_new>( + sequence: Vec, + ) -> Result { // Build car-to-index mapping and count occurrences - let mut car_count: HashMap = HashMap::new(); - let mut car_to_index: HashMap = HashMap::new(); + let mut car_count: HashMap<&str, usize> = HashMap::new(); + let mut car_to_index: HashMap<&str, usize> = HashMap::new(); let mut car_labels: Vec = Vec::new(); for item in &sequence { - let count = car_count.entry(item.clone()).or_insert(0); + let item = item.as_ref(); + let count = car_count.entry(item).or_insert(0); if *count == 0 { - car_to_index.insert(item.clone(), car_labels.len()); - car_labels.push(item.clone()); + car_to_index.insert(item, car_labels.len()); + car_labels.push(item.to_owned()); } *count += 1; } // Verify each car appears exactly twice for (car, count) in &car_count { - assert_eq!( - *count, 2, - "Each car must appear exactly twice, but '{}' appears {} times", - car, count - ); + if *count != 2 { + return Err(format!( + "each car must appear exactly twice, but '{car}' appears {count} times" + ) + .into()); + } } // Convert sequence to indices - let sequence_indices: Vec = sequence.iter().map(|item| car_to_index[item]).collect(); + let sequence_indices: Vec = sequence + .iter() + .map(|item| car_to_index[item.as_ref()]) + .collect(); // Determine which positions are first occurrences let mut seen: HashSet = HashSet::new(); @@ -107,12 +138,12 @@ impl PaintShop { let num_cars = car_labels.len(); - Self { + Ok(Self { sequence_indices, car_labels, is_first, num_cars, - } + }) } /// Get the sequence length. @@ -183,6 +214,11 @@ impl PaintShop { ) }) } + + /// Create from a vector of strings. + pub fn from_strings(sequence: Vec) -> Self { + Self::new(sequence) + } } /// Count color switches in a painted sequence. diff --git a/src/models/misc/partially_ordered_knapsack.rs b/src/models/misc/partially_ordered_knapsack.rs index 375a82d22..3170ce153 100644 --- a/src/models/misc/partially_ordered_knapsack.rs +++ b/src/models/misc/partially_ordered_knapsack.rs @@ -127,12 +127,7 @@ impl TryFrom for PartiallyOrderedKnapsack { { return Err(format!("precedences contain a cycle involving item {item}").into()); } - Ok(Self::new( - spec.weights, - spec.values, - precedences, - spec.capacity, - )) + Self::try_new(spec.weights, spec.values, precedences, spec.capacity) } } @@ -151,12 +146,8 @@ impl Serialize for PartiallyOrderedKnapsack { impl<'de> Deserialize<'de> for PartiallyOrderedKnapsack { fn deserialize>(deserializer: D) -> Result { let raw = PartiallyOrderedKnapsackRaw::deserialize(deserializer)?; - Ok(Self::new( - raw.weights, - raw.values, - raw.precedences, - raw.capacity, - )) + Self::try_new(raw.weights, raw.values, raw.precedences, raw.capacity) + .map_err(serde::de::Error::custom) } } @@ -179,38 +170,55 @@ impl PartiallyOrderedKnapsack { precedences: Vec<(usize, usize)>, capacity: i64, ) -> Self { - assert_eq!( - weights.len(), - values.len(), - "weights and values must have the same length" - ); - assert!(capacity >= 0, "capacity must be non-negative"); + Self::try_new(weights, values, precedences, capacity) + .unwrap_or_else(|error| panic!("{error}")) + } + + fn try_new( + weights: Vec, + values: Vec, + precedences: Vec<(usize, usize)>, + capacity: i64, + ) -> Result { + if weights.len() != values.len() { + return Err("weights and values must have the same length".into()); + }; + if !(capacity >= 0) { + return Err("capacity must be non-negative".into()); + }; for (i, &w) in weights.iter().enumerate() { - assert!(w >= 0, "weight[{i}] must be non-negative, got {w}"); + if !(w >= 0) { + return Err(format!("weight[{i}] must be non-negative, got {w}").into()); + }; } for (i, &v) in values.iter().enumerate() { - assert!(v >= 0, "value[{i}] must be non-negative, got {v}"); + if !(v >= 0) { + return Err(format!("value[{i}] must be non-negative, got {v}").into()); + }; } let n = weights.len(); for &(a, b) in &precedences { - assert!(a < n, "precedence index {a} out of bounds (n={n})"); - assert!(b < n, "precedence index {b} out of bounds (n={n})"); + if !(a < n) { + return Err(format!("precedence index {a} out of bounds (n={n})").into()); + }; + if !(b < n) { + return Err(format!("precedence index {b} out of bounds (n={n})").into()); + }; } let predecessors = Self::compute_predecessors(&precedences, n); // Check for cycles: if any item is its own transitive predecessor, the DAG has a cycle for (i, preds) in predecessors.iter().enumerate() { - assert!( - !preds.contains(&i), - "precedences contain a cycle involving item {i}" - ); + if !(!preds.contains(&i)) { + return Err(format!("precedences contain a cycle involving item {i}").into()); + }; } - Self { + Ok(Self { weights, values, precedences, capacity, predecessors, - } + }) } /// Compute transitive predecessors for each item via Floyd-Warshall. diff --git a/src/models/misc/precedence_constrained_scheduling.rs b/src/models/misc/precedence_constrained_scheduling.rs index bb60830be..9380bece8 100644 --- a/src/models/misc/precedence_constrained_scheduling.rs +++ b/src/models/misc/precedence_constrained_scheduling.rs @@ -47,6 +47,7 @@ inventory::submit! { /// assert!(solution.is_some()); /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "PrecedenceConstrainedSchedulingData")] pub struct PrecedenceConstrainedScheduling { num_tasks: usize, num_processors: usize, @@ -54,6 +55,26 @@ pub struct PrecedenceConstrainedScheduling { precedences: Vec<(usize, usize)>, } +#[derive(Deserialize)] +struct PrecedenceConstrainedSchedulingData { + num_tasks: usize, + num_processors: usize, + deadline: i64, + precedences: Vec<(usize, usize)>, +} + +impl TryFrom for PrecedenceConstrainedScheduling { + type Error = crate::registry::ConstructionError; + fn try_from(data: PrecedenceConstrainedSchedulingData) -> Result { + Self::try_new( + data.num_tasks, + data.num_processors, + data.deadline, + data.precedences, + ) + } +} + #[derive(Debug, Deserialize, crate::CreateSpec)] struct PrecedenceConstrainedSchedulingCreateSpec { num_tasks: usize, @@ -90,12 +111,12 @@ impl TryFrom for PrecedenceConstraine ) .into()); } - Ok(Self::new( + Self::try_new( spec.num_tasks, spec.num_processors, spec.deadline, precedences, - )) + ) } } @@ -112,29 +133,42 @@ impl PrecedenceConstrainedScheduling { deadline: i64, precedences: Vec<(usize, usize)>, ) -> Self { + Self::try_new(num_tasks, num_processors, deadline, precedences) + .unwrap_or_else(|error| panic!("{error}")) + } + + fn try_new( + num_tasks: usize, + num_processors: usize, + deadline: i64, + precedences: Vec<(usize, usize)>, + ) -> Result { if num_tasks > 0 { - assert!( - num_processors > 0, - "num_processors must be > 0 when there are tasks" - ); - assert!(deadline > 0, "deadline must be > 0 when there are tasks"); + if num_processors == 0 { + return Err("num_processors must be > 0 when there are tasks".into()); + } + if deadline <= 0 { + return Err("deadline must be > 0 when there are tasks".into()); + } + } + if !(deadline >= 0) { + return Err("deadline must be nonnegative".into()); } - assert!(deadline >= 0, "deadline must be nonnegative"); for &(i, j) in &precedences { - assert!( - i < num_tasks && j < num_tasks, - "Precedence ({}, {}) out of bounds for {} tasks", - i, - j, - num_tasks - ); + if !(i < num_tasks && j < num_tasks) { + return Err(format!( + "Precedence ({}, {}) out of bounds for {} tasks", + i, j, num_tasks + ) + .into()); + } } - Self { + Ok(Self { num_tasks, num_processors, deadline, precedences, - } + }) } /// Get the number of tasks. diff --git a/src/models/misc/production_planning.rs b/src/models/misc/production_planning.rs index 0b7d9ce85..8f1af36f3 100644 --- a/src/models/misc/production_planning.rs +++ b/src/models/misc/production_planning.rs @@ -24,8 +24,8 @@ inventory::submit! { } #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "ProductionPlanningCreateSpec")] pub struct ProductionPlanning { - #[serde(deserialize_with = "positive_usize::deserialize")] num_periods: usize, demands: Vec, capacities: Vec, @@ -74,7 +74,7 @@ impl TryFrom for ProductionPlanning { if spec.capacities.iter().any(|&capacity| capacity < 0) { return Err("capacities must be nonnegative".into()); } - Ok(Self::new( + Self::try_new( spec.num_periods, spec.demands, spec.capacities, @@ -82,7 +82,7 @@ impl TryFrom for ProductionPlanning { spec.production_costs, spec.inventory_costs, spec.cost_bound, - )) + ) } } @@ -96,7 +96,30 @@ impl ProductionPlanning { inventory_costs: Vec, cost_bound: i64, ) -> Self { - assert!(num_periods > 0, "num_periods must be positive"); + Self::try_new( + num_periods, + demands, + capacities, + setup_costs, + production_costs, + inventory_costs, + cost_bound, + ) + .unwrap_or_else(|error| panic!("{error}")) + } + + fn try_new( + num_periods: usize, + demands: Vec, + capacities: Vec, + setup_costs: Vec, + production_costs: Vec, + inventory_costs: Vec, + cost_bound: i64, + ) -> Result { + if num_periods == 0 { + return Err("num_periods must be positive".into()); + } for len in [ demands.len(), capacities.len(), @@ -104,24 +127,25 @@ impl ProductionPlanning { production_costs.len(), inventory_costs.len(), ] { - assert_eq!( - len, num_periods, - "all per-period vectors must have length num_periods" - ); + if len != num_periods { + return Err("all per-period vectors must have length num_periods".into()); + } } - assert!( - demands - .iter() - .chain(&capacities) - .chain(&setup_costs) - .chain(&production_costs) - .chain(&inventory_costs) - .all(|&value| value >= 0), - "demands, capacities, and costs must be nonnegative" - ); - assert!(cost_bound >= 0, "cost bound must be nonnegative"); - - Self { + if !(demands + .iter() + .chain(&capacities) + .chain(&setup_costs) + .chain(&production_costs) + .chain(&inventory_costs) + .all(|&value| value >= 0)) + { + return Err("demands, capacities, and costs must be nonnegative".into()); + } + if !(cost_bound >= 0) { + return Err("cost bound must be nonnegative".into()); + } + + Ok(Self { num_periods, demands, capacities, @@ -129,7 +153,7 @@ impl ProductionPlanning { production_costs, inventory_costs, cost_bound, - } + }) } pub fn num_periods(&self) -> usize { @@ -306,22 +330,6 @@ pub(crate) fn canonical_model_example_specs() -> Vec(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - let value = usize::deserialize(deserializer)?; - if value == 0 { - return Err(D::Error::custom("expected positive integer, got 0")); - } - Ok(value) - } -} - #[cfg(test)] #[path = "../../unit_tests/models/misc/production_planning.rs"] mod tests; diff --git a/src/models/misc/rectilinear_picture_compression.rs b/src/models/misc/rectilinear_picture_compression.rs index 3266e3a39..22ef20b3b 100644 --- a/src/models/misc/rectilinear_picture_compression.rs +++ b/src/models/misc/rectilinear_picture_compression.rs @@ -78,7 +78,7 @@ impl<'de> Deserialize<'de> for RectilinearPictureCompression { bound: i64, } let inner = Inner::deserialize(deserializer)?; - Ok(Self::new(inner.matrix, inner.bound)) + Self::try_new(inner.matrix, inner.bound).map_err(serde::de::Error::custom) } } @@ -89,20 +89,30 @@ impl RectilinearPictureCompression { /// /// Panics if `matrix` is empty or has inconsistent row lengths. pub fn new(matrix: Vec>, bound: i64) -> Self { - assert!(!matrix.is_empty(), "Matrix must not be empty"); + Self::try_new(matrix, bound).unwrap_or_else(|error| panic!("{error}")) + } + + fn try_new( + matrix: Vec>, + bound: i64, + ) -> Result { + if matrix.is_empty() { + return Err("Matrix must not be empty".into()); + }; let cols = matrix[0].len(); - assert!(cols > 0, "Matrix must have at least one column"); - assert!( - matrix.iter().all(|row| row.len() == cols), - "All rows must have the same length" - ); + if !(cols > 0) { + return Err("Matrix must have at least one column".into()); + }; + if !(matrix.iter().all(|row| row.len() == cols)) { + return Err("All rows must have the same length".into()); + }; let mut instance = Self { matrix, bound, maximal_rects: Vec::new(), }; instance.maximal_rects = instance.compute_maximal_rectangles(); - instance + Ok(instance) } /// Returns the number of rows in the matrix. diff --git a/src/models/misc/register_sufficiency.rs b/src/models/misc/register_sufficiency.rs index d478fb965..1769be996 100644 --- a/src/models/misc/register_sufficiency.rs +++ b/src/models/misc/register_sufficiency.rs @@ -56,6 +56,7 @@ inventory::submit! { /// assert!(solution.is_some()); /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "RegisterSufficiencyData")] pub struct RegisterSufficiency { /// Number of vertices. num_vertices: usize, @@ -65,6 +66,21 @@ pub struct RegisterSufficiency { bound: usize, } +#[derive(Deserialize)] +struct RegisterSufficiencyData { + num_vertices: usize, + arcs: Vec<(usize, usize)>, + bound: usize, +} + +impl TryFrom for RegisterSufficiency { + type Error = crate::registry::ConstructionError; + + fn try_from(data: RegisterSufficiencyData) -> Result { + Self::try_new(data.num_vertices, data.arcs, data.bound) + } +} + impl RegisterSufficiency { /// Create a new Register Sufficiency instance. /// @@ -73,21 +89,31 @@ impl RegisterSufficiency { /// Panics if any arc index is out of bounds (>= num_vertices), /// or if any arc is a self-loop. pub fn new(num_vertices: usize, arcs: Vec<(usize, usize)>, bound: usize) -> Self { + Self::try_new(num_vertices, arcs, bound).unwrap_or_else(|error| panic!("{error}")) + } + + fn try_new( + num_vertices: usize, + arcs: Vec<(usize, usize)>, + bound: usize, + ) -> Result { for &(v, u) in &arcs { - assert!( - v < num_vertices && u < num_vertices, - "Arc ({}, {}) out of bounds for {} vertices", - v, - u, - num_vertices - ); - assert!(v != u, "Self-loop ({}, {}) not allowed in a DAG", v, u); + if !(v < num_vertices && u < num_vertices) { + return Err(format!( + "Arc ({}, {}) out of bounds for {} vertices", + v, u, num_vertices + ) + .into()); + } + if v == u { + return Err(format!("Self-loop ({}, {}) not allowed in a DAG", v, u).into()); + } } - Self { + Ok(Self { num_vertices, arcs, bound, - } + }) } /// Get the number of vertices. diff --git a/src/models/misc/scheduling_with_individual_deadlines.rs b/src/models/misc/scheduling_with_individual_deadlines.rs index 0dc746fa5..065055559 100644 --- a/src/models/misc/scheduling_with_individual_deadlines.rs +++ b/src/models/misc/scheduling_with_individual_deadlines.rs @@ -29,6 +29,7 @@ inventory::submit! { /// satisfies `sigma(u) + 1 <= sigma(v)` and no time slot hosts more than /// `num_processors` tasks. #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "SchedulingWithIndividualDeadlinesData")] pub struct SchedulingWithIndividualDeadlines { num_tasks: usize, num_processors: usize, @@ -36,6 +37,26 @@ pub struct SchedulingWithIndividualDeadlines { precedences: Vec<(usize, usize)>, } +#[derive(Deserialize)] +struct SchedulingWithIndividualDeadlinesData { + num_tasks: usize, + num_processors: usize, + deadlines: Vec, + precedences: Vec<(usize, usize)>, +} + +impl TryFrom for SchedulingWithIndividualDeadlines { + type Error = crate::registry::ConstructionError; + fn try_from(data: SchedulingWithIndividualDeadlinesData) -> Result { + Self::try_new( + data.num_tasks, + data.num_processors, + data.deadlines, + data.precedences, + ) + } +} + #[derive(Debug, Deserialize, crate::CreateSpec)] struct SchedulingWithIndividualDeadlinesCreateSpec { /// Number of tasks. @@ -72,12 +93,12 @@ impl TryFrom for SchedulingWithIndi ) .into()); } - Ok(Self::new( + Self::try_new( spec.num_tasks, spec.num_processors, spec.deadlines, precedences, - )) + ) } } @@ -88,36 +109,45 @@ impl SchedulingWithIndividualDeadlines { deadlines: Vec, precedences: Vec<(usize, usize)>, ) -> Self { - assert_eq!( - deadlines.len(), - num_tasks, - "deadlines length must equal num_tasks" - ); - assert!( - deadlines.iter().all(|&deadline| deadline >= 0), - "deadlines must be nonnegative" - ); + Self::try_new(num_tasks, num_processors, deadlines, precedences) + .unwrap_or_else(|error| panic!("{error}")) + } + + fn try_new( + num_tasks: usize, + num_processors: usize, + deadlines: Vec, + precedences: Vec<(usize, usize)>, + ) -> Result { + if deadlines.len() != num_tasks { + return Err("deadlines length must equal num_tasks".into()); + } + if !(deadlines.iter().all(|&deadline| deadline >= 0)) { + return Err("deadlines must be nonnegative".into()); + } for &(pred, succ) in &precedences { - assert!( - pred < num_tasks, - "predecessor index {} out of range (num_tasks = {})", - pred, - num_tasks - ); - assert!( - succ < num_tasks, - "successor index {} out of range (num_tasks = {})", - succ, - num_tasks - ); + if !(pred < num_tasks) { + return Err(format!( + "predecessor index {} out of range (num_tasks = {})", + pred, num_tasks + ) + .into()); + } + if !(succ < num_tasks) { + return Err(format!( + "successor index {} out of range (num_tasks = {})", + succ, num_tasks + ) + .into()); + } } - Self { + Ok(Self { num_tasks, num_processors, deadlines, precedences, - } + }) } pub fn num_tasks(&self) -> usize { diff --git a/src/models/misc/sequencing_to_minimize_weighted_tardiness.rs b/src/models/misc/sequencing_to_minimize_weighted_tardiness.rs index 076009147..0be845151 100644 --- a/src/models/misc/sequencing_to_minimize_weighted_tardiness.rs +++ b/src/models/misc/sequencing_to_minimize_weighted_tardiness.rs @@ -52,6 +52,7 @@ inventory::submit! { /// assert!(solver.solve(&problem).unwrap().is_some()); /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "SequencingToMinimizeWeightedTardinessCreateSpec")] pub struct SequencingToMinimizeWeightedTardiness { lengths: Vec, weights: Vec, @@ -103,35 +104,39 @@ impl SequencingToMinimizeWeightedTardiness { /// /// Panics if the input vectors do not have the same length. pub fn new(lengths: Vec, weights: Vec, deadlines: Vec, bound: i64) -> Self { - assert_eq!( - lengths.len(), - weights.len(), - "weights length must equal lengths length" - ); - assert_eq!( - lengths.len(), - deadlines.len(), - "deadlines length must equal lengths length" - ); - assert!( - lengths.iter().all(|&length| length >= 0), - "task lengths must be nonnegative" - ); - assert!( - weights.iter().all(|&weight| weight >= 0), - "task weights must be nonnegative" - ); - assert!( - deadlines.iter().all(|&deadline| deadline >= 0), - "deadlines must be nonnegative" - ); - assert!(bound >= 0, "bound must be nonnegative"); - Self { + Self::try_new(lengths, weights, deadlines, bound).unwrap_or_else(|error| panic!("{error}")) + } + + fn try_new( + lengths: Vec, + weights: Vec, + deadlines: Vec, + bound: i64, + ) -> Result { + if lengths.len() != weights.len() { + return Err("weights length must equal lengths length".into()); + } + if lengths.len() != deadlines.len() { + return Err("deadlines length must equal lengths length".into()); + } + if !(lengths.iter().all(|&length| length >= 0)) { + return Err("task lengths must be nonnegative".into()); + } + if !(weights.iter().all(|&weight| weight >= 0)) { + return Err("task weights must be nonnegative".into()); + } + if !(deadlines.iter().all(|&deadline| deadline >= 0)) { + return Err("deadlines must be nonnegative".into()); + } + if !(bound >= 0) { + return Err("bound must be nonnegative".into()); + } + Ok(Self { lengths, weights, deadlines, bound, - } + }) } /// Returns the job lengths. 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 af6a27405..b96aa1a31 100644 --- a/src/models/misc/sequencing_with_release_times_and_deadlines.rs +++ b/src/models/misc/sequencing_with_release_times_and_deadlines.rs @@ -57,12 +57,29 @@ inventory::submit! { /// assert!(solution.is_some()); /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "SequencingWithReleaseTimesAndDeadlinesData")] pub struct SequencingWithReleaseTimesAndDeadlines { lengths: Vec, release_times: Vec, deadlines: Vec, } +#[derive(Deserialize)] +struct SequencingWithReleaseTimesAndDeadlinesData { + lengths: Vec, + release_times: Vec, + deadlines: Vec, +} + +impl TryFrom + for SequencingWithReleaseTimesAndDeadlines +{ + type Error = crate::registry::ConstructionError; + fn try_from(data: SequencingWithReleaseTimesAndDeadlinesData) -> Result { + Self::try_new(data.lengths, data.release_times, data.deadlines) + } +} + impl SequencingWithReleaseTimesAndDeadlines { /// Create a new instance. /// @@ -70,25 +87,34 @@ impl SequencingWithReleaseTimesAndDeadlines { /// /// Panics if the three vectors have different lengths. pub fn new(lengths: Vec, release_times: Vec, deadlines: Vec) -> Self { - assert_eq!(lengths.len(), release_times.len()); - assert_eq!(lengths.len(), deadlines.len()); - assert!( - lengths.iter().all(|&length| length >= 0), - "task lengths must be nonnegative" - ); - assert!( - release_times.iter().all(|&release| release >= 0), - "release times must be nonnegative" - ); - assert!( - deadlines.iter().all(|&deadline| deadline >= 0), - "deadlines must be nonnegative" - ); - Self { + Self::try_new(lengths, release_times, deadlines).unwrap_or_else(|error| panic!("{error}")) + } + + fn try_new( + lengths: Vec, + release_times: Vec, + deadlines: Vec, + ) -> Result { + if lengths.len() != release_times.len() { + return Err("lengths and release_times must have the same length".into()); + } + if lengths.len() != deadlines.len() { + return Err("lengths and deadlines must have the same length".into()); + } + if !(lengths.iter().all(|&length| length >= 0)) { + return Err("task lengths must be nonnegative".into()); + } + if !(release_times.iter().all(|&release| release >= 0)) { + return Err("release times must be nonnegative".into()); + } + if !(deadlines.iter().all(|&deadline| deadline >= 0)) { + return Err("deadlines must be nonnegative".into()); + } + Ok(Self { lengths, release_times, deadlines, - } + }) } /// Returns the processing times. diff --git a/src/models/misc/shortest_common_supersequence.rs b/src/models/misc/shortest_common_supersequence.rs index e15e8d169..e91462df9 100644 --- a/src/models/misc/shortest_common_supersequence.rs +++ b/src/models/misc/shortest_common_supersequence.rs @@ -55,12 +55,27 @@ inventory::submit! { /// assert!(solution.is_some()); /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "ShortestCommonSupersequenceData")] pub struct ShortestCommonSupersequence { alphabet_size: usize, strings: Vec>, max_length: usize, } +#[derive(Deserialize)] +struct ShortestCommonSupersequenceData { + alphabet_size: usize, + strings: Vec>, +} + +impl TryFrom for ShortestCommonSupersequence { + type Error = crate::registry::ConstructionError; + + fn try_from(data: ShortestCommonSupersequenceData) -> Result { + Self::try_new(data.alphabet_size, data.strings) + } +} + #[derive(Debug, Deserialize, crate::CreateSpec)] struct ShortestCommonSupersequenceCreateSpec { /// Input strings; the alphabet and maximum length are inferred from them. @@ -114,17 +129,29 @@ impl ShortestCommonSupersequence { /// Panics if `strings` is empty, or if `alphabet_size` is 0 and any input /// string is non-empty. pub fn new(alphabet_size: usize, strings: Vec>) -> Self { - assert!(!strings.is_empty(), "must have at least one string"); - let max_length: usize = strings.iter().map(|s| s.len()).sum(); - assert!( - alphabet_size > 0 || strings.iter().all(|s| s.is_empty()), - "alphabet_size must be > 0 when any input string is non-empty" - ); - Self { + Self::try_new(alphabet_size, strings).unwrap_or_else(|error| panic!("{error}")) + } + + fn try_new( + alphabet_size: usize, + strings: Vec>, + ) -> Result { + if strings.is_empty() { + return Err("must have at least one string".into()); + } + let max_length = strings.iter().try_fold(0usize, |total, string| { + total + .checked_add(string.len()) + .ok_or("maximum string length overflows usize") + })?; + if !(alphabet_size > 0 || strings.iter().all(|s| s.is_empty())) { + return Err("alphabet_size must be > 0 when any input string is non-empty".into()); + } + Ok(Self { alphabet_size, strings, max_length, - } + }) } /// Returns the alphabet size. diff --git a/src/models/misc/shortest_common_superstring.rs b/src/models/misc/shortest_common_superstring.rs index 00b98217d..9f00fabc7 100644 --- a/src/models/misc/shortest_common_superstring.rs +++ b/src/models/misc/shortest_common_superstring.rs @@ -63,12 +63,27 @@ inventory::submit! { /// assert!(solution.is_some()); /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "ShortestCommonSuperstringData")] pub struct ShortestCommonSuperstring { alphabet_size: usize, strings: Vec>, max_length: usize, } +#[derive(Deserialize)] +struct ShortestCommonSuperstringData { + alphabet_size: usize, + strings: Vec>, +} + +impl TryFrom for ShortestCommonSuperstring { + type Error = crate::registry::ConstructionError; + + fn try_from(data: ShortestCommonSuperstringData) -> Result { + Self::try_new(data.alphabet_size, data.strings) + } +} + impl ShortestCommonSuperstring { /// Create a new ShortestCommonSuperstring instance. /// @@ -80,17 +95,29 @@ impl ShortestCommonSuperstring { /// Panics if `strings` is empty, or if `alphabet_size` is 0 and any input /// string is non-empty. pub fn new(alphabet_size: usize, strings: Vec>) -> Self { - assert!(!strings.is_empty(), "must have at least one string"); - let max_length: usize = strings.iter().map(|s| s.len()).sum(); - assert!( - alphabet_size > 0 || strings.iter().all(|s| s.is_empty()), - "alphabet_size must be > 0 when any input string is non-empty" - ); - Self { + Self::try_new(alphabet_size, strings).unwrap_or_else(|error| panic!("{error}")) + } + + fn try_new( + alphabet_size: usize, + strings: Vec>, + ) -> Result { + if strings.is_empty() { + return Err("must have at least one string".into()); + } + let max_length = strings.iter().try_fold(0usize, |total, string| { + total + .checked_add(string.len()) + .ok_or("maximum string length overflows usize") + })?; + if !(alphabet_size > 0 || strings.iter().all(|s| s.is_empty())) { + return Err("alphabet_size must be > 0 when any input string is non-empty".into()); + } + Ok(Self { alphabet_size, strings, max_length, - } + }) } /// Returns the alphabet size. diff --git a/src/models/misc/staff_scheduling.rs b/src/models/misc/staff_scheduling.rs index ce658db02..b41a3d3a3 100644 --- a/src/models/misc/staff_scheduling.rs +++ b/src/models/misc/staff_scheduling.rs @@ -27,6 +27,7 @@ inventory::submit! { /// pattern. A configuration is satisfying iff the total assigned workers does /// not exceed `num_workers` and every period's staffing requirement is met. #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "StaffSchedulingData")] pub struct StaffScheduling { shifts_per_schedule: usize, schedules: Vec>, @@ -34,6 +35,27 @@ pub struct StaffScheduling { num_workers: i64, } +#[derive(Deserialize)] +struct StaffSchedulingData { + shifts_per_schedule: usize, + schedules: Vec>, + requirements: Vec, + num_workers: i64, +} + +impl TryFrom for StaffScheduling { + type Error = crate::registry::ConstructionError; + + fn try_from(data: StaffSchedulingData) -> Result { + Self::try_new( + data.shifts_per_schedule, + data.schedules, + data.requirements, + data.num_workers, + ) + } +} + #[derive(Debug, Deserialize, crate::CreateSpec)] struct StaffSchedulingCreateSpec { /// Required number of active periods in each schedule pattern. @@ -71,12 +93,7 @@ impl TryFrom for StaffScheduling { .into()); } } - Ok(Self::new( - spec.k, - spec.schedules, - spec.requirements, - spec.num_workers, - )) + Self::try_new(spec.k, spec.schedules, spec.requirements, spec.num_workers) } } @@ -95,32 +112,47 @@ impl StaffScheduling { requirements: Vec, num_workers: i64, ) -> Self { - assert!(num_workers >= 0, "num_workers must be nonnegative"); + Self::try_new(shifts_per_schedule, schedules, requirements, num_workers) + .unwrap_or_else(|error| panic!("{error}")) + } + + fn try_new( + shifts_per_schedule: usize, + schedules: Vec>, + requirements: Vec, + num_workers: i64, + ) -> Result { + if !(num_workers >= 0) { + return Err("num_workers must be nonnegative".into()); + } let num_periods = requirements.len(); for (index, schedule) in schedules.iter().enumerate() { - assert_eq!( - schedule.len(), - num_periods, - "schedule {} has {} periods, expected {}", - index, - schedule.len(), - num_periods - ); + if schedule.len() != num_periods { + return Err(format!( + "schedule {} has {} periods, expected {}", + index, + schedule.len(), + num_periods + ) + .into()); + } let ones = schedule.iter().filter(|&&active| active).count(); - assert_eq!( - ones, shifts_per_schedule, - "schedule {} has {} active periods, expected {}", - index, ones, shifts_per_schedule - ); + if ones != shifts_per_schedule { + return Err(format!( + "schedule {} has {} active periods, expected {}", + index, ones, shifts_per_schedule + ) + .into()); + } } - Self { + Ok(Self { shifts_per_schedule, schedules, requirements, num_workers, - } + }) } /// Get the number of periods. diff --git a/src/models/misc/string_to_string_correction.rs b/src/models/misc/string_to_string_correction.rs index 965b13120..e14395ec5 100644 --- a/src/models/misc/string_to_string_correction.rs +++ b/src/models/misc/string_to_string_correction.rs @@ -66,6 +66,7 @@ inventory::submit! { /// assert!(solution.is_some()); /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "StringToStringCorrectionData")] pub struct StringToStringCorrection { alphabet_size: usize, source: Vec, @@ -73,6 +74,22 @@ pub struct StringToStringCorrection { bound: usize, } +#[derive(Deserialize)] +struct StringToStringCorrectionData { + alphabet_size: usize, + source: Vec, + target: Vec, + bound: usize, +} + +impl TryFrom for StringToStringCorrection { + type Error = crate::registry::ConstructionError; + + fn try_from(data: StringToStringCorrectionData) -> Result { + Self::try_new(data.alphabet_size, data.source, data.target, data.bound) + } +} + #[derive(Debug, Deserialize, crate::CreateSpec)] struct StringToStringCorrectionCreateSpec { /// Optional alphabet size; omitted values are inferred from both strings. @@ -133,24 +150,31 @@ impl StringToStringCorrection { /// non-empty, or if any symbol in `source` or `target` is /// `>= alphabet_size`. pub fn new(alphabet_size: usize, source: Vec, target: Vec, bound: usize) -> Self { - assert!( - alphabet_size > 0 || (source.is_empty() && target.is_empty()), - "alphabet_size must be > 0 when source or target is non-empty" - ); - assert!( - source.iter().all(|&s| s < alphabet_size), - "all source symbols must be < alphabet_size" - ); - assert!( - target.iter().all(|&s| s < alphabet_size), - "all target symbols must be < alphabet_size" - ); - Self { + Self::try_new(alphabet_size, source, target, bound) + .unwrap_or_else(|error| panic!("{error}")) + } + + fn try_new( + alphabet_size: usize, + source: Vec, + target: Vec, + bound: usize, + ) -> Result { + if !(alphabet_size > 0 || (source.is_empty() && target.is_empty())) { + return Err("alphabet_size must be > 0 when source or target is non-empty".into()); + } + if !(source.iter().all(|&s| s < alphabet_size)) { + return Err("all source symbols must be < alphabet_size".into()); + } + if !(target.iter().all(|&s| s < alphabet_size)) { + return Err("all target symbols must be < alphabet_size".into()); + } + Ok(Self { alphabet_size, source, target, bound, - } + }) } /// Returns the alphabet size. diff --git a/src/models/misc/subset_product.rs b/src/models/misc/subset_product.rs index 06f5aa938..1eb642982 100644 --- a/src/models/misc/subset_product.rs +++ b/src/models/misc/subset_product.rs @@ -51,6 +51,7 @@ inventory::submit! { /// assert!(solution.is_some()); /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "SubsetProductData")] pub struct SubsetProduct { #[serde(with = "super::biguint_serde::decimal_biguint_vec")] sizes: Vec, @@ -58,6 +59,30 @@ pub struct SubsetProduct { target: BigUint, } +#[derive(Deserialize)] +struct SubsetProductData { + #[serde(with = "super::biguint_serde::decimal_biguint_vec")] + sizes: Vec, + #[serde(with = "super::biguint_serde::decimal_biguint")] + target: BigUint, +} + +impl TryFrom for SubsetProduct { + type Error = crate::registry::ConstructionError; + fn try_from(data: SubsetProductData) -> Result { + if data.sizes.iter().any(BigUint::is_zero) { + return Err("all sizes must be positive (> 0)".into()); + } + if data.target.is_zero() { + return Err("SubsetProduct target must be positive".into()); + } + Ok(Self { + sizes: data.sizes, + target: data.target, + }) + } +} + impl SubsetProduct { /// Create a new SubsetProduct instance. /// @@ -69,19 +94,22 @@ impl SubsetProduct { S: ToBigUint, T: ToBigUint, { - let sizes: Vec = sizes + Self::try_new(sizes, target).unwrap_or_else(|error| panic!("{error}")) + } + + fn try_new(sizes: Vec, target: T) -> Result + where + S: ToBigUint, + T: ToBigUint, + { + let sizes = sizes .into_iter() - .map(|s| s.to_biguint().expect("All sizes must be positive (> 0)")) - .collect(); - assert!( - sizes.iter().all(|s| !s.is_zero()), - "All sizes must be positive (> 0)" - ); + .map(|size| size.to_biguint().ok_or("all sizes must be positive (> 0)")) + .collect::, _>>()?; let target = target .to_biguint() - .expect("SubsetProduct target must be nonnegative"); - assert!(!target.is_zero(), "SubsetProduct target must be positive"); - Self { sizes, target } + .ok_or("SubsetProduct target must be nonnegative")?; + SubsetProductData { sizes, target }.try_into() } /// Create a SubsetProduct without validating sizes (for testing edge cases). diff --git a/src/models/misc/subset_sum.rs b/src/models/misc/subset_sum.rs index 5a8d771c2..3d7f302e7 100644 --- a/src/models/misc/subset_sum.rs +++ b/src/models/misc/subset_sum.rs @@ -51,6 +51,7 @@ inventory::submit! { /// assert!(solution.is_some()); /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "SubsetSumData")] pub struct SubsetSum { #[serde(with = "super::biguint_serde::decimal_biguint_vec")] sizes: Vec, @@ -58,6 +59,27 @@ pub struct SubsetSum { target: BigUint, } +#[derive(Deserialize)] +struct SubsetSumData { + #[serde(with = "super::biguint_serde::decimal_biguint_vec")] + sizes: Vec, + #[serde(with = "super::biguint_serde::decimal_biguint")] + target: BigUint, +} + +impl TryFrom for SubsetSum { + type Error = crate::registry::ConstructionError; + fn try_from(data: SubsetSumData) -> Result { + if data.sizes.iter().any(BigUint::is_zero) { + return Err("all sizes must be positive (> 0)".into()); + } + Ok(Self { + sizes: data.sizes, + target: data.target, + }) + } +} + impl SubsetSum { /// Create a new SubsetSum instance. /// @@ -69,18 +91,22 @@ impl SubsetSum { S: ToBigUint, T: ToBigUint, { - let sizes: Vec = sizes + Self::try_new(sizes, target).unwrap_or_else(|error| panic!("{error}")) + } + + fn try_new(sizes: Vec, target: T) -> Result + where + S: ToBigUint, + T: ToBigUint, + { + let sizes = sizes .into_iter() - .map(|s| s.to_biguint().expect("All sizes must be positive (> 0)")) - .collect(); - assert!( - sizes.iter().all(|s| !s.is_zero()), - "All sizes must be positive (> 0)" - ); + .map(|size| size.to_biguint().ok_or("all sizes must be positive (> 0)")) + .collect::, _>>()?; let target = target .to_biguint() - .expect("SubsetSum target must be nonnegative"); - Self { sizes, target } + .ok_or("SubsetSum target must be nonnegative")?; + SubsetSumData { sizes, target }.try_into() } /// Create a new SubsetSum instance without validating sizes. diff --git a/src/models/misc/timetable_design.rs b/src/models/misc/timetable_design.rs index 44aa4dcec..40d9f934a 100644 --- a/src/models/misc/timetable_design.rs +++ b/src/models/misc/timetable_design.rs @@ -27,6 +27,7 @@ inventory::submit! { /// task-next, period-last order: /// `idx = ((c * num_tasks) + t) * num_periods + h`. #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "TimetableDesignCreateSpec")] pub struct TimetableDesign { num_periods: usize, num_craftsmen: usize, @@ -117,14 +118,14 @@ impl TryFrom for TimetableDesign { ) .into()); } - Ok(Self::new( + Self::try_new( spec.num_periods, spec.num_craftsmen, spec.num_tasks, spec.craftsman_avail, spec.task_avail, spec.requirements, - )) + ) } } @@ -142,68 +143,93 @@ impl TimetableDesign { task_avail: Vec>, requirements: Vec>, ) -> Self { - assert_eq!( - craftsman_avail.len(), + Self::try_new( + num_periods, num_craftsmen, - "craftsman_avail has {} rows, expected {}", - craftsman_avail.len(), - num_craftsmen - ); + num_tasks, + craftsman_avail, + task_avail, + requirements, + ) + .unwrap_or_else(|error| panic!("{error}")) + } + + fn try_new( + num_periods: usize, + num_craftsmen: usize, + num_tasks: usize, + craftsman_avail: Vec>, + task_avail: Vec>, + requirements: Vec>, + ) -> Result { + if craftsman_avail.len() != num_craftsmen { + return Err(format!( + "craftsman_avail has {} rows, expected {}", + craftsman_avail.len(), + num_craftsmen + ) + .into()); + } for (craftsman, row) in craftsman_avail.iter().enumerate() { - assert_eq!( - row.len(), - num_periods, - "craftsman {} availability has {} periods, expected {}", - craftsman, - row.len(), - num_periods - ); + if row.len() != num_periods { + return Err(format!( + "craftsman {} availability has {} periods, expected {}", + craftsman, + row.len(), + num_periods + ) + .into()); + } } - assert_eq!( - task_avail.len(), - num_tasks, - "task_avail has {} rows, expected {}", - task_avail.len(), - num_tasks - ); + if task_avail.len() != num_tasks { + return Err(format!( + "task_avail has {} rows, expected {}", + task_avail.len(), + num_tasks + ) + .into()); + } for (task, row) in task_avail.iter().enumerate() { - assert_eq!( - row.len(), - num_periods, - "task {} availability has {} periods, expected {}", - task, - row.len(), - num_periods - ); + if row.len() != num_periods { + return Err(format!( + "task {} availability has {} periods, expected {}", + task, + row.len(), + num_periods + ) + .into()); + } } - assert_eq!( - requirements.len(), - num_craftsmen, - "requirements has {} rows, expected {}", - requirements.len(), - num_craftsmen - ); + if requirements.len() != num_craftsmen { + return Err(format!( + "requirements has {} rows, expected {}", + requirements.len(), + num_craftsmen + ) + .into()); + } for (craftsman, row) in requirements.iter().enumerate() { - assert_eq!( - row.len(), - num_tasks, - "requirements row {} has {} tasks, expected {}", - craftsman, - row.len(), - num_tasks - ); + if row.len() != num_tasks { + return Err(format!( + "requirements row {} has {} tasks, expected {}", + craftsman, + row.len(), + num_tasks + ) + .into()); + } } - Self { + Ok(Self { num_periods, num_craftsmen, num_tasks, craftsman_avail, task_avail, requirements, - } + }) } /// Get the number of periods. diff --git a/src/models/set/consecutive_sets.rs b/src/models/set/consecutive_sets.rs index babec6e76..a15167f65 100644 --- a/src/models/set/consecutive_sets.rs +++ b/src/models/set/consecutive_sets.rs @@ -71,6 +71,7 @@ inventory::submit! { /// .unwrap()); /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "ConsecutiveSetsData")] pub struct ConsecutiveSets { /// Size of the alphabet (elements are 0..alphabet_size-1). alphabet_size: usize, @@ -80,6 +81,21 @@ pub struct ConsecutiveSets { bound_k: usize, } +#[derive(Deserialize)] +struct ConsecutiveSetsData { + alphabet_size: usize, + subsets: Vec>, + bound_k: usize, +} + +impl TryFrom for ConsecutiveSets { + type Error = crate::registry::ConstructionError; + + fn try_from(data: ConsecutiveSetsData) -> Result { + Self::try_new(data.alphabet_size, data.subsets, data.bound_k) + } +} + impl ConsecutiveSets { /// Create a new Consecutive Sets problem. /// @@ -88,31 +104,34 @@ impl ConsecutiveSets { /// Panics if `bound_k` is zero, if any subset contains duplicate elements, /// or if any element is outside the alphabet. pub fn new(alphabet_size: usize, subsets: Vec>, bound_k: usize) -> Self { - assert!(bound_k > 0, "bound_k must be positive, got 0"); - let mut subsets = subsets; - for (i, subset) in subsets.iter_mut().enumerate() { + Self::try_new(alphabet_size, subsets, bound_k).unwrap_or_else(|error| panic!("{error}")) + } + + fn try_new( + alphabet_size: usize, + mut subsets: Vec>, + bound_k: usize, + ) -> Result { + if bound_k == 0 { + return Err("bound_k must be positive, got 0".into()); + } + for (index, subset) in subsets.iter_mut().enumerate() { let mut seen = HashSet::with_capacity(subset.len()); - for &elem in subset.iter() { - assert!( - elem < alphabet_size, - "Subset {} contains element {} which is outside alphabet of size {}", - i, - elem, - alphabet_size - ); - assert!( - seen.insert(elem), - "Subset {} contains duplicate elements", - i - ); + for &element in subset.iter() { + if element >= alphabet_size { + return Err(format!("subset {index} contains element {element} outside alphabet of size {alphabet_size}").into()); + } + if !seen.insert(element) { + return Err(format!("subset {index} contains duplicate elements").into()); + } } subset.sort(); } - Self { + Ok(Self { alphabet_size, subsets, bound_k, - } + }) } /// Get the alphabet size. diff --git a/src/models/set/exact_cover_by_3_sets.rs b/src/models/set/exact_cover_by_3_sets.rs index d9f13cce8..2512a7c27 100644 --- a/src/models/set/exact_cover_by_3_sets.rs +++ b/src/models/set/exact_cover_by_3_sets.rs @@ -52,6 +52,7 @@ inventory::submit! { /// assert!(problem.evaluate(&solutions[0]).unwrap()); /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "ExactCoverBy3SetsCreateSpec")] pub struct ExactCoverBy3Sets { /// Size of the universe (elements are 0..universe_size, must be divisible by 3). universe_size: usize, @@ -101,34 +102,33 @@ impl ExactCoverBy3Sets { /// Panics if `universe_size` is not divisible by 3, or if any subset /// contains duplicate elements or elements outside the universe. pub fn new(universe_size: usize, subsets: Vec<[usize; 3]>) -> Self { - assert!( - universe_size.is_multiple_of(3), - "Universe size must be divisible by 3, got {}", - universe_size - ); - let mut subsets = subsets; - for (i, subset) in subsets.iter_mut().enumerate() { - assert!( - subset[0] != subset[1] && subset[0] != subset[2] && subset[1] != subset[2], - "Subset {} contains duplicate elements: {:?}", - i, - subset + Self::try_new(universe_size, subsets).unwrap_or_else(|error| panic!("{error}")) + } + + fn try_new( + universe_size: usize, + mut subsets: Vec<[usize; 3]>, + ) -> Result { + if !universe_size.is_multiple_of(3) { + return Err( + format!("Universe size must be divisible by 3, got {universe_size}").into(), ); - for &elem in subset.iter() { - assert!( - elem < universe_size, - "Subset {} contains element {} which is outside universe of size {}", - i, - elem, - universe_size + } + for (index, subset) in subsets.iter_mut().enumerate() { + if subset[0] == subset[1] || subset[0] == subset[2] || subset[1] == subset[2] { + return Err(format!("subset {index} contains duplicate elements").into()); + } + if let Some(&element) = subset.iter().find(|&&element| element >= universe_size) { + return Err( + format!("Subset {index} contains element {element} which is outside universe of size {universe_size}").into(), ); } subset.sort(); } - Self { + Ok(Self { universe_size, subsets, - } + }) } /// Get the universe size. diff --git a/src/models/set/minimum_cardinality_key.rs b/src/models/set/minimum_cardinality_key.rs index 81c143fae..896bc039e 100644 --- a/src/models/set/minimum_cardinality_key.rs +++ b/src/models/set/minimum_cardinality_key.rs @@ -31,6 +31,7 @@ inventory::submit! { /// find a subset `K ⊆ A` of minimum cardinality such that the closure of `K` /// under `F` equals `A` (i.e., `K` is a key). #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "MinimumCardinalityKeyData")] pub struct MinimumCardinalityKey { /// Number of attributes (elements are `0..num_attributes`). num_attributes: usize, @@ -38,6 +39,20 @@ pub struct MinimumCardinalityKey { dependencies: Vec<(Vec, Vec)>, } +#[derive(Deserialize)] +struct MinimumCardinalityKeyData { + num_attributes: usize, + dependencies: Vec<(Vec, Vec)>, +} + +impl TryFrom for MinimumCardinalityKey { + type Error = crate::registry::ConstructionError; + + fn try_from(data: MinimumCardinalityKeyData) -> Result { + Self::try_new(data.num_attributes, data.dependencies) + } +} + impl MinimumCardinalityKey { /// Create a new Minimum Cardinality Key instance. /// @@ -45,27 +60,30 @@ impl MinimumCardinalityKey { /// /// Panics if any attribute index in a dependency lies outside the attribute set. pub fn new(num_attributes: usize, dependencies: Vec<(Vec, Vec)>) -> Self { - let mut dependencies = dependencies; - for (dep_index, (lhs, rhs)) in dependencies.iter_mut().enumerate() { + Self::try_new(num_attributes, dependencies).unwrap_or_else(|error| panic!("{error}")) + } + + fn try_new( + num_attributes: usize, + mut dependencies: Vec<(Vec, Vec)>, + ) -> Result { + for (index, (lhs, rhs)) in dependencies.iter_mut().enumerate() { lhs.sort_unstable(); lhs.dedup(); rhs.sort_unstable(); rhs.dedup(); - for &attr in lhs.iter().chain(rhs.iter()) { - assert!( - attr < num_attributes, - "Dependency {} contains attribute {} which is outside attribute set of size {}", - dep_index, - attr, - num_attributes - ); + if let Some(attribute) = lhs + .iter() + .chain(rhs.iter()) + .find(|&&attribute| attribute >= num_attributes) + { + return Err(format!("dependency {index} contains attribute {attribute} outside attribute set of size {num_attributes}").into()); } } - - Self { + Ok(Self { num_attributes, dependencies, - } + }) } /// Return the number of attributes. diff --git a/src/models/set/minimum_hitting_set.rs b/src/models/set/minimum_hitting_set.rs index 6b2b69e35..5b2e35a6f 100644 --- a/src/models/set/minimum_hitting_set.rs +++ b/src/models/set/minimum_hitting_set.rs @@ -26,11 +26,26 @@ inventory::submit! { /// Given a universe `U` and a collection of subsets of `U`, find a minimum-size /// subset `H ⊆ U` such that `H` intersects every set in the collection. #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "MinimumHittingSetData")] pub struct MinimumHittingSet { universe_size: usize, sets: Vec>, } +#[derive(Deserialize)] +struct MinimumHittingSetData { + universe_size: usize, + sets: Vec>, +} + +impl TryFrom for MinimumHittingSet { + type Error = crate::registry::ConstructionError; + + fn try_from(data: MinimumHittingSetData) -> Result { + Self::try_new(data.universe_size, data.sets) + } +} + #[derive(Debug, Deserialize, crate::CreateSpec)] struct MinimumHittingSetCreateSpec { /// Size of the universe U. @@ -52,7 +67,7 @@ impl TryFrom for MinimumHittingSet { .into()); } } - Ok(Self::new(spec.universe_size, spec.subsets)) + Self::try_new(spec.universe_size, spec.subsets) } } @@ -63,22 +78,24 @@ impl MinimumHittingSet { /// /// Panics if any set contains an element outside `0..universe_size`. pub fn new(universe_size: usize, sets: Vec>) -> Self { - let mut sets = sets; - for (set_index, set) in sets.iter_mut().enumerate() { + Self::try_new(universe_size, sets).unwrap_or_else(|error| panic!("{error}")) + } + + fn try_new( + universe_size: usize, + mut sets: Vec>, + ) -> Result { + for (index, set) in sets.iter_mut().enumerate() { set.sort_unstable(); set.dedup(); - for &element in set.iter() { - assert!( - element < universe_size, - "Set {set_index} contains element {element} which is outside universe of size {universe_size}" - ); + if let Some(element) = set.iter().find(|&&element| element >= universe_size) { + return Err(format!("set {index} contains element {element} outside universe of size {universe_size}").into()); } } - - Self { + Ok(Self { universe_size, sets, - } + }) } /// Get the universe size. diff --git a/src/models/set/minimum_set_covering.rs b/src/models/set/minimum_set_covering.rs index f1f7de407..5fd1b3d08 100644 --- a/src/models/set/minimum_set_covering.rs +++ b/src/models/set/minimum_set_covering.rs @@ -55,7 +55,7 @@ inventory::submit! { /// assert!(problem.evaluate(&sol).unwrap().is_valid()); /// } /// ``` -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize)] pub struct MinimumSetCovering { /// Size of the universe (elements are 0..universe_size). universe_size: usize, @@ -65,6 +65,21 @@ pub struct MinimumSetCovering { weights: Vec, } +#[derive(Deserialize)] +struct MinimumSetCoveringData { + universe_size: usize, + sets: Vec>, + weights: Vec, +} + +impl<'de, W: Clone + Default + Deserialize<'de>> Deserialize<'de> for MinimumSetCovering { + fn deserialize>(deserializer: D) -> Result { + let data = MinimumSetCoveringData::deserialize(deserializer)?; + Self::try_with_weights(data.universe_size, data.sets, data.weights) + .map_err(serde::de::Error::custom) + } +} + #[derive(Debug, Deserialize, crate::CreateSpec)] struct MinimumSetCoveringCreateSpec { /// Size of the universe U. @@ -96,11 +111,7 @@ impl TryFrom for MinimumSetCovering { .into()); } } - Ok(Self::with_weights( - spec.universe_size, - spec.subsets, - spec.weights, - )) + Self::try_with_weights(spec.universe_size, spec.subsets, spec.weights) } } @@ -110,23 +121,49 @@ impl MinimumSetCovering { where W: WeightElement, { - let num_sets = sets.len(); - let weights = vec![W::unit(); num_sets]; - Self { - universe_size, - sets, - weights, - } + Self::try_new(universe_size, sets).unwrap_or_else(|error| panic!("{error}")) + } + + fn try_new( + universe_size: usize, + sets: Vec>, + ) -> Result + where + W: WeightElement, + { + let weights = vec![W::unit(); sets.len()]; + Self::try_with_weights(universe_size, sets, weights) } /// Create a new Set Covering problem with custom weights. pub fn with_weights(universe_size: usize, sets: Vec>, weights: Vec) -> Self { - assert_eq!(sets.len(), weights.len()); - Self { + Self::try_with_weights(universe_size, sets, weights) + .unwrap_or_else(|error| panic!("{error}")) + } + + fn try_with_weights( + universe_size: usize, + sets: Vec>, + weights: Vec, + ) -> Result { + if sets.len() != weights.len() { + return Err(format!( + "weights has {} entries, expected one for each of {} subsets", + weights.len(), + sets.len() + ) + .into()); + } + for (index, set) in sets.iter().enumerate() { + if let Some(element) = set.iter().find(|&&element| element >= universe_size) { + return Err(format!("set {index} contains element {element} outside universe of size {universe_size}").into()); + } + } + Ok(Self { universe_size, sets, weights, - } + }) } /// Get the universe size. diff --git a/src/models/set/prime_attribute_name.rs b/src/models/set/prime_attribute_name.rs index c552c51b0..8258ab39d 100644 --- a/src/models/set/prime_attribute_name.rs +++ b/src/models/set/prime_attribute_name.rs @@ -60,6 +60,7 @@ inventory::submit! { /// assert!(solution.is_some()); /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "PrimeAttributeNameData")] pub struct PrimeAttributeName { /// Number of attributes (elements are 0..num_attributes). num_attributes: usize, @@ -69,6 +70,21 @@ pub struct PrimeAttributeName { query_attribute: usize, } +#[derive(Deserialize)] +struct PrimeAttributeNameData { + num_attributes: usize, + dependencies: Vec<(Vec, Vec)>, + query_attribute: usize, +} + +impl TryFrom for PrimeAttributeName { + type Error = crate::registry::ConstructionError; + + fn try_from(data: PrimeAttributeNameData) -> Result { + Self::try_new(data.num_attributes, data.dependencies, data.query_attribute) + } +} + #[derive(Debug, Deserialize, crate::CreateSpec)] struct PrimeAttributeNameCreateSpec { /// Number of attributes. @@ -107,11 +123,7 @@ impl TryFrom for PrimeAttributeName { ).into()); } } - Ok(Self::new( - spec.universe_size, - spec.dependencies, - spec.query_attribute, - )) + Self::try_new(spec.universe_size, spec.dependencies, spec.query_attribute) } } @@ -127,29 +139,35 @@ impl PrimeAttributeName { dependencies: Vec<(Vec, Vec)>, query_attribute: usize, ) -> Self { - assert!( - query_attribute < num_attributes, - "Query attribute {} is outside attribute set of size {}", - query_attribute, - num_attributes - ); - for (i, (lhs, rhs)) in dependencies.iter().enumerate() { - assert!(!lhs.is_empty(), "Dependency {} has empty LHS", i); - for &attr in lhs.iter().chain(rhs.iter()) { - assert!( - attr < num_attributes, - "Dependency {} references attribute {} which is outside attribute set of size {}", - i, - attr, - num_attributes - ); + Self::try_new(num_attributes, dependencies, query_attribute) + .unwrap_or_else(|error| panic!("{error}")) + } + + fn try_new( + num_attributes: usize, + dependencies: Vec<(Vec, Vec)>, + query_attribute: usize, + ) -> Result { + if query_attribute >= num_attributes { + return Err(format!("Query attribute {query_attribute} is outside attribute set of size {num_attributes}").into()); + } + for (index, (lhs, rhs)) in dependencies.iter().enumerate() { + if lhs.is_empty() { + return Err(format!("Dependency {index} has empty LHS").into()); + } + if let Some(attribute) = lhs + .iter() + .chain(rhs.iter()) + .find(|&&attribute| attribute >= num_attributes) + { + return Err(format!("dependency {index} contains attribute {attribute} outside attribute set of size {num_attributes}").into()); } } - Self { + Ok(Self { num_attributes, dependencies, query_attribute, - } + }) } /// Get the number of attributes. diff --git a/src/models/set/set_basis.rs b/src/models/set/set_basis.rs index 8eaba1aa1..6a53f43c9 100644 --- a/src/models/set/set_basis.rs +++ b/src/models/set/set_basis.rs @@ -28,6 +28,7 @@ inventory::submit! { /// `S` such that every set in `C` can be expressed as the union of some /// subcollection of `B`. #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "SetBasisData")] pub struct SetBasis { /// Size of the universe (elements are `0..universe_size`). universe_size: usize, @@ -37,6 +38,21 @@ pub struct SetBasis { k: usize, } +#[derive(Deserialize)] +struct SetBasisData { + universe_size: usize, + collection: Vec>, + k: usize, +} + +impl TryFrom for SetBasis { + type Error = crate::registry::ConstructionError; + + fn try_from(data: SetBasisData) -> Result { + Self::try_new(data.universe_size, data.collection, data.k) + } +} + #[derive(Debug, Deserialize, crate::CreateSpec)] struct SetBasisCreateSpec { /// Size of the ground set S. @@ -60,7 +76,7 @@ impl TryFrom for SetBasis { .into()); } } - Ok(Self::new(spec.universe_size, spec.subsets, spec.k)) + Self::try_new(spec.universe_size, spec.subsets, spec.k) } } @@ -71,26 +87,26 @@ impl SetBasis { /// /// Panics if any element in `collection` lies outside the universe. pub fn new(universe_size: usize, collection: Vec>, k: usize) -> Self { - let mut collection = collection; - for (set_index, set) in collection.iter_mut().enumerate() { + Self::try_new(universe_size, collection, k).unwrap_or_else(|error| panic!("{error}")) + } + + fn try_new( + universe_size: usize, + mut collection: Vec>, + k: usize, + ) -> Result { + for (index, set) in collection.iter_mut().enumerate() { set.sort_unstable(); set.dedup(); - for &element in set.iter() { - assert!( - element < universe_size, - "Set {} contains element {} which is outside universe of size {}", - set_index, - element, - universe_size - ); + if let Some(element) = set.iter().find(|&&element| element >= universe_size) { + return Err(format!("set {index} contains element {element} outside universe of size {universe_size}").into()); } } - - Self { + Ok(Self { universe_size, collection, k, - } + }) } /// Return the universe size. diff --git a/src/models/set/three_dimensional_matching.rs b/src/models/set/three_dimensional_matching.rs index 47f38ac38..5dbd3fd06 100644 --- a/src/models/set/three_dimensional_matching.rs +++ b/src/models/set/three_dimensional_matching.rs @@ -56,6 +56,7 @@ inventory::submit! { /// assert!(problem.evaluate(&solutions[0]).unwrap()); /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "ThreeDimensionalMatchingData")] pub struct ThreeDimensionalMatching { /// Size of each set W, X, Y (elements are 0..universe_size). universe_size: usize, @@ -63,6 +64,20 @@ pub struct ThreeDimensionalMatching { triples: Vec<(usize, usize, usize)>, } +#[derive(Deserialize)] +struct ThreeDimensionalMatchingData { + universe_size: usize, + triples: Vec<(usize, usize, usize)>, +} + +impl TryFrom for ThreeDimensionalMatching { + type Error = crate::registry::ConstructionError; + + fn try_from(data: ThreeDimensionalMatchingData) -> Result { + Self::try_new(data.universe_size, data.triples) + } +} + impl ThreeDimensionalMatching { /// Create a new 3DM problem. /// @@ -70,33 +85,25 @@ impl ThreeDimensionalMatching { /// /// Panics if any triple contains an element outside 0..universe_size. pub fn new(universe_size: usize, triples: Vec<(usize, usize, usize)>) -> Self { - for (i, &(w, x, y)) in triples.iter().enumerate() { - assert!( - w < universe_size, - "Triple {} has w-coordinate {} which is outside 0..{}", - i, - w, - universe_size - ); - assert!( - x < universe_size, - "Triple {} has x-coordinate {} which is outside 0..{}", - i, - x, - universe_size - ); - assert!( - y < universe_size, - "Triple {} has y-coordinate {} which is outside 0..{}", - i, - y, - universe_size - ); + Self::try_new(universe_size, triples).unwrap_or_else(|error| panic!("{error}")) + } + + fn try_new( + universe_size: usize, + triples: Vec<(usize, usize, usize)>, + ) -> Result { + for (index, &(w, x, y)) in triples.iter().enumerate() { + if w >= universe_size || x >= universe_size || y >= universe_size { + return Err(format!( + "triple {index} contains a coordinate outside 0..{universe_size}" + ) + .into()); + } } - Self { + Ok(Self { universe_size, triples, - } + }) } /// Get the universe size (q). diff --git a/src/topology/bipartite_graph.rs b/src/topology/bipartite_graph.rs index a99d5dbdf..306d3f8ac 100644 --- a/src/topology/bipartite_graph.rs +++ b/src/topology/bipartite_graph.rs @@ -22,6 +22,7 @@ use serde::{Deserialize, Serialize}; /// assert!(g.has_edge(0, 2)); // left 0 -> right 0 (unified index 2) /// ``` #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(try_from = "BipartiteGraphData")] pub struct BipartiteGraph { left_size: usize, right_size: usize, @@ -29,6 +30,20 @@ pub struct BipartiteGraph { edges: Vec<(usize, usize)>, } +#[derive(Deserialize)] +struct BipartiteGraphData { + left_size: usize, + right_size: usize, + edges: Vec<(usize, usize)>, +} + +impl TryFrom for BipartiteGraph { + type Error = crate::registry::ConstructionError; + fn try_from(data: BipartiteGraphData) -> Result { + Self::try_new(data.left_size, data.right_size, data.edges) + } +} + impl BipartiteGraph { /// Create a new bipartite graph. /// @@ -42,25 +57,36 @@ impl BipartiteGraph { /// /// Panics if any edge references an out-of-bounds left or right vertex index. pub fn new(left_size: usize, right_size: usize, edges: Vec<(usize, usize)>) -> Self { + Self::try_new(left_size, right_size, edges).unwrap_or_else(|error| panic!("{error}")) + } + + pub(crate) fn try_new( + left_size: usize, + right_size: usize, + edges: Vec<(usize, usize)>, + ) -> Result { + left_size + .checked_add(right_size) + .ok_or("bipartite vertex count overflows usize")?; for &(u, v) in &edges { - assert!( - u < left_size, - "left vertex {} out of bounds (left_size={})", - u, - left_size - ); - assert!( - v < right_size, - "right vertex {} out of bounds (right_size={})", - v, - right_size - ); + if !(u < left_size) { + return Err( + format!("left vertex {} out of bounds (left_size={})", u, left_size).into(), + ); + } + if !(v < right_size) { + return Err(format!( + "right vertex {} out of bounds (right_size={})", + v, right_size + ) + .into()); + } } - Self { + Ok(Self { left_size, right_size, edges, - } + }) } /// Returns the number of vertices in the left partition. diff --git a/src/topology/directed_graph.rs b/src/topology/directed_graph.rs index 84fe30027..bd2b7f2c8 100644 --- a/src/topology/directed_graph.rs +++ b/src/topology/directed_graph.rs @@ -53,21 +53,28 @@ impl DirectedGraph { /// /// Panics if any arc references a vertex index >= `num_vertices`. pub fn new(num_vertices: usize, arcs: Vec<(usize, usize)>) -> Self { + Self::try_new(num_vertices, arcs).unwrap_or_else(|error| panic!("{error}")) + } + + pub(crate) fn try_new( + num_vertices: usize, + arcs: Vec<(usize, usize)>, + ) -> Result { let mut inner = DiGraph::new(); for _ in 0..num_vertices { inner.add_node(()); } for (u, v) in arcs { - assert!( - u < num_vertices && v < num_vertices, - "arc ({}, {}) references vertex >= num_vertices ({})", - u, - v, - num_vertices - ); + if !(u < num_vertices && v < num_vertices) { + return Err(format!( + "arc ({}, {}) references vertex >= num_vertices ({})", + u, v, num_vertices + ) + .into()); + } inner.add_edge(NodeIndex::new(u), NodeIndex::new(v), ()); } - Self { inner } + Ok(Self { inner }) } /// Creates an empty directed graph with the given number of vertices and no arcs. @@ -263,7 +270,7 @@ impl<'de> Deserialize<'de> for DirectedGraph { arcs: Vec<(usize, usize)>, } let data = GraphData::deserialize(deserializer)?; - Ok(DirectedGraph::new(data.num_vertices, data.arcs)) + DirectedGraph::try_new(data.num_vertices, data.arcs).map_err(serde::de::Error::custom) } } diff --git a/src/topology/graph.rs b/src/topology/graph.rs index 263b64aeb..6038b6cd2 100644 --- a/src/topology/graph.rs +++ b/src/topology/graph.rs @@ -116,21 +116,28 @@ impl SimpleGraph { /// /// Panics if any edge references a vertex index >= num_vertices. pub fn new(num_vertices: usize, edges: Vec<(usize, usize)>) -> Self { + Self::try_new(num_vertices, edges).unwrap_or_else(|error| panic!("{error}")) + } + + pub(crate) fn try_new( + num_vertices: usize, + edges: Vec<(usize, usize)>, + ) -> Result { let mut inner = UnGraph::new_undirected(); for _ in 0..num_vertices { inner.add_node(()); } for (u, v) in edges { - assert!( - u < num_vertices && v < num_vertices, - "edge ({}, {}) references vertex >= num_vertices ({})", - u, - v, - num_vertices - ); + if !(u < num_vertices && v < num_vertices) { + return Err(format!( + "edge ({}, {}) references vertex >= num_vertices ({})", + u, v, num_vertices + ) + .into()); + } inner.add_edge(NodeIndex::new(u), NodeIndex::new(v), ()); } - Self { inner } + Ok(Self { inner }) } /// Creates an empty graph with the given number of vertices. @@ -279,7 +286,7 @@ impl<'de> Deserialize<'de> for SimpleGraph { edges: Vec<(usize, usize)>, } let data = GraphData::deserialize(deserializer)?; - Ok(SimpleGraph::new(data.num_vertices, data.edges)) + SimpleGraph::try_new(data.num_vertices, data.edges).map_err(serde::de::Error::custom) } } diff --git a/src/topology/mixed_graph.rs b/src/topology/mixed_graph.rs index 9d95fbac9..94c908783 100644 --- a/src/topology/mixed_graph.rs +++ b/src/topology/mixed_graph.rs @@ -12,12 +12,27 @@ use serde::{Deserialize, Serialize}; /// so higher-level models can use that order as part of their configuration /// semantics, but edge-membership queries treat them as unordered pairs. #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "MixedGraphData")] pub struct MixedGraph { num_vertices: usize, arcs: Vec<(usize, usize)>, edges: Vec<(usize, usize)>, } +#[derive(Deserialize)] +struct MixedGraphData { + num_vertices: usize, + arcs: Vec<(usize, usize)>, + edges: Vec<(usize, usize)>, +} + +impl TryFrom for MixedGraph { + type Error = crate::registry::ConstructionError; + fn try_from(data: MixedGraphData) -> Result { + Self::try_new(data.num_vertices, data.arcs, data.edges) + } +} + impl MixedGraph { /// Create a new mixed graph. /// @@ -25,31 +40,39 @@ impl MixedGraph { /// /// Panics if any endpoint references a vertex outside `0..num_vertices`. pub fn new(num_vertices: usize, arcs: Vec<(usize, usize)>, edges: Vec<(usize, usize)>) -> Self { + Self::try_new(num_vertices, arcs, edges).unwrap_or_else(|error| panic!("{error}")) + } + + pub(crate) fn try_new( + num_vertices: usize, + arcs: Vec<(usize, usize)>, + edges: Vec<(usize, usize)>, + ) -> Result { for &(u, v) in &arcs { - assert!( - u < num_vertices && v < num_vertices, - "arc ({}, {}) references vertex >= num_vertices ({})", - u, - v, - num_vertices - ); + if !(u < num_vertices && v < num_vertices) { + return Err(format!( + "arc ({}, {}) references vertex >= num_vertices ({})", + u, v, num_vertices + ) + .into()); + } } for &(u, v) in &edges { - assert!( - u < num_vertices && v < num_vertices, - "edge ({}, {}) references vertex >= num_vertices ({})", - u, - v, - num_vertices - ); + if !(u < num_vertices && v < num_vertices) { + return Err(format!( + "edge ({}, {}) references vertex >= num_vertices ({})", + u, v, num_vertices + ) + .into()); + } } - Self { + Ok(Self { num_vertices, arcs, edges, - } + }) } /// Create an empty mixed graph with no arcs or undirected edges. diff --git a/src/topology/planar_graph.rs b/src/topology/planar_graph.rs index a29a2b042..c37b752ef 100644 --- a/src/topology/planar_graph.rs +++ b/src/topology/planar_graph.rs @@ -19,7 +19,7 @@ use serde::{Deserialize, Serialize}; /// assert_eq!(g.num_vertices(), 4); /// assert_eq!(g.num_edges(), 6); /// ``` -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize)] pub struct PlanarGraph { inner: SimpleGraph, } @@ -30,18 +30,14 @@ impl PlanarGraph { /// # Panics /// Panics if the graph violates the necessary planarity condition |E| <= 3|V| - 6. pub fn new(num_vertices: usize, edges: Vec<(usize, usize)>) -> Self { - let inner = SimpleGraph::new(num_vertices, edges); - if num_vertices >= 3 { - let max_edges = 3 * num_vertices - 6; - assert!( - inner.num_edges() <= max_edges, - "graph has {} edges but a planar graph on {} vertices can have at most {} edges", - inner.num_edges(), - num_vertices, - max_edges - ); - } - Self { inner } + Self::try_new(num_vertices, edges).unwrap_or_else(|error| panic!("{error}")) + } + + pub(crate) fn try_new( + num_vertices: usize, + edges: Vec<(usize, usize)>, + ) -> Result { + Self::try_from(SimpleGraph::try_new(num_vertices, edges)?) } /// Get a reference to the underlying SimpleGraph. @@ -50,6 +46,31 @@ impl PlanarGraph { } } +impl TryFrom for PlanarGraph { + type Error = crate::registry::ConstructionError; + fn try_from(inner: SimpleGraph) -> Result { + let num_vertices = inner.num_vertices(); + if num_vertices >= 3 { + let max_edges = 3 * (num_vertices as u128) - 6; + if inner.num_edges() as u128 > max_edges { + return Err(format!("graph has {} edges but a planar graph on {num_vertices} vertices can have at most {max_edges} edges", inner.num_edges()).into()); + } + } + Ok(Self { inner }) + } +} + +impl<'de> Deserialize<'de> for PlanarGraph { + fn deserialize>(deserializer: D) -> Result { + #[derive(Deserialize)] + struct PlanarGraphData { + inner: SimpleGraph, + } + let data = PlanarGraphData::deserialize(deserializer)?; + Self::try_from(data.inner).map_err(serde::de::Error::custom) + } +} + impl Graph for PlanarGraph { const NAME: &'static str = "PlanarGraph"; diff --git a/src/unit_tests/models/algebraic/bmf.rs b/src/unit_tests/models/algebraic/bmf.rs index 3e4425a4a..9dfc763ab 100644 --- a/src/unit_tests/models/algebraic/bmf.rs +++ b/src/unit_tests/models/algebraic/bmf.rs @@ -299,3 +299,19 @@ fn test_bmf_paper_example() { let best = solver.solve(&problem).unwrap().unwrap(); assert!(problem.is_exact(&best).unwrap()); } + +#[test] +fn json_rejects_invalid_instance() { + assert!( + serde_json::from_value::(serde_json::json!({"matrix":[[true],[]],"k":1})).is_err() + ); +} + +#[test] +fn deserialize_rebuilds_matrix_dimensions() { + let model: BMF = serde_json::from_value(serde_json::json!({ + "matrix": [[true, false]], "k": 1, "m": 99, "n": 99 + })) + .unwrap(); + assert_eq!((model.rows(), model.cols(), model.rank()), (1, 2, 1)); +} diff --git a/src/unit_tests/models/algebraic/consecutive_ones_submatrix.rs b/src/unit_tests/models/algebraic/consecutive_ones_submatrix.rs index 0e88e23d0..586277a8d 100644 --- a/src/unit_tests/models/algebraic/consecutive_ones_submatrix.rs +++ b/src/unit_tests/models/algebraic/consecutive_ones_submatrix.rs @@ -238,3 +238,11 @@ fn test_consecutive_ones_submatrix_inconsistent_rows() { let matrix = vec![vec![true, false], vec![true]]; ConsecutiveOnesSubmatrix::new(matrix, 1); } + +#[test] +fn json_rejects_invalid_instance() { + assert!(serde_json::from_value::( + serde_json::json!({"matrix":[[true]],"bound":2}) + ) + .is_err()); +} diff --git a/src/unit_tests/models/algebraic/feasible_basis_extension.rs b/src/unit_tests/models/algebraic/feasible_basis_extension.rs index d9c3453e2..6bd9fff2e 100644 --- a/src/unit_tests/models/algebraic/feasible_basis_extension.rs +++ b/src/unit_tests/models/algebraic/feasible_basis_extension.rs @@ -250,3 +250,11 @@ fn test_feasible_basis_extension_duplicate_required() { vec![0, 0], ); } + +#[test] +fn json_rejects_invalid_instance() { + assert!(serde_json::from_value::( + serde_json::json!({"matrix":[[1]],"rhs":[1],"required_columns":[]}) + ) + .is_err()); +} diff --git a/src/unit_tests/models/algebraic/minimum_matrix_domination.rs b/src/unit_tests/models/algebraic/minimum_matrix_domination.rs index f2e107fd8..b9ece5831 100644 --- a/src/unit_tests/models/algebraic/minimum_matrix_domination.rs +++ b/src/unit_tests/models/algebraic/minimum_matrix_domination.rs @@ -205,3 +205,20 @@ fn test_minimum_matrix_domination_inconsistent_rows() { let matrix = vec![vec![true, false], vec![true]]; MinimumMatrixDomination::new(matrix); } + +#[test] +fn json_rejects_invalid_instance() { + assert!(serde_json::from_value::( + serde_json::json!({"matrix":[[true],[]]}) + ) + .is_err()); +} + +#[test] +fn deserialize_rebuilds_nonzero_positions() { + let model: MinimumMatrixDomination = serde_json::from_value(serde_json::json!({ + "matrix": [[true, false], [false, true]], "ones": [[99, 99]] + })) + .unwrap(); + assert_eq!(model.ones(), &[(0, 0), (1, 1)]); +} diff --git a/src/unit_tests/models/algebraic/minimum_weight_decoding.rs b/src/unit_tests/models/algebraic/minimum_weight_decoding.rs index 5915ab151..e098cbb22 100644 --- a/src/unit_tests/models/algebraic/minimum_weight_decoding.rs +++ b/src/unit_tests/models/algebraic/minimum_weight_decoding.rs @@ -178,3 +178,11 @@ fn test_minimum_weight_decoding_target_mismatch() { let matrix = vec![vec![true, false], vec![false, true]]; MinimumWeightDecoding::new(matrix, vec![true]); } + +#[test] +fn json_rejects_invalid_instance() { + assert!(serde_json::from_value::( + serde_json::json!({"matrix":[[true]],"target":[]}) + ) + .is_err()); +} diff --git a/src/unit_tests/models/algebraic/minimum_weight_solution_to_linear_equations.rs b/src/unit_tests/models/algebraic/minimum_weight_solution_to_linear_equations.rs index 668e02a4f..8d9b0b31a 100644 --- a/src/unit_tests/models/algebraic/minimum_weight_solution_to_linear_equations.rs +++ b/src/unit_tests/models/algebraic/minimum_weight_solution_to_linear_equations.rs @@ -162,3 +162,13 @@ fn test_minimum_weight_solution_rhs_mismatch() { let matrix = vec![vec![1, 2], vec![3, 4]]; MinimumWeightSolutionToLinearEquations::new(matrix, vec![1]); } + +#[test] +fn json_rejects_invalid_instance() { + assert!( + serde_json::from_value::( + serde_json::json!({"matrix":[[1]],"rhs":[]}) + ) + .is_err() + ); +} diff --git a/src/unit_tests/models/algebraic/quadratic_assignment.rs b/src/unit_tests/models/algebraic/quadratic_assignment.rs index 038eb17ff..d25e32d92 100644 --- a/src/unit_tests/models/algebraic/quadratic_assignment.rs +++ b/src/unit_tests/models/algebraic/quadratic_assignment.rs @@ -165,3 +165,11 @@ fn test_quadratic_assignment_solver() { Min(Some(56)) ); } + +#[test] +fn json_rejects_invalid_instance() { + assert!(serde_json::from_value::( + serde_json::json!({"cost_matrix":[[1]],"distance_matrix":[[]]}) + ) + .is_err()); +} diff --git a/src/unit_tests/models/algebraic/sparse_matrix_compression.rs b/src/unit_tests/models/algebraic/sparse_matrix_compression.rs index 843481a86..bf13a8773 100644 --- a/src/unit_tests/models/algebraic/sparse_matrix_compression.rs +++ b/src/unit_tests/models/algebraic/sparse_matrix_compression.rs @@ -146,3 +146,11 @@ fn test_sparse_matrix_compression_rejects_zero_bound() { fn test_sparse_matrix_compression_rejects_ragged_matrix() { let _ = SparseMatrixCompression::new(vec![vec![true, false], vec![true]], 2); } + +#[test] +fn json_rejects_invalid_instance() { + assert!(serde_json::from_value::( + serde_json::json!({"matrix":[[true]],"bound_k":0}) + ) + .is_err()); +} diff --git a/src/unit_tests/models/graph/max_cut.rs b/src/unit_tests/models/graph/max_cut.rs index 863cf42cc..9a0f98bd1 100644 --- a/src/unit_tests/models/graph/max_cut.rs +++ b/src/unit_tests/models/graph/max_cut.rs @@ -178,3 +178,11 @@ fn create_specs_use_edge_weights_for_both_weight_variants() { assert_eq!(unit.edge_weights(), vec![One]); assert_eq!(MaxCutI64CreateSpec::FIELDS[2].name, "edge_weights"); } + +#[test] +fn construction_and_json_reject_mismatched_weights() { + let graph = SimpleGraph::try_new(2, vec![(0, 1)]).unwrap(); + assert!(MaxCut::try_new(graph.clone(), Vec::::new()).is_err()); + let json = serde_json::json!({"graph": graph, "edge_weights": []}); + assert!(serde_json::from_value::>(json).is_err()); +} diff --git a/src/unit_tests/models/graph/maximal_is.rs b/src/unit_tests/models/graph/maximal_is.rs index 7f51f0fb8..52c9e94e7 100644 --- a/src/unit_tests/models/graph/maximal_is.rs +++ b/src/unit_tests/models/graph/maximal_is.rs @@ -215,3 +215,16 @@ fn test_maximal_is_paper_example() { let best = solver.solve(&problem).unwrap().unwrap(); assert_eq!(problem.evaluate(&best).unwrap().unwrap(), 3); } + +#[test] +fn construction_and_json_reject_mismatched_weights() { + let graph = SimpleGraph::try_new(1, vec![]).unwrap(); + assert!(MaximalIS::try_new(graph.clone(), Vec::::new()).is_err()); + let json = serde_json::json!({"graph": graph, "weights": []}); + assert!(serde_json::from_value::>(json.clone()).is_err()); + let variant = std::collections::BTreeMap::from([ + ("graph".into(), "SimpleGraph".into()), + ("weight".into(), "i64".into()), + ]); + assert!(crate::registry::load_dyn("MaximalIS", &variant, json).is_err()); +} diff --git a/src/unit_tests/models/graph/maximum_clique.rs b/src/unit_tests/models/graph/maximum_clique.rs index 76e182aac..dfe9d6800 100644 --- a/src/unit_tests/models/graph/maximum_clique.rs +++ b/src/unit_tests/models/graph/maximum_clique.rs @@ -369,3 +369,16 @@ fn test_clique_paper_example() { let best = solver.solve(&problem).unwrap().unwrap(); assert_eq!(problem.evaluate(&best).unwrap().unwrap(), 3); } + +#[test] +fn construction_and_json_reject_mismatched_weights() { + let graph = SimpleGraph::try_new(1, vec![]).unwrap(); + assert!(MaximumClique::try_new(graph.clone(), Vec::::new()).is_err()); + let json = serde_json::json!({"graph": graph, "weights": []}); + assert!(serde_json::from_value::>(json.clone()).is_err()); + let variant = std::collections::BTreeMap::from([ + ("graph".into(), "SimpleGraph".into()), + ("weight".into(), "i64".into()), + ]); + assert!(crate::registry::load_dyn("MaximumClique", &variant, json).is_err()); +} diff --git a/src/unit_tests/models/graph/maximum_co_k_plex.rs b/src/unit_tests/models/graph/maximum_co_k_plex.rs index 711c1a05b..78d23a56e 100644 --- a/src/unit_tests/models/graph/maximum_co_k_plex.rs +++ b/src/unit_tests/models/graph/maximum_co_k_plex.rs @@ -189,3 +189,14 @@ fn test_maximum_co_k_plex_rejects_missing_bound_k_on_load() { "error should mention the missing field `bound_k`, got: {msg}" ); } + +#[test] +fn deserialize_checks_fixed_k_and_weight_count() { + for (weights, bound_k) in [(vec![1, 1], 2), (vec![1], 1)] { + assert!(serde_json::from_value::>( + serde_json::json!({ + "graph": {"num_vertices": 2, "edges": []}, "weights": weights, "bound_k": bound_k + }) + ).is_err()); + } +} diff --git a/src/unit_tests/models/graph/maximum_common_edge_subgraph.rs b/src/unit_tests/models/graph/maximum_common_edge_subgraph.rs index 924d142e7..4d451cfcb 100644 --- a/src/unit_tests/models/graph/maximum_common_edge_subgraph.rs +++ b/src/unit_tests/models/graph/maximum_common_edge_subgraph.rs @@ -196,3 +196,20 @@ fn test_labelled_digraph_deduplicates_arcs() { ); assert_eq!(g.num_arcs(), 2); } + +#[test] +fn deserialize_checks_and_normalizes_labelled_arcs() { + assert!( + serde_json::from_value::(serde_json::json!({ + "num_vertices": 2, "arcs": [{"src": 0, "label": 1, "dst": 2}] + })) + .is_err() + ); + let graph: LabelledDigraph = serde_json::from_value(serde_json::json!({ + "num_vertices": 2, "arcs": [ + {"src": 0, "label": 1, "dst": 1}, {"src": 0, "label": 1, "dst": 1} + ] + })) + .unwrap(); + assert_eq!(graph.arcs(), &[LabelledArc::new(0, 1, 1)]); +} diff --git a/src/unit_tests/models/graph/maximum_independent_set.rs b/src/unit_tests/models/graph/maximum_independent_set.rs index abdb3b7a4..f9e7ebbb8 100644 --- a/src/unit_tests/models/graph/maximum_independent_set.rs +++ b/src/unit_tests/models/graph/maximum_independent_set.rs @@ -253,3 +253,18 @@ fn test_mis_paper_example() { let best = solver.solve(&problem).unwrap().unwrap(); assert_eq!(problem.evaluate(&best).unwrap().unwrap(), 4); } + +#[test] +fn construction_and_json_reject_mismatched_weights() { + let graph = SimpleGraph::try_new(1, vec![]).unwrap(); + assert!(MaximumIndependentSet::try_new(graph.clone(), Vec::::new()).is_err()); + let json = serde_json::json!({"graph": graph, "weights": []}); + assert!( + serde_json::from_value::>(json.clone()).is_err() + ); + let variant = std::collections::BTreeMap::from([ + ("graph".into(), "SimpleGraph".into()), + ("weight".into(), "i64".into()), + ]); + assert!(crate::registry::load_dyn("MaximumIndependentSet", &variant, json).is_err()); +} diff --git a/src/unit_tests/models/graph/maximum_matching.rs b/src/unit_tests/models/graph/maximum_matching.rs index c06d8a30a..a328fe508 100644 --- a/src/unit_tests/models/graph/maximum_matching.rs +++ b/src/unit_tests/models/graph/maximum_matching.rs @@ -199,3 +199,11 @@ fn create_spec_uses_edge_weights_and_defaults_to_one() { assert_eq!(problem.weights(), vec![1]); assert_eq!(MaximumMatchingCreateSpec::FIELDS[2].name, "edge_weights"); } + +#[test] +fn construction_and_json_reject_mismatched_weights() { + let graph = SimpleGraph::try_new(2, vec![(0, 1)]).unwrap(); + assert!(MaximumMatching::try_new(graph.clone(), Vec::::new()).is_err()); + let json = serde_json::json!({"graph": graph, "edge_weights": []}); + assert!(serde_json::from_value::>(json).is_err()); +} diff --git a/src/unit_tests/models/graph/minimum_dominating_set.rs b/src/unit_tests/models/graph/minimum_dominating_set.rs index 5e1e167e1..2f0b3b621 100644 --- a/src/unit_tests/models/graph/minimum_dominating_set.rs +++ b/src/unit_tests/models/graph/minimum_dominating_set.rs @@ -204,3 +204,18 @@ fn test_mds_paper_example() { let best = solver.solve(&problem).unwrap().unwrap(); assert_eq!(problem.evaluate(&best).unwrap().unwrap(), 2); } + +#[test] +fn construction_and_json_reject_mismatched_weights() { + let graph = SimpleGraph::try_new(1, vec![]).unwrap(); + assert!(MinimumDominatingSet::try_new(graph.clone(), Vec::::new()).is_err()); + let json = serde_json::json!({"graph": graph, "weights": []}); + assert!( + serde_json::from_value::>(json.clone()).is_err() + ); + let variant = std::collections::BTreeMap::from([ + ("graph".into(), "SimpleGraph".into()), + ("weight".into(), "i64".into()), + ]); + assert!(crate::registry::load_dyn("MinimumDominatingSet", &variant, json).is_err()); +} diff --git a/src/unit_tests/models/graph/minimum_feedback_arc_set.rs b/src/unit_tests/models/graph/minimum_feedback_arc_set.rs index a7e710644..a446a1eea 100644 --- a/src/unit_tests/models/graph/minimum_feedback_arc_set.rs +++ b/src/unit_tests/models/graph/minimum_feedback_arc_set.rs @@ -214,3 +214,11 @@ fn test_minimum_feedback_arc_set_accessors() { problem.set_weights(vec![2, 3, 4]); assert_eq!(problem.weights(), &[2, 3, 4]); } + +#[test] +fn construction_and_json_reject_mismatched_weights() { + let graph = DirectedGraph::try_new(2, vec![(0, 1)]).unwrap(); + assert!(MinimumFeedbackArcSet::try_new(graph.clone(), Vec::::new()).is_err()); + let json = serde_json::json!({"graph": graph, "weights": []}); + assert!(serde_json::from_value::>(json).is_err()); +} diff --git a/src/unit_tests/models/graph/minimum_feedback_vertex_set.rs b/src/unit_tests/models/graph/minimum_feedback_vertex_set.rs index e0d640a4b..274a0e050 100644 --- a/src/unit_tests/models/graph/minimum_feedback_vertex_set.rs +++ b/src/unit_tests/models/graph/minimum_feedback_vertex_set.rs @@ -244,3 +244,11 @@ fn test_minimum_feedback_vertex_set_unit_create_and_roundtrip() { .is_err() ); } + +#[test] +fn construction_and_json_reject_mismatched_weights() { + let graph = DirectedGraph::try_new(2, vec![(0, 1)]).unwrap(); + assert!(MinimumFeedbackVertexSet::try_new(graph.clone(), Vec::::new()).is_err()); + let json = serde_json::json!({"graph": graph, "weights": []}); + assert!(serde_json::from_value::>(json).is_err()); +} diff --git a/src/unit_tests/models/graph/minimum_vertex_cover.rs b/src/unit_tests/models/graph/minimum_vertex_cover.rs index 200394243..e9cfe95e1 100644 --- a/src/unit_tests/models/graph/minimum_vertex_cover.rs +++ b/src/unit_tests/models/graph/minimum_vertex_cover.rs @@ -202,3 +202,16 @@ fn test_mvc_paper_example() { let best = solver.solve(&problem).unwrap().unwrap(); assert_eq!(problem.evaluate(&best).unwrap().unwrap(), 3); } + +#[test] +fn construction_and_json_reject_mismatched_weights() { + let graph = SimpleGraph::try_new(1, vec![]).unwrap(); + assert!(MinimumVertexCover::try_new(graph.clone(), Vec::::new()).is_err()); + let json = serde_json::json!({"graph": graph, "weights": []}); + assert!(serde_json::from_value::>(json.clone()).is_err()); + let variant = std::collections::BTreeMap::from([ + ("graph".into(), "SimpleGraph".into()), + ("weight".into(), "i64".into()), + ]); + assert!(crate::registry::load_dyn("MinimumVertexCover", &variant, json).is_err()); +} diff --git a/src/unit_tests/models/graph/traveling_salesman.rs b/src/unit_tests/models/graph/traveling_salesman.rs index 3b62235b7..9bb65f81d 100644 --- a/src/unit_tests/models/graph/traveling_salesman.rs +++ b/src/unit_tests/models/graph/traveling_salesman.rs @@ -296,3 +296,11 @@ fn create_spec_uses_edge_weights_and_defaults_to_one() { assert_eq!(problem.weights(), vec![1, 1, 1]); assert_eq!(TravelingSalesmanCreateSpec::FIELDS[2].name, "edge_weights"); } + +#[test] +fn construction_and_json_reject_mismatched_weights() { + let graph = SimpleGraph::try_new(2, vec![(0, 1)]).unwrap(); + assert!(TravelingSalesman::try_new(graph.clone(), Vec::::new()).is_err()); + let json = serde_json::json!({"graph": graph, "edge_weights": []}); + assert!(serde_json::from_value::>(json).is_err()); +} diff --git a/src/unit_tests/models/misc/feasible_register_assignment.rs b/src/unit_tests/models/misc/feasible_register_assignment.rs index 3c84f3948..d21b23ef5 100644 --- a/src/unit_tests/models/misc/feasible_register_assignment.rs +++ b/src/unit_tests/models/misc/feasible_register_assignment.rs @@ -175,3 +175,15 @@ fn test_feasible_register_assignment_same_register_pair_count() { let problem = FeasibleRegisterAssignment::new(5, vec![], 3, vec![0, 1, 0, 2, 0]); assert_eq!(problem.num_same_register_pairs(), 3); } + +#[test] +fn deserialize_rejects_invalid_indices_before_building_adjacency() { + for (arcs, assignment) in [(vec![(0, 2)], vec![0, 0]), (vec![(0, 1)], vec![0, 1])] { + assert!( + serde_json::from_value::(serde_json::json!({ + "num_vertices": 2, "arcs": arcs, "num_registers": 1, "assignment": assignment + })) + .is_err() + ); + } +} diff --git a/src/unit_tests/models/misc/minimum_disjunctive_normal_form.rs b/src/unit_tests/models/misc/minimum_disjunctive_normal_form.rs index 4daf64f6e..6992cfbd8 100644 --- a/src/unit_tests/models/misc/minimum_disjunctive_normal_form.rs +++ b/src/unit_tests/models/misc/minimum_disjunctive_normal_form.rs @@ -146,3 +146,14 @@ fn test_minimum_dnf_wrong_config_length() { fn test_minimum_dnf_all_false() { MinimumDisjunctiveNormalForm::new(2, vec![false, false, false, false]); } + +#[test] +fn deserialize_rebuilds_prime_implicants() { + let model: MinimumDisjunctiveNormalForm = serde_json::from_value(serde_json::json!({ + "num_variables": 2, "truth_table": [false, true, true, false], + "prime_implicants": [], "minterms": [99] + })) + .unwrap(); + assert_eq!(model.minterms(), &[1, 2]); + assert_eq!(model.num_prime_implicants(), 2); +} diff --git a/src/unit_tests/models/misc/minimum_fault_detection_test_set.rs b/src/unit_tests/models/misc/minimum_fault_detection_test_set.rs index 532e40ff6..27d755868 100644 --- a/src/unit_tests/models/misc/minimum_fault_detection_test_set.rs +++ b/src/unit_tests/models/misc/minimum_fault_detection_test_set.rs @@ -196,3 +196,15 @@ fn test_minimum_fault_detection_test_set_paper_example() { vec![vec![true, false], vec![false, true]] ); } + +#[test] +fn deserialize_rejects_invalid_vertices_before_building_coverage() { + for (arcs, inputs) in [(vec![(0, 2)], vec![0]), (vec![(0, 1)], vec![2])] { + assert!( + serde_json::from_value::(serde_json::json!({ + "num_vertices": 2, "arcs": arcs, "inputs": inputs, "outputs": [1] + })) + .is_err() + ); + } +} diff --git a/src/unit_tests/models/misc/minimum_weight_and_or_graph.rs b/src/unit_tests/models/misc/minimum_weight_and_or_graph.rs index 1142adcdf..35d1e7850 100644 --- a/src/unit_tests/models/misc/minimum_weight_and_or_graph.rs +++ b/src/unit_tests/models/misc/minimum_weight_and_or_graph.rs @@ -207,3 +207,16 @@ fn test_minimum_weight_and_or_graph_paper_example() { vec![true, true, false, true, false, true] ); } + +#[test] +fn deserialize_rejects_invalid_graph_before_building_outgoing_arcs() { + for (arcs, source) in [(vec![(2, 1)], 0), (vec![(0, 1)], 2)] { + assert!( + serde_json::from_value::(serde_json::json!({ + "num_vertices": 2, "arcs": arcs, "source": source, + "gate_types": [true, null], "arc_weights": [1] + })) + .is_err() + ); + } +} diff --git a/src/unit_tests/models/misc/multiprocessor_scheduling.rs b/src/unit_tests/models/misc/multiprocessor_scheduling.rs index 622523157..de7d12807 100644 --- a/src/unit_tests/models/misc/multiprocessor_scheduling.rs +++ b/src/unit_tests/models/misc/multiprocessor_scheduling.rs @@ -184,7 +184,7 @@ fn test_multiprocessor_scheduling_deserialization_rejects_zero_processors() { })) .unwrap_err(); assert!( - err.to_string().contains("expected positive integer, got 0"), + err.to_string().contains("num_processors must be positive"), "unexpected error: {err}" ); } diff --git a/src/unit_tests/models/misc/paintshop.rs b/src/unit_tests/models/misc/paintshop.rs index db68a3b4d..a5f08fa7b 100644 --- a/src/unit_tests/models/misc/paintshop.rs +++ b/src/unit_tests/models/misc/paintshop.rs @@ -154,3 +154,26 @@ fn test_paintshop_paper_example() { let best = solver.solve(&problem).unwrap().unwrap(); assert_eq!(problem.evaluate(&best).unwrap().unwrap(), 2); } + +#[test] +fn deserialize_rebuilds_occurrence_metadata() { + let expected = PaintShop::try_new(vec!["a", "b", "a", "b"]).unwrap(); + let mut json = serde_json::to_value(&expected).unwrap(); + json["is_first"] = serde_json::json!([false]); + json["num_cars"] = serde_json::json!(99); + let restored: PaintShop = serde_json::from_value(json).unwrap(); + assert_eq!( + serde_json::to_value(restored).unwrap(), + serde_json::to_value(expected).unwrap() + ); +} + +#[test] +fn deserialize_rejects_invalid_car_indices_and_counts() { + for indices in [vec![0, 1], vec![0]] { + assert!(serde_json::from_value::(serde_json::json!({ + "sequence_indices": indices, "car_labels": ["a"] + })) + .is_err()); + } +} diff --git a/src/unit_tests/models/misc/rectilinear_picture_compression.rs b/src/unit_tests/models/misc/rectilinear_picture_compression.rs index e1f2c2486..cdb5ecd28 100644 --- a/src/unit_tests/models/misc/rectilinear_picture_compression.rs +++ b/src/unit_tests/models/misc/rectilinear_picture_compression.rs @@ -247,3 +247,15 @@ fn test_rectilinear_picture_compression_empty_row_panics() { fn test_rectilinear_picture_compression_inconsistent_rows_panics() { RectilinearPictureCompression::new(vec![vec![true, false], vec![true]], 1); } + +#[test] +fn deserialize_rejects_invalid_matrix_before_building_rectangles() { + for matrix in [vec![], vec![vec![]], vec![vec![true], vec![]]] { + assert!( + serde_json::from_value::(serde_json::json!({ + "matrix": matrix, "bound": 1 + })) + .is_err() + ); + } +} diff --git a/src/unit_tests/models/set/consecutive_sets.rs b/src/unit_tests/models/set/consecutive_sets.rs index 76db9b531..cef98afb0 100644 --- a/src/unit_tests/models/set/consecutive_sets.rs +++ b/src/unit_tests/models/set/consecutive_sets.rs @@ -142,3 +142,10 @@ fn test_consecutive_sets_duplicate_elements() { fn test_consecutive_sets_zero_bound() { ConsecutiveSets::new(3, vec![vec![0, 1]], 0); } + +#[test] +fn json_rejects_invalid_instance() { + let json = serde_json::json!({"alphabet_size":3,"subsets":[[0,0]],"bound_k":3}); + assert!(serde_json::from_value::(json.clone()).is_err()); + assert!(crate::registry::load_dyn("ConsecutiveSets", &Default::default(), json).is_err()); +} diff --git a/src/unit_tests/models/set/exact_cover_by_3_sets.rs b/src/unit_tests/models/set/exact_cover_by_3_sets.rs index 9de0ed2e8..b955cac74 100644 --- a/src/unit_tests/models/set/exact_cover_by_3_sets.rs +++ b/src/unit_tests/models/set/exact_cover_by_3_sets.rs @@ -172,3 +172,28 @@ fn test_exact_cover_by_3_sets_element_out_of_range() { fn test_exact_cover_by_3_sets_duplicate_elements() { ExactCoverBy3Sets::new(6, vec![[0, 0, 1]]); } + +#[test] +fn construction_and_json_reject_invalid_triples() { + for (universe_size, subsets) in [ + (5, vec![[0, 1, 2]]), + (6, vec![[0, 1, 7]]), + (6, vec![[0, 0, 1]]), + ] { + assert!(ExactCoverBy3Sets::try_new(universe_size, subsets.clone()).is_err()); + let json = serde_json::json!({"universe_size": universe_size, "subsets": subsets}); + assert!(serde_json::from_value::(json.clone()).is_err()); + assert!(crate::registry::load_dyn("ExactCoverBy3Sets", &Default::default(), json).is_err()); + } +} + +#[test] +fn construction_and_json_sort_triples() { + let expected = ExactCoverBy3Sets::try_new(3, vec![[2, 0, 1]]).unwrap(); + let loaded: ExactCoverBy3Sets = serde_json::from_value(serde_json::json!({ + "universe_size": 3, "subsets": [[2, 0, 1]] + })) + .unwrap(); + assert_eq!(expected.subsets(), &[[0, 1, 2]]); + assert_eq!(loaded.subsets(), expected.subsets()); +} diff --git a/src/unit_tests/models/set/minimum_cardinality_key.rs b/src/unit_tests/models/set/minimum_cardinality_key.rs index ef8c29c12..deaaa101a 100644 --- a/src/unit_tests/models/set/minimum_cardinality_key.rs +++ b/src/unit_tests/models/set/minimum_cardinality_key.rs @@ -180,3 +180,10 @@ fn test_minimum_cardinality_key_paper_example() { let witness = solver.solve(&problem).unwrap().unwrap(); assert_eq!(witness, solution); } + +#[test] +fn json_rejects_invalid_instance() { + let json = serde_json::json!({"num_attributes":3,"dependencies":[[[0,3],[1]]]}); + assert!(serde_json::from_value::(json.clone()).is_err()); + assert!(crate::registry::load_dyn("MinimumCardinalityKey", &Default::default(), json).is_err()); +} diff --git a/src/unit_tests/models/set/minimum_hitting_set.rs b/src/unit_tests/models/set/minimum_hitting_set.rs index dd14c99f1..af083aa4a 100644 --- a/src/unit_tests/models/set/minimum_hitting_set.rs +++ b/src/unit_tests/models/set/minimum_hitting_set.rs @@ -179,3 +179,10 @@ fn test_minimum_hitting_set_canonical_example_spec() { let best = solver.solve(&problem).unwrap().unwrap(); assert_eq!(problem.evaluate(&best).unwrap(), Min(Some(3))); } + +#[test] +fn json_rejects_invalid_instance() { + let json = serde_json::json!({"universe_size":3,"sets":[[0,3]]}); + assert!(serde_json::from_value::(json.clone()).is_err()); + assert!(crate::registry::load_dyn("MinimumHittingSet", &Default::default(), json).is_err()); +} diff --git a/src/unit_tests/models/set/minimum_set_covering.rs b/src/unit_tests/models/set/minimum_set_covering.rs index ccc1dc652..4fa0155bf 100644 --- a/src/unit_tests/models/set/minimum_set_covering.rs +++ b/src/unit_tests/models/set/minimum_set_covering.rs @@ -138,3 +138,15 @@ fn test_setcovering_paper_example() { let best = solver.solve(&problem).unwrap().unwrap(); assert_eq!(problem.evaluate(&best).unwrap().unwrap(), 2); } + +#[test] +fn construction_and_json_reject_invalid_data() { + assert!(MinimumSetCovering::::try_new(2, vec![vec![2]]).is_err()); + assert!(MinimumSetCovering::try_with_weights(2, vec![vec![0]], Vec::::new()).is_err()); + for json in [ + serde_json::json!({"universe_size":2,"sets":[[2]],"weights":[1]}), + serde_json::json!({"universe_size":2,"sets":[[0]],"weights":[]}), + ] { + assert!(serde_json::from_value::>(json).is_err()); + } +} diff --git a/src/unit_tests/models/set/prime_attribute_name.rs b/src/unit_tests/models/set/prime_attribute_name.rs index ca34b79d5..a6b1b0e22 100644 --- a/src/unit_tests/models/set/prime_attribute_name.rs +++ b/src/unit_tests/models/set/prime_attribute_name.rs @@ -213,3 +213,11 @@ fn test_prime_attribute_name_empty_lhs() { fn test_prime_attribute_name_dep_out_of_range() { PrimeAttributeName::new(3, vec![(vec![0], vec![5])], 0); } + +#[test] +fn json_rejects_invalid_instance() { + let json = + serde_json::json!({"num_attributes":3,"dependencies":[[[],[1]]],"query_attribute":0}); + assert!(serde_json::from_value::(json.clone()).is_err()); + assert!(crate::registry::load_dyn("PrimeAttributeName", &Default::default(), json).is_err()); +} diff --git a/src/unit_tests/models/set/set_basis.rs b/src/unit_tests/models/set/set_basis.rs index 913d3bdb2..8f1ae7af6 100644 --- a/src/unit_tests/models/set/set_basis.rs +++ b/src/unit_tests/models/set/set_basis.rs @@ -134,17 +134,11 @@ fn test_set_basis_rejects_wrong_config_length() { } #[test] -fn test_set_basis_deserialized_invalid_target_returns_false() { - let problem: SetBasis = serde_json::from_value(serde_json::json!({ - "universe_size": 4, - "collection": [[0, 4]], - "k": 1 +fn test_set_basis_deserialization_rejects_invalid_target() { + assert!(serde_json::from_value::(serde_json::json!({ + "universe_size": 4, "collection": [[0, 4]], "k": 1 })) - .unwrap(); - - assert!(!problem - .evaluate(&vec![vec![true, false, false, false]]) - .unwrap()); + .is_err()); } #[test] @@ -214,3 +208,10 @@ fn test_set_basis_empty_collection_with_k_positive() { assert!(problem.evaluate(&vec![vec![false; 2]; 2]).unwrap()); assert!(problem.evaluate(&vec![vec![true; 2]; 2]).unwrap()); } + +#[test] +fn json_rejects_invalid_instance() { + let json = serde_json::json!({"universe_size":3,"collection":[[0,3]],"k":1}); + assert!(serde_json::from_value::(json.clone()).is_err()); + assert!(crate::registry::load_dyn("SetBasis", &Default::default(), json).is_err()); +} diff --git a/src/unit_tests/models/set/three_dimensional_matching.rs b/src/unit_tests/models/set/three_dimensional_matching.rs index d560df96a..9ea102aff 100644 --- a/src/unit_tests/models/set/three_dimensional_matching.rs +++ b/src/unit_tests/models/set/three_dimensional_matching.rs @@ -155,3 +155,12 @@ fn test_three_dimensional_matching_duplicate_coordinates() { assert!(!problem.evaluate(&vec![true, false, true]).unwrap()); // T0+T2: w={0,0} not distinct assert!(!problem.evaluate(&vec![false, true, true]).unwrap()); // T1+T2: x={1,1} not distinct } + +#[test] +fn json_rejects_invalid_instance() { + let json = serde_json::json!({"universe_size":2,"triples":[[0,2,0]]}); + assert!(serde_json::from_value::(json.clone()).is_err()); + assert!( + crate::registry::load_dyn("ThreeDimensionalMatching", &Default::default(), json).is_err() + ); +} diff --git a/src/unit_tests/registry/dispatch.rs b/src/unit_tests/registry/dispatch.rs index 20c6be252..b3fa582df 100644 --- a/src/unit_tests/registry/dispatch.rs +++ b/src/unit_tests/registry/dispatch.rs @@ -10,6 +10,101 @@ use crate::Problem; use std::any::Any; use std::collections::BTreeMap; +#[test] +fn timetable_construction_and_loading_reject_inconsistent_dimensions() { + let input = serde_json::json!({ + "num_periods": 1, "num_craftsmen": 1, "num_tasks": 1, + "craftsman_avail": [[true]], "task_avail": [[true]], "requirements": [[1]] + }); + let variant = BTreeMap::new(); + crate::registry::construct_dyn("TimetableDesign", &variant, input.clone()).unwrap(); + load_dyn("TimetableDesign", &variant, input.clone()).unwrap(); + for field in ["craftsman_avail", "task_avail", "requirements"] { + for value in [serde_json::json!([]), serde_json::json!([[]])] { + let mut invalid = input.clone(); + invalid[field] = value; + let error = + crate::registry::construct_dyn("TimetableDesign", &variant, invalid.clone()) + .err() + .expect("inconsistent dimensions must fail"); + assert!(error.to_string().contains(field), "{error}"); + let error = load_dyn("TimetableDesign", &variant, invalid).unwrap_err(); + assert!(error.to_string().contains(field), "{error}"); + } + } +} + +#[test] +fn graph_construction_and_loading_reject_inconsistent_fields() { + use serde_json::json; + let variant = BTreeMap::from([ + ("graph".into(), "SimpleGraph".into()), + ("weight".into(), "i64".into()), + ]); + for (name, input, mutations) in [ + ( + "MinMaxMulticenter", + json!({"graph": [[0,1]], "k": 1}), + vec![ + ("weights", "vertex_weights", json!([])), + ("edge_weights", "edge_lengths", json!([])), + ], + ), + ( + "MinimumSumMulticenter", + json!({"graph": [[0,1]], "k": 1}), + vec![ + ("weights", "vertex_weights", json!([])), + ("edge_weights", "edge_lengths", json!([])), + ], + ), + ( + "BoundedDiameterSpanningTree", + json!({"graph": [[0,1]], "weight_bound": 1, "diameter_bound": 1}), + vec![ + ("edge_weights", "edge_weights", json!([])), + ("edge_weights", "edge_weights", json!([0])), + ("weight_bound", "weight_bound", json!(0)), + ("diameter_bound", "diameter_bound", json!(0)), + ], + ), + ( + "RuralPostman", + json!({"graph": [[0,1]], "required_edges": [0]}), + vec![ + ("edge_weights", "edge_lengths", json!([])), + ("required_edges", "required_edges", json!([1])), + ], + ), + ( + "MinimumMultiwayCut", + json!({"graph": {"num_vertices": 2, "edges": [[0,1]]}, "terminals": [0,1], "edge_weights": [1]}), + vec![ + ("edge_weights", "edge_weights", json!([])), + ("terminals", "terminals", json!([0, 2])), + ], + ), + ] { + let problem = crate::registry::construct_dyn(name, &variant, input.clone()).unwrap(); + let stored = problem.serialize_json(); + load_dyn(name, &variant, stored.clone()).unwrap(); + for (create_field, stored_field, value) in mutations { + let mut invalid = input.clone(); + invalid[create_field] = value.clone(); + assert!( + crate::registry::construct_dyn(name, &variant, invalid).is_err(), + "{name}: {create_field}" + ); + let mut invalid = stored.clone(); + invalid[stored_field] = value; + assert!( + load_dyn(name, &variant, invalid).is_err(), + "{name}: {stored_field}" + ); + } + } +} + #[derive(Clone, serde::Serialize, serde::Deserialize)] struct SolutionProblem { weights: Vec, diff --git a/src/unit_tests/rules/bottlenecktravelingsalesman_ilp.rs b/src/unit_tests/rules/bottlenecktravelingsalesman_ilp.rs index 63b831bbd..635b06813 100644 --- a/src/unit_tests/rules/bottlenecktravelingsalesman_ilp.rs +++ b/src/unit_tests/rules/bottlenecktravelingsalesman_ilp.rs @@ -235,9 +235,10 @@ fn test_bottleneck_ilp_dimensions_and_malformed_weights() { for (n, m) in [(usize::MAX, 0), (1, usize::MAX), (0, usize::MAX)] { assert!(ReductionBTSPToILP::dimensions(n, m).is_err()); } - let source: BottleneckTravelingSalesman = serde_json::from_value(serde_json::json!({ - "graph": {"num_vertices": 0, "edges": []}, "edge_weights": [1] - })) - .unwrap(); - assert!(ReduceTo::>::reduce_to(&source).is_err()); + assert!( + serde_json::from_value::(serde_json::json!({ + "graph": {"num_vertices": 0, "edges": []}, "edge_weights": [1] + })) + .is_err() + ); } diff --git a/src/unit_tests/rules/partitionintocliques_minimumcoveringbycliques.rs b/src/unit_tests/rules/partitionintocliques_minimumcoveringbycliques.rs index 4a04b991f..fc2d1c934 100644 --- a/src/unit_tests/rules/partitionintocliques_minimumcoveringbycliques.rs +++ b/src/unit_tests/rules/partitionintocliques_minimumcoveringbycliques.rs @@ -35,10 +35,7 @@ fn test_partitionintocliques_aggregate_applies_gadget_offset() { #[test] fn test_partitionintocliques_to_minimumcoveringbycliques_closed_loop() { - let source: PartitionIntoCliques = serde_json::from_value(serde_json::json!({ - "graph": {"num_vertices": 0, "edges": []}, "num_cliques": 0 - })) - .unwrap(); + let source = PartitionIntoCliques::new(SimpleGraph::empty(1), 1); let reduction = ReduceTo::>::reduce_to(&source) .expect("reduction should succeed"); @@ -154,11 +151,15 @@ fn test_partitionintocliques_native_bounds_and_adjacency_semantics() { (3, vec![(0, 1), (1, 0), (0, 0)]), ] { for bound in [0, 1, n, n + 1, usize::MAX] { - let source: PartitionIntoCliques = - serde_json::from_value(serde_json::json!({ + let source = + serde_json::from_value::>(serde_json::json!({ "graph": {"num_vertices": n, "edges": edges}, "num_cliques": bound - })) - .unwrap(); + })); + if bound == 0 || bound > n { + assert!(source.is_err()); + continue; + } + let source = source.unwrap(); let reduction = ReduceTo::>::reduce_to(&source).unwrap(); let target = ReductionResult::target_problem(&reduction); diff --git a/src/unit_tests/topology/bipartite_graph.rs b/src/unit_tests/topology/bipartite_graph.rs index 46e3b381a..ede1b866a 100644 --- a/src/unit_tests/topology/bipartite_graph.rs +++ b/src/unit_tests/topology/bipartite_graph.rs @@ -58,3 +58,19 @@ fn test_bipartite_graph_invalid_left_index() { fn test_bipartite_graph_invalid_right_index() { BipartiteGraph::new(2, 2, vec![(0, 2)]); } + +#[test] +fn deserialize_checks_partition_endpoints_and_total_size() { + for edges in [vec![(1, 0)], vec![(0, 1)]] { + assert!(serde_json::from_value::(serde_json::json!({ + "left_size": 1, "right_size": 1, "edges": edges + })) + .is_err()); + } + assert!(BipartiteGraph::try_new(usize::MAX, 1, vec![]).is_err()); + let graph: BipartiteGraph = serde_json::from_value(serde_json::json!({ + "left_size": 1, "right_size": 1, "edges": [[0, 0]] + })) + .unwrap(); + assert_eq!(graph.edges(), vec![(0, 1)]); +} diff --git a/src/unit_tests/topology/directed_graph.rs b/src/unit_tests/topology/directed_graph.rs index 086728ae0..98b8e420b 100644 --- a/src/unit_tests/topology/directed_graph.rs +++ b/src/unit_tests/topology/directed_graph.rs @@ -234,3 +234,11 @@ fn test_directed_graph_json_format() { fn test_directed_graph_invalid_arc() { DirectedGraph::new(3, vec![(0, 5)]); } + +#[test] +fn deserialize_rejects_out_of_range_arcs() { + assert!(serde_json::from_value::(serde_json::json!({ + "num_vertices": 2, "arcs": [[2, 0]] + })) + .is_err()); +} diff --git a/src/unit_tests/topology/graph.rs b/src/unit_tests/topology/graph.rs index da89fb5ba..f98e2e4d2 100644 --- a/src/unit_tests/topology/graph.rs +++ b/src/unit_tests/topology/graph.rs @@ -153,3 +153,11 @@ fn test_simplegraph_json_format() { assert!(!json_str.contains("node_holes")); assert!(json_str.contains("num_vertices")); } + +#[test] +fn deserialize_rejects_out_of_range_edges() { + assert!(serde_json::from_value::(serde_json::json!({ + "num_vertices": 2, "edges": [[0, 2]] + })) + .is_err()); +} diff --git a/src/unit_tests/topology/mixed_graph.rs b/src/unit_tests/topology/mixed_graph.rs index 316e4ac02..8dcea1800 100644 --- a/src/unit_tests/topology/mixed_graph.rs +++ b/src/unit_tests/topology/mixed_graph.rs @@ -56,3 +56,13 @@ fn test_mixed_graph_serialization_roundtrip() { fn test_mixed_graph_panics_on_out_of_bounds_arc() { MixedGraph::new(3, vec![(0, 3)], vec![]); } + +#[test] +fn deserialize_checks_both_arc_and_edge_endpoints() { + for (arcs, edges) in [(vec![(0, 2)], vec![]), (vec![], vec![(2, 0)])] { + assert!(serde_json::from_value::(serde_json::json!({ + "num_vertices": 2, "arcs": arcs, "edges": edges + })) + .is_err()); + } +} diff --git a/src/unit_tests/topology/planar_graph.rs b/src/unit_tests/topology/planar_graph.rs index 7fae09cb6..463985412 100644 --- a/src/unit_tests/topology/planar_graph.rs +++ b/src/unit_tests/topology/planar_graph.rs @@ -44,3 +44,19 @@ fn test_planar_graph_tree() { let g = PlanarGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]); assert_eq!(g.num_edges(), 3); } + +#[test] +fn deserialize_checks_the_edge_bound() { + let edges: Vec<_> = (0..5) + .flat_map(|u| ((u + 1)..5).map(move |v| (u, v))) + .collect(); + assert!(serde_json::from_value::(serde_json::json!({ + "inner": {"num_vertices": 5, "edges": edges} + })) + .is_err()); + let graph: PlanarGraph = serde_json::from_value(serde_json::json!({ + "inner": {"num_vertices": 2, "edges": [[0, 1]]} + })) + .unwrap(); + assert_eq!(graph.num_edges(), 1); +} diff --git a/tests/suites/reductions.rs b/tests/suites/reductions.rs index 2fba18d41..c238b034b 100644 --- a/tests/suites/reductions.rs +++ b/tests/suites/reductions.rs @@ -337,10 +337,7 @@ mod partition_into_cliques_covering_by_cliques_reductions { #[test] fn test_partition_into_cliques_to_covering_by_cliques_closed_loop() { - let source: PartitionIntoCliques = serde_json::from_value(serde_json::json!({ - "graph": {"num_vertices": 0, "edges": []}, "num_cliques": 0 - })) - .unwrap(); + let source = PartitionIntoCliques::new(SimpleGraph::empty(1), 1); let reduction = ReduceTo::>::reduce_to(&source) .expect("reduction should succeed"); From c62e41c7b9dc277c973d18e28e8afb7261769d9f Mon Sep 17 00:00:00 2001 From: Xiwei Pan Date: Tue, 15 Sep 2026 00:47:45 +0800 Subject: [PATCH 05/42] refactor: unify decision targets and complete-result recovery --- .claude/CLAUDE.md | 23 +- docs/paper/reductions.typ | 203 +++++------ docs/src/cli-commands.md | 14 +- docs/src/design.md | 216 ++++++----- docs/src/getting-started.md | 10 +- docs/website/assets/site.js | 5 +- ...hained_reduction_factoring_to_spinglass.rs | 16 +- problemreductions-cli/src/cli.rs | 14 +- problemreductions-cli/src/commands/extract.rs | 81 ++--- problemreductions-cli/src/commands/solve.rs | 14 +- problemreductions-cli/src/dispatch.rs | 84 +++-- problemreductions-cli/src/main.rs | 2 +- problemreductions-cli/src/test_support.rs | 42 +-- problemreductions-cli/tests/cli_tests.rs | 237 +++++++++++-- problemreductions-macros/src/lib.rs | 86 +---- scripts/build_website.py | 4 +- src/example_db/specs.rs | 10 +- src/lib.rs | 2 +- .../algebraic/closest_vector_problem.rs | 42 +++ .../algebraic/feasible_basis_extension.rs | 33 +- .../algebraic/minimum_weight_decoding.rs | 18 +- ...mum_weight_solution_to_linear_equations.rs | 18 +- src/models/algebraic/quadratic_assignment.rs | 49 +++ src/models/algebraic/qubo.rs | 35 ++ src/models/decision.rs | 92 ++--- .../formula/maximum_2_satisfiability.rs | 47 +++ src/models/graph/longest_circuit.rs | 59 ++++ src/models/graph/longest_path.rs | 41 +++ src/models/graph/max_cut.rs | 43 +++ src/models/graph/min_max_multicenter.rs | 51 +++ .../graph/minimum_covering_by_cliques.rs | 56 +++ src/models/graph/minimum_dominating_set.rs | 10 +- src/models/graph/minimum_sum_multicenter.rs | 62 ++++ .../graph/optimal_linear_arrangement.rs | 5 +- src/models/graph/rural_postman.rs | 54 +++ src/models/graph/spin_glass.rs | 53 +++ src/models/misc/minimum_decision_tree.rs | 33 +- src/models/misc/mod.rs | 6 +- src/models/misc/open_shop_scheduling.rs | 39 ++ ...equencing_to_minimize_tardy_task_weight.rs | 38 ++ src/models/misc/stacker_crane.rs | 45 +++ src/models/set/exact_cover_by_3_sets.rs | 24 +- src/rules/acyclicpartition_ilp.rs | 38 +- .../balancedcompletebipartitesubgraph_ilp.rs | 30 +- src/rules/bicliquecover_bmf.rs | 30 +- src/rules/biconnectivityaugmentation_ilp.rs | 38 +- src/rules/binpacking_ilp.rs | 30 +- src/rules/bmf_bicliquecover.rs | 32 +- src/rules/bmf_ilp.rs | 30 +- src/rules/bottlenecktravelingsalesman_ilp.rs | 30 +- .../boundedcomponentspanningforest_ilp.rs | 38 +- src/rules/capacityassignment_ilp.rs | 30 +- src/rules/circuit_ilp.rs | 30 +- src/rules/circuit_sat.rs | 42 ++- src/rules/circuit_spinglass.rs | 68 ++-- src/rules/closeststring_ilp.rs | 30 +- src/rules/closestsubstring_ilp.rs | 30 +- src/rules/closestvectorproblem_qubo.rs | 30 +- src/rules/clustering_ilp.rs | 30 +- src/rules/coloring_ilp.rs | 33 +- src/rules/coloring_qubo.rs | 89 +++-- src/rules/consecutiveblockminimization_ilp.rs | 30 +- .../consecutiveonesmatrixaugmentation_ilp.rs | 39 +- src/rules/consecutiveonessubmatrix_ilp.rs | 39 +- ...onsistencyofdatabasefrequencytables_ilp.rs | 30 +- ...ximumindependentset_integralflowbundles.rs | 39 +- ...imumdominatingset_minimumsummulticenter.rs | 60 ++-- ...nminimumdominatingset_minmaxmulticenter.rs | 56 +-- ...onminimumvertexcover_hamiltoniancircuit.rs | 30 +- src/rules/directedhamiltonianpath_ilp.rs | 30 +- .../directedtwocommodityintegralflow_ilp.rs | 30 +- src/rules/disjointconnectingpaths_ilp.rs | 30 +- src/rules/ensemblecomputation_ilp.rs | 30 +- src/rules/eulerianpath_ilp.rs | 30 +- ...tcoverby3sets_algebraicequationsovergf2.rs | 17 +- ...overby3sets_boundeddiameterspanningtree.rs | 34 +- src/rules/exactcoverby3sets_ilp.rs | 30 +- .../exactcoverby3sets_maximumsetpacking.rs | 47 ++- .../exactcoverby3sets_minimumaxiomset.rs | 52 ++- ...verby3sets_minimumfaultdetectiontestset.rs | 47 ++- .../exactcoverby3sets_staffscheduling.rs | 30 +- src/rules/exactcoverby3sets_subsetproduct.rs | 17 +- src/rules/expectedretrievalcost_ilp.rs | 30 +- src/rules/factoring_circuit.rs | 30 +- src/rules/factoring_ilp.rs | 30 +- src/rules/feasibleregisterassignment_ilp.rs | 30 +- src/rules/flowshopscheduling_ilp.rs | 30 +- src/rules/graph.rs | 226 ++++-------- src/rules/graphpartitioning_ilp.rs | 30 +- src/rules/graphpartitioning_maxcut.rs | 20 +- src/rules/graphpartitioning_qubo.rs | 20 +- ...oniancircuit_biconnectivityaugmentation.rs | 30 +- ...niancircuit_bottlenecktravelingsalesman.rs | 47 ++- .../hamiltoniancircuit_hamiltonianpath.rs | 30 +- .../hamiltoniancircuit_longestcircuit.rs | 72 ++-- .../hamiltoniancircuit_quadraticassignment.rs | 55 +-- src/rules/hamiltoniancircuit_ruralpostman.rs | 68 ++-- src/rules/hamiltoniancircuit_stackercrane.rs | 81 +++-- ...ncircuit_strongconnectivityaugmentation.rs | 30 +- .../hamiltoniancircuit_travelingsalesman.rs | 47 ++- ...onianpath_degreeconstrainedspanningtree.rs | 30 +- src/rules/hamiltonianpath_ilp.rs | 30 +- .../hamiltonianpath_isomorphicspanningtree.rs | 17 +- ...onianpathbetweentwovertices_longestpath.rs | 84 +++-- src/rules/highlyconnecteddeletion_ilp.rs | 30 +- src/rules/ilp_bool_ilp_i64.rs | 17 +- src/rules/ilp_i64_ilp_bool.rs | 30 +- src/rules/ilp_qubo.rs | 54 ++- src/rules/integerknapsack_ilp.rs | 30 +- src/rules/integralflowbundles_ilp.rs | 30 +- src/rules/integralflowhomologousarcs_ilp.rs | 30 +- src/rules/integralflowwithmultipliers_ilp.rs | 30 +- src/rules/isomorphicspanningtree_ilp.rs | 30 +- ...lique_balancedcompletebipartitesubgraph.rs | 30 +- src/rules/kclique_conjunctivebooleanquery.rs | 30 +- src/rules/kclique_ilp.rs | 30 +- src/rules/kclique_subgraphisomorphism.rs | 30 +- src/rules/kcoloring_bicliquecover.rs | 32 +- src/rules/kcoloring_casts.rs | 2 +- src/rules/kcoloring_clustering.rs | 30 +- src/rules/kcoloring_partitionintocliques.rs | 17 +- ...kcoloring_twodimensionalconsecutivesets.rs | 30 +- src/rules/knapsack_ilp.rs | 30 +- src/rules/knapsack_qubo.rs | 29 +- src/rules/ksatisfiability_acyclicpartition.rs | 32 +- src/rules/ksatisfiability_bicliquecover.rs | 30 +- src/rules/ksatisfiability_casts.rs | 4 +- src/rules/ksatisfiability_cyclicordering.rs | 30 +- ...tisfiability_decisionminimumvertexcover.rs | 118 +++++-- ...bility_directedtwocommodityintegralflow.rs | 30 +- ...tisfiability_feasibleregisterassignment.rs | 48 ++- src/rules/ksatisfiability_kclique.rs | 30 +- src/rules/ksatisfiability_kernel.rs | 30 +- .../ksatisfiability_minimumvertexcover.rs | 179 ---------- .../ksatisfiability_monochromatictriangle.rs | 41 ++- ...satisfiability_oneinthreesatisfiability.rs | 30 +- .../ksatisfiability_preemptivescheduling.rs | 47 ++- .../ksatisfiability_quadraticcongruences.rs | 30 +- ...fiability_quadraticdiophantineequations.rs | 35 +- src/rules/ksatisfiability_qubo.rs | 142 ++++---- .../ksatisfiability_registersufficiency.rs | 40 ++- ...atisfiability_simultaneousincongruences.rs | 30 +- src/rules/ksatisfiability_subsetsum.rs | 30 +- src/rules/ksatisfiability_timetabledesign.rs | 30 +- src/rules/lengthboundeddisjointpaths_ilp.rs | 30 +- src/rules/longestcircuit_ilp.rs | 30 +- src/rules/longestcommonsubsequence_ilp.rs | 30 +- ...commonsubsequence_maximumindependentset.rs | 30 +- src/rules/longestpath_ilp.rs | 30 +- src/rules/maxcut_minimumcutintoboundedsets.rs | 30 +- src/rules/maxcut_minimummatrixcover.rs | 17 +- src/rules/maximalis_ilp.rs | 30 +- src/rules/maximum2satisfiability_ilp.rs | 30 +- src/rules/maximum2satisfiability_maxcut.rs | 30 +- src/rules/maximumclique_ilp.rs | 30 +- .../maximumclique_maximumindependentset.rs | 17 +- src/rules/maximumcokplex_ilp.rs | 33 +- src/rules/maximumcommonedgesubgraph_ilp.rs | 30 +- src/rules/maximumcontactmapoverlap_ilp.rs | 30 +- src/rules/maximumdomaticnumber_ilp.rs | 32 +- src/rules/maximumedgeweightedkclique_ilp.rs | 33 +- src/rules/maximumindependentset_casts.rs | 16 +- src/rules/maximumindependentset_gridgraph.rs | 29 +- .../maximumindependentset_maximumclique.rs | 17 +- ...maximumindependentset_maximumsetpacking.rs | 32 +- src/rules/maximumindependentset_triangular.rs | 29 +- src/rules/maximumleafspanningtree_ilp.rs | 30 +- src/rules/maximumlikelihoodranking_ilp.rs | 30 +- src/rules/maximummatching_ilp.rs | 30 +- .../maximummatching_maximumsetpacking.rs | 17 +- src/rules/maximumsetpacking_casts.rs | 2 +- src/rules/maximumsetpacking_ilp.rs | 30 +- src/rules/maximumsetpacking_qubo.rs | 17 +- .../minimumcapacitatedspanningtree_ilp.rs | 30 +- ...mcostmaximumflow_minimumcostcirculation.rs | 30 +- src/rules/minimumcoveringbycliques_ilp.rs | 30 +- ...bycliques_minimumintersectiongraphbasis.rs | 30 +- src/rules/minimumcutintoboundedsets_ilp.rs | 30 +- ...mumdiscreteplanarinversekinematics_qubo.rs | 57 ++- src/rules/minimumdominatingset_ilp.rs | 30 +- src/rules/minimumedgecostflow_ilp.rs | 30 +- ...minimumexternalmacrodatacompression_ilp.rs | 43 ++- src/rules/minimumfaultdetectiontestset_ilp.rs | 30 +- src/rules/minimumfeedbackarcset_ilp.rs | 30 +- src/rules/minimumfeedbackvertexset_ilp.rs | 30 +- ...minimumcodegenerationunlimitedregisters.rs | 39 +- src/rules/minimumgraphbandwidth_ilp.rs | 30 +- src/rules/minimumhittingset_ilp.rs | 30 +- ...minimuminternalmacrodatacompression_ilp.rs | 39 +- src/rules/minimummatrixcover_ilp.rs | 30 +- src/rules/minimummaximalmatching_ilp.rs | 30 +- ...maximalmatching_maximumachromaticnumber.rs | 32 +- ...maximalmatching_minimummatrixdomination.rs | 32 +- src/rules/minimummetricdimension_ilp.rs | 30 +- src/rules/minimummultiwaycut_ilp.rs | 30 +- src/rules/minimummultiwaycut_qubo.rs | 29 +- src/rules/minimumsetcovering_ilp.rs | 30 +- src/rules/minimumsummulticenter_ilp.rs | 30 +- src/rules/minimumtardinesssequencing_ilp.rs | 58 ++- ...nimumvertexcover_comparativecontainment.rs | 17 +- .../minimumvertexcover_ensemblecomputation.rs | 30 +- ...mumvertexcover_longestcommonsubsequence.rs | 30 +- ...inimumvertexcover_maximumindependentset.rs | 64 +++- ...inimumvertexcover_minimumfeedbackarcset.rs | 39 +- ...mumvertexcover_minimumfeedbackvertexset.rs | 17 +- .../minimumvertexcover_minimumhittingset.rs | 17 +- ...nimumvertexcover_minimummaximalmatching.rs | 1 - .../minimumvertexcover_minimumsetcovering.rs | 17 +- ...imumvertexcover_minimumweightandorgraph.rs | 30 +- src/rules/minimumweightdecoding_ilp.rs | 30 +- src/rules/minmaxmulticenter_ilp.rs | 30 +- src/rules/mixedchinesepostman_ilp.rs | 30 +- src/rules/mod.rs | 41 ++- src/rules/monochromatictriangle_ilp.rs | 30 +- src/rules/multiplechoicebranching_ilp.rs | 30 +- src/rules/multiplecopyfileallocation_ilp.rs | 30 +- src/rules/multiprocessorscheduling_ilp.rs | 30 +- src/rules/naesatisfiability_ilp.rs | 30 +- src/rules/naesatisfiability_maxcut.rs | 72 ++-- ...fiability_partitionintoperfectmatchings.rs | 30 +- src/rules/naesatisfiability_setsplitting.rs | 30 +- ...atching_numericalmatchingwithtargetsums.rs | 30 +- .../numericalmatchingwithtargetsums_ilp.rs | 30 +- src/rules/openshopscheduling_ilp.rs | 30 +- ...ement_consecutiveonesmatrixaugmentation.rs | 30 +- src/rules/optimallineararrangement_ilp.rs | 30 +- ...uencingtominimizeweightedcompletiontime.rs | 39 +- .../optimumcommunicationspanningtree_ilp.rs | 30 +- src/rules/paintshop_ilp.rs | 39 +- src/rules/paintshop_qubo.rs | 17 +- src/rules/partiallyorderedknapsack_ilp.rs | 30 +- src/rules/partition_binpacking.rs | 47 ++- .../partition_cosineproductintegration.rs | 17 +- .../partition_integralflowwithmultipliers.rs | 30 +- src/rules/partition_knapsack.rs | 47 ++- .../partition_multiprocessorscheduling.rs | 30 +- src/rules/partition_openshopscheduling.rs | 98 ++--- src/rules/partition_productionplanning.rs | 30 +- ...ion_sequencingtominimizetardytaskweight.rs | 67 ++-- src/rules/partition_subsetsum.rs | 17 +- src/rules/partition_sumofsquarespartition.rs | 55 ++- src/rules/partitionintocliques_ilp.rs | 30 +- ...ionintocliques_minimumcoveringbycliques.rs | 99 +++--- ...flength2_boundedcomponentspanningforest.rs | 17 +- src/rules/partitionintopathsoflength2_ilp.rs | 30 +- src/rules/partitionintotriangles_ilp.rs | 30 +- src/rules/pathconstrainednetworkflow_ilp.rs | 30 +- .../precedenceconstrainedscheduling_ilp.rs | 30 +- src/rules/preemptivescheduling_ilp.rs | 30 +- ...rizecollectingsteinerforest_steinertree.rs | 30 +- src/rules/quadraticassignment_ilp.rs | 30 +- src/rules/qubo_ilp.rs | 33 +- .../rectilinearpicturecompression_ilp.rs | 30 +- src/rules/registersufficiency_ilp.rs | 30 +- src/rules/registry.rs | 32 +- .../resourceconstrainedscheduling_ilp.rs | 30 +- ...arrangement_rootedtreestorageassignment.rs | 30 +- src/rules/rootedtreestorageassignment_ilp.rs | 39 +- src/rules/ruralpostman_ilp.rs | 30 +- src/rules/sat_circuitsat.rs | 30 +- src/rules/sat_coloring.rs | 30 +- src/rules/sat_ksat.rs | 58 ++- src/rules/sat_maximumindependentset.rs | 69 ++-- src/rules/sat_minimumdominatingset.rs | 74 ++-- ...tisfiability_integralflowhomologousarcs.rs | 30 +- .../satisfiability_maximum2satisfiability.rs | 91 +++-- src/rules/satisfiability_naesatisfiability.rs | 30 +- src/rules/satisfiability_nontautology.rs | 17 +- ...ingtominimizeweightedcompletiontime_ilp.rs | 30 +- .../schedulingwithindividualdeadlines_ilp.rs | 30 +- ...cingtominimizemaximumcumulativecost_ilp.rs | 30 +- ...sequencingtominimizetardytaskweight_ilp.rs | 30 +- ...ingtominimizeweightedcompletiontime_ilp.rs | 30 +- ...quencingtominimizeweightedtardiness_ilp.rs | 30 +- ...equencingwithdeadlinesandsetuptimes_ilp.rs | 30 +- src/rules/sequencingwithinintervals_ilp.rs | 39 +- ...uencingwithreleasetimesanddeadlines_ilp.rs | 30 +- src/rules/setsplitting_betweenness.rs | 30 +- src/rules/setsplitting_ilp.rs | 30 +- src/rules/shortestcommonsupersequence_ilp.rs | 39 +- .../shortestweightconstrainedpath_ilp.rs | 30 +- src/rules/sparsematrixcompression_ilp.rs | 39 +- src/rules/spinglass_maxcut.rs | 78 +++- src/rules/spinglass_qubo.rs | 61 +++- src/rules/stackercrane_ilp.rs | 30 +- src/rules/steinertree_ilp.rs | 30 +- src/rules/stringtostringcorrection_ilp.rs | 39 +- .../strongconnectivityaugmentation_ilp.rs | 38 +- src/rules/subgraphisomorphism_ilp.rs | 30 +- src/rules/subsetsum_closestvectorproblem.rs | 74 ++-- .../subsetsum_integerexpressionmembership.rs | 30 +- src/rules/subsetsum_integerknapsack.rs | 1 - src/rules/subsetsum_partition.rs | 30 +- src/rules/sumofsquarespartition_ilp.rs | 30 +- src/rules/test_helpers.rs | 186 ++++++++-- src/rules/threedimensionalmatching_ilp.rs | 30 +- ...mensionalmatching_minimumweightdecoding.rs | 53 ++- ...threedimensionalmatching_threepartition.rs | 30 +- ...partition_resourceconstrainedscheduling.rs | 17 +- ..._sequencingwithreleasetimesanddeadlines.rs | 30 +- src/rules/timetabledesign_ilp.rs | 30 +- src/rules/traits.rs | 254 ++++++------- src/rules/travelingsalesman_ilp.rs | 30 +- src/rules/travelingsalesman_qubo.rs | 52 ++- src/rules/undirectedflowlowerbounds_ilp.rs | 30 +- .../undirectedtwocommodityintegralflow_ilp.rs | 39 +- src/solvers/customized/solver.rs | 11 + src/solvers/mod.rs | 7 +- src/solvers/outcome.rs | 117 ++++++ src/solvers/pipelines.rs | 69 ++++ src/solvers/registry.rs | 47 ++- src/solvers/resolver.rs | 61 +--- src/unit_tests/example_db.rs | 110 +++--- .../algebraic/closest_vector_problem.rs | 3 +- src/unit_tests/models/decision.rs | 127 +++++-- .../models/graph/minimum_edge_cost_flow.rs | 27 ++ .../models/misc/conjunctive_boolean_query.rs | 84 +++++ .../models/misc/minimum_axiom_set.rs | 17 + .../minimum_code_generation_one_register.rs | 18 + ...mum_code_generation_unlimited_registers.rs | 21 ++ .../optimum_communication_spanning_tree.rs | 44 +++ .../models/misc/timetable_design.rs | 38 ++ src/unit_tests/reduction_graph.rs | 86 +++-- src/unit_tests/registry/variant.rs | 4 + src/unit_tests/rules/acyclicpartition_ilp.rs | 28 +- .../balancedcompletebipartitesubgraph_ilp.rs | 10 +- src/unit_tests/rules/bicliquecover_bmf.rs | 19 +- .../rules/biconnectivityaugmentation_ilp.rs | 83 ++++- src/unit_tests/rules/binpacking_ilp.rs | 46 ++- src/unit_tests/rules/bmf_bicliquecover.rs | 19 +- .../rules/bottlenecktravelingsalesman_ilp.rs | 75 +++- .../boundedcomponentspanningforest_ilp.rs | 28 +- .../rules/capacityassignment_ilp.rs | 28 +- src/unit_tests/rules/circuit_ilp.rs | 56 ++- src/unit_tests/rules/circuit_sat.rs | 10 +- src/unit_tests/rules/circuit_spinglass.rs | 172 ++++++--- src/unit_tests/rules/closeststring_ilp.rs | 28 +- src/unit_tests/rules/closestsubstring_ilp.rs | 28 +- .../rules/closestvectorproblem_casts.rs | 13 +- .../rules/closestvectorproblem_qubo.rs | 61 +++- src/unit_tests/rules/clustering_ilp.rs | 14 +- src/unit_tests/rules/coloring_ilp.rs | 82 ++++- src/unit_tests/rules/coloring_qubo.rs | 95 +++-- .../rules/consecutiveblockminimization_ilp.rs | 10 +- .../consecutiveonesmatrixaugmentation_ilp.rs | 19 +- .../rules/consecutiveonessubmatrix_ilp.rs | 37 +- ...onsistencyofdatabasefrequencytables_ilp.rs | 37 +- ...ximumindependentset_integralflowbundles.rs | 52 ++- ...imumdominatingset_minimumsummulticenter.rs | 124 ++++--- ...nminimumdominatingset_minmaxmulticenter.rs | 126 +++++-- ...onminimumvertexcover_hamiltoniancircuit.rs | 37 +- .../rules/directedhamiltonianpath_ilp.rs | 19 +- .../directedtwocommodityintegralflow_ilp.rs | 19 +- .../rules/disjointconnectingpaths_ilp.rs | 19 +- .../rules/ensemblecomputation_ilp.rs | 19 +- src/unit_tests/rules/eulerianpath_ilp.rs | 28 +- ...tcoverby3sets_algebraicequationsovergf2.rs | 11 +- ...overby3sets_boundeddiameterspanningtree.rs | 66 +++- src/unit_tests/rules/exactcoverby3sets_ilp.rs | 19 +- .../exactcoverby3sets_maximumsetpacking.rs | 28 +- .../exactcoverby3sets_minimumaxiomset.rs | 37 +- ...verby3sets_minimumfaultdetectiontestset.rs | 32 +- .../exactcoverby3sets_staffscheduling.rs | 39 +- .../rules/exactcoverby3sets_subsetproduct.rs | 11 +- .../rules/expectedretrievalcost_ilp.rs | 31 +- src/unit_tests/rules/factoring_circuit.rs | 55 ++- src/unit_tests/rules/factoring_ilp.rs | 91 ++++- .../rules/feasibleregisterassignment_ilp.rs | 10 +- .../rules/flowshopscheduling_ilp.rs | 28 +- src/unit_tests/rules/graph.rs | 334 ++++++++++++------ src/unit_tests/rules/graphpartitioning_ilp.rs | 19 +- .../rules/graphpartitioning_maxcut.rs | 33 +- .../rules/graphpartitioning_qubo.rs | 23 ++ ...oniancircuit_biconnectivityaugmentation.rs | 60 ++-- ...niancircuit_bottlenecktravelingsalesman.rs | 10 +- .../hamiltoniancircuit_hamiltonianpath.rs | 19 +- .../hamiltoniancircuit_longestcircuit.rs | 104 +++--- .../hamiltoniancircuit_quadraticassignment.rs | 201 +++++++---- .../rules/hamiltoniancircuit_ruralpostman.rs | 80 +++-- .../rules/hamiltoniancircuit_stackercrane.rs | 94 +++-- ...ncircuit_strongconnectivityaugmentation.rs | 10 +- .../hamiltoniancircuit_travelingsalesman.rs | 10 +- ...onianpath_degreeconstrainedspanningtree.rs | 10 +- src/unit_tests/rules/hamiltonianpath_ilp.rs | 28 +- .../hamiltonianpath_isomorphicspanningtree.rs | 10 +- ...onianpathbetweentwovertices_longestpath.rs | 80 +++-- .../rules/highlyconnecteddeletion_ilp.rs | 10 +- src/unit_tests/rules/ilp_bool_ilp_i64.rs | 10 +- src/unit_tests/rules/ilp_i64_ilp_bool.rs | 10 +- src/unit_tests/rules/ilp_i64_ilp_f64.rs | 28 +- src/unit_tests/rules/ilp_qubo.rs | 150 +++++--- src/unit_tests/rules/integerknapsack_ilp.rs | 19 +- .../rules/integralflowbundles_ilp.rs | 20 +- .../rules/integralflowhomologousarcs_ilp.rs | 10 +- .../rules/integralflowwithmultipliers_ilp.rs | 10 +- .../rules/isomorphicspanningtree_ilp.rs | 19 +- ...lique_balancedcompletebipartitesubgraph.rs | 28 +- .../rules/kclique_conjunctivebooleanquery.rs | 19 +- src/unit_tests/rules/kclique_ilp.rs | 19 +- .../rules/kclique_subgraphisomorphism.rs | 28 +- .../rules/kcoloring_bicliquecover.rs | 111 ++++-- src/unit_tests/rules/kcoloring_clustering.rs | 22 +- .../rules/kcoloring_partitionintocliques.rs | 13 +- ...kcoloring_twodimensionalconsecutivesets.rs | 61 +++- src/unit_tests/rules/knapsack_ilp.rs | 37 +- src/unit_tests/rules/knapsack_qubo.rs | 28 +- .../rules/ksatisfiability_acyclicpartition.rs | 83 +++-- .../rules/ksatisfiability_bicliquecover.rs | 58 ++- .../rules/ksatisfiability_cyclicordering.rs | 89 ++++- ...tisfiability_decisionminimumvertexcover.rs | 24 +- ...bility_directedtwocommodityintegralflow.rs | 31 +- ...tisfiability_feasibleregisterassignment.rs | 70 +++- .../rules/ksatisfiability_kclique.rs | 83 ++++- .../rules/ksatisfiability_kernel.rs | 49 ++- .../ksatisfiability_minimumvertexcover.rs | 156 -------- .../ksatisfiability_monochromatictriangle.rs | 84 ++++- ...satisfiability_oneinthreesatisfiability.rs | 45 ++- .../ksatisfiability_preemptivescheduling.rs | 37 +- .../ksatisfiability_quadraticcongruences.rs | 48 ++- ...fiability_quadraticdiophantineequations.rs | 19 +- src/unit_tests/rules/ksatisfiability_qubo.rs | 188 +++++++--- .../ksatisfiability_registersufficiency.rs | 72 +++- ...atisfiability_simultaneousincongruences.rs | 19 +- .../rules/ksatisfiability_subsetsum.rs | 37 +- .../rules/ksatisfiability_timetabledesign.rs | 37 +- .../rules/lengthboundeddisjointpaths_ilp.rs | 44 ++- src/unit_tests/rules/longestcircuit_ilp.rs | 37 +- .../rules/longestcommonsubsequence_ilp.rs | 46 ++- ...commonsubsequence_maximumindependentset.rs | 10 +- src/unit_tests/rules/longestpath_ilp.rs | 28 +- .../rules/maxcut_minimumcutintoboundedsets.rs | 10 +- .../rules/maxcut_minimummatrixcover.rs | 13 +- src/unit_tests/rules/maximalis_ilp.rs | 19 +- .../rules/maximum2satisfiability_ilp.rs | 28 +- .../rules/maximum2satisfiability_maxcut.rs | 47 ++- src/unit_tests/rules/maximumclique_ilp.rs | 73 +++- .../maximumclique_maximumindependentset.rs | 10 +- src/unit_tests/rules/maximumcokplex_ilp.rs | 19 +- .../rules/maximumcommonedgesubgraph_ilp.rs | 37 +- .../rules/maximumcontactmapoverlap_ilp.rs | 37 +- .../rules/maximumdomaticnumber_ilp.rs | 37 +- .../rules/maximumedgeweightedkclique_ilp.rs | 10 +- .../rules/maximumindependentset_gridgraph.rs | 19 +- .../rules/maximumindependentset_ilp.rs | 25 +- .../maximumindependentset_maximumclique.rs | 10 +- ...maximumindependentset_maximumsetpacking.rs | 19 +- .../rules/maximumindependentset_qubo.rs | 25 +- .../rules/maximumindependentset_triangular.rs | 28 +- .../rules/maximumleafspanningtree_ilp.rs | 64 +++- .../rules/maximumlikelihoodranking_ilp.rs | 37 +- src/unit_tests/rules/maximummatching_ilp.rs | 64 +++- .../maximummatching_maximumsetpacking.rs | 10 +- .../rules/maximumsetpacking_casts.rs | 19 +- src/unit_tests/rules/maximumsetpacking_ilp.rs | 93 ++++- .../rules/maximumsetpacking_qubo.rs | 28 +- .../minimumcapacitatedspanningtree_ilp.rs | 46 ++- ...mcostmaximumflow_minimumcostcirculation.rs | 46 ++- .../rules/minimumcoveringbycliques_ilp.rs | 19 +- ...bycliques_minimumintersectiongraphbasis.rs | 19 +- .../rules/minimumcutintoboundedsets_ilp.rs | 10 +- ...mumdiscreteplanarinversekinematics_qubo.rs | 75 ++-- .../rules/minimumdominatingset_ilp.rs | 73 +++- .../rules/minimumedgecostflow_ilp.rs | 28 +- ...minimumexternalmacrodatacompression_ilp.rs | 46 ++- .../rules/minimumfaultdetectiontestset_ilp.rs | 19 +- .../rules/minimumfeedbackarcset_ilp.rs | 28 +- .../rules/minimumfeedbackvertexset_ilp.rs | 64 +++- ...minimumcodegenerationunlimitedregisters.rs | 49 ++- .../rules/minimumgraphbandwidth_ilp.rs | 19 +- src/unit_tests/rules/minimumhittingset_ilp.rs | 19 +- ...minimuminternalmacrodatacompression_ilp.rs | 55 ++- .../rules/minimummatrixcover_ilp.rs | 55 ++- .../rules/minimummaximalmatching_ilp.rs | 28 +- ...maximalmatching_maximumachromaticnumber.rs | 37 +- ...maximalmatching_minimummatrixdomination.rs | 28 +- .../rules/minimummetricdimension_ilp.rs | 46 ++- .../rules/minimummultiwaycut_ilp.rs | 37 +- .../rules/minimummultiwaycut_qubo.rs | 31 +- .../rules/minimumsetcovering_ilp.rs | 46 ++- .../rules/minimumsummulticenter_ilp.rs | 37 +- .../rules/minimumtardinesssequencing_ilp.rs | 37 +- ...nimumvertexcover_comparativecontainment.rs | 58 ++- .../minimumvertexcover_ensemblecomputation.rs | 92 ++++- .../rules/minimumvertexcover_ilp.rs | 25 +- ...inimumvertexcover_minimumfeedbackarcset.rs | 10 +- ...mumvertexcover_minimumfeedbackvertexset.rs | 14 +- .../minimumvertexcover_minimumhittingset.rs | 10 +- ...nimumvertexcover_minimummaximalmatching.rs | 2 +- ...imumvertexcover_minimumweightandorgraph.rs | 10 +- .../rules/minimumvertexcover_qubo.rs | 25 +- .../rules/minimumweightdecoding_ilp.rs | 28 +- src/unit_tests/rules/minmaxmulticenter_ilp.rs | 37 +- .../rules/mixedchinesepostman_ilp.rs | 37 +- .../rules/monochromatictriangle_ilp.rs | 19 +- .../rules/multiplechoicebranching_ilp.rs | 19 +- .../rules/multiplecopyfileallocation_ilp.rs | 37 +- .../rules/multiprocessorscheduling_ilp.rs | 28 +- src/unit_tests/rules/naesatisfiability_ilp.rs | 19 +- .../rules/naesatisfiability_maxcut.rs | 125 ++++--- ...fiability_partitionintoperfectmatchings.rs | 19 +- .../rules/naesatisfiability_setsplitting.rs | 23 +- ...atching_numericalmatchingwithtargetsums.rs | 19 +- .../numericalmatchingwithtargetsums_ilp.rs | 28 +- .../rules/openshopscheduling_ilp.rs | 46 ++- ...ement_consecutiveonesmatrixaugmentation.rs | 77 +++- .../rules/optimallineararrangement_ilp.rs | 28 +- ...uencingtominimizeweightedcompletiontime.rs | 10 +- .../optimumcommunicationspanningtree_ilp.rs | 37 +- src/unit_tests/rules/paintshop_ilp.rs | 19 +- src/unit_tests/rules/paintshop_qubo.rs | 10 +- .../rules/partiallyorderedknapsack_ilp.rs | 19 +- src/unit_tests/rules/partition_binpacking.rs | 19 +- .../partition_cosineproductintegration.rs | 10 +- .../partition_integralflowwithmultipliers.rs | 14 +- src/unit_tests/rules/partition_knapsack.rs | 19 +- .../partition_multiprocessorscheduling.rs | 10 +- .../rules/partition_openshopscheduling.rs | 157 +++++--- .../rules/partition_productionplanning.rs | 11 +- ...ion_sequencingtominimizetardytaskweight.rs | 147 +++++--- src/unit_tests/rules/partition_subsetsum.rs | 10 +- .../rules/partition_sumofsquarespartition.rs | 62 ++-- .../rules/partitionintocliques_ilp.rs | 10 +- ...ionintocliques_minimumcoveringbycliques.rs | 154 ++++---- ...flength2_boundedcomponentspanningforest.rs | 10 +- .../rules/partitionintopathsoflength2_ilp.rs | 28 +- .../rules/partitionintotriangles_ilp.rs | 28 +- .../rules/pathconstrainednetworkflow_ilp.rs | 10 +- .../precedenceconstrainedscheduling_ilp.rs | 19 +- .../rules/preemptivescheduling_ilp.rs | 28 +- ...rizecollectingsteinerforest_steinertree.rs | 37 +- .../rules/quadraticassignment_ilp.rs | 37 +- src/unit_tests/rules/qubo_casts.rs | 11 +- src/unit_tests/rules/qubo_ilp.rs | 28 +- .../rectilinearpicturecompression_ilp.rs | 19 +- src/unit_tests/rules/reduction_path_parity.rs | 22 +- .../rules/registersufficiency_ilp.rs | 19 +- src/unit_tests/rules/registry.rs | 1 - .../resourceconstrainedscheduling_ilp.rs | 10 +- ...arrangement_rootedtreestorageassignment.rs | 10 +- .../rules/rootedtreestorageassignment_ilp.rs | 20 +- src/unit_tests/rules/ruralpostman_ilp.rs | 28 +- src/unit_tests/rules/sat_circuitsat.rs | 10 +- src/unit_tests/rules/sat_coloring.rs | 55 ++- src/unit_tests/rules/sat_ksat.rs | 19 +- .../rules/sat_maximumindependentset.rs | 154 +++++--- .../rules/sat_minimumdominatingset.rs | 173 +++++---- ...tisfiability_integralflowhomologousarcs.rs | 10 +- .../satisfiability_maximum2satisfiability.rs | 122 ++++--- .../rules/satisfiability_naesatisfiability.rs | 65 +++- .../rules/satisfiability_nontautology.rs | 10 +- ...ingtominimizeweightedcompletiontime_ilp.rs | 46 ++- .../schedulingwithindividualdeadlines_ilp.rs | 28 +- ...cingtominimizemaximumcumulativecost_ilp.rs | 28 +- ...sequencingtominimizetardytaskweight_ilp.rs | 88 ++++- ...ingtominimizeweightedcompletiontime_ilp.rs | 37 +- ...quencingtominimizeweightedtardiness_ilp.rs | 28 +- ...equencingwithdeadlinesandsetuptimes_ilp.rs | 37 +- .../rules/sequencingwithinintervals_ilp.rs | 19 +- ...uencingwithreleasetimesanddeadlines_ilp.rs | 19 +- .../rules/setsplitting_betweenness.rs | 14 +- src/unit_tests/rules/setsplitting_ilp.rs | 19 +- .../rules/shortestcommonsupersequence_ilp.rs | 28 +- .../shortestweightconstrainedpath_ilp.rs | 29 +- .../rules/sparsematrixcompression_ilp.rs | 10 +- src/unit_tests/rules/spinglass_maxcut.rs | 28 +- src/unit_tests/rules/spinglass_qubo.rs | 11 +- src/unit_tests/rules/stackercrane_ilp.rs | 11 +- src/unit_tests/rules/steinertree_ilp.rs | 66 +++- .../rules/stringtostringcorrection_ilp.rs | 28 +- .../strongconnectivityaugmentation_ilp.rs | 28 +- .../rules/subgraphisomorphism_ilp.rs | 28 +- .../rules/subsetsum_closestvectorproblem.rs | 136 +++++-- .../subsetsum_integerexpressionmembership.rs | 22 +- src/unit_tests/rules/subsetsum_partition.rs | 40 ++- .../rules/sumofsquarespartition_ilp.rs | 37 +- .../rules/threedimensionalmatching_ilp.rs | 19 +- ...mensionalmatching_minimumweightdecoding.rs | 46 ++- ...threedimensionalmatching_threepartition.rs | 91 ++++- ...partition_resourceconstrainedscheduling.rs | 10 +- ..._sequencingwithreleasetimesanddeadlines.rs | 10 +- src/unit_tests/rules/timetabledesign_ilp.rs | 19 +- src/unit_tests/rules/traits.rs | 136 +++++-- src/unit_tests/rules/travelingsalesman_ilp.rs | 37 +- .../rules/travelingsalesman_qubo.rs | 65 ++-- .../rules/undirectedflowlowerbounds_ilp.rs | 19 +- .../undirectedtwocommodityintegralflow_ilp.rs | 19 +- src/unit_tests/solvers/registry.rs | 67 ++-- src/unit_tests/solvers/resolver.rs | 64 +++- ...tisfiability_simultaneous_incongruences.rs | 10 +- tests/suites/reductions.rs | 311 +++++++++++++--- .../suites/register_assignment_reductions.rs | 17 +- 591 files changed, 18906 insertions(+), 5065 deletions(-) delete mode 100644 src/rules/ksatisfiability_minimumvertexcover.rs create mode 100644 src/solvers/outcome.rs delete mode 100644 src/unit_tests/rules/ksatisfiability_minimumvertexcover.rs diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index c8eb87e49..f3c25a420 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -109,7 +109,7 @@ make papers-pull # Pull PDFs from shared remote - `src/models/decision.rs` - Generic `Decision

` wrapper converting optimization problems to decision problems - `src/solvers/` - BruteForce reference solver returning problem solutions, ILP solver, decision search (binary search via Decision queries), and the exact-variant solver capability registry. Solver dispatch uses only registered customized implementations and fixed ILP pipelines; reduction-graph reachability does not imply solver availability. Run `pred inspect ` to see the registered capabilities for that instance. - `src/traits.rs` - `Problem` trait -- `src/rules/traits.rs` - `ReduceTo`, `ReduceToAggregate`, `ReductionResult`, `AggregateReductionResult` traits +- `src/rules/traits.rs` - `ReduceTo` and mandatory `ReductionResult::recover_result` - `src/registry/` - Compile-time reduction metadata collection - `problemreductions-cli/` - `pred` CLI tool (separate crate in workspace) - `src/unit_tests/` - Unit test files (mirroring `src/` structure, referenced via `#[path]`) @@ -161,14 +161,14 @@ Max, Min, Sum, Or, And, Extremum, ExtremumSense - `variant_params!` macro implements `Problem::variant()` — e.g., `crate::variant_params![G, W]` for two type params, `crate::variant_params![]` for none (see `src/variant.rs`) - `declare_variants!` proc macro registers concrete type instantiations with best-known complexity and registry-backed load/serialize/solution-solve metadata. One entry per problem may be marked `default`, and variable names in complexity strings are validated against the problem-owned parameter schema. Ordinary models are constructed directly from their construction schema. When user-facing construction differs from persisted JSON, define a model-local `#[derive(CreateSpec)]` DTO plus `TryFrom`, use its generated `FIELDS` in `ProblemSchemaEntry`, and register it with `create LocalSpec`; never add model-name branches in CLI or MCP code. - `decision_problem_meta!` macro registers `DecisionProblemMeta` for a concrete inner type, providing the `DECISION_NAME` constant. -- `register_decision_variant!` macro generates `declare_variants!`, `ProblemSchemaEntry`, and both `ReductionEntry` submissions (witness/aggregate Decision→Opt + Turing Opt→Decision) for a `Decision

` variant. Callers must define inherent getters (`num_vertices()`, `num_edges()`, `k()`) on `Decision

` before invoking. Accepts an explicit structural `category` plus `dims`, `fields`, and `parameter_getters` parameters for problem-specific parameters. +- `register_decision_variant!` macro generates `declare_variants!`, `ProblemSchemaEntry`, and both `ReductionEntry` submissions (complete-result Decision→Opt + Turing Opt→Decision) for a `Decision

` variant. Callers must define inherent getters (`num_vertices()`, `num_edges()`, `k()`) on `Decision

` before invoking. Accepts an explicit structural `category` plus `dims`, `fields`, and `parameter_getters` parameters for problem-specific parameters. - Problems parameterized by graph type `G` and optionally weight type `W` (problem-dependent) - `BruteForce::solve()` returns `Result, SolveError>`; `None` means exhaustive search proved infeasibility - `BruteForce::find_all_witnesses()` is a reference-testing helper for collecting every optimal or satisfying solution -- Each executed witness step constructs one result and shares its witness/value views through `Rc`. Document the rule's domain, witness premise, source guarantee, and infeasibility interpretation; all tied qualifying optima must map correctly. +- Each executed step constructs one result and shares it through `Rc`. Document the rule's domain, witness premise, source guarantee, and infeasibility interpretation; all tied qualifying optima must map correctly. - `SolutionAggregate` belongs to `solvers::BruteForce` witness selection. Models, pure reduction mappings, dynamic evaluation, and non-enumerative solving do not require it. See [executed lifecycle](../docs/src/design.md#executed-reduction-lifecycle). -- `ReductionResult` provides `target_problem()` and `extract_solution()` for witness/config workflows; `AggregateReductionResult` provides `extract_value()` for aggregate/value workflows -- Direct `extract_solution()` maps solutions under the reduction's mathematical premises. `pred extract` has the same witness precondition. Neither validates feasibility or optimality. Transport parses/types inputs; solver orchestration interprets aggregate outcomes before mapping. +- `ReductionResult` provides `target_problem()` and mandatory `recover_result(source, target_outcome)`. Recovery returns typed `Optimal`, `Feasible`, or `Infeasible` outcomes, including solution and evaluation. Each rule handles all statuses explicitly; no optional completion callback or separate value-only path exists. +- `pred solve bundle.json` and `pred extract bundle.json --result target-result.json` use the same complete recovery. External results declare their status; the transport boundary validates target feasibility, while the external solver supplies the optimality claim. Insufficient witness quality is an error, never evidence of source infeasibility. - Decode only the reduction's defined mathematical mapping. Preserve reachable mathematical and representation errors; do not add fallback values or recovery branches for violations already excluded by the calling contract. Explicit mathematical alternatives and sentinels are allowed. - CLI-facing dynamic formatting uses aggregate wrapper names directly (for example `Max(2)`, `Min(None)`, `Or(true)`, or `Sum(56)`) - Graph types: SimpleGraph, PlanarGraph, BipartiteGraph, UnitDiskGraph, KingsSubgraph, TriangularSubgraph @@ -210,9 +210,9 @@ Reduction graph nodes use variant key-value pairs from `Problem::variant()`: - Nodes come from concrete `declare_variants!` registrations - 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 -- `Decision

→ P` supplies witness and aggregate operations on one result (solve optimization, compare to bound, extract when the bound is met); `P → Decision

` is a Turing edge (binary search over decision bound) +- Reduction edges carry `EdgeCapabilities { witness, turing }`; witness mode executes complete-result reductions, and Turing mode describes multi-query procedures +- `#[reduction]` requires one `transform = exact`, `transform = upper_bound`, or `transform = unavailable` declaration and currently registers complete-result reductions; proof-only and Turing edges require manual `ReductionEntry` registration +- `Decision

→ P` recovers the decision result from the optimized inner problem and its bound; `P → Decision

` is a Turing edge (binary search over decision bound) ### Extension Points - New models register dynamic load/serialize metadata through `declare_variants!` and, when finite enumeration exists, register it separately through `register_brute_force!`; neither belongs in CLI match arms @@ -221,7 +221,7 @@ Reduction graph nodes use variant key-value pairs from `Problem::variant()`: - **Each construction input has one name and one concrete type per variant.** Do not add compatibility aliases or infer types from flag names. `CreateSpec` field names render as `snake_case → kebab-case` in CLI and remain `snake_case` in MCP. Add a reusable codec only for a genuinely new transport representation, never a model-name parser branch. - **Random generation is optional and variant-owned.** Not every model has a useful, well-defined random-instance distribution. Add `RandomGenerate` only when the generator has clear semantics and a concrete use (for example, testing or examples); never invent arbitrary bounds or distributions merely to make every model support `--random`. Implement it beside the model (normally through `impl_random_generate!` and a typed `CreateSpec` input DTO), then add `random` only to the applicable `declare_variants!` entries. CLI and MCP discover the exact variant's inputs and callback; never add a model-name random dispatch or advertise random generation on an unsupported variant. - **Decision variants** of optimization problems use `Decision

` wrapper. Add via: (1) `decision_problem_meta!` for the inner type, (2) inherent methods on `Decision`, (3) `register_decision_variant!` with `dims`, `fields`, `parameter_getters`. The generated construction spec accepts flat inner fields plus `bound`; persisted JSON remains `{inner: {...}, bound}`. `Decision

` delegates canonical parameters to `P`; its objective bound is semantic instance data, not a problem parameter. -- Aggregate-only and Turing reduction edges still need manual `ReductionEntry` wiring because `#[reduction]` only registers solution-mapping reductions today; this edge capability does not imply that a problem may solve successfully without a `Solution` +- Proof-only and Turing reduction edges still need manual `ReductionEntry` wiring because `#[reduction]` only registers solution-mapping reductions today; this edge capability does not imply that a problem may solve successfully without a `Solution` - Exact registry dispatch lives in `src/registry/`; alias resolution and partial/default variant resolution live in `problemreductions-cli/src/problem_name.rs` - `pred create` schema-driven dispatch lives in `problemreductions-cli/src/commands/create.rs` (`create_schema_driven()`) - Canonical model examples live in `src/example_db/model_builders.rs`; rule examples live beside their rules and are collected by `src/rules/mod.rs` @@ -244,13 +244,12 @@ fields to issue templates. Changes to issue templates require user approval. ### Reduction and Solver Boundary Follow the canonical [responsibility boundaries](../docs/src/design.md#responsibility-boundaries), -[witness/aggregate contracts](../docs/src/design.md#witness-and-aggregate-reductions), +[complete recovery contracts](../docs/src/design.md#complete-result-recovery), and [validation policy](../docs/src/design.md#validation-evidence). Models own mathematical semantics; rules own construction and witness mappings; adapters own numerical transport, termination interpretation, and returned-target validation. Orchestration uses those results and maps solutions under the rules' premises. -External extraction parses and types submitted witnesses and assumes the rule's mathematical premises. Solver completion interprets the rule's value relationship before mapping; extraction does not validate feasibility or optimality. Fix shared paths and update all callers rather -than adding model-specific branches or independent backend optimality checks. +External recovery parses complete target results and validates target witnesses. Rules own the mathematical interpretation of optimum values, source infeasibility, and insufficient feasible candidates. Solver and CLI callers invoke the same mandatory recovery; they do not add model-specific interpretation branches. In ILP tests, only `ILPSolveError::Infeasible` means infeasibility. Other errors must fail with their details. Solver integration failures must be distinguished diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index ad8527b32..76bbcc984 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -189,6 +189,7 @@ "MaximumLeafSpanningTree": [Maximum Leaf Spanning Tree], "MinimumVertexCover": [Minimum Vertex Cover], "MaxCut": [Max-Cut], + "DecisionMaxCut": [Decision Max-Cut], "GeneralizedHex": [Generalized Hex], "GraphPartitioning": [Graph Partitioning], "HamiltonianCircuit": [Hamiltonian Circuit], @@ -200,7 +201,9 @@ "DirectedHamiltonianPath": [Directed Hamiltonian Path], "IntegralFlowBundles": [Integral Flow with Bundles], "LongestCircuit": [Longest Circuit], + "DecisionLongestCircuit": [Decision Longest Circuit], "LongestPath": [Longest Path], + "DecisionLongestPath": [Decision Longest Path], "ShortestWeightConstrainedPath": [Shortest Weight-Constrained Path], "UndirectedFlowLowerBounds": [Undirected Flow with Lower Bounds], "UndirectedTwoCommodityIntegralFlow": [Undirected Two-Commodity Integral Flow], @@ -213,6 +216,7 @@ "KColoring": [$k$-Coloring], "KClique": [$k$-Clique], "MinimumCoveringByCliques": [Minimum Covering by Cliques], + "DecisionMinimumCoveringByCliques": [Decision Minimum Covering by Cliques], "MinimumIntersectionGraphBasis": [Minimum Intersection Graph Basis], "MinimumDominatingSet": [Minimum Dominating Set], "MinimumGeometricConnectedDominatingSet": [Minimum Geometric Connected Dominating Set], @@ -236,7 +240,9 @@ "SetSplitting": [Set Splitting], "MinimumCardinalityKey": [Minimum Cardinality Key], "SpinGlass": [Spin Glass], + "DecisionSpinGlass": [Decision Spin Glass], "QUBO": [QUBO], + "DecisionQUBO": [Decision QUBO], "ILP": [Integer Linear Programming], "IntegerKnapsack": [Integer Knapsack], "Knapsack": [Knapsack], @@ -245,6 +251,7 @@ "NAESatisfiability": [NAE-SAT], "KSatisfiability": [$k$-SAT], "Maximum2Satisfiability": [Maximum 2-Satisfiability], + "DecisionMaximum2Satisfiability": [Decision Maximum 2-Satisfiability], "NonTautology": [Non-Tautology], "OneInThreeSatisfiability": [1-in-3 SAT], "Planar3Satisfiability": [Planar 3-SAT], @@ -266,14 +273,17 @@ "CapacityAssignment": [Capacity Assignment], "ConsistencyOfDatabaseFrequencyTables": [Consistency of Database Frequency Tables], "ClosestVectorProblem": [Closest Vector Problem], + "DecisionClosestVectorProblem": [Decision Closest Vector Problem], "ConsecutiveSets": [Consecutive Sets], "DisjointConnectingPaths": [Disjoint Connecting Paths], "MinimumMultiwayCut": [Minimum Multiway Cut], "OptimalLinearArrangement": [Optimal Linear Arrangement], "RootedTreeArrangement": [Rooted Tree Arrangement], "RuralPostman": [Rural Postman], + "DecisionRuralPostman": [Decision Rural Postman], "MixedChinesePostman": [Mixed Chinese Postman], "StackerCrane": [Stacker Crane], + "DecisionStackerCrane": [Decision Stacker Crane], "LongestCommonSubsequence": [Longest Common Subsequence], "ClosestString": [Closest String], "ClosestSubstring": [Closest Substring], @@ -307,14 +317,17 @@ "IntegralFlowHomologousArcs": [Integral Flow with Homologous Arcs], "IntegralFlowWithMultipliers": [Integral Flow With Multipliers], "MinMaxMulticenter": [Min-Max Multicenter], + "DecisionMinMaxMulticenter": [Decision Min-Max Multicenter], "FlowShopScheduling": [Flow Shop Scheduling], "JobShopScheduling": [Job-Shop Scheduling], "OpenShopScheduling": [Open Shop Scheduling], + "DecisionOpenShopScheduling": [Decision Open Shop Scheduling], "GroupingBySwapping": [Grouping by Swapping], "IntegerExpressionMembership": [Integer Expression Membership], "MinimumCutIntoBoundedSets": [Minimum Cut Into Bounded Sets], "MinimumDummyActivitiesPert": [Minimum Dummy Activities in PERT Networks], "MinimumSumMulticenter": [Minimum Sum Multicenter], + "DecisionMinimumSumMulticenter": [Decision Minimum Sum Multicenter], "MinimumTardinessSequencing": [Minimum Tardiness Sequencing], "MonochromaticTriangle": [Monochromatic Triangle], "MultipleChoiceBranching": [Multiple Choice Branching], @@ -333,6 +346,7 @@ "PreemptiveScheduling": [Preemptive Scheduling], "PrimeAttributeName": [Prime Attribute Name], "QuadraticAssignment": [Quadratic Assignment], + "DecisionQuadraticAssignment": [Decision Quadratic Assignment], "EquilibriumPoint": [Equilibrium Point], "QuadraticCongruences": [Quadratic Congruences], "QuadraticDiophantineEquations": [Quadratic Diophantine Equations], @@ -360,6 +374,7 @@ "SchedulingWithIndividualDeadlines": [Scheduling With Individual Deadlines], "SequencingToMinimizeMaximumCumulativeCost": [Sequencing to Minimize Maximum Cumulative Cost], "SequencingToMinimizeTardyTaskWeight": [Sequencing to Minimize Tardy Task Weight], + "DecisionSequencingToMinimizeTardyTaskWeight": [Decision Sequencing to Minimize Tardy Task Weight], "SequencingToMinimizeWeightedCompletionTime": [Sequencing to Minimize Weighted Completion Time], "SequencingToMinimizeWeightedTardiness": [Sequencing to Minimize Weighted Tardiness], "SequencingWithDeadlinesAndSetUpTimes": [Sequencing with Deadlines and Set-Up Times], @@ -11475,14 +11490,21 @@ the displayed rule, extracted from the corresponding `pred path` entry. _Solution extraction._ For IS solution $S$, return $C = V backslash S$, i.e.\ flip each variable: $c_v = 1 - s_v$. ] +The decision reductions below construct `Decision`: the target contains an +optimization instance `inner` and a bound `bound`. Feasibility requires an inner +solution whose objective is at most the bound for minimization, or at least the +bound for maximization. A numeric objective discussed in a proof refers to +`inner`; the decision target itself returns YES or NO. The bound is serialized +with the target, rather than stored separately by solution extraction. + #let dmds_mmmc = load-example( "DecisionMinimumDominatingSet", - "MinMaxMulticenter", + "DecisionMinMaxMulticenter", source-variant: (graph: "SimpleGraph", weight: "One"), target-variant: (graph: "SimpleGraph", weight: "One"), ) #let dmds_mmmc_sol = dmds_mmmc.solutions.at(0) -#reduction-rule("DecisionMinimumDominatingSet", "MinMaxMulticenter", +#reduction-rule("DecisionMinimumDominatingSet", "DecisionMinMaxMulticenter", example: true, example-source-variant: (graph: "SimpleGraph", weight: "One"), example-target-variant: (graph: "SimpleGraph", weight: "One"), @@ -11496,28 +11518,28 @@ the displayed rule, extracted from the corresponding `pred path` entry. ) *Step 1 -- Source instance.* The source graph has vertices ${0, 1, 2, 3, 4, 5}$, edges #{dmds_mmmc.source.instance.inner.graph.edges.map(e => $(#e.at(0), #e.at(1))$).join(", ")}, and bound $K = #dmds_mmmc.source.instance.bound$. The stored dominating-set witness is $D = {#dmds_mmmc_sol.source_config.enumerate().filter(((i, x)) => x).map(((i, _)) => str(i)).join(", ")}$. - *Step 2 -- Build the target instance.* Append two isolated vertices, assign weight $1$ to every vertex and length $1$ to every edge, and set the number of centers to $k = #dmds_mmmc.target.instance.k$. The target therefore has $#graph-num-vertices(dmds_mmmc.target.instance)$ vertices and $#graph-num-edges(dmds_mmmc.target.instance)$ edges. + *Step 2 -- Build the target instance.* Append two isolated vertices, assign weight $1$ to every vertex and length $1$ to every edge, and set the number of centers to $k = #dmds_mmmc.target.instance.inner.k$. The target therefore has $#graph-num-vertices(dmds_mmmc.target.instance.inner)$ vertices and $#graph-num-edges(dmds_mmmc.target.instance.inner)$ edges. *Step 3 -- Verify a witness.* Choosing centers $P = {#dmds_mmmc_sol.target_config.enumerate().filter(((i, x)) => x).map(((i, _)) => str(i)).join(", ")}$ yields distances $(0, 1, 1, 0, 1, 1, 0, 0)$ to the nearest center, so the maximum weighted distance is $1$. Discarding the two auxiliary center bits recovers a dominating set of size $2$ #sym.checkmark ], )[ - 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. + 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 includes the radius bound in the target decision instance. ][ _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. - _Solution extraction and NO instances._ Evaluate the full target indicator first. A finite radius at most $1$ permits extraction of its first $n$ bits. Any larger radius or infeasible placement is rejected. The formal aggregate map sends an optimum $r<=1$ to true, and an optimum $r>1$ or infeasibility to false. In particular, a four-vertex path with $K=1$ produces optimum radius $2$, not an infeasible target. Checked parameter arithmetic precedes allocation; unrepresentable counts return the formal numeric error. Target sizes are exactly $n+2$ vertices and $m$ edge records. + _Solution extraction and NO instances._ Evaluate the full target indicator first. A finite radius at most $1$ permits extraction of its first $n$ bits. Any larger radius or infeasible placement is rejected. The target bound is $1$. Its evaluation accepts exactly the placements of finite radius at most $1$; the aggregate map preserves this Boolean value. In particular, a four-vertex path with $K=1$ has inner optimum radius $2$; with target bound $1$, the decision target is infeasible. Checked parameter arithmetic precedes allocation; unrepresentable counts return the formal numeric error. Target sizes are exactly $n+2$ vertices and $m$ edge records. ] #let dmds_msmc = load-example( "DecisionMinimumDominatingSet", - "MinimumSumMulticenter", + "DecisionMinimumSumMulticenter", source-variant: (graph: "SimpleGraph", weight: "One"), target-variant: (graph: "SimpleGraph", weight: "i64"), ) #let dmds_msmc_sol = dmds_msmc.solutions.at(0) -#reduction-rule("DecisionMinimumDominatingSet", "MinimumSumMulticenter", +#reduction-rule("DecisionMinimumDominatingSet", "DecisionMinimumSumMulticenter", example: true, example-source-variant: (graph: "SimpleGraph", weight: "One"), example-target-variant: (graph: "SimpleGraph", weight: "i64"), @@ -11531,7 +11553,7 @@ the displayed rule, extracted from the corresponding `pred path` entry. ) *Step 1 -- Source instance.* The source graph has vertices ${0, 1, 2, 3, 4, 5}$, edges #{dmds_msmc.source.instance.inner.graph.edges.map(e => $(#e.at(0), #e.at(1))$).join(", ")}, and decision bound $K = #dmds_msmc.source.instance.bound$. The stored dominating-set witness is $D = {#dmds_msmc_sol.source_config.enumerate().filter(((i, x)) => x).map(((i, _)) => str(i)).join(", ")}$. - *Step 2 -- Build the target instance.* Add one isolated vertex $z$, assign vertex weight $1$ everywhere, assign edge length $1$ everywhere, and set the target center count to $k = #dmds_msmc.target.instance.k$. The comparison threshold is $B = |V| - K = 6 - 2 = 4$. + *Step 2 -- Build the target instance.* Add one isolated vertex $z$, assign vertex weight $1$ everywhere, assign edge length $1$ everywhere, and set the target center count to $k = #dmds_msmc.target.instance.inner.k$. The comparison threshold is $B = |V| - K = 6 - 2 = 4$. *Step 3 -- Verify a witness.* Choosing centers $P = {#dmds_msmc_sol.target_config.enumerate().filter(((i, x)) => x).map(((i, _)) => str(i)).join(", ")}$ yields distances $(0, 1, 1, 0, 1, 1, 0)$ to the nearest center, so the total weighted distance is $4 = B$. The extracted source witness removes the coordinate of $z$, hence a valid YES witness for the original decision instance #sym.checkmark ], @@ -11978,9 +12000,9 @@ The _penalty method_ @glover2019 @lucas2014 converts a constrained optimization $ f(bold(x)) = "obj"(bold(x)) + P sum_k g_k (bold(x))^2 $ where $P$ is a penalty weight large enough that any constraint violation costs more than the entire objective range. Since $g_k (bold(x))^2 >= 0$ with equality iff $g_k (bold(x)) = 0$, minimizers of $f$ are feasible and optimal for the original problem. Because binary variables satisfy $x_i^2 = x_i$, the resulting $f$ is a quadratic in $bold(x)$, i.e.\ a QUBO. -#let kc_qubo = load-example("KColoring", "QUBO") +#let kc_qubo = load-example("KColoring", "DecisionQUBO") #let kc_qubo_sol = kc_qubo.solutions.at(0) -#reduction-rule("KColoring", "QUBO", +#reduction-rule("KColoring", "DecisionQUBO", example: true, example-caption: [House graph ($n = 5$, $|E| = 6$, $chi = 3$) with $k = 3$ colors], extra: [ @@ -12049,7 +12071,7 @@ where $P$ is a penalty weight large enough that any constraint violation costs m _Solution extraction._ Return $bold(x)$ directly. There are exactly $m$ target variables. ] -#reduction-rule("KSatisfiability", "QUBO")[ +#reduction-rule("KSatisfiability", "DecisionQUBO")[ Clause falsification penalties become a quadratic objective using Rosenberg quadratization. Retain its omitted constant to decode the SAT decision, rather than interpreting an arbitrary QUBO configuration as a satisfying assignment. ][ _Construction._ Let $n$ be the number of source variables and $m$ the clause count. For each literal let $y$ be its falsity indicator: $y=1-x$ for a positive literal and $y=x$ for a negative one. For widths zero, one and two, the clause penalty is respectively $1$, $y_1$, and $y_1 y_2$. For width three use @@ -12168,17 +12190,17 @@ where $P$ is a penalty weight large enough that any constraint violation costs m ] #{ - let ss-cvp = load-example("SubsetSum", "ClosestVectorProblem") + let ss-cvp = load-example("SubsetSum", "DecisionClosestVectorProblem") let ss-cvp-sol = ss-cvp.solutions.at(0) let ss-cvp-sizes = ss-cvp.source.instance.sizes let ss-cvp-target = ss-cvp.source.instance.target - let ss-cvp-basis = ss-cvp.target.instance.basis - let ss-cvp-target-vec = ss-cvp.target.instance.target + let ss-cvp-basis = ss-cvp.target.instance.inner.basis + let ss-cvp-target-vec = ss-cvp.target.instance.inner.target let ss-cvp-n = ss-cvp-sizes.len() let ss-cvp-x = ss-cvp-sol.target_config let to-mat(m) = math.mat(..m.map(row => row.map(v => $#v$))) [ - #reduction-rule("SubsetSum", "ClosestVectorProblem", + #reduction-rule("SubsetSum", "DecisionClosestVectorProblem", example: true, example-caption: [#ss-cvp-n elements, target sum $B = #ss-cvp-target$], extra: [ @@ -12644,9 +12666,9 @@ where $P$ is a penalty weight large enough that any constraint violation costs m == Non-Trivial Reductions -#let sat_mis = load-example("Satisfiability", "MaximumIndependentSet") +#let sat_mis = load-example("Satisfiability", "DecisionMaximumIndependentSet") #let sat_mis_sol = sat_mis.solutions.at(0) -#reduction-rule("Satisfiability", "MaximumIndependentSet", +#reduction-rule("Satisfiability", "DecisionMaximumIndependentSet", example: true, example-caption: [3-SAT with 5 variables and 7 clauses], extra: [ @@ -12657,7 +12679,7 @@ where $P$ is a penalty weight large enough that any constraint violation costs m "pred evaluate sat.json --config " + cli-config(sat_mis_sol.source_config), ) SAT assignment: $(x_1, ..., x_5) = (#fmt-values(sat_mis_sol.source_config))$ \ - IS graph: #graph-num-vertices(sat_mis.target.instance) vertices ($= 3 times #sat-num-clauses(sat_mis.source.instance)$ literals), #graph-num-edges(sat_mis.target.instance) edges \ + IS graph: #graph-num-vertices(sat_mis.target.instance.inner) vertices ($= 3 times #sat-num-clauses(sat_mis.source.instance)$ literals), #graph-num-edges(sat_mis.target.instance.inner) edges \ IS of size #sat-num-clauses(sat_mis.source.instance) $= m$: one vertex per clause $arrow.r$ satisfying assignment #sym.checkmark ], )[ @@ -12704,9 +12726,9 @@ where $P$ is a penalty weight large enough that any constraint violation costs m _Solution extraction._ Set $x_i = 1$ iff $"color"("pos"_i) = "color"("TRUE")$. ] -#let sat_ds = load-example("Satisfiability", "MinimumDominatingSet") +#let sat_ds = load-example("Satisfiability", "DecisionMinimumDominatingSet") #let sat_ds_sol = sat_ds.solutions.at(0) -#reduction-rule("Satisfiability", "MinimumDominatingSet", +#reduction-rule("Satisfiability", "DecisionMinimumDominatingSet", example: true, example-caption: [5-variable 7-clause 3-SAT to dominating set], extra: [ @@ -12717,7 +12739,7 @@ where $P$ is a penalty weight large enough that any constraint violation costs m "pred evaluate sat.json --config " + cli-config(sat_ds_sol.source_config), ) SAT assignment: $(x_1, ..., x_5) = (#fmt-values(sat_ds_sol.source_config))$ \ - Vertex structure: $#graph-num-vertices(sat_ds.target.instance) = 3 times #sat_ds.source.instance.num_vars + #sat-num-clauses(sat_ds.source.instance)$ (variable triangles + clause vertices) \ + Vertex structure: $#graph-num-vertices(sat_ds.target.instance.inner) = 3 times #sat_ds.source.instance.num_vars + #sat-num-clauses(sat_ds.source.instance)$ (variable triangles + clause vertices) \ Dominating set of size $n = #sat_ds.source.instance.num_vars$: one vertex per variable triangle #sym.checkmark ], )[ @@ -12819,9 +12841,9 @@ where $P$ is a penalty weight large enough that any constraint violation costs m _Solution extraction._ Discard auxiliary variables; return original variable assignments. ] -#let sat_max2sat = load-example("Satisfiability", "Maximum2Satisfiability") +#let sat_max2sat = load-example("Satisfiability", "DecisionMaximum2Satisfiability") #let sat_max2sat_sol = sat_max2sat.solutions.at(0) -#reduction-rule("Satisfiability", "Maximum2Satisfiability", +#reduction-rule("Satisfiability", "DecisionMaximum2Satisfiability", example: true, example-caption: [3-variable 2-clause SAT to MAX-2-SAT], extra: [ @@ -12845,7 +12867,7 @@ where $P$ is a penalty weight large enough that any constraint violation costs m $ The normalized formula therefore has $4$ variables and $3$ clauses. - *Step 3 -- Build the MAX-2-SAT gadgets.* Introduce one gadget variable per normalized clause, so the target has $#sat_max2sat.target.instance.num_vars$ variables and #sat_max2sat.target.instance.clauses.len() clauses. The stored witness is $(x_1, x_2, x_3, y_1, w_1, w_2, w_3) = (#fmt-values(sat_max2sat_sol.target_config))$. With $(y_1, w_1, w_2, w_3) = (0, 1, 0, 1)$, each of the three gadgets satisfies exactly $7$ clauses, so the target objective reaches $21 = 7 times 3$ #sym.checkmark. + *Step 3 -- Build the MAX-2-SAT gadgets.* Introduce one gadget variable per normalized clause, so the target has $#sat_max2sat.target.instance.inner.num_vars$ variables and #sat_max2sat.target.instance.inner.clauses.len() clauses. The stored witness is $(x_1, x_2, x_3, y_1, w_1, w_2, w_3) = (#fmt-values(sat_max2sat_sol.target_config))$. With $(y_1, w_1, w_2, w_3) = (0, 1, 0, 1)$, each of the three gadgets satisfies exactly $7$ clauses, so the target objective reaches $21 = 7 times 3$ #sym.checkmark. *Multiplicity:* The fixture stores one canonical optimum. Auxiliary variables such as $y_1$ can vary across optimal witnesses, but truncating any optimal target assignment to the first $3$ coordinates still yields a satisfying assignment of the original SAT formula. ], @@ -12959,9 +12981,9 @@ where $P$ is a penalty weight large enough that any constraint violation costs m _Solution extraction._ Return the values of the named circuit variables and discard the auxiliary Tseitin variables. ] -#let cs_sg = load-example("CircuitSAT", "SpinGlass") +#let cs_sg = load-example("CircuitSAT", "DecisionSpinGlass") #let cs_sg_sol = cs_sg.solutions.at(0) -#reduction-rule("CircuitSAT", "SpinGlass", +#reduction-rule("CircuitSAT", "DecisionSpinGlass", example: true, example-caption: [1-bit full adder to Ising model], extra: [ @@ -12972,7 +12994,7 @@ where $P$ is a penalty weight large enough that any constraint violation costs m "pred evaluate circuitsat.json --config " + cli-config(cs_sg_sol.source_config), ) Circuit: #circuit-num-gates(cs_sg.source.instance) gates (2 XOR, 2 AND, 1 OR), #circuit-num-variables(cs_sg.source.instance) variables \ - Target: #spin-num-spins(cs_sg.target.instance) spins (each gate allocates I/O + auxiliary spins) \ + Target: #spin-num-spins(cs_sg.target.instance.inner) spins (each gate allocates I/O + auxiliary spins) \ Canonical ground-state witness shown ($2^3$ valid input combinations exist for the full adder) #sym.checkmark ], )[ @@ -14529,7 +14551,7 @@ The following reductions to Integer Linear Programming are straightforward formu *Step 4 -- Target optimum.* The fixture selects the matrix-domination set $C = {(0, 2), (1, 3)}$ at 1-entry indices #selected-target.map(i => str(i)).join(", "). Every other 1-entry of $M$ shares row $0$ or row $1$ with one of the chosen entries, so $C$ is dominating with $|C| = #selected-target.len() = 2 = "mm"(B) #sym.checkmark$. - *Extraction.* For this fixture the selected 1-entries happen to be pairwise independent (no shared row or column), so they already correspond to a matching of $B$. In general a matrix-domination witness only maps back to an edge dominating set $F_C subset.eq F$ that need not be a matching: for instance, the optimal witness $C = {(0, 3), (0, 4)}$ corresponds to source edges ${(l_0, r_1), (l_0, r_2)}$, which share endpoint $l_0$. In such cases the polynomial-time Yannakakis-Gavril transformation @yannakakis1980 -- implemented in `extract_solution` as a sequence of drop / swap moves on $F_C$ -- converts $F_C$ into a maximal matching of $B$ of the same or smaller size, here e.g. ${(l_0, r_0), (l_1, r_1)}$ or ${(l_0, r_1), (l_1, r_2)}$. + *Extraction.* For this fixture the selected 1-entries happen to be pairwise independent (no shared row or column), so they already correspond to a matching of $B$. In general a matrix-domination witness only maps back to an edge dominating set $F_C subset.eq F$ that need not be a matching: for instance, the optimal witness $C = {(0, 3), (0, 4)}$ corresponds to source edges ${(l_0, r_1), (l_0, r_2)}$, which share endpoint $l_0$. In such cases the polynomial-time Yannakakis-Gavril transformation @yannakakis1980 -- implemented in `recover_result` as a sequence of drop / swap moves on $F_C$ -- converts $F_C$ into a maximal matching of $B$ of the same or smaller size, here e.g. ${(l_0, r_0), (l_1, r_1)}$ or ${(l_0, r_1), (l_1, r_2)}$. ] } ], @@ -14555,7 +14577,7 @@ The following reductions to Integer Linear Programming are straightforward formu ($arrow.l.double$) A dominating set $C$ of $M$ with $|C| <= K'$ corresponds to an EDS $F_C$ of size $|C| <= K' = K$. Applying the polynomial-time Yannakakis-Gavril transformation to $F_C$ yields a maximal matching of $B$ of the same size, so $"mm"(B) <= K$. - _Solution extraction._ Read the selected 1-entries of the matrix-domination witness, map each $(i, m + j)$ back to the bipartite edge $(l_i, r_j)$ to obtain an EDS $F_C$ of $B$. Arbitrary optimal MMD witnesses may select 1-entries whose corresponding source edges form a connected subgraph (e.g. two edges sharing a left endpoint) rather than a matching; in that case the polynomial-time Yannakakis-Gavril EDS-to-IEDS transformation iteratively resolves each adjacent pair in $F_C$ by either dropping a redundant edge or swapping it for an edge whose new endpoint lies outside the current vertex cover, terminating in $O(|F|^3)$ time. The result is a maximal matching $M^*$ of $B$ with $|M^*| <= |F_C|$, which `extract_solution` returns as the source-side configuration. + _Solution extraction._ Read the selected 1-entries of the matrix-domination witness, map each $(i, m + j)$ back to the bipartite edge $(l_i, r_j)$ to obtain an EDS $F_C$ of $B$. Arbitrary optimal MMD witnesses may select 1-entries whose corresponding source edges form a connected subgraph (e.g. two edges sharing a left endpoint) rather than a matching; in that case the polynomial-time Yannakakis-Gavril EDS-to-IEDS transformation iteratively resolves each adjacent pair in $F_C$ by either dropping a redundant edge or swapping it for an edge whose new endpoint lies outside the current vertex cover, terminating in $O(|F|^3)$ time. The result is a maximal matching $M^*$ of $B$ with $|M^*| <= |F_C|$, which `recover_result` returns as the source-side configuration. _Note on source variant._ The reduction crucially requires the source graph to be bipartite. The biadjacency matrix faithfully represents the edge structure of $B$ (each edge contributes exactly one 1-entry). The adjacency matrix of a general undirected graph would produce two symmetric 1-entries per edge that do not preserve the row/column sharing pattern. ] @@ -15504,14 +15526,14 @@ The following reductions to Integer Linear Programming are straightforward formu _Solution extraction._ Evaluate the target once, reject an infeasible assignment, and select precisely the stored edges whose edge-use block contains a one. Parallel edges keep their individual identities. ] -#let hc_lc = load-example("HamiltonianCircuit", "LongestCircuit") +#let hc_lc = load-example("HamiltonianCircuit", "DecisionLongestCircuit") #let hc_lc_sol = hc_lc.solutions.at(0) #let hc_lc_n = graph-num-vertices(hc_lc.source.instance) #let hc_lc_source_edges = hc_lc.source.instance.graph.edges -#let hc_lc_target_edges = hc_lc.target.instance.graph.edges -#let hc_lc_target_weights = hc_lc.target.instance.edge_lengths +#let hc_lc_target_edges = hc_lc.target.instance.inner.graph.edges +#let hc_lc_target_weights = hc_lc.target.instance.inner.edge_lengths #let hc_lc_selected_edges = hc_lc_target_edges.enumerate().filter(((i, _)) => hc_lc_sol.target_config.at(i)).map(((i, e)) => (e.at(0), e.at(1))) -#reduction-rule("HamiltonianCircuit", "LongestCircuit", +#reduction-rule("HamiltonianCircuit", "DecisionLongestCircuit", example: true, example-caption: [Cycle graph on $#hc_lc_n$ vertices with unit edge lengths], extra: [ @@ -16636,22 +16658,22 @@ The following table shows concrete target-variable counts for example instances, ), (source: "QUBO", target: "SpinGlass"), (source: "ClosestVectorProblem", target: "QUBO"), - (source: "KColoring", target: "QUBO"), + (source: "KColoring", target: "DecisionQUBO"), (source: "MaximumSetPacking", target: "QUBO"), ( source: "KSatisfiability", - target: "QUBO", + target: "DecisionQUBO", source-variant: (k: "K3"), target-variant: (weight: "i64"), ), (source: "ILP", target: "QUBO"), - (source: "Satisfiability", target: "MaximumIndependentSet"), - (source: "Satisfiability", target: "Maximum2Satisfiability"), + (source: "Satisfiability", target: "DecisionMaximumIndependentSet"), + (source: "Satisfiability", target: "DecisionMaximum2Satisfiability"), (source: "Satisfiability", target: "KColoring"), - (source: "Satisfiability", target: "MinimumDominatingSet"), + (source: "Satisfiability", target: "DecisionMinimumDominatingSet"), (source: "Satisfiability", target: "KSatisfiability"), (source: "CircuitSAT", target: "Satisfiability"), - (source: "CircuitSAT", target: "SpinGlass"), + (source: "CircuitSAT", target: "DecisionSpinGlass"), (source: "Factoring", target: "CircuitSAT"), (source: "MaximumSetPacking", target: "ILP"), (source: "MaximumMatching", target: "ILP"), @@ -17201,47 +17223,6 @@ The following table shows concrete target-variable counts for example instances, _Solution extraction._ Given a Hamiltonian circuit witness, inspect the two endpoints of each source vertex-path. Set $x_v = 1$ iff both path endpoints are adjacent to selector vertices in the cycle; otherwise set $x_v = 0$. The resulting indicator vector is a valid source-side vertex cover. ] -#let ksat_mvc = load-example("KSatisfiability", "MinimumVertexCover") -#let ksat_mvc_sol = ksat_mvc.solutions.at(0) -#reduction-rule("KSatisfiability", "MinimumVertexCover", - example: true, - example-caption: [3-SAT with $n = #ksat_mvc.source.instance.num_vars$ variables, $m = #sat-num-clauses(ksat_mvc.source.instance)$ clauses], - extra: [ - #pred-commands( - "pred create --example " + problem-spec(ksat_mvc.source) + " -o ksat.json", - "pred reduce ksat.json --via route.json -o bundle.json", - "pred solve bundle.json", - "pred evaluate ksat.json --config " + cli-config(ksat_mvc_sol.source_config), - ) - - *Step 1 -- Source instance.* The 3-SAT formula has $n = #ksat_mvc.source.instance.num_vars$ variables and $m = #sat-num-clauses(ksat_mvc.source.instance)$ clauses: #{ksat_mvc.source.instance.clauses.enumerate().map(((j, c)) => { - let lits = c.literals.map(l => if l > 0 { $x_#l$ } else { $overline(x)_#calc.abs(l)$ }) - [$c_#j = (#lits.join($or$))$] - }).join(", ")}. A satisfying assignment is $(#fmt-values(ksat_mvc_sol.source_config))$, i.e.\ #{range(ksat_mvc.source.instance.num_vars).map(i => { - let v = ksat_mvc_sol.source_config.at(i) - if v { $x_#(i+1) = 1$ } else { $x_#(i+1) = 0$ } - }).join(", ")}. - - *Step 2 -- Truth-setting edges.* For each variable $x_i$, create vertices $u_i$ (index $2(i-1)$) and $overline(u)_i$ (index $2(i-1)+1$) connected by a truth-setting edge. This gives $2n = #(2 * ksat_mvc.source.instance.num_vars)$ literal vertices and $n = #ksat_mvc.source.instance.num_vars$ edges. - - *Step 3 -- Clause triangles and communication edges.* For each clause $c_j$, create a triangle of 3 vertices at indices $2n + 3j, 2n + 3j + 1, 2n + 3j + 2$, connected by 3 internal edges. Each triangle vertex $t^j_k$ is also connected to its literal vertex by a communication edge (3 per clause). Total: $3m = #(3 * sat-num-clauses(ksat_mvc.source.instance))$ clause vertices, $3m = #(3 * sat-num-clauses(ksat_mvc.source.instance))$ triangle edges, $3m = #(3 * sat-num-clauses(ksat_mvc.source.instance))$ communication edges. - - *Step 4 -- Target graph dimensions.* The resulting graph has $|V| = 2n + 3m = #ksat_mvc.target.instance.graph.num_vertices$ vertices and $|E| = n + 6m = #ksat_mvc.target.instance.graph.edges.len()$ edges, with unit weights. - - *Step 5 -- Verify a solution.* The satisfying assignment $(#fmt-values(ksat_mvc_sol.source_config))$ maps to a vertex cover of size $n + 2m = #(ksat_mvc.source.instance.num_vars + 2 * sat-num-clauses(ksat_mvc.source.instance))$. The target configuration is $(#fmt-values(ksat_mvc_sol.target_config))$: the cover selects #ksat_mvc_sol.target_config.filter(x => x).len() vertices. For each truth-setting edge, exactly one endpoint is in the cover #sym.checkmark. For each clause triangle, exactly two of three vertices are covered #sym.checkmark. Each communication edge has at least one endpoint in the cover #sym.checkmark. - - *Multiplicity:* The fixture stores one canonical witness. Other valid covers correspond to different satisfying assignments of the formula. - ], -)[ - Each variable contributes a truth-setting edge; each clause contributes a satisfaction-testing triangle. The formula is satisfiable iff the graph has a vertex cover of size $n + 2m$. -][ - _Construction._ Given 3-CNF $phi$ with $n$ variables and $m$ clauses, construct $G = (V, E)$ with $|V| = 2n + 3m$. For each variable $x_i$: vertices $u_i$ (index $2i$) and $overline(u)_i$ (index $2i+1$) with edge $(u_i, overline(u)_i)$. For each clause $c_j$: triangle vertices $t^j_0, t^j_1, t^j_2$ at indices $2n + 3j, 2n+3j+1, 2n+3j+2$. Communication edges connect each $t^j_k$ to the literal vertex of its $k$-th literal. - - _Correctness._ ($arrow.r.double$) A satisfying assignment selects literal vertices ($n$ total) and two triangle vertices per clause ($2m$ total), covering all edges. ($arrow.l.double$) A cover of size $n + 2m$ must include exactly one literal vertex per variable and two triangle vertices per clause; the uncovered triangle vertex's communication edge forces the corresponding literal to be true. - - _Solution extraction._ For variable $x_i$, set $x_i = 1$ if the cover indicator at position $2i$ is 1. -] - #let ksat_mono = load-example("KSatisfiability", "MonochromaticTriangle") #let ksat_mono_sol = ksat_mono.solutions.at(0) #reduction-rule("KSatisfiability", "MonochromaticTriangle", @@ -17778,13 +17759,13 @@ The following table shows concrete target-variable counts for example instances, _Solution extraction._ Follow unique successors from vertex 0 to recover the Hamiltonian permutation. ] -#let hc_sc = load-example("HamiltonianCircuit", "StackerCrane") +#let hc_sc = load-example("HamiltonianCircuit", "DecisionStackerCrane") #let hc_sc_sol = hc_sc.solutions.at(0) #let hc_sc_n = graph-num-vertices(hc_sc.source.instance) #let hc_sc_source_edges = hc_sc.source.instance.graph.edges -#let hc_sc_target_arcs = hc_sc.target.instance.arcs -#let hc_sc_target_edges = hc_sc.target.instance.edges -#reduction-rule("HamiltonianCircuit", "StackerCrane", +#let hc_sc_target_arcs = hc_sc.target.instance.inner.arcs +#let hc_sc_target_edges = hc_sc.target.instance.inner.edges +#reduction-rule("HamiltonianCircuit", "DecisionStackerCrane", example: true, example-caption: [Cycle $C_#hc_sc_n$ ($n = #hc_sc_n$): vertex splitting to Stacker Crane], extra: [ @@ -17797,7 +17778,7 @@ The following table shows concrete target-variable counts for example instances, *Step 1 -- Source instance.* The canonical source fixture is the cycle $C_#hc_sc_n$ on vertices ${0, dots, #(hc_sc_n - 1)}$ with #hc_sc_source_edges.len() edges: #hc_sc_source_edges.map(e => $(#e.at(0), #e.at(1))$).join(", "). The stored Hamiltonian-circuit witness is the permutation $[#fmt-values(hc_sc_sol.source_config)]$.\ - *Step 2 -- Construction.* Each vertex $v_i$ splits into $v_i^"in" = 2i$ and $v_i^"out" = 2i + 1$, giving $2 dot #hc_sc_n = #hc_sc.target.instance.num_vertices$ vertices. The reduction creates #hc_sc_target_arcs.len() mandatory arcs: #hc_sc_target_arcs.map(a => $(#a.at(0) arrow #a.at(1))$).join(", "), each of length 1. For each source edge, two undirected connector edges of length 1 are added, giving $2 dot #hc_sc_source_edges.len() = #hc_sc_target_edges.len()$ connector edges: #hc_sc_target_edges.map(e => ${#e.at(0), #e.at(1)}$).join(", ").\ + *Step 2 -- Construction.* Each vertex $v_i$ splits into $v_i^"in" = 2i$ and $v_i^"out" = 2i + 1$, giving $2 dot #hc_sc_n = #hc_sc.target.instance.inner.num_vertices$ vertices. The reduction creates #hc_sc_target_arcs.len() mandatory arcs: #hc_sc_target_arcs.map(a => $(#a.at(0) arrow #a.at(1))$).join(", "), each of length 1. For each source edge, two undirected connector edges of length 1 are added, giving $2 dot #hc_sc_source_edges.len() = #hc_sc_target_edges.len()$ connector edges: #hc_sc_target_edges.map(e => ${#e.at(0), #e.at(1)}$).join(", ").\ *Step 3 -- Verify a solution.* The stored target configuration $[#fmt-values(hc_sc_sol.target_config)]$ is a permutation of arcs. Following this order: arc #hc_sc_sol.target_config.at(0) serves $(#hc_sc_target_arcs.at(hc_sc_sol.target_config.at(0)).at(0) arrow #hc_sc_target_arcs.at(hc_sc_sol.target_config.at(0)).at(1))$, then a connector edge leads to the next arc, and so on. The tour traverses $#hc_sc_target_arcs.len()$ arcs (cost $#hc_sc_target_arcs.len()$) and $#hc_sc_target_arcs.len()$ connector edges (cost $#hc_sc_target_arcs.len()$), for total cost $2 dot #hc_sc_n = #(hc_sc_n * 2)$. Recovering the source witness: arc $i$ corresponds to vertex $i$, so the permutation $[#fmt-values(hc_sc_sol.source_config)]$ is the Hamiltonian circuit #sym.checkmark\ @@ -17817,10 +17798,10 @@ The following table shows concrete target-variable counts for example instances, _Solution extraction._ Evaluate once, apply the same aggregate certificate predicate, and reject non-certifying tours with an extraction error. Otherwise the service permutation is the source vertex order. The target evaluator permits service arcs on connector paths; the proof remains valid because equality forces each connector to be a single undirected edge. No target-definition change is required. ] -#let hc_rp = load-example("HamiltonianCircuit", "RuralPostman") +#let hc_rp = load-example("HamiltonianCircuit", "DecisionRuralPostman") #let hc_rp_sol = hc_rp.solutions.at(0) #let hc_rp_n = graph-num-vertices(hc_rp.source.instance) -#reduction-rule("HamiltonianCircuit", "RuralPostman", +#reduction-rule("HamiltonianCircuit", "DecisionRuralPostman", example: true, example-caption: [Cycle $C_#hc_rp_n$ ($n = #hc_rp_n$): vertex splitting to Rural Postman], extra: [ @@ -17833,9 +17814,9 @@ The following table shows concrete target-variable counts for example instances, *Step 1 -- Source instance.* The canonical HC instance is a cycle $C_#hc_rp_n$ with $n = #hc_rp_n$ vertices and $|E| = #graph-num-edges(hc_rp.source.instance)$ edges. The stored witness is the permutation $(#fmt-values(hc_rp_sol.source_config))$. - *Step 2 -- Construction.* Each vertex splits into $(v_i^a, v_i^b)$, producing $2n = #graph-num-vertices(hc_rp.target.instance)$ vertices. The target graph has #graph-num-edges(hc_rp.target.instance) edges: #hc_rp.target.instance.required_edges.len() required edges (one per source vertex) and #(graph-num-edges(hc_rp.target.instance) - hc_rp.target.instance.required_edges.len()) connector edges (two per source edge). All edge lengths are 1. + *Step 2 -- Construction.* Each vertex splits into $(v_i^a, v_i^b)$, producing $2n = #graph-num-vertices(hc_rp.target.instance.inner)$ vertices. The target graph has #graph-num-edges(hc_rp.target.instance.inner) edges: #hc_rp.target.instance.inner.required_edges.len() required edges (one per source vertex) and #(graph-num-edges(hc_rp.target.instance.inner) - hc_rp.target.instance.inner.required_edges.len()) connector edges (two per source edge). All edge lengths are 1. - *Step 3 -- Verify a solution.* The target solution assigns edge multiplicities $(#fmt-values(hc_rp_sol.target_config))$. The tour traverses all #hc_rp.target.instance.required_edges.len() required edges plus #hc_rp_n connector edges, for total cost $= #(2 * hc_rp_n) = 2n$ #sym.checkmark. + *Step 3 -- Verify a solution.* The target solution assigns edge multiplicities $(#fmt-values(hc_rp_sol.target_config))$. The tour traverses all #hc_rp.target.instance.inner.required_edges.len() required edges plus #hc_rp_n connector edges, for total cost $= #(2 * hc_rp_n) = 2n$ #sym.checkmark. *Multiplicity:* The fixture stores one canonical witness. The $#hc_rp_n$-cycle has $#hc_rp_n$ rotations $times$ 2 reflections $= #(2 * hc_rp_n)$ directed Hamiltonian circuits. ], @@ -17878,9 +17859,9 @@ The following table shows concrete target-variable counts for example instances, _Solution extraction._ After validating target feasibility, select original vertex $i$ exactly when its outgoing arc has flow 1. The auxiliary path is omitted. Repeated source edges add repeated constraints and do not change the proof. Allocation counts and the shifted threshold are checked before construction; no source solver is invoked during construction or extraction. ] -#let hc_qa = load-example("HamiltonianCircuit", "QuadraticAssignment") +#let hc_qa = load-example("HamiltonianCircuit", "DecisionQuadraticAssignment") #let hc_qa_sol = hc_qa.solutions.at(0) -#reduction-rule("HamiltonianCircuit", "QuadraticAssignment", +#reduction-rule("HamiltonianCircuit", "DecisionQuadraticAssignment", example: true, example-caption: [Cycle graph $C_#hc_qa.source.instance.graph.num_vertices$ ($n = #hc_qa.source.instance.graph.num_vertices$, $|E| = #hc_qa.source.instance.graph.edges.len()$)], extra: [ @@ -17893,7 +17874,7 @@ The following table shows concrete target-variable counts for example instances, *Step 1 -- Source instance.* The graph $G$ has $n = #hc_qa.source.instance.graph.num_vertices$ vertices and edges ${#hc_qa.source.instance.graph.edges.map(e => "(" + str(e.at(0)) + "," + str(e.at(1)) + ")").join(", ")}$, forming a cycle $C_#hc_qa.source.instance.graph.num_vertices$. - *Step 2 -- Construction.* The cost matrix $C$ encodes a directed cycle on positions: $c[i][(i+1) mod #hc_qa.source.instance.graph.num_vertices] = 1$, all other entries 0. The distance matrix $D$ encodes graph adjacency: $d[k][l] = 0$ if ${k,l} in E$, $d[k][l] = 1$ for distinct non-edges, $d[k][k] = 0$. Both matrices are $#hc_qa.source.instance.graph.num_vertices times #hc_qa.source.instance.graph.num_vertices$, so the QAP has $n = #hc_qa.target.instance.cost_matrix.len()$ facilities and $n = #hc_qa.target.instance.distance_matrix.len()$ locations. + *Step 2 -- Construction.* The cost matrix $C$ encodes a directed cycle on positions: $c[i][(i+1) mod #hc_qa.source.instance.graph.num_vertices] = 1$, all other entries 0. The distance matrix $D$ encodes graph adjacency: $d[k][l] = 0$ if ${k,l} in E$, $d[k][l] = 1$ for distinct non-edges, $d[k][k] = 0$. Both matrices are $#hc_qa.source.instance.graph.num_vertices times #hc_qa.source.instance.graph.num_vertices$, so the QAP has $n = #hc_qa.target.instance.inner.cost_matrix.len()$ facilities and $n = #hc_qa.target.instance.inner.distance_matrix.len()$ locations. *Step 3 -- Verify a solution.* The canonical Hamiltonian circuit visits vertices in order $gamma = (#fmt-values(hc_qa_sol.source_config))$. The QAP permutation is the same: $(#fmt-values(hc_qa_sol.target_config))$. The QAP cost is $sum_(i=0)^(n-1) c[i][(i+1) mod n] dot d[gamma(i)][gamma((i+1) mod n)]$. Since $gamma$ maps each position $i$ to vertex $i$, each consecutive pair $(gamma(i), gamma(i+1 mod n))$ is an edge in $G$, contributing $1 dot 0 = 0$. Total cost $= 0$ #sym.checkmark @@ -18356,9 +18337,9 @@ The following table shows concrete target-variable counts for example instances, ] // 5. PartitionIntoCliques → MinimumCoveringByCliques (#889) -#let pic_mcbc = load-example("PartitionIntoCliques", "MinimumCoveringByCliques") +#let pic_mcbc = load-example("PartitionIntoCliques", "DecisionMinimumCoveringByCliques") #let pic_mcbc_sol = pic_mcbc.solutions.at(0) -#reduction-rule("PartitionIntoCliques", "MinimumCoveringByCliques", +#reduction-rule("PartitionIntoCliques", "DecisionMinimumCoveringByCliques", example: true, example-caption: [$n = #graph-num-vertices(pic_mcbc.source.instance)$ vertices, $m = #graph-num-edges(pic_mcbc.source.instance)$ edges, $K = #pic_mcbc.source.instance.num_cliques$], extra: [ @@ -18371,7 +18352,7 @@ The following table shows concrete target-variable counts for example instances, *Step 1 -- Source instance.* Graph $G$ with $n = #graph-num-vertices(pic_mcbc.source.instance)$ vertices, $m = #graph-num-edges(pic_mcbc.source.instance)$ edge, and clique bound $K = #pic_mcbc.source.instance.num_cliques$. The stored partition witness is $(#fmt-values(pic_mcbc_sol.source_config))$, namely the cliques ${0,1}$ and ${2}$. - *Step 2 -- Orlin construction.* The target graph has $#graph-num-vertices(pic_mcbc.target.instance)$ vertices and $#graph-num-edges(pic_mcbc.target.instance)$ edges. Because the source has two directed edge copies, the construction adds the gadgets $Q_(0,1)$ and $Q_(1,0)$, plus the side cliques $L^*$ and $R^*$. The threshold is $K' = K + 2m + 2 = #(pic_mcbc.source.instance.num_cliques + 2 * graph-num-edges(pic_mcbc.source.instance) + 2)$. + *Step 2 -- Orlin construction.* The target graph has $#graph-num-vertices(pic_mcbc.target.instance.inner)$ vertices and $#graph-num-edges(pic_mcbc.target.instance.inner)$ edges. Because the source has two directed edge copies, the construction adds the gadgets $Q_(0,1)$ and $Q_(1,0)$, plus the side cliques $L^*$ and $R^*$. The threshold is $K' = K + 2m + 2 = #(pic_mcbc.source.instance.num_cliques + 2 * graph-num-edges(pic_mcbc.source.instance) + 2)$. *Step 3 -- Verify the witness.* The target witness labels $#pic_mcbc_sol.target_config.len()$ target edges with 6 clique IDs, corresponding to $D_1 = {x_0, x_1, y_0, y_1}$, $D_2 = {x_2, y_2}$, $Q_(0,1)$, $Q_(1,0)$, $L^*$, and $R^*$. Reading only the labels on the matching edges $x_i y_i$ recovers the source partition $(#fmt-values(pic_mcbc_sol.source_config))$ #sym.checkmark. @@ -18776,9 +18757,9 @@ The following table shows concrete target-variable counts for example instances, ] // 12. Partition → SequencingToMinimizeTardyTaskWeight (#471) -#let part_stw = load-example("Partition", "SequencingToMinimizeTardyTaskWeight") +#let part_stw = load-example("Partition", "DecisionSequencingToMinimizeTardyTaskWeight") #let part_stw_sol = part_stw.solutions.at(0) -#reduction-rule("Partition", "SequencingToMinimizeTardyTaskWeight", +#reduction-rule("Partition", "DecisionSequencingToMinimizeTardyTaskWeight", example: true, example-caption: [#part_stw.source.instance.sizes.len() elements, total $= #part_stw.source.instance.sizes.sum()$], extra: [ @@ -18790,9 +18771,9 @@ The following table shows concrete target-variable counts for example instances, ) #{ - let lengths = part_stw.target.instance.lengths - let weights = part_stw.target.instance.weights - let deadline = part_stw.target.instance.deadlines.at(0) + let lengths = part_stw.target.instance.inner.lengths + let weights = part_stw.target.instance.inner.weights + let deadline = part_stw.target.instance.inner.deadlines.at(0) let on-time-sum = part_stw_sol.source_config.enumerate().filter(((i, x)) => not x).map(((i, x)) => part_stw.source.instance.sizes.at(i)).sum() let tardy-sum = part_stw_sol.source_config.enumerate().filter(((i, x)) => x).map(((i, x)) => part_stw.source.instance.sizes.at(i)).sum() [ @@ -18833,11 +18814,11 @@ The following table shows concrete target-variable counts for example instances, ] // 12. Partition → OpenShopScheduling (#481) -#let part_oss = load-example("Partition", "OpenShopScheduling") +#let part_oss = load-example("Partition", "DecisionOpenShopScheduling") #let part_oss_sol = part_oss.solutions.at(0) -#reduction-rule("Partition", "OpenShopScheduling", +#reduction-rule("Partition", "DecisionOpenShopScheduling", example: true, - example-caption: [#part_oss.source.instance.sizes.len() elements, $m = #part_oss.target.instance.num_machines$ machines], + example-caption: [#part_oss.source.instance.sizes.len() elements, $m = #part_oss.target.instance.inner.num_machines$ machines], extra: [ #pred-commands( "pred create --example " + problem-spec(part_oss.source) + " -o partition.json", @@ -18848,7 +18829,7 @@ The following table shows concrete target-variable counts for example instances, #{ let q = part_oss.source.instance.sizes.sum() / 2 - let p = part_oss.target.instance.processing_times + let p = part_oss.target.instance.inner.processing_times let left-sum = part_oss_sol.source_config.enumerate().filter(((i, x)) => not x).map(((i, x)) => part_oss.source.instance.sizes.at(i)).sum() let right-sum = part_oss_sol.source_config.enumerate().filter(((i, x)) => x).map(((i, x)) => part_oss.source.instance.sizes.at(i)).sum() [ @@ -18887,9 +18868,9 @@ The following table shows concrete target-variable counts for example instances, _Aggregation and extraction._ Map a finite optimum equal to $D$ to true and all other values to false. Validate a target configuration once, apply this same certificate, then identify the middle machine and select its element jobs completing by $Q$. Reject invalid schedules and feasible schedules that do not attain the certificate. The existing checked target constructor validates its total horizon $3(S+Q)$ before computing $D$, so the smaller nonnegative certificate is representable. Target construction failures retain their formal error type. ] // 13. NAESatisfiability → MaxCut (#166) -#let nae_mc = load-example("NAESatisfiability", "MaxCut") +#let nae_mc = load-example("NAESatisfiability", "DecisionMaxCut") #let nae_mc_sol = nae_mc.solutions.at(0) -#reduction-rule("NAESatisfiability", "MaxCut", +#reduction-rule("NAESatisfiability", "DecisionMaxCut", example: true, example-caption: [$n = #nae_mc.source.instance.num_vars$ variables, $m = #sat-num-clauses(nae_mc.source.instance)$ clauses, $M = #(sat-num-clauses(nae_mc.source.instance) + 1)$], extra: [ @@ -18904,12 +18885,12 @@ The following table shows concrete target-variable counts for example instances, let n = nae_mc.source.instance.num_vars let m = sat-num-clauses(nae_mc.source.instance) let big-m = m + 1 - let clause-edge-count = graph-num-edges(nae_mc.target.instance) - n + let clause-edge-count = graph-num-edges(nae_mc.target.instance.inner) - n let cut-value = n * big-m + 2 * m [ *Step 1 -- Source instance.* NAE-SAT with $n = #n$ variables and $m = #m$ clauses. The implementation uses forcing weight $M = m + 1 = #big-m$. - *Step 2 -- Construct the weighted graph.* Variable gadgets contribute #n heavy edges of weight $M$. Because the canonical fixture has 3 literals per clause, each clause contributes one unit-weight triangle, so the target has #clause-edge-count unit-weight clause edges and $#graph-num-edges(nae_mc.target.instance)$ edges total on $#graph-num-vertices(nae_mc.target.instance)$ vertices. + *Step 2 -- Construct the weighted graph.* Variable gadgets contribute #n heavy edges of weight $M$. Because the canonical fixture has 3 literals per clause, each clause contributes one unit-weight triangle, so the target has #clause-edge-count unit-weight clause edges and $#graph-num-edges(nae_mc.target.instance.inner)$ edges total on $#graph-num-vertices(nae_mc.target.instance.inner)$ vertices. *Step 3 -- Verify the canonical witness.* Source assignment $(#fmt-values(nae_mc_sol.source_config))$ induces target cut $(#fmt-values(nae_mc_sol.target_config))$. All #n heavy edges are cut, and each of the #m clause triangles has a 1-2 split contributing 2, so the total cut weight is $#cut-value$ #sym.checkmark. ] @@ -19379,9 +19360,9 @@ The following table shows concrete target-variable counts for example instances, ] // 17. HamiltonianPathBetweenTwoVertices → LongestPath (#359) -#let hpbtv_lp = load-example("HamiltonianPathBetweenTwoVertices", "LongestPath") +#let hpbtv_lp = load-example("HamiltonianPathBetweenTwoVertices", "DecisionLongestPath") #let hpbtv_lp_sol = hpbtv_lp.solutions.at(0) -#reduction-rule("HamiltonianPathBetweenTwoVertices", "LongestPath", +#reduction-rule("HamiltonianPathBetweenTwoVertices", "DecisionLongestPath", example: true, example-caption: [$n = #graph-num-vertices(hpbtv_lp.source.instance)$ vertices, $s = #hpbtv_lp.source.instance.source_vertex$, $t = #hpbtv_lp.source.instance.target_vertex$], extra: [ @@ -19407,7 +19388,7 @@ The following table shows concrete target-variable counts for example instances, _Correctness._ ($arrow.r.double$) A Hamiltonian $s$-$t$ path has $n - 1$ edges of length 1 each, giving total length $n - 1 = K$. ($arrow.l.double$) A simple $s'$-$t'$ path of length $>= K = n - 1$ has $>= n - 1$ edges. Since a simple path on $n$ vertices can have at most $n - 1$ edges, it has exactly $n - 1$ edges and visits all vertices -- it is a Hamiltonian $s$-$t$ path. - _Solution extraction._ Evaluate the target configuration once and apply the aggregate predicate: the value must be finite and equal to $n-1$. Reject infeasible selections and shorter paths before traversing any edges. The target feasibility check guarantees a single connected simple path from $s$ to $t$; its $n-1$ edges visit all $n$ vertices. Start at $s$ and repeatedly take the neighbor other than the preceding vertex. This terminates at $t$ and yields the source permutation without repetitions. The same argument applies to every supplied target configuration, independently of any caller claim of optimality. An absent target maximum or a proven maximum below $n-1$ certifies a NO source instance. The source requires distinct valid endpoints, so $n>=2$; no empty-graph or equal-endpoint convention is introduced. The target value resolves to `Max` because unit weight `One` has sum type `i64`. Checked evaluation errors are propagated, and the threshold comparison uses exact integer conversion. + _Solution extraction._ Evaluate the target configuration once and apply the aggregate predicate: the value must be finite and equal to $n-1$. Reject infeasible selections and shorter paths before traversing any edges. The target feasibility check guarantees a single connected simple path from $s$ to $t$; its $n-1$ edges visit all $n$ vertices. Start at $s$ and repeatedly take the neighbor other than the preceding vertex. This terminates at $t$ and yields the source permutation without repetitions. The same argument applies to every supplied target configuration, independently of any caller claim of optimality. An absent target maximum or a proven maximum below $n-1$ certifies a NO source instance. The source requires distinct valid endpoints, so $n>=2$; no empty-graph or equal-endpoint convention is introduced. The inner value is `Max` because unit weight `One` has sum type `i64`; the decision target evaluates to `Or` with bound $n-1$. Checked evaluation errors are propagated, and the threshold comparison uses exact integer conversion. ] // 18. GraphPartitioning → MaxCut (from main codebase) diff --git a/docs/src/cli-commands.md b/docs/src/cli-commands.md index 97eb52217..3775afda9 100644 --- a/docs/src/cli-commands.md +++ b/docs/src/cli-commands.md @@ -93,10 +93,20 @@ For a problem file, JSON inspection includes `parameter_values`, the model's act pred path MIS QUBO --json -o paths.json python3 -c 'import json; print(json.dumps(json.load(open("paths.json"))["paths"][0]))' > path.json pred reduce problem.json --via path.json -o reduced.json -pred extract reduced.json --config '[1,0,1,0]' +pred extract reduced.json --result target-result.json ``` -The bundle contains the source instance, the target instance, and the variant-level path; keep it whole to preserve solution recovery. `--via` replays one route extracted from the `paths` envelope, whose source variant must match the input. `extract` maps a target-space configuration back to the source. +The bundle contains the source instance, the target instance, and the variant-level path; keep it whole to preserve solution recovery. `--via` replays one route extracted from the `paths` envelope, whose source variant must match the input. `extract` recovers the complete source result from an external target result: + +```json +{"status":"optimal","solution":[true,false,true,false],"evaluation":"Min(-2)"} +``` + +The example shape assumes a Boolean target solution; use the actual target's +solution representation. Use `feasible` when optimality is not established, or +`{"status":"infeasible"}` when the target solver proves infeasibility. +The command checks the target witness and recomputes its evaluation. Insufficient +candidate quality is an error, not a source NO answer. ## Solve diff --git a/docs/src/design.md b/docs/src/design.md index 4cabef52c..caeef8b29 100644 --- a/docs/src/design.md +++ b/docs/src/design.md @@ -100,9 +100,9 @@ Supported weight variants are `One`, `i64`, and `f64`. | Layer | Contract | |-------|----------| | Model (`Problem`) | Defines instances, witnesses, feasibility, and objectives in its declared mathematical representation. Evaluation is independent of backend tolerances, statuses, and enumeration capacity. | -| Reduction (`ReduceTo`, `ReductionResult`) | Constructs the target within the rule's mathematical domain and maps target witnesses satisfying the stated preconditions to source witnesses. It owns coefficient arithmetic, parameter relationships, and mapping correctness. | +| Reduction (`ReduceTo`, `ReductionResult`) | Constructs the target and recovers a complete source result through its mandatory `recover_result`. It owns objective relations, witness mappings, and infeasibility semantics. | | Backend adapter | Encodes the target, executes the backend, interprets statuses, decodes numerical results, and validates the returned witness against the original target model. | -| Solver orchestration | Executes registered capabilities and reduction chains, interprets aggregate results, and extracts source witnesses under the reduction contracts. | +| Solver orchestration | Executes registered capabilities and calls each stored reduction result in reverse order. | | CLI / MCP | Uses public construction, evaluation, and solving APIs and presents their results. | Models and rules do not repair backend results, change constraints to make a @@ -114,79 +114,75 @@ downgrading every successful result. Search-space cardinalities belong to the solver capability, not the mathematical model. Actual model storage and witness representation constraints still apply. -### Witness and aggregate reductions - -`ReductionResult::extract_solution()` maps `Target::Solution` to -`Source::Solution`; it does not require equal `Problem::Value` types. Resolve -concrete associated types from the implementation, then check the mathematical -mapping and its Rust implementation rather than applying a wrapper-pair whitelist. - -For an optimization reduction, explain why target optima map to source optima. -Opposite directions are valid when the objective relationship reverses order: -independent-set size `k` corresponds to vertex-cover size `n-k` by complementing -the witness. Different numeric value types do not require conversion of an -objective that the extractor never converts. Check the domain and arithmetic of -conversions the construction or mapping actually performs. - -Value-only operations use `ReduceToAggregate` / `AggregateReductionResult` and -must justify their actual `extract_value()` relationship. Multi-query algorithms -use the existing Turing reduction capability. A feasibility witness alone does -not establish an optimization result without the required mathematical argument. - -`Problem::evaluate()` defines feasibility as well as objective values. A successful -call can return an infeasible value such as `Or(false)` or `Max(None)`; absence -of an `EvaluationError` does not imply a valid witness. The adapter validates -backend output before returning it. Both typed extraction and `pred extract` -assume witnesses satisfying the reduction's documented premises; neither checks -feasibility or optimality. JSON parsing and type conversion remain at the transport -boundary. Evaluation may supply requested display values without acting as an -acceptance gate. Solver orchestration interprets aggregate mappings to determine -source outcomes before invoking witness mappings. +### Complete result recovery + +Every `ReductionResult` implements `recover_result(source, target_result)`. +It returns the source status, solution, and evaluation together. There is no +optional interpretation callback or separate value-only execution path. + +| Target result | Rule's obligation | +|---|---| +| `Optimal { solution, evaluation }` | Recover a source optimum, or establish source infeasibility using the rule's mathematical relation | +| `Feasible { solution, evaluation }` | Recover a feasible source solution when justified; otherwise return `InsufficientSolutionQuality` | +| `Infeasible` | Apply the rule's infeasibility relation | +| Evaluation, decoding, or execution error | Preserve the error; never turn it into `Infeasible` | + +`ProblemOutcome

` retains `P::Solution` and `P::Value` as concrete Rust types. +`SolveOutcome::optimal` evaluates an already established optimum; it does not +prove optimality. The solver or external caller supplies that conclusion. +Likewise, `SolveOutcome::feasible` packages an established feasible witness. + +For example, an independent set of size 2 in a four-vertex graph maps to a +vertex cover of size 2. Recovery complements the solution and evaluates the +source cover. An optimal target result produces `Optimal`; a merely feasible +target result produces `Feasible`. + +For `Decision

-> P`, an optimum missing the bound establishes NO and recovers +`Infeasible`. A merely feasible candidate missing it establishes no such result +and returns `InsufficientSolutionQuality`. Penalty reductions own their analogous +energy relationships. A decoded invalid witness alone is not a general proof +that the source is infeasible. ### Executed reduction lifecycle -A witness reduction is one algorithm with construction and reverse mapping. -`reduce_to()` returns the target and all mapping state in one result. Each -executed chain step constructs that result once. Its witness and optional -aggregate `Rc` views share one allocation; obtaining another view does not -reconstruct or copy the target. `Decision

-> P` stores the bound with that -same result. +```text +source ──reduce_to──> stored result A ──reduce_to──> stored result B + target A target B + │ solve + ▼ +source result <── A.recover_result <── B.recover_result <── target result +``` -For every rule, document its instance domain, required target witness quality -and conditions, source guarantee, and treatment of source infeasibility. -The guarantee applies to every qualifying witness, including tied optima. -A witness-capable edge alone does not establish a complete-solving procedure: -composition must establish the preceding edge's witness premise. +Each step constructs one result. `Rc` shares that result across paths with a +common prefix; recovery never reconstructs the target. The original source and +intermediate targets supply borrowed source references during reverse traversal. +Rules do not need extra source-instance fields for evaluation. -| Example | Required recovery | +| Caller | Recovery entry point | |---|---| -| MVC -> MIS | Complement a maximum independent set to obtain a minimum cover | -| SAT -> MIS | With `m` clauses, optimum size `m` permits witness extraction; an optimum below `m` means UNSAT | -| Binary ILP -> QUBO | Use the constructed energy relationship to obtain a source optimum or source infeasibility; a QUBO optimum alone does not establish ILP feasibility | -| MVC -> MIS -> SetPacking -> ILP | Apply the stored ILP-to-packing and packing-to-MIS mappings, then the complement mapping | -| TSP -> QUBO | Shift signed edge costs uniformly; the energy threshold distinguishes source infeasibility, and the stored offset recovers tour cost | -| Discrete inverse kinematics -> QUBO | Restore omitted constants and compare against the gap between feasible distance and constraint penalties before decoding orientations | -| MultiwayCut -> QUBO | Always delete negative edges; optimize nonnegative cut cost and decode an optimal terminal partition | -| Aggregate-only operation | Map the final value without selecting any witness, including `Sum` | - -The mathematical thresholds and objective relationships belong to the rule. -Solver completion invokes the executed step's concrete `interpret_optimum` -operation before its witness mapping. This operation shares the constructed -result and does not query the model registry. Ordinary extraction uses only the -witness mapping. Typed chain, executed path, and JSON extraction share the same -reverse traversal; dynamic/JSON methods perform necessary representation -conversion rather than introducing another extraction contract. - -`SolutionAggregate` is defined in `solvers/brute_force.rs` and exported through -`solvers` for enumeration clients. It compares candidate and aggregate values; -it is not a model-feasibility interface. Concrete variant declarations generate -`DynProblem` transport implementations using the value's own `is_valid` -semantics, without aggregation or solver-registration requirements. A concrete -hand-registered dynamic type can use `impl_dyn_problem!` directly. - -Witness and aggregate describe what can be recovered. Turing describes a -potentially adaptive query procedure. Exact witness recovery does not establish -approximation or counting preservation; those require their own proofs. +| Concrete rule | `ReductionResult::recover_result` | +| Typed chain or executed path | `recover_result::` | +| Registered ILP pipeline | The same chain's erased recovery | +| `pred solve bundle.json` | Solve the stored target, then recover the complete result | +| `pred extract bundle.json --result target-result.json` | Read the external result, validate its target witness, then use the same recovery | + +Dynamic methods convert representations and delegate to the typed rule. +External optimality claims belong to the external solver; parsing or evaluating +a configuration cannot establish optimality. `pred extract` accepts explicit +`optimal`, `feasible`, or `infeasible` status, rather than a bare configuration. + +Bound-owning `Decision

` targets serialize their `inner` instance and `bound`. +Their `evaluate()` returns `Or`. The `Decision

-> P` bridge makes these targets +usable with optimization backends. Each other rule must explicitly implement +its source-result relation, including thresholds and sentinel constructions. +Guarantees must cover every qualifying witness, including tied optima. + +`SolutionAggregate` remains a brute-force solver capability for selecting from +an enumeration. Mathematical wrappers such as `Min`, `Max`, `Or`, and `Sum` +remain model values. They do not require separate reduction traits or graph +modes. Turing reductions describe adaptive queries and remain a separate +execution capability; exact recovery alone does not imply approximation or +counting preservation. ### Arithmetic @@ -383,49 +379,42 @@ The result struct holds the target problem and the logic to map solutions back: ```rust,ignore #[derive(Debug, Clone)] -pub struct ReductionISToVC { - target: MinimumVertexCover, +pub struct ReductionISToVC { + target: MinimumVertexCover, } -impl ReductionResult for ReductionISToVC { - type Source = MaximumIndependentSet; - type Target = MinimumVertexCover; +impl ReductionResult for ReductionISToVC { + type Source = MaximumIndependentSet; + type Target = MinimumVertexCover; fn target_problem(&self) -> &Self::Target { &self.target } - fn extract_solution( + fn recover_result( &self, - target_sol: &Vec, - ) -> crate::rules::ExtractionResult> { - Ok(target_sol.iter().map(|&x| !x).collect()) + source: &Self::Source, + target: ProblemOutcome, + ) -> ExtractionResult> { + Ok(match target { + SolveOutcome::Optimal { solution, .. } => + SolveOutcome::optimal(source, solution.into_iter().map(|x| !x).collect())?, + SolveOutcome::Feasible { solution, .. } => + SolveOutcome::feasible(source, solution.into_iter().map(|x| !x).collect())?, + SolveOutcome::Infeasible => SolveOutcome::Infeasible, + }) } } ``` -### Solution extraction contract - -`ReductionResult::extract_solution` maps a complete target solution satisfying -the rule's mathematical premises into a source solution. The adapter establishes -target validity for internal solves. External callers supply witnesses under the -same contract. Rules requiring optimal target solutions document that requirement. -Source YES/NO and optimization outcomes are interpreted by solver orchestration, -not by the extraction chain. Invalid external witnesses have no mapping-correctness -guarantee. - -Do not repeat checks implied by target constraints or successful construction. -Do not truncate or pad input, substitute values for missing data, retry another -mapping, or add runtime acceptance checks to compensate for a rule defect. -Keep actual mathematical case distinctions and representation errors that can -occur for inputs satisfying the mapping's premises. +### Recovery contract -Zero and sentinel values remain valid when the source model explicitly gives -them meaning. For example, `MaximumCommonEdgeSubgraph` includes an "unmapped" -sentinel in its source dimensions. Missing target data must never be -interpreted as that sentinel. +Construction returns `ReductionError`; recovery returns `ExtractionError`. +Recovery must explicitly cover each result status. Required witness quality +comes from the rule's proof, not from which caller happens to invoke it. -Each conditional in an extractor should implement a case in the reduction's -mathematics or report an error that remains reachable under its premises. -The external boundary handles parsing and type conversion; extraction does not -accumulate feasibility checks, compatibility branches, or fallbacks. +The adapter checks backend output against the target model. Recovery performs +the mathematical reverse mapping and computes the source evaluation. Do not +repeat conditions already guaranteed by the target, repair malformed input, or +add a second solver to certify the supplied optimum. Mathematical sentinels +retain their model-defined meaning; missing data is an error. The `#[reduction]` attribute on the `ReduceTo` impl registers the reduction in the global registry (via `inventory`): @@ -486,10 +475,13 @@ Execute an explicitly selected path with `ReductionGraph::reduce_along_path`: ```rust,ignore let reduction = graph.reduce_along_path(rpath, &factoring_instance)?.unwrap(); let target: &SpinGlass = reduction.target_problem(); -let source_solution = reduction.extract_solution(&target_solution)?; +let target_result = SolveOutcome::optimal(target, target_solution)?; +let source_result = reduction.recover_result::>( + &factoring_instance, target_result, +)?; ``` -The returned `ReductionChain` stores each intermediate reduction and extracts the source solution by applying the inverse mappings in reverse order. Construction returns `ReductionError`; extraction returns `ExtractionError`. +The returned `ReductionChain` stores each intermediate result and recovers complete results in reverse order. Construction returns `ReductionError`; recovery returns `ExtractionError`.

Parameter contracts @@ -554,8 +546,7 @@ proved infeasibility, and `Err` reports an operational failure. `ILPSolver::solve

() -> Result` is the typed entry point. Adapter failures retain their classified errors. Registry lookup, -concrete-terminal dispatch, aggregate interpretation, -and reduction-chain extraction belong to orchestration. Integer pipelines end +concrete-terminal dispatch, and reduction-chain execution belong to orchestration. Integer pipelines end at native integer ILPs; they do not need a float-coefficient cast edge to execute. Explicit coefficient-conversion rules retain their own mathematical contracts. @@ -574,17 +565,10 @@ non-optimal termination, and invalid results are errors, not infeasibility. Variable decoding tolerances belong to the adapter; they do not define source or target feasibility, nor a universal objective-error allowance for tests. -After accepting a target optimum, orchestration must apply the reduction's -aggregate mapping to interpret a source decision threshold. If that optimum -cannot meet the threshold, the source answer is NO. A merely feasible witness -or failed solve is insufficient for that conclusion. Typed solving, dynamic -solving, and explicit CLI bundles must share the same interpretation and witness -mapping. - -Fixed pipelines and explicit CLI bundles reuse the executed `ReductionChain` -and the solver completion path. Aggregate mappings interpret an accepted target -optimum before witness extraction. Source evaluation computes requested output -values and propagates evaluation errors; it is not another feasibility gate. +After accepting a target result, orchestration invokes the stored reduction +chain's complete recovery. Every intermediate status passes through the previous +rule. Typed solving, dynamic solving, and explicit CLI bundles share this reverse +traversal; no caller performs its own source-threshold interpretation. ## JSON Serialization diff --git a/docs/src/getting-started.md b/docs/src/getting-started.md index 872b4e900..bb7f5b8ea 100644 --- a/docs/src/getting-started.md +++ b/docs/src/getting-started.md @@ -42,13 +42,15 @@ fn main() { assert_eq!(target.num_constraints(), 2); let target_solution = ILPSolver::new().solve(target).unwrap(); - let solution = reduction.extract_solution(&target_solution).unwrap(); + let target_result = SolveOutcome::optimal(target, target_solution).unwrap(); + let result = reduction.recover_result(&problem, target_result).unwrap(); + let solution = result.into_solution().unwrap(); assert_eq!(solution, vec![true, false, true, true]); println!("{}", problem.evaluate(&solution).unwrap()); // Max(3) } ``` -The target has one binary variable per set and a constraint for each element shared by multiple sets. `extract_solution` maps a target solution back to the source solution type. `ILPSolver::new().solve(&problem)` executes the exact variant’s registered ILP pipeline and returns its source solution. +The target has one binary variable per set and a constraint for each element shared by multiple sets. `recover_result` returns the source status, solution, and evaluation together. `ILPSolver::new().solve(&problem)` executes the exact variant’s registered ILP pipeline and returns its source solution. ## Discover and run a path @@ -61,10 +63,10 @@ Search uses exact variants. This discovers a route from `Factoring` to `SpinGlas let reduction = graph.reduce_along_path(rpath, &factoring).unwrap().unwrap(); let target: &SpinGlass = reduction.target_problem(); -// Solve `target`, then call reduction.extract_solution(&target_solution). +// Solve `target`, then pass its ProblemOutcome to reduction.recover_result. ``` -`extract_solution` walks the intermediate mappings in reverse. The full [example](https://github.com/CodingThrust/problem-reductions/blob/main/examples/chained_reduction_factoring_to_spinglass.rs) also solves factoring through a direct ILP reduction and checks that the recovered factors multiply to 6: +`recover_result` walks the intermediate mappings in reverse. The full [example](https://github.com/CodingThrust/problem-reductions/blob/main/examples/chained_reduction_factoring_to_spinglass.rs) also solves factoring through a direct ILP reduction and checks that the recovered factors multiply to 6: ```bash cargo run --example chained_reduction_factoring_to_spinglass diff --git a/docs/website/assets/site.js b/docs/website/assets/site.js index 7d09f9def..c0cd3f96c 100644 --- a/docs/website/assets/site.js +++ b/docs/website/assets/site.js @@ -284,7 +284,7 @@ target = data.nodes[edge.target]; const outgoing = edge.source === currentIndex; const other = outgoing ? target : source; - return `

${escape(variantLabel(other))}

${[edge.witness && "Witness recovery", edge.aggregate && "Aggregate value", edge.turing && "Turing reduction"].filter(Boolean).join(" · ") || "See reduction contract"}

`; + return `
${outgoing ? "→ " : "← "}${escape(nameOf(other.name))}

${escape(variantLabel(other))}

${[edge.witness && "Result recovery", edge.turing && "Turing reduction"].filter(Boolean).join(" · ") || "See reduction contract"}

`; } function demoPanel() { @@ -446,8 +446,7 @@ const vcToMis = sourceName === "MinimumVertexCover"; document.title = `${nameOf(sourceName)} → ${nameOf(targetName)} — ${baseTitle}`; const capabilities = [ - edge.witness && "Witness recovery", - edge.aggregate && "Aggregate value", + edge.witness && "Result recovery", edge.turing && "Turing reduction", ] .filter(Boolean) diff --git a/examples/chained_reduction_factoring_to_spinglass.rs b/examples/chained_reduction_factoring_to_spinglass.rs index 48067cd3a..232d4b937 100644 --- a/examples/chained_reduction_factoring_to_spinglass.rs +++ b/examples/chained_reduction_factoring_to_spinglass.rs @@ -1,3 +1,4 @@ +use problemreductions::solvers::SolveOutcome; // # Chained Reduction: Factoring -> SpinGlass // // Mirrors Julia's examples/Ising.jl — reduces a Factoring problem @@ -27,8 +28,10 @@ pub fn run() -> std::result::Result<(), Box> { ); let rpath = paths .iter() - .find(|path| path.type_names() == ["Factoring", "CircuitSAT", "SpinGlass"]) - .expect("explicit Factoring -> CircuitSAT -> SpinGlass route"); + .find(|path| { + path.type_names() == ["Factoring", "CircuitSAT", "DecisionSpinGlass", "SpinGlass"] + }) + .expect("explicit Factoring -> CircuitSAT -> DecisionSpinGlass -> SpinGlass route"); println!(" {}", rpath); // ANCHOR_END: step1 @@ -45,7 +48,14 @@ pub fn run() -> std::result::Result<(), Box> { let solver = ILPSolver::new(); 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(); + let solution = reduction + .recover_result( + &factoring, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); // ANCHOR_END: step3 // ANCHOR: step4 diff --git a/problemreductions-cli/src/cli.rs b/problemreductions-cli/src/cli.rs index dd1518a1b..0fc09ef13 100644 --- a/problemreductions-cli/src/cli.rs +++ b/problemreductions-cli/src/cli.rs @@ -209,12 +209,12 @@ Examples: Inspect(InspectArgs), /// Solve a problem instance Solve(SolveArgs), - /// Extract a source-space solution from a reduction bundle and a target-space config + /// Recover a source result from a reduction bundle and an external target result #[command(after_help = "\ Examples: - pred extract bundle.json --config '[1,0,1,0]' - pred extract bundle.json --config '[1,0,1,0]' -o source.json - cat bundle.json | pred extract - --config '[1,0,1,0]' + pred extract bundle.json --result target-result.json + pred extract bundle.json --result target-result.json -o source.json + cat bundle.json | pred extract - --result target-result.json Use this when an external solver has solved the bundle's target problem (e.g. a QUBO sampler, a neutral-atom platform, a QAOA runtime) and you want @@ -335,11 +335,11 @@ pub struct ReduceArgs { #[derive(clap::Args)] pub struct ExtractArgs { - /// Reduction bundle JSON (from `pred reduce`). Use - for stdin. + /// Reduction bundle JSON (from pred reduce). pub input: PathBuf, - /// Target problem solution encoded as JSON (for example, [1,0,1,0]) + /// JSON result file from the target solver, with an explicit solve status. #[arg(long)] - pub config: String, + pub result: PathBuf, } #[derive(clap::Args)] diff --git a/problemreductions-cli/src/commands/extract.rs b/problemreductions-cli/src/commands/extract.rs index 4b6a60db2..997944a3c 100644 --- a/problemreductions-cli/src/commands/extract.rs +++ b/problemreductions-cli/src/commands/extract.rs @@ -1,59 +1,44 @@ use crate::dispatch::{read_input, BundleReplay, ReductionBundle}; use crate::output::OutputConfig; use anyhow::{Context, Result}; +use problemreductions::solvers::{SolveOutcome, SolverExecution}; use std::path::Path; -/// Extract a source-space configuration from a target-space configuration and a reduction bundle. -/// -/// This lets external solvers (that solved the bundle's target problem on their own) -/// recover a solution in the original source problem space without having to -/// re-solve through `pred solve`. -pub fn extract(input: &Path, config_str: &str, out: &OutputConfig) -> Result<()> { - let content = read_input(input)?; - let json: serde_json::Value = - serde_json::from_str(&content).context("Input is not valid JSON")?; - - if !(json.get("source").is_some() && json.get("target").is_some() && json.get("path").is_some()) - { - anyhow::bail!( - "Input is not a reduction bundle.\n\ - `pred extract` requires a bundle produced by `pred reduce`.\n\ - Got a plain problem file; did you mean `pred evaluate`?" - ); - } - - let bundle: ReductionBundle = - serde_json::from_value(json).context("Failed to parse reduction bundle")?; - - let target_config: serde_json::Value = - serde_json::from_str(config_str).context("Target config is not valid JSON")?; - +/// Recover the source result from an external solver's explicit target result. +pub fn extract(input: &Path, result_path: &Path, out: &OutputConfig) -> Result<()> { + let bundle: ReductionBundle = serde_json::from_str(&read_input(input)?) + .context("pred extract requires a reduction bundle produced by pred reduce")?; + let mut target: SolveOutcome = serde_json::from_str(&read_input(result_path)?) + .context("Target result must declare optimal, feasible, or infeasible status")?; let replay = BundleReplay::prepare(&bundle)?; - - let (source_config, source_eval, target_eval) = replay.extract(&target_config)?; - + match &mut target { + SolveOutcome::Optimal { + solution, + evaluation, + } + | SolveOutcome::Feasible { + solution, + evaluation, + } => { + let (value, feasible) = replay.target.evaluate_dyn(solution)?; + anyhow::ensure!( + feasible, + "external result contains an infeasible target solution" + ); + *evaluation = value; + } + SolveOutcome::Infeasible => {} + } + let result = replay.recover_result(target, SolverExecution::External)?; out.emit( || { - format!( - "Problem: {}\nSolver: external (via {})\nSolution: {:?}\nEvaluation: {}", - replay.source_name, replay.target_name, source_config, source_eval, - ) - }, - || { - // Schema aligned with `pred solve` on a bundle. `solver` is "external" - // because pred did not run the solver that produced the target config. - Ok(serde_json::json!({ - "problem": replay.source_name, - "solver": "external", - "reduced_to": replay.target_name, - "solution": source_config, - "evaluation": source_eval, - "intermediate": { - "problem": replay.target_name, - "solution": target_config, - "evaluation": target_eval, - }, - })) + let mut text = format!( + "Problem: {}\nSolver: external (via {})", + result.source_name, result.target_name + ); + super::solve::append_outcome_text(&mut text, &result.source_outcome); + text }, + || Ok(result.to_json()), ) } diff --git a/problemreductions-cli/src/commands/solve.rs b/problemreductions-cli/src/commands/solve.rs index ce3c471de..81f2c330c 100644 --- a/problemreductions-cli/src/commands/solve.rs +++ b/problemreductions-cli/src/commands/solve.rs @@ -32,8 +32,9 @@ fn parse_input(path: &Path) -> Result { } } -fn solver_text(solver: &SolverExecution) -> String { +pub(crate) fn solver_text(solver: &SolverExecution) -> String { match solver { + SolverExecution::External => "external".to_string(), SolverExecution::Customized { implementation } => format!("customized ({implementation})"), SolverExecution::Ilp { reduction_path } => { format!("ilp ({})", reduction_path.join(" -> ")) @@ -52,7 +53,7 @@ fn solve_result_text(problem: &str, result: &SolveResult) -> String { text } -fn append_outcome_text(text: &mut String, outcome: &SolveOutcome) { +pub(crate) fn append_outcome_text(text: &mut String, outcome: &SolveOutcome) { match outcome { SolveOutcome::Optimal { solution, @@ -62,6 +63,15 @@ fn append_outcome_text(text: &mut String, outcome: &SolveOutcome) { text.push_str(&format!("\nSolution: {:?}", solution)); text.push_str(&format!("\nEvaluation: {evaluation}")); } + SolveOutcome::Feasible { + solution, + evaluation, + } => { + text.push_str("\nStatus: feasible"); + text.push_str(&format!( + "\nSolution: {solution:?}\nEvaluation: {evaluation}" + )); + } SolveOutcome::Infeasible => text.push_str("\nStatus: infeasible"), } } diff --git a/problemreductions-cli/src/dispatch.rs b/problemreductions-cli/src/dispatch.rs index 62c946295..3261fe6e3 100644 --- a/problemreductions-cli/src/dispatch.rs +++ b/problemreductions-cli/src/dispatch.rs @@ -3,7 +3,7 @@ use problemreductions::registry::{DynProblem, LoadedDynProblem}; use problemreductions::rules::ReductionGraph; use problemreductions::solvers::{ brute_force_dimensions, solve, solver_capabilities, ExactProblemKey, SolveOutcome, SolveResult, - SolverRequest, + SolverExecution, SolverRequest, }; use serde_json::Value; use std::any::Any; @@ -292,29 +292,15 @@ impl BundleReplay { }) } - /// Map a target witness under the reduction contract and evaluate for display. - pub fn extract( + /// Recover an externally supplied or internally solved target result. + pub(crate) fn recover_result( &self, - target_config: &serde_json::Value, - ) -> Result<(serde_json::Value, String, String)> { - let (target_eval, _) = self.target.evaluate_dyn(target_config)?; - let source_config = self.chain.extract_solution_json(target_config.clone())?; - let (source_eval, _) = self.source.evaluate_dyn(&source_config)?; - Ok((source_config, source_eval, target_eval)) - } - - /// Solve the target and map the result back to the source problem. - /// - pub(crate) fn solve(&self, request: SolverRequest) -> Result { - let target_result = self.target.solve(request)?; - let solver = target_result.solver; - let target_outcome = target_result.outcome; - let source_outcome = problemreductions::solvers::complete_reduction( - &*self.source, - &self.chain, - &target_outcome, - )?; - + target_outcome: SolveOutcome, + solver: SolverExecution, + ) -> Result { + let source_outcome = self + .chain + .recover_result_json(self.source.as_any(), target_outcome.clone())?; Ok(BundleSolveResult { source_name: self.source_name.clone(), target_name: self.target_name.clone(), @@ -323,6 +309,11 @@ impl BundleReplay { target_outcome, }) } + + pub(crate) fn solve(&self, request: SolverRequest) -> Result { + let result = self.target.solve(request)?; + self.recover_result(result.outcome, result.solver) + } } fn format_step(name: &str, variant: &BTreeMap) -> String { @@ -450,7 +441,21 @@ mod tests { panic!("the QUBO has an optimum"); }; assert_eq!(target, json!([true, false, false])); - assert_eq!(replay.extract(&target).unwrap().0, solution); + assert_eq!( + replay + .recover_result( + SolveOutcome::Optimal { + solution: target, + evaluation: String::new() + }, + SolverExecution::External + ) + .unwrap() + .source_outcome + .into_solution() + .unwrap(), + solution + ); } else { assert_eq!(result.source_outcome, SolveOutcome::Infeasible); assert!(matches!( @@ -463,7 +468,7 @@ mod tests { } #[test] - fn bundle_maps_satisfiability_outcomes_through_the_value_relation() { + fn bundle_preserves_decision_target_bound_and_infeasibility() { for (clauses, feasible) in [ (vec![vec![1, 1, 1], vec![-1, -1, -1]], false), (vec![vec![1, 1, 1], vec![1, 1, 1]], true), @@ -485,20 +490,22 @@ mod tests { let route = crate::commands::reduce::parse_path_json( r#"{"path":[{ "from":{"name":"KSatisfiability","variant":{"k":"K3"}}, - "to":{"name":"MinimumVertexCover","variant":{"graph":"SimpleGraph","weight":"i64"}} + "to":{"name":"DecisionMinimumVertexCover","variant":{"graph":"SimpleGraph","weight":"i64"}} }]}"#, ).unwrap(); let bundle = crate::commands::reduce::execute_route(source, route).unwrap(); let replay = BundleReplay::prepare(&bundle).unwrap(); - let result = replay.solve(SolverRequest::BruteForce); + assert_eq!(replay.target.serialize_json()["bound"], json!(5)); + let result = replay.solve(SolverRequest::BruteForce).unwrap(); + assert_eq!( + matches!(result.target_outcome, SolveOutcome::Infeasible), + !feasible + ); if feasible { - assert!(matches!(result.unwrap().source_outcome, + assert!(matches!(result.source_outcome, SolveOutcome::Optimal { evaluation, .. } if evaluation == "Or(true)")); } else { - assert!(matches!( - result.unwrap().source_outcome, - SolveOutcome::Infeasible - )); + assert!(matches!(result.source_outcome, SolveOutcome::Infeasible)); } } } @@ -527,7 +534,18 @@ mod tests { let replay = BundleReplay::prepare(&bundle).unwrap(); if bound == 1 { assert_eq!( - replay.extract(&json!([true, false])).unwrap().0, + replay + .recover_result( + SolveOutcome::Optimal { + solution: json!([true, false]), + evaluation: String::new() + }, + SolverExecution::External + ) + .unwrap() + .source_outcome + .into_solution() + .unwrap(), json!([true, false]) ); } diff --git a/problemreductions-cli/src/main.rs b/problemreductions-cli/src/main.rs index 9b0b23c1c..902ef0698 100644 --- a/problemreductions-cli/src/main.rs +++ b/problemreductions-cli/src/main.rs @@ -80,7 +80,7 @@ fn main() -> anyhow::Result<()> { } Commands::Reduce(args) => commands::reduce::reduce(&args.input, &args.via, &out), Commands::Evaluate(args) => commands::evaluate::evaluate(&args.input, &args.config, &out), - Commands::Extract(args) => commands::extract::extract(&args.input, &args.config, &out), + Commands::Extract(args) => commands::extract::extract(&args.input, &args.result, &out), #[cfg(feature = "mcp")] Commands::Mcp => mcp::run(), Commands::Completions { shell } => { diff --git a/problemreductions-cli/src/test_support.rs b/problemreductions-cli/src/test_support.rs index a63f94154..52344257c 100644 --- a/problemreductions-cli/src/test_support.rs +++ b/problemreductions-cli/src/test_support.rs @@ -1,13 +1,12 @@ use crate::dispatch::{PathStep, ProblemJsonOutput, ReductionBundle}; -use problemreductions::models::algebraic::{ObjectiveSense, ILP}; +use problemreductions::models::algebraic::ILP; use problemreductions::registry::{ CreateInputCodec, CreateInputInfo, FieldInfo, ProblemSchemaEntry, VariantEntry, }; use problemreductions::rules::registry::{ReductionEntry, ReductionParameterDeclarations}; -use problemreductions::rules::{AggregateReductionResult, VariantReductionResult}; use problemreductions::solvers::SolutionAggregate; use problemreductions::traits::Problem; -use problemreductions::types::{Aggregate, Extremum, Max}; +use problemreductions::types::{Aggregate, Max}; use serde::{Deserialize, Serialize}; use std::any::Any; use std::collections::BTreeMap; @@ -123,24 +122,6 @@ impl problemreductions::solvers::BruteForceProblem for AggregateValueTarget { } } -#[derive(Debug, Clone)] -struct AggregateValueToIlpReduction { - target: ILP, -} - -impl AggregateReductionResult for AggregateValueToIlpReduction { - type Source = AggregateValueSource; - type Target = ILP; - - fn target_problem(&self) -> &Self::Target { - &self.target - } - - fn extract_value(&self, _target_value: Extremum) -> Max { - Max(Some(0)) - } -} - fn decode_bits(indices: Vec) -> Vec { indices.into_iter().map(|index| index != 0).collect() } @@ -411,16 +392,6 @@ problemreductions::inventory::submit! { }, module_path: module_path!(), reduce_fn: None, - reduce_aggregate_fn: Some(|any: &dyn Any| { - let source = any - .downcast_ref::() - .expect("aggregate reduction downcast failed"); - Ok(Box::new(VariantReductionResult::::new( - AggregateValueTarget { - base: source.values.iter().sum(), - }, - ))) - }), turing: false, } } @@ -451,15 +422,6 @@ problemreductions::inventory::submit! { }, module_path: module_path!(), reduce_fn: None, - reduce_aggregate_fn: Some(|any: &dyn Any| { - let _source = any - .downcast_ref::() - .expect("aggregate ILP reduction downcast failed"); - Ok(Box::new(AggregateValueToIlpReduction { - target: ILP::new(0, vec![], vec![], ObjectiveSense::Minimize) - .expect("empty ILP is valid"), - })) - }), turing: false, } } diff --git a/problemreductions-cli/tests/cli_tests.rs b/problemreductions-cli/tests/cli_tests.rs index fd7fa51be..816772633 100644 --- a/problemreductions-cli/tests/cli_tests.rs +++ b/problemreductions-cli/tests/cli_tests.rs @@ -116,7 +116,7 @@ fn test_list_json_respects_category_filter() { assert!(output.status.success()); let json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap(); let variants = json["variants"].as_array().unwrap(); - assert_eq!(json["num_types"], 9); + assert_eq!(json["num_types"], 10); assert!(variants .iter() .all(|variant| variant["name"] != "MaximumIndependentSet")); @@ -9598,13 +9598,23 @@ fn test_extract_roundtrip_mis_to_qubo() { // independent of the reduction path selected by the graph search. let (target_cfg, expected_source_eval) = extract_test_solve_bundle(&bundle_file); + let result_file = + std::env::temp_dir().join("test_extract_roundtrip_mis_to_qubo_target_result.json"); + std::fs::write( + &result_file, + format!( + r#"{{"status":"optimal","solution":{},"evaluation":""}}"#, + target_cfg + ), + ) + .unwrap(); let extract_out = pred() .args([ "--json", "extract", bundle_file.to_str().unwrap(), - "--config", - &target_cfg, + "--result", + result_file.to_str().unwrap(), ]) .output() .unwrap(); @@ -9616,8 +9626,8 @@ fn test_extract_roundtrip_mis_to_qubo() { let stdout = String::from_utf8(extract_out.stdout).unwrap(); let json: serde_json::Value = serde_json::from_str(&stdout).unwrap(); assert_eq!(json["problem"].as_str().unwrap(), "MaximumIndependentSet"); - assert_eq!(json["reduced_to"].as_str().unwrap(), "QUBO"); - assert_eq!(json["solver"].as_str().unwrap(), "external"); + assert_eq!(json["intermediate"]["problem"], "QUBO"); + assert_eq!(json["solver"]["kind"], "external"); // extract on pred-solve's own target config must round-trip to the same source evaluation. assert_eq!(json["evaluation"].as_str().unwrap(), expected_source_eval); assert_eq!(json["intermediate"]["problem"].as_str().unwrap(), "QUBO"); @@ -9645,6 +9655,7 @@ fn test_extract_roundtrip_mis_to_qubo() { std::fs::remove_file(&problem_file).ok(); std::fs::remove_file(&bundle_file).ok(); + std::fs::remove_file(&result_file).unwrap(); } #[test] @@ -9684,13 +9695,23 @@ fn test_extract_decodes_a_qualifying_tour() { String::from_utf8_lossy(&reduce_out.stderr) ); + let result_file = + std::env::temp_dir().join("test_extract_decodes_a_qualifying_tour_target_result.json"); + std::fs::write( + &result_file, + format!( + r#"{{"status":"optimal","solution":{},"evaluation":""}}"#, + "[true,false,false,false,true,false,false,false,true]" + ), + ) + .unwrap(); let extract_out = pred() .args([ "--json", "extract", bundle_file.to_str().unwrap(), - "--config", - "[true,false,false,false,true,false,false,false,true]", + "--result", + result_file.to_str().unwrap(), ]) .output() .unwrap(); @@ -9705,6 +9726,7 @@ fn test_extract_decodes_a_qualifying_tour() { std::fs::remove_file(&problem_file).ok(); std::fs::remove_file(&bundle_file).ok(); + std::fs::remove_file(&result_file).unwrap(); } #[test] @@ -9724,23 +9746,34 @@ fn test_extract_rejects_plain_problem_file() { .unwrap(); assert!(create_out.status.success()); + let result_file = + std::env::temp_dir().join("test_extract_rejects_plain_problem_file_target_result.json"); + std::fs::write( + &result_file, + format!( + r#"{{"status":"optimal","solution":{},"evaluation":""}}"#, + "[false,true,false]" + ), + ) + .unwrap(); let extract_out = pred() .args([ "extract", problem_file.to_str().unwrap(), - "--config", - "[false,true,false]", + "--result", + result_file.to_str().unwrap(), ]) .output() .unwrap(); assert!(!extract_out.status.success()); let stderr = String::from_utf8(extract_out.stderr).unwrap(); assert!( - stderr.contains("not a reduction bundle"), + stderr.contains("requires a reduction bundle"), "unexpected stderr: {stderr}" ); std::fs::remove_file(&problem_file).ok(); + std::fs::remove_file(&result_file).unwrap(); } #[test] @@ -9773,12 +9806,22 @@ fn test_extract_rejects_wrong_config_length() { &bundle_file, ); + let result_file = + std::env::temp_dir().join("test_extract_rejects_wrong_config_length_target_result.json"); + std::fs::write( + &result_file, + format!( + r#"{{"status":"optimal","solution":{},"evaluation":""}}"#, + "[false,true]" + ), + ) + .unwrap(); let extract_out = pred() .args([ "extract", bundle_file.to_str().unwrap(), - "--config", - "[false,true]", + "--result", + result_file.to_str().unwrap(), ]) .output() .unwrap(); @@ -9791,6 +9834,7 @@ fn test_extract_rejects_wrong_config_length() { std::fs::remove_file(&problem_file).ok(); std::fs::remove_file(&bundle_file).ok(); + std::fs::remove_file(&result_file).unwrap(); } #[test] @@ -9830,12 +9874,22 @@ fn test_extract_rejects_non_boolean_solution_value() { bad_cfg.as_array_mut().unwrap()[0] = serde_json::json!(9); let bad_cfg = bad_cfg.to_string(); + let result_file = std::env::temp_dir() + .join("test_extract_rejects_non_boolean_solution_value_target_result.json"); + std::fs::write( + &result_file, + format!( + r#"{{"status":"optimal","solution":{},"evaluation":""}}"#, + bad_cfg + ), + ) + .unwrap(); let extract_out = pred() .args([ "extract", bundle_file.to_str().unwrap(), - "--config", - &bad_cfg, + "--result", + result_file.to_str().unwrap(), ]) .output() .unwrap(); @@ -9848,6 +9902,7 @@ fn test_extract_rejects_non_boolean_solution_value() { std::fs::remove_file(&problem_file).ok(); std::fs::remove_file(&bundle_file).ok(); + std::fs::remove_file(&result_file).unwrap(); } #[test] @@ -9890,12 +9945,22 @@ fn test_extract_rejects_malformed_bundle_path_source_mismatch() { let mut f = std::fs::File::create(&tampered_file).unwrap(); f.write_all(bundle.to_string().as_bytes()).unwrap(); + let result_file = std::env::temp_dir() + .join("test_extract_rejects_malformed_bundle_path_source_mismatch_target_result.json"); + std::fs::write( + &result_file, + format!( + r#"{{"status":"optimal","solution":{},"evaluation":""}}"#, + "[false,true,false]" + ), + ) + .unwrap(); let extract_out = pred() .args([ "extract", tampered_file.to_str().unwrap(), - "--config", - "[false,true,false]", + "--result", + result_file.to_str().unwrap(), ]) .output() .unwrap(); @@ -9913,6 +9978,7 @@ fn test_extract_rejects_malformed_bundle_path_source_mismatch() { std::fs::remove_file(&problem_file).ok(); std::fs::remove_file(&bundle_file).ok(); std::fs::remove_file(&tampered_file).ok(); + std::fs::remove_file(&result_file).unwrap(); } #[test] @@ -9960,12 +10026,22 @@ fn test_extract_rejects_tampered_target_data() { // Any config long enough to reach the coherence check; it must fail before // config validation kicks in because prepare() runs first. let (target_cfg, _) = extract_test_solve_bundle(&bundle_file); + let result_file = + std::env::temp_dir().join("test_extract_rejects_tampered_target_data_target_result.json"); + std::fs::write( + &result_file, + format!( + r#"{{"status":"optimal","solution":{},"evaluation":""}}"#, + target_cfg + ), + ) + .unwrap(); let extract_out = pred() .args([ "extract", tampered_file.to_str().unwrap(), - "--config", - &target_cfg, + "--result", + result_file.to_str().unwrap(), ]) .output() .unwrap(); @@ -10001,6 +10077,7 @@ fn test_extract_rejects_tampered_target_data() { std::fs::remove_file(&problem_file).ok(); std::fs::remove_file(&bundle_file).ok(); std::fs::remove_file(&tampered_file).ok(); + std::fs::remove_file(&result_file).unwrap(); } #[test] @@ -10038,8 +10115,24 @@ fn test_extract_reads_bundle_from_stdin() { let (target_cfg, _) = extract_test_solve_bundle(&bundle_file); let bundle_text = std::fs::read_to_string(&bundle_file).unwrap(); + let result_file = + std::env::temp_dir().join("test_extract_reads_bundle_from_stdin_target_result.json"); + std::fs::write( + &result_file, + format!( + r#"{{"status":"optimal","solution":{},"evaluation":""}}"#, + target_cfg + ), + ) + .unwrap(); let mut child = pred() - .args(["--json", "extract", "-", "--config", &target_cfg]) + .args([ + "--json", + "extract", + "-", + "--result", + result_file.to_str().unwrap(), + ]) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) @@ -10060,10 +10153,112 @@ fn test_extract_reads_bundle_from_stdin() { let stdout = String::from_utf8(output.stdout).unwrap(); let json: serde_json::Value = serde_json::from_str(&stdout).unwrap(); assert_eq!(json["problem"].as_str().unwrap(), "MaximumIndependentSet"); - assert_eq!(json["reduced_to"].as_str().unwrap(), "QUBO"); - assert_eq!(json["solver"].as_str().unwrap(), "external"); + assert_eq!(json["intermediate"]["problem"], "QUBO"); + assert_eq!(json["solver"]["kind"], "external"); assert_eq!(json["evaluation"].as_str().unwrap(), "Max(2)"); std::fs::remove_file(&problem_file).ok(); std::fs::remove_file(&bundle_file).ok(); + std::fs::remove_file(&result_file).unwrap(); +} + +#[test] +fn test_create_decision_closest_vector_preserves_rational_bound() { + let bound = serde_json::json!([num_bigint::BigInt::from(3), num_bigint::BigInt::from(2)]); + let output = pred() + .args([ + "create", + "DecisionClosestVectorProblem", + "--basis", + "1", + "--target-vec", + "0", + "--bound", + &bound.to_string(), + ]) + .output() + .unwrap(); + assert!( + output.status.success(), + "{}", + String::from_utf8_lossy(&output.stderr) + ); + let instance: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap(); + assert_eq!(instance["type"], "DecisionClosestVectorProblem"); + assert_eq!(instance["data"]["bound"], bound); + assert_eq!(instance["data"]["inner"]["basis"], serde_json::json!([[1]])); +} + +#[test] +fn test_extract_preserves_feasible_status_and_rejects_invalid_witnesses() { + let directory = std::env::temp_dir().join("pred_extract_complete_results"); + std::fs::create_dir_all(&directory).unwrap(); + let source_file = directory.join("source.json"); + let bundle_file = directory.join("bundle.json"); + let result_file = directory.join("result.json"); + let source = problemreductions::models::graph::MaximumIndependentSet::new( + problemreductions::topology::SimpleGraph::path(3), + vec![1i64; 3], + ); + std::fs::write( + &source_file, + serde_json::json!({ + "type": "MaximumIndependentSet", "variant": {"graph":"SimpleGraph", "weight":"i64"}, + "data": source, + }) + .to_string(), + ) + .unwrap(); + let reduced = reduce_named_to_file( + &source_file, + "MIS/SimpleGraph/i64", + "MVC/SimpleGraph/i64", + &["MaximumIndependentSet", "MinimumVertexCover"], + &bundle_file, + ); + assert!( + reduced.status.success(), + "{}", + String::from_utf8_lossy(&reduced.stderr) + ); + for (solution, valid) in [ + (vec![true, true, true], true), + (vec![false, false, false], false), + ] { + std::fs::write( + &result_file, + serde_json::json!({ + "status":"feasible", "solution":solution, "evaluation":"", + }) + .to_string(), + ) + .unwrap(); + let output = pred() + .args([ + "--json", + "extract", + bundle_file.to_str().unwrap(), + "--result", + result_file.to_str().unwrap(), + ]) + .output() + .unwrap(); + assert_eq!( + output.status.success(), + valid, + "{}", + String::from_utf8_lossy(&output.stderr) + ); + if valid { + let result: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap(); + assert_eq!(result["status"], "feasible"); + assert_eq!(result["solution"], serde_json::json!([false, false, false])); + assert_eq!(result["evaluation"], "Max(0)"); + assert_eq!(result["intermediate"]["status"], "feasible"); + assert_eq!(result["intermediate"]["evaluation"], "Min(3)"); + } else { + assert!(String::from_utf8_lossy(&output.stderr).contains("infeasible target solution")); + } + } + std::fs::remove_dir_all(directory).unwrap(); } diff --git a/problemreductions-macros/src/lib.rs b/problemreductions-macros/src/lib.rs index a50843782..ce150f673 100644 --- a/problemreductions-macros/src/lib.rs +++ b/problemreductions-macros/src/lib.rs @@ -205,8 +205,6 @@ fn option_inner_type(ty: &Type) -> Option<&Type> { /// - `transform = upper_bound { field = expression, ... }` — one rule-level upper bound /// - `transform = unavailable { field = "reason", ... }` — no symbolic parameter transform /// - `unavailable = { field = "reason", ... }` — fields that cannot be propagated -/// - `aggregate = identity` or `aggregate = custom` — register the reduction result's -/// `AggregateReductionResult` implementation alongside its witness extractor /// /// ## Syntax /// ```ignore @@ -239,7 +237,6 @@ struct ReductionAttrs { relation: Option, fields: Option>, unavailable: Option>, - aggregate: bool, } #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -255,7 +252,6 @@ impl syn::parse::Parse for ReductionAttrs { relation: None, fields: None, unavailable: None, - aggregate: false, }; while !input.is_empty() { @@ -305,16 +301,6 @@ impl syn::parse::Parse for ReductionAttrs { syn::braced!(content in input); attrs.unavailable = Some(parse_unavailable_fields(&content)?); } - "aggregate" => { - let value: syn::Ident = input.parse()?; - if value != "identity" && value != "custom" { - return Err(syn::Error::new( - value.span(), - "expected `identity` or `custom`", - )); - } - attrs.aggregate = true; - } _ => { return Err(syn::Error::new( ident.span(), @@ -517,44 +503,6 @@ fn generate_reduction_entry( .ok_or_else(|| syn::Error::new_spanned(source_type, "Cannot extract source type name"))?; let target_name = extract_type_name(&target_type) .ok_or_else(|| syn::Error::new_spanned(&target_type, "Cannot extract target type name"))?; - let reduce_aggregate_fn = if attrs.aggregate { - quote! { - Some(|src: &dyn std::any::Any| -> Result, crate::rules::ReductionError> { - let src = src.downcast_ref::<#source_type>().ok_or_else( - crate::rules::ReductionError::source_type_mismatch::<#source_type, #target_type>, - )?; - let result = <#source_type as crate::rules::ReduceTo<#target_type>>::reduce_to(src)?; - Ok(Box::new(result)) - }) - } - } else { - quote! { None } - }; - - let aggregate_view = if attrs.aggregate { - quote! { Some(result.clone()) } - } else { - quote! { None } - }; - - let interpret_optimum = if attrs.aggregate { - quote! { - Some({ - let result = result.clone(); - std::rc::Rc::new(move |solution: &dyn std::any::Any| { - let solution = solution.downcast_ref::<<#target_type as crate::traits::Problem>::Solution>() - .ok_or_else(|| crate::rules::ExtractionError::invalid("target solution type mismatch"))?; - let target = crate::rules::ReductionResult::target_problem(result.as_ref()); - let value = crate::traits::Problem::evaluate(target, solution)?; - let value = crate::rules::AggregateReductionResult::extract_value(result.as_ref(), value); - Ok(value.is_valid()) - }) - }) - } - } else { - quote! { None } - }; - // Collect generic parameter info from the impl block let type_generics = collect_type_generic_names(&impl_block.generics); @@ -603,12 +551,9 @@ fn generate_reduction_entry( let result = <#source_type as crate::rules::ReduceTo<#target_type>>::reduce_to(src)?; let result = std::rc::Rc::new(result); Ok(crate::rules::registry::ExecutedStep { - aggregate: #aggregate_view, - interpret_optimum: #interpret_optimum, witness: result, }) }), - reduce_aggregate_fn: #reduce_aggregate_fn, turing: false, } } @@ -1308,24 +1253,25 @@ mod tests { let implementation: syn::ItemImpl = syn::parse_quote! { impl ReduceTo for Source {} }; - for (declaration, enabled) in [ - (quote! {}, false), - (quote! { aggregate = identity, }, true), - (quote! { aggregate = custom, }, true), + let attrs: ReductionAttrs = syn::parse2(quote! { + transform = exact { num_vertices = "num_vertices" } + }) + .unwrap(); + let tokens = generate_reduction_entry(&attrs, &implementation) + .unwrap() + .to_string(); + assert!(tokens.contains("reduce_fn : Some")); + assert!(tokens.contains("ExecutedStep")); + for declaration in [ + quote! { aggregate = identity }, + quote! { aggregate = custom }, + quote! { aggregate = unknown }, ] { - let attrs: ReductionAttrs = syn::parse2(quote! { - #declaration transform = exact { num_vertices = "num_vertices" } + assert!(syn::parse2::(quote! { + #declaration, transform = exact { num_vertices = "num_vertices" } }) - .unwrap(); - let tokens = generate_reduction_entry(&attrs, &implementation) - .unwrap() - .to_string(); - assert_eq!(tokens.contains("reduce_aggregate_fn : Some"), enabled); + .is_err()); } - assert!(syn::parse2::(quote! { - aggregate = unknown, transform = exact { num_vertices = "num_vertices" } - }) - .is_err()); } #[test] diff --git a/scripts/build_website.py b/scripts/build_website.py index 33a36f40c..3f5ab2898 100644 --- a/scripts/build_website.py +++ b/scripts/build_website.py @@ -173,8 +173,8 @@ def build(output, graph_path, schemas_path): # Rule modules are private; rustdoc publishes the shared public contracts. contract = ( "rules/enum.ReductionMode.html" if edge.get("turing") else - "rules/trait.ReduceTo.html" if edge.get("witness") else - "rules/trait.ReduceToAggregate.html" + "rules/trait.ReductionResult.html" if edge.get("witness") else + "rules/struct.ReductionEntry.html" ) site_edges.append({ **edge, diff --git a/src/example_db/specs.rs b/src/example_db/specs.rs index f07dddf17..7dac2acf1 100644 --- a/src/example_db/specs.rs +++ b/src/example_db/specs.rs @@ -105,7 +105,15 @@ where let ilp_solution = crate::solvers::ILPSolver::new() .solve(reduction.target_problem()) .expect("canonical example must be ILP-solvable"); - let source_config = reduction.extract_solution(&ilp_solution).unwrap(); + let source_config = reduction + .recover_result( + &source, + crate::solvers::SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()) + .unwrap(), + ) + .unwrap() + .into_solution() + .unwrap(); assemble_rule_example( &source, reduction.target_problem(), diff --git a/src/lib.rs b/src/lib.rs index ae4edab0c..2ab0c154b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -101,7 +101,7 @@ pub mod prelude { // Core traits pub use crate::rules::{ReduceTo, ReductionResult}; - pub use crate::solvers::BruteForce; + pub use crate::solvers::{BruteForce, ProblemOutcome, SolveOutcome}; pub use crate::traits::Problem; // Types diff --git a/src/models/algebraic/closest_vector_problem.rs b/src/models/algebraic/closest_vector_problem.rs index d2ab02c83..37456f6a5 100644 --- a/src/models/algebraic/closest_vector_problem.rs +++ b/src/models/algebraic/closest_vector_problem.rs @@ -279,3 +279,45 @@ pub(crate) fn canonical_model_example_specs() -> Vec, "DecisionClosestVectorProblem"); + +inventory::submit! { + crate::registry::ProblemSchemaEntry { + name: "DecisionClosestVectorProblem", display_name: "Decision ClosestVectorProblem", aliases: &[], + dimensions: &[VariantDimension::new("target", "i64", &["i64"])], category: crate::registry::ProblemCategory::Algebraic, module_path: module_path!(), + description: "Does a feasible solution meet the objective bound?", + fields: &[ + crate::registry::FieldInfo { name: "basis", type_name: "Vec>", description: "Integer basis matrix as semicolon-separated column vectors." }, + crate::registry::FieldInfo { name: "target_vec", type_name: "Vec", description: "Target vector." }, + crate::registry::FieldInfo { name: "bound", type_name: "BigRational", description: "Decision objective bound" }, + ], + } +} +crate::declare_variants! { + default crate::models::decision::Decision> => "2^(num_basis_vectors * log(num_basis_vectors))" create crate::models::decision::DecisionCreateSpec>, +} +crate::register_decision_variant!(@edges ClosestVectorProblem, "DecisionClosestVectorProblem"); + +#[cfg(feature = "example-db")] +pub(crate) fn decision_canonical_rule_example_specs( +) -> Vec { + vec![crate::example_db::specs::RuleExampleSpec { + id: "decision_closest_vector_problem_to_closest_vector_problem", + build: || { + let source = crate::models::decision::Decision::new( + ClosestVectorProblem::new(vec![vec![2, 0], vec![1, 2]], vec![3_i64, 2]) + .expect("canonical closest-vector instance must be valid"), + BigRational::zero(), + ); + let witness = serde_json::json!(vec![1, 1]); + crate::example_db::specs::rule_example_with_witness::<_, ClosestVectorProblem>( + source, + crate::export::SolutionPair { + source_config: witness.clone(), + target_config: witness, + }, + ) + }, + }] +} diff --git a/src/models/algebraic/feasible_basis_extension.rs b/src/models/algebraic/feasible_basis_extension.rs index c9c336db6..18fd530fe 100644 --- a/src/models/algebraic/feasible_basis_extension.rs +++ b/src/models/algebraic/feasible_basis_extension.rs @@ -80,38 +80,7 @@ struct FeasibleBasisExtensionCreateSpec { impl TryFrom for FeasibleBasisExtension { type Error = crate::registry::ConstructionError; fn try_from(spec: FeasibleBasisExtensionCreateSpec) -> Result { - let m = spec.matrix.len(); - let first = spec - .matrix - .first() - .ok_or("matrix must have at least one row")?; - let n = first.len(); - if spec.matrix.iter().any(|row| row.len() != n) { - return Err("all matrix rows must have the same length".into()); - } - if m >= n { - return Err("number of rows must be less than number of columns".into()); - } - if spec.rhs.len() != m { - return Err("rhs length must equal number of rows".into()); - } - if spec.required_columns.len() >= m { - return Err("required_columns length must be less than number of rows".into()); - } - let mut seen = std::collections::HashSet::new(); - for &column in &spec.required_columns { - if column >= n { - return Err(format!("required column {column} is out of bounds").into()); - } - if !seen.insert(column) { - return Err(format!("duplicate required column {column}").into()); - } - } - Ok(Self { - matrix: spec.matrix, - rhs: spec.rhs, - required_columns: spec.required_columns, - }) + Self::try_new(spec.matrix, spec.rhs, spec.required_columns) } } diff --git a/src/models/algebraic/minimum_weight_decoding.rs b/src/models/algebraic/minimum_weight_decoding.rs index 8521362ac..07d45585f 100644 --- a/src/models/algebraic/minimum_weight_decoding.rs +++ b/src/models/algebraic/minimum_weight_decoding.rs @@ -73,23 +73,7 @@ struct MinimumWeightDecodingCreateSpec { impl TryFrom for MinimumWeightDecoding { type Error = crate::registry::ConstructionError; fn try_from(spec: MinimumWeightDecodingCreateSpec) -> Result { - let first = spec - .matrix - .first() - .ok_or("matrix must have at least one row")?; - if first.is_empty() { - return Err("matrix must have at least one column".into()); - } - if spec.matrix.iter().any(|row| row.len() != first.len()) { - return Err("all matrix rows must have the same length".into()); - } - if spec.target.len() != spec.matrix.len() { - return Err("Target length must equal number of rows".into()); - } - Ok(Self { - matrix: spec.matrix, - target: spec.target, - }) + Self::try_new(spec.matrix, spec.target) } } diff --git a/src/models/algebraic/minimum_weight_solution_to_linear_equations.rs b/src/models/algebraic/minimum_weight_solution_to_linear_equations.rs index c90f66f83..b08c18d92 100644 --- a/src/models/algebraic/minimum_weight_solution_to_linear_equations.rs +++ b/src/models/algebraic/minimum_weight_solution_to_linear_equations.rs @@ -72,23 +72,7 @@ struct MinimumWeightSolutionCreateSpec { impl TryFrom for MinimumWeightSolutionToLinearEquations { type Error = crate::registry::ConstructionError; fn try_from(spec: MinimumWeightSolutionCreateSpec) -> Result { - let first = spec - .matrix - .first() - .ok_or("matrix must have at least one row")?; - if first.is_empty() { - return Err("matrix must have at least one column".into()); - } - if spec.matrix.iter().any(|row| row.len() != first.len()) { - return Err("all matrix rows must have the same length".into()); - } - if spec.rhs.len() != spec.matrix.len() { - return Err("RHS length must equal number of rows".into()); - } - Ok(Self { - matrix: spec.matrix, - rhs: spec.rhs, - }) + Self::try_new(spec.matrix, spec.rhs) } } diff --git a/src/models/algebraic/quadratic_assignment.rs b/src/models/algebraic/quadratic_assignment.rs index 5df31475a..48a8d6be0 100644 --- a/src/models/algebraic/quadratic_assignment.rs +++ b/src/models/algebraic/quadratic_assignment.rs @@ -250,3 +250,52 @@ pub(crate) fn canonical_model_example_specs() -> Vec>", description: "Flow/cost matrix between facilities" }, + crate::registry::FieldInfo { name: "distance_matrix", type_name: "Vec>", description: "Distance matrix between locations" }, + crate::registry::FieldInfo { name: "bound", type_name: "i64", description: "Decision objective bound" }, + ], + decode: |_, indices: Vec| indices +); + +#[cfg(feature = "example-db")] +pub(crate) fn decision_canonical_rule_example_specs( +) -> Vec { + vec![crate::example_db::specs::RuleExampleSpec { + id: "decision_quadratic_assignment_to_quadratic_assignment", + build: || { + let source = crate::models::decision::Decision::new( + QuadraticAssignment::new( + vec![ + vec![0, 5, 2, 0], + vec![5, 0, 0, 3], + vec![2, 0, 0, 4], + vec![0, 3, 4, 0], + ], + vec![ + vec![0, 4, 1, 1], + vec![4, 0, 3, 4], + vec![1, 3, 0, 4], + vec![1, 4, 4, 0], + ], + ), + 56, + ); + let witness = serde_json::json!(vec![3, 0, 1, 2]); + crate::example_db::specs::rule_example_with_witness::<_, QuadraticAssignment>( + source, + crate::export::SolutionPair { + source_config: witness.clone(), + target_config: witness, + }, + ) + }, + }] +} diff --git a/src/models/algebraic/qubo.rs b/src/models/algebraic/qubo.rs index 86466e024..7ba5d10fd 100644 --- a/src/models/algebraic/qubo.rs +++ b/src/models/algebraic/qubo.rs @@ -308,3 +308,38 @@ pub(crate) fn canonical_model_example_specs() -> Vec, "DecisionQUBO"); +crate::register_decision_variant!( + QUBO, "DecisionQUBO", "2^num_vars", &[], + "Does a feasible solution meet the objective bound?", + category: crate::registry::ProblemCategory::Algebraic, + dims: [VariantDimension::new("weight", "i64", &["i64"])], + fields: [ + crate::registry::FieldInfo { name: "matrix", type_name: "Vec>", description: "Q matrix; the number of variables is its row count." }, + crate::registry::FieldInfo { name: "bound", type_name: "i64", description: "Decision objective bound" }, + ], + decode: |_, indices: Vec| crate::config::config_to_bits(&indices) +); + +#[cfg(feature = "example-db")] +pub(crate) fn decision_canonical_rule_example_specs( +) -> Vec { + vec![crate::example_db::specs::RuleExampleSpec { + id: "decision_qubo_to_qubo", + build: || { + let source = crate::models::decision::Decision::new( + QUBO::from_matrix(vec![vec![-1, 2, 0], vec![0, -1, 2], vec![0, 0, -1]]).unwrap(), + -2, + ); + let witness = serde_json::json!(vec![true, false, true]); + crate::example_db::specs::rule_example_with_witness::<_, QUBO>( + source, + crate::export::SolutionPair { + source_config: witness.clone(), + target_config: witness, + }, + ) + }, + }] +} diff --git a/src/models/decision.rs b/src/models/decision.rs index be8918fed..ba9e3255a 100644 --- a/src/models/decision.rs +++ b/src/models/decision.rs @@ -1,6 +1,6 @@ //! Generic decision wrapper for optimization problems. -use crate::rules::{AggregateReductionResult, ReduceTo, ReduceToAggregate, ReductionResult}; +use crate::rules::{ReduceTo, ReductionResult}; use crate::traits::Problem; use crate::types::{OptimizationValue, Or}; use serde::de::DeserializeOwned; @@ -98,31 +98,9 @@ macro_rules! register_decision_variant { <$crate::models::decision::Decision<$inner> as $crate::rules::ReduceTo<$inner>>::reduce_to(source)?; let result = std::rc::Rc::new(result); Ok($crate::rules::registry::ExecutedStep { - aggregate: Some(result.clone()), - interpret_optimum: Some({ - let result = result.clone(); - std::rc::Rc::new(move |solution: &dyn std::any::Any| { - let solution = solution.downcast_ref::<<$inner as $crate::traits::Problem>::Solution>() - .ok_or_else(|| $crate::rules::ExtractionError::invalid("target solution type mismatch"))?; - let target = $crate::rules::ReductionResult::target_problem(result.as_ref()); - let value = $crate::traits::Problem::evaluate(target, solution)?; - Ok($crate::rules::AggregateReductionResult::extract_value(result.as_ref(), value).is_valid()) - }) - }), witness: result, }) }), - reduce_aggregate_fn: Some(|any| { - let source = any - .downcast_ref::<$crate::models::decision::Decision<$inner>>() - .ok_or_else($crate::rules::ReductionError::source_type_mismatch::< - $crate::models::decision::Decision<$inner>, - $inner, - >)?; - let result = - <$crate::models::decision::Decision<$inner> as $crate::rules::ReduceToAggregate<$inner>>::reduce_to_aggregate(source)?; - Ok(Box::new(result)) - }), turing: false, } } @@ -144,7 +122,6 @@ macro_rules! register_decision_variant { }, module_path: module_path!(), reduce_fn: None, - reduce_aggregate_fn: None, turing: true, } } @@ -216,7 +193,7 @@ where type_name: std::any::type_name::<::Inner>(), description: "Decision objective bound", required: true, - codec: crate::registry::CreateInputCodec::Scalar, + codec: crate::registry::CreateInputCodec::Json, }); inputs } @@ -344,8 +321,8 @@ where /// /// The target and decision bound belong to the same execution. An optimum /// meeting the bound supplies a decision witness; an optimum missing the bound -/// establishes NO through `extract_value`. Witness extraction copies a target -/// witness that meets the bound and does not repeat the comparison. +/// recovers `Infeasible`. A feasible candidate missing the bound instead returns +/// `InsufficientSolutionQuality`, since it does not establish NO. #[derive(Debug, Clone)] pub struct DecisionToOptimizationResult

where @@ -356,19 +333,12 @@ where bound: ::Inner, } -impl

AggregateReductionResult for DecisionToOptimizationResult

+impl

DecisionToOptimizationResult

where P: DecisionProblemMeta + 'static, - P::Value: OptimizationValue + Serialize + DeserializeOwned, + P::Value: OptimizationValue, { - type Source = Decision

; - type Target = P; - - fn target_problem(&self) -> &Self::Target { - &self.target - } - - fn extract_value(&self, target_value: P::Value) -> Or { + fn map_value(&self, target_value: P::Value) -> Or { Or(::meets_bound( &target_value, &self.bound, @@ -376,25 +346,9 @@ where } } -impl

ReduceToAggregate

for Decision

-where - P: DecisionProblemMeta + Clone + 'static, - P::Value: OptimizationValue + Serialize + DeserializeOwned, -{ - type Result = DecisionToOptimizationResult

; - - fn reduce_to_aggregate(&self) -> Result { - Ok(DecisionToOptimizationResult { - target: self.inner.clone(), - bound: self.bound.clone(), - }) - } -} - impl

ReductionResult for DecisionToOptimizationResult

where P: DecisionProblemMeta + 'static, - P::Solution: Clone, P::Value: OptimizationValue, { type Source = Decision

; @@ -404,18 +358,40 @@ where &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { - Ok(target_solution.clone()) + source: &Self::Source, + target: crate::solvers::ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + crate::solvers::SolveOutcome::Infeasible => { + Ok(crate::solvers::SolveOutcome::Infeasible) + } + crate::solvers::SolveOutcome::Optimal { + solution, + evaluation, + } => { + if !self.map_value(evaluation).is_valid() { + return Ok(crate::solvers::SolveOutcome::Infeasible); + } + Ok(crate::solvers::SolveOutcome::optimal(source, solution)?) + } + crate::solvers::SolveOutcome::Feasible { + solution, + evaluation, + } => { + if !self.map_value(evaluation).is_valid() { + return Err(crate::rules::ExtractionError::InsufficientSolutionQuality); + } + Ok(crate::solvers::SolveOutcome::feasible(source, solution)?) + } + } } } impl

ReduceTo

for Decision

where P: DecisionProblemMeta + Clone + 'static, - P::Solution: Clone, P::Value: OptimizationValue, { type Result = DecisionToOptimizationResult

; diff --git a/src/models/formula/maximum_2_satisfiability.rs b/src/models/formula/maximum_2_satisfiability.rs index 98d6bc3ba..ccf43b08e 100644 --- a/src/models/formula/maximum_2_satisfiability.rs +++ b/src/models/formula/maximum_2_satisfiability.rs @@ -196,3 +196,50 @@ pub(crate) fn canonical_model_example_specs() -> Vec", description: "Collection of 2-literal clauses" }, + crate::registry::FieldInfo { name: "bound", type_name: "i64", description: "Decision objective bound" }, + ], + decode: |_, indices: Vec| crate::config::config_to_bits(&indices) +); + +#[cfg(feature = "example-db")] +pub(crate) fn decision_canonical_rule_example_specs( +) -> Vec { + vec![crate::example_db::specs::RuleExampleSpec { + id: "decision_maximum_2_satisfiability_to_maximum_2_satisfiability", + build: || { + let source = crate::models::decision::Decision::new( + Maximum2Satisfiability::new( + 4, + vec![ + CNFClause::new(vec![1, 2]), + CNFClause::new(vec![1, -2]), + CNFClause::new(vec![-1, 3]), + CNFClause::new(vec![-1, -3]), + CNFClause::new(vec![2, 4]), + CNFClause::new(vec![-3, -4]), + CNFClause::new(vec![3, 4]), + ], + ), + 6, + ); + let witness = serde_json::json!(vec![true, true, false, true]); + crate::example_db::specs::rule_example_with_witness::<_, Maximum2Satisfiability>( + source, + crate::export::SolutionPair { + source_config: witness.clone(), + target_config: witness, + }, + ) + }, + }] +} diff --git a/src/models/graph/longest_circuit.rs b/src/models/graph/longest_circuit.rs index 808a17e29..07827bbbb 100644 --- a/src/models/graph/longest_circuit.rs +++ b/src/models/graph/longest_circuit.rs @@ -386,3 +386,62 @@ crate::register_brute_force! { #[cfg(test)] #[path = "../../unit_tests/models/graph/longest_circuit.rs"] mod tests; + +crate::decision_problem_meta!(LongestCircuit, "DecisionLongestCircuit"); +crate::register_decision_variant!( + LongestCircuit, "DecisionLongestCircuit", "2^num_vertices * num_vertices^2", &[], + "Does a feasible solution meet the objective bound?", + category: crate::registry::ProblemCategory::Graph, + dims: [ + VariantDimension::new("graph", "SimpleGraph", &["SimpleGraph"]), + VariantDimension::new("weight", "i64", &["i64"]), + ], + fields: [ + crate::registry::FieldInfo { name: "graph", type_name: "Vec<(usize,usize)>", description: "" }, + crate::registry::FieldInfo { name: "num_vertices", type_name: "usize", description: "" }, + crate::registry::FieldInfo { name: "edge_weights", type_name: "Vec", description: "" }, + crate::registry::FieldInfo { name: "bound", type_name: "i64", description: "Decision objective bound" }, + ], + decode: |_, indices: Vec| crate::config::config_to_bits(&indices) +); + +#[cfg(feature = "example-db")] +pub(crate) fn decision_canonical_rule_example_specs( +) -> Vec { + vec![crate::example_db::specs::RuleExampleSpec { + id: "decision_longest_circuit_to_longest_circuit", + build: || { + let source = crate::models::decision::Decision::new( + LongestCircuit::new( + SimpleGraph::new( + 6, + vec![ + (0, 1), + (1, 2), + (2, 3), + (3, 4), + (4, 5), + (5, 0), + (0, 3), + (1, 4), + (2, 5), + (3, 5), + ], + ), + vec![3, 2, 4, 1, 5, 2, 3, 2, 1, 2], + ), + 18, + ); + let witness = serde_json::json!(vec![ + true, false, true, false, true, false, true, true, true, false + ]); + crate::example_db::specs::rule_example_with_witness::<_, LongestCircuit>( + source, + crate::export::SolutionPair { + source_config: witness.clone(), + target_config: witness, + }, + ) + }, + }] +} diff --git a/src/models/graph/longest_path.rs b/src/models/graph/longest_path.rs index 029e06af5..527534cfc 100644 --- a/src/models/graph/longest_path.rs +++ b/src/models/graph/longest_path.rs @@ -355,3 +355,44 @@ pub(crate) fn canonical_model_example_specs() -> Vec, "DecisionLongestPath"); +crate::register_decision_variant!( + LongestPath, "DecisionLongestPath", "num_vertices * 2^num_vertices", &[], + "Does a feasible solution meet the objective bound?", + category: crate::registry::ProblemCategory::Graph, + dims: [ + VariantDimension::new("graph", "SimpleGraph", &["SimpleGraph"]), + VariantDimension::new("weight", "One", &["One"]), + ], + fields: [ + crate::registry::FieldInfo { name: "graph", type_name: "Vec<(usize,usize)>", description: "" }, + crate::registry::FieldInfo { name: "num_vertices", type_name: "usize", description: "" }, + crate::registry::FieldInfo { name: "source_vertex", type_name: "usize", description: "" }, + crate::registry::FieldInfo { name: "target_vertex", type_name: "usize", description: "" }, + crate::registry::FieldInfo { name: "bound", type_name: "i64", description: "Decision objective bound" }, + ], + decode: |_, indices: Vec| crate::config::config_to_bits(&indices) +); + +#[cfg(feature = "example-db")] +pub(crate) fn decision_canonical_rule_example_specs( +) -> Vec { + vec![crate::example_db::specs::RuleExampleSpec { + id: "decision_longest_path_to_longest_path", + build: || { + let source = crate::models::decision::Decision::new( + LongestPath::new(SimpleGraph::path(3), vec![crate::types::One; 2], 0, 2), + 2, + ); + let witness = serde_json::json!(vec![true, true]); + crate::example_db::specs::rule_example_with_witness::<_, LongestPath>( + source, + crate::export::SolutionPair { + source_config: witness.clone(), + target_config: witness, + }, + ) + }, + }] +} diff --git a/src/models/graph/max_cut.rs b/src/models/graph/max_cut.rs index 9a0a9a645..de1d015ac 100644 --- a/src/models/graph/max_cut.rs +++ b/src/models/graph/max_cut.rs @@ -360,3 +360,46 @@ pub(crate) fn canonical_model_example_specs() -> Vec, "DecisionMaxCut"); +crate::register_decision_variant!( + MaxCut, "DecisionMaxCut", "2^(2.372 * num_vertices / 3)", &[], + "Does a feasible solution meet the objective bound?", + category: crate::registry::ProblemCategory::Graph, + dims: [ + VariantDimension::new("graph", "SimpleGraph", &["SimpleGraph"]), + VariantDimension::new("weight", "i64", &["i64"]), + ], + fields: [ + crate::registry::FieldInfo { name: "graph", type_name: "Vec<(usize,usize)>", description: "" }, + crate::registry::FieldInfo { name: "num_vertices", type_name: "usize", description: "" }, + crate::registry::FieldInfo { name: "edge_weights", type_name: "Vec", description: "" }, + crate::registry::FieldInfo { name: "bound", type_name: "i64", description: "Decision objective bound" }, + ], + decode: |_, indices: Vec| crate::config::config_to_bits(&indices) +); + +#[cfg(feature = "example-db")] +pub(crate) fn decision_canonical_rule_example_specs( +) -> Vec { + vec![crate::example_db::specs::RuleExampleSpec { + id: "decision_max_cut_to_max_cut", + build: || { + let source = crate::models::decision::Decision::new( + MaxCut::<_, i64>::unweighted(SimpleGraph::new( + 5, + vec![(0, 1), (0, 2), (1, 3), (2, 3), (2, 4), (3, 4)], + )), + 5, + ); + let witness = serde_json::json!(vec![true, false, false, true, false]); + crate::example_db::specs::rule_example_with_witness::<_, MaxCut>( + source, + crate::export::SolutionPair { + source_config: witness.clone(), + target_config: witness, + }, + ) + }, + }] +} diff --git a/src/models/graph/min_max_multicenter.rs b/src/models/graph/min_max_multicenter.rs index da1b374ef..6057a4d07 100644 --- a/src/models/graph/min_max_multicenter.rs +++ b/src/models/graph/min_max_multicenter.rs @@ -452,3 +452,54 @@ pub(crate) fn canonical_model_example_specs() -> Vec, "DecisionMinMaxMulticenter"); +crate::register_decision_variant!( + MinMaxMulticenter, "DecisionMinMaxMulticenter", "1.4969^num_vertices", &[], + "Does a feasible solution meet the objective bound?", + category: crate::registry::ProblemCategory::Graph, + dims: [ + VariantDimension::new("graph", "SimpleGraph", &["SimpleGraph"]), + VariantDimension::new("weight", "One", &["One"]), + ], + fields: [ + crate::registry::FieldInfo { name: "graph", type_name: "Vec<(usize,usize)>", description: "" }, + crate::registry::FieldInfo { name: "num_vertices", type_name: "usize", description: "" }, + crate::registry::FieldInfo { name: "k", type_name: "usize", description: "" }, + crate::registry::FieldInfo { name: "bound", type_name: "i64", description: "Decision objective bound" }, + ], + decode: |_, indices: Vec| crate::config::config_to_bits(&indices) +); + +#[cfg(feature = "example-db")] +pub(crate) fn decision_canonical_rule_example_specs( +) -> Vec { + vec![crate::example_db::specs::RuleExampleSpec { + id: "decision_min_max_multicenter_to_min_max_multicenter", + build: || { + let source = crate::models::decision::Decision::new( + MinMaxMulticenter::new( + SimpleGraph::new( + 6, + vec![(0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (0, 5), (1, 4)], + ), + vec![crate::types::One; 6], + vec![crate::types::One; 7], + 2, + ), + 1, + ); + let witness = serde_json::json!(vec![false, true, false, false, true, false]); + crate::example_db::specs::rule_example_with_witness::< + _, + MinMaxMulticenter, + >( + source, + crate::export::SolutionPair { + source_config: witness.clone(), + target_config: witness, + }, + ) + }, + }] +} diff --git a/src/models/graph/minimum_covering_by_cliques.rs b/src/models/graph/minimum_covering_by_cliques.rs index a96498573..6c943fbf0 100644 --- a/src/models/graph/minimum_covering_by_cliques.rs +++ b/src/models/graph/minimum_covering_by_cliques.rs @@ -227,3 +227,59 @@ pub(crate) fn canonical_model_example_specs() -> Vec, + "DecisionMinimumCoveringByCliques" +); +crate::register_decision_variant!( + MinimumCoveringByCliques, "DecisionMinimumCoveringByCliques", "2^num_edges", &[], + "Does a feasible solution meet the objective bound?", + category: crate::registry::ProblemCategory::Graph, + dims: [ + VariantDimension::new("graph", "SimpleGraph", &["SimpleGraph"]), + ], + fields: [ +crate::registry::FieldInfo { name: "graph", type_name: "G", description: "The underlying graph G=(V,E)" }, +crate::registry::FieldInfo { name: "bound", type_name: "i64", description: "Decision objective bound" }, +], + decode: |_, indices: Vec| indices +); + +#[cfg(feature = "example-db")] +pub(crate) fn decision_canonical_rule_example_specs( +) -> Vec { + vec![crate::example_db::specs::RuleExampleSpec { + id: "decision_minimum_covering_by_cliques_to_minimum_covering_by_cliques", + build: || { + let source = crate::models::decision::Decision::new( + MinimumCoveringByCliques::new(SimpleGraph::new( + 6, + vec![ + (0, 1), + (1, 2), + (2, 3), + (3, 0), + (0, 2), + (4, 0), + (4, 1), + (5, 2), + (5, 3), + ], + )), + 4, + ); + let witness = serde_json::json!(vec![0, 0, 1, 1, 0, 2, 2, 3, 3]); + crate::example_db::specs::rule_example_with_witness::< + _, + MinimumCoveringByCliques, + >( + source, + crate::export::SolutionPair { + source_config: witness.clone(), + target_config: witness, + }, + ) + }, + }] +} diff --git a/src/models/graph/minimum_dominating_set.rs b/src/models/graph/minimum_dominating_set.rs index 1fc1d6895..bd06c3580 100644 --- a/src/models/graph/minimum_dominating_set.rs +++ b/src/models/graph/minimum_dominating_set.rs @@ -383,7 +383,7 @@ pub(crate) fn decision_canonical_rule_example_specs( build: || { use crate::example_db::specs::assemble_rule_example; use crate::export::SolutionPair; - use crate::rules::{AggregateReductionResult, ReduceToAggregate}; + use crate::rules::{ReduceTo, ReductionResult}; let source = crate::models::decision::Decision::new( MinimumDominatingSet::new( @@ -392,8 +392,7 @@ pub(crate) fn decision_canonical_rule_example_specs( ), 2, ); - let result = source - .reduce_to_aggregate() + let result = ReduceTo::>::reduce_to(&source) .expect("reduction should succeed"); let target = result.target_problem(); let config = vec![false, false, true, true, false]; @@ -413,7 +412,7 @@ pub(crate) fn decision_canonical_rule_example_specs( build: || { use crate::example_db::specs::assemble_rule_example; use crate::export::SolutionPair; - use crate::rules::{AggregateReductionResult, ReduceToAggregate}; + use crate::rules::{ReduceTo, ReductionResult}; let source = crate::models::decision::Decision::new( MinimumDominatingSet::new( @@ -422,8 +421,7 @@ pub(crate) fn decision_canonical_rule_example_specs( ), 2, ); - let result = source - .reduce_to_aggregate() + let result = ReduceTo::>::reduce_to(&source) .expect("reduction should succeed"); let target = result.target_problem(); let config = vec![false, false, true, true, false]; diff --git a/src/models/graph/minimum_sum_multicenter.rs b/src/models/graph/minimum_sum_multicenter.rs index e1f6cbe72..33f5e097e 100644 --- a/src/models/graph/minimum_sum_multicenter.rs +++ b/src/models/graph/minimum_sum_multicenter.rs @@ -444,3 +444,65 @@ pub(crate) fn canonical_model_example_specs() -> Vec, "DecisionMinimumSumMulticenter"); +crate::register_decision_variant!( + MinimumSumMulticenter, "DecisionMinimumSumMulticenter", "2^num_vertices", &[], + "Does a feasible solution meet the objective bound?", + category: crate::registry::ProblemCategory::Graph, + dims: [ + VariantDimension::new("graph", "SimpleGraph", &["SimpleGraph"]), + VariantDimension::new("weight", "i64", &["i64"]), + ], + fields: [ + crate::registry::FieldInfo { name: "graph", type_name: "Vec<(usize,usize)>", description: "" }, + crate::registry::FieldInfo { name: "num_vertices", type_name: "usize", description: "" }, + crate::registry::FieldInfo { name: "weights", type_name: "Vec", description: "" }, + crate::registry::FieldInfo { name: "edge_weights", type_name: "Vec", description: "" }, + crate::registry::FieldInfo { name: "k", type_name: "usize", description: "" }, + crate::registry::FieldInfo { name: "bound", type_name: "i64", description: "Decision objective bound" }, + ], + decode: |_, indices: Vec| crate::config::config_to_bits(&indices) +); + +#[cfg(feature = "example-db")] +pub(crate) fn decision_canonical_rule_example_specs( +) -> Vec { + vec![crate::example_db::specs::RuleExampleSpec { + id: "decision_minimum_sum_multicenter_to_minimum_sum_multicenter", + build: || { + let source = crate::models::decision::Decision::new( + MinimumSumMulticenter::new( + SimpleGraph::new( + 7, + vec![ + (0, 1), + (1, 2), + (2, 3), + (3, 4), + (4, 5), + (5, 6), + (0, 6), + (2, 5), + ], + ), + vec![1i64; 7], + vec![1i64; 8], + 2, + ), + 6, + ); + let witness = serde_json::json!(vec![false, false, true, false, false, true, false]); + crate::example_db::specs::rule_example_with_witness::< + _, + MinimumSumMulticenter, + >( + source, + crate::export::SolutionPair { + source_config: witness.clone(), + target_config: witness, + }, + ) + }, + }] +} diff --git a/src/models/graph/optimal_linear_arrangement.rs b/src/models/graph/optimal_linear_arrangement.rs index 6224884e3..0531df50c 100644 --- a/src/models/graph/optimal_linear_arrangement.rs +++ b/src/models/graph/optimal_linear_arrangement.rs @@ -288,7 +288,7 @@ pub(crate) fn decision_canonical_rule_example_specs( build: || { use crate::example_db::specs::assemble_rule_example; use crate::export::SolutionPair; - use crate::rules::{AggregateReductionResult, ReduceToAggregate}; + use crate::rules::{ReduceTo, ReductionResult}; use crate::topology::SimpleGraph; // Path P_4 (0-1-2-3): optimal arrangement has cost 3; bound 3 is YES. @@ -296,8 +296,7 @@ pub(crate) fn decision_canonical_rule_example_specs( OptimalLinearArrangement::new(SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)])), 3, ); - let result = source - .reduce_to_aggregate() + let result = ReduceTo::>::reduce_to(&source) .expect("reduction should succeed"); let target = result.target_problem(); let config = vec![0, 1, 2, 3]; diff --git a/src/models/graph/rural_postman.rs b/src/models/graph/rural_postman.rs index 5265da012..6d032b580 100644 --- a/src/models/graph/rural_postman.rs +++ b/src/models/graph/rural_postman.rs @@ -437,3 +437,57 @@ pub(crate) fn canonical_model_example_specs() -> Vec, "DecisionRuralPostman"); +crate::register_decision_variant!( + RuralPostman, "DecisionRuralPostman", "2^num_vertices * num_vertices^2", &[], + "Does a feasible solution meet the objective bound?", + category: crate::registry::ProblemCategory::Graph, + dims: [ + VariantDimension::new("graph", "SimpleGraph", &["SimpleGraph"]), + VariantDimension::new("weight", "i64", &["i64"]), + ], + fields: [ + crate::registry::FieldInfo { name: "graph", type_name: "Vec<(usize,usize)>", description: "" }, + crate::registry::FieldInfo { name: "num_vertices", type_name: "usize", description: "" }, + crate::registry::FieldInfo { name: "edge_weights", type_name: "Vec", description: "" }, + crate::registry::FieldInfo { name: "required_edges", type_name: "Vec", description: "" }, + crate::registry::FieldInfo { name: "bound", type_name: "i64", description: "Decision objective bound" }, + ], + decode: |_, indices: Vec| indices +); + +#[cfg(feature = "example-db")] +pub(crate) fn decision_canonical_rule_example_specs( +) -> Vec { + vec![crate::example_db::specs::RuleExampleSpec { + id: "decision_rural_postman_to_rural_postman", + build: || { + let graph = SimpleGraph::new( + 6, + vec![ + (0, 1), + (1, 2), + (2, 3), + (3, 4), + (4, 5), + (5, 0), + (0, 3), + (1, 4), + ], + ); + let source = crate::models::decision::Decision::new( + RuralPostman::new(graph, vec![1, 1, 1, 1, 1, 1, 2, 2], vec![0, 2, 4]), + 6, + ); + let witness = serde_json::json!(vec![1, 1, 1, 1, 1, 1, 0, 0]); + crate::example_db::specs::rule_example_with_witness::<_, RuralPostman>( + source, + crate::export::SolutionPair { + source_config: witness.clone(), + target_config: witness, + }, + ) + }, + }] +} diff --git a/src/models/graph/spin_glass.rs b/src/models/graph/spin_glass.rs index fe3a3d53f..1f774e8a8 100644 --- a/src/models/graph/spin_glass.rs +++ b/src/models/graph/spin_glass.rs @@ -448,3 +448,56 @@ pub(crate) fn canonical_model_example_specs() -> Vec, "DecisionSpinGlass"); +crate::register_decision_variant!( + SpinGlass, "DecisionSpinGlass", "2^num_spins", &[], + "Does a feasible solution meet the objective bound?", + category: crate::registry::ProblemCategory::Graph, + dims: [ + VariantDimension::new("graph", "SimpleGraph", &["SimpleGraph"]), + VariantDimension::new("weight", "i64", &["i64"]), + ], + fields: [ + crate::registry::FieldInfo { name: "graph", type_name: "Vec<(usize,usize)>", description: "Undirected interaction graph edges." }, + crate::registry::FieldInfo { name: "num_vertices", type_name: "usize", description: "Vertex count, needed to preserve isolated spins." }, + crate::registry::FieldInfo { name: "couplings", type_name: "Vec", description: "Pairwise couplings; defaults to one per edge." }, + crate::registry::FieldInfo { name: "fields", type_name: "Vec", description: "On-site fields; defaults to zero per vertex." }, + crate::registry::FieldInfo { name: "bound", type_name: "i64", description: "Decision objective bound" }, + ], + decode: |_, indices: Vec| SpinGlass::::config_to_spins(&indices).expect("enumerated spin bits are valid") +); + +#[cfg(feature = "example-db")] +pub(crate) fn decision_canonical_rule_example_specs( +) -> Vec { + vec![crate::example_db::specs::RuleExampleSpec { + id: "decision_spin_glass_to_spin_glass", + build: || { + let source = crate::models::decision::Decision::new( + SpinGlass::::without_fields( + 5, + vec![ + ((0, 1), 1), + ((1, 2), 1), + ((3, 4), 1), + ((0, 3), 1), + ((1, 3), 1), + ((1, 4), 1), + ((2, 4), 1), + ], + ) + .unwrap(), + -3, + ); + let witness = serde_json::json!(vec![1, -1, 1, 1, -1]); + crate::example_db::specs::rule_example_with_witness::<_, SpinGlass>( + source, + crate::export::SolutionPair { + source_config: witness.clone(), + target_config: witness, + }, + ) + }, + }] +} diff --git a/src/models/misc/minimum_decision_tree.rs b/src/models/misc/minimum_decision_tree.rs index c42680885..791d8c659 100644 --- a/src/models/misc/minimum_decision_tree.rs +++ b/src/models/misc/minimum_decision_tree.rs @@ -74,38 +74,7 @@ struct MinimumDecisionTreeCreateSpec { impl TryFrom for MinimumDecisionTree { type Error = crate::registry::ConstructionError; fn try_from(spec: MinimumDecisionTreeCreateSpec) -> Result { - if spec.num_objects < 2 { - return Err("num_objects must be at least 2".into()); - } - if spec.num_tests == 0 { - return Err("num_tests must be positive".into()); - } - if spec.test_matrix.len() != spec.num_tests { - return Err("test_matrix row count must equal num_tests".into()); - } - if spec - .test_matrix - .iter() - .any(|row| row.len() != spec.num_objects) - { - return Err("each test_matrix row must have num_objects columns".into()); - } - for a in 0..spec.num_objects { - for b in a + 1..spec.num_objects { - if !(0..spec.num_tests) - .any(|test| spec.test_matrix[test][a] != spec.test_matrix[test][b]) - { - return Err( - format!("objects {a} and {b} are not distinguished by any test").into(), - ); - } - } - } - Ok(Self { - test_matrix: spec.test_matrix, - num_objects: spec.num_objects, - num_tests: spec.num_tests, - }) + Self::try_new(spec.test_matrix, spec.num_objects, spec.num_tests) } } diff --git a/src/models/misc/mod.rs b/src/models/misc/mod.rs index 46a4fbbde..e18f75d24 100644 --- a/src/models/misc/mod.rs +++ b/src/models/misc/mod.rs @@ -155,7 +155,7 @@ mod multiprocessor_scheduling; mod non_liveness_free_petri_net; mod numerical_3_dimensional_matching; mod numerical_matching_with_target_sums; -mod open_shop_scheduling; +pub(crate) mod open_shop_scheduling; pub(crate) mod optimum_communication_spanning_tree; pub(crate) mod paintshop; pub(crate) mod partially_ordered_knapsack; @@ -169,7 +169,7 @@ pub(crate) mod resource_constrained_scheduling; mod scheduling_to_minimize_weighted_completion_time; mod scheduling_with_individual_deadlines; mod sequencing_to_minimize_maximum_cumulative_cost; -mod sequencing_to_minimize_tardy_task_weight; +pub(crate) mod sequencing_to_minimize_tardy_task_weight; mod sequencing_to_minimize_weighted_completion_time; mod sequencing_to_minimize_weighted_tardiness; mod sequencing_with_deadlines_and_set_up_times; @@ -178,7 +178,7 @@ mod sequencing_within_intervals; pub(crate) mod shortest_common_supersequence; pub(crate) mod shortest_common_superstring; mod square_tiling; -mod stacker_crane; +pub(crate) mod stacker_crane; mod staff_scheduling; pub(crate) mod string_to_string_correction; mod subset_product; diff --git a/src/models/misc/open_shop_scheduling.rs b/src/models/misc/open_shop_scheduling.rs index 9aad80df4..a6b1b702d 100644 --- a/src/models/misc/open_shop_scheduling.rs +++ b/src/models/misc/open_shop_scheduling.rs @@ -313,3 +313,42 @@ pub(crate) fn canonical_model_example_specs() -> Vec>", description: "Processing time of each job on each machine (n x m)." }, + crate::registry::FieldInfo { name: "bound", type_name: "i64", description: "Decision objective bound" }, + ], + decode: |_, indices: Vec| indices +); + +#[cfg(feature = "example-db")] +pub(crate) fn decision_canonical_rule_example_specs( +) -> Vec { + vec![crate::example_db::specs::RuleExampleSpec { + id: "decision_open_shop_scheduling_to_open_shop_scheduling", + build: || { + let source = crate::models::decision::Decision::new( + OpenShopScheduling::new( + 3, + vec![vec![3, 1, 2], vec![2, 3, 1], vec![1, 2, 3], vec![2, 2, 1]], + ), + 8, + ); + let witness = serde_json::json!(vec![0, 3, 4, 3, 0, 6, 5, 6, 0, 6, 4, 3]); + crate::example_db::specs::rule_example_with_witness::<_, OpenShopScheduling>( + source, + crate::export::SolutionPair { + source_config: witness.clone(), + target_config: witness, + }, + ) + }, + }] +} 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 56119e70a..1710eb310 100644 --- a/src/models/misc/sequencing_to_minimize_tardy_task_weight.rs +++ b/src/models/misc/sequencing_to_minimize_tardy_task_weight.rs @@ -263,3 +263,41 @@ pub(crate) fn canonical_model_example_specs() -> Vec", description: "Lengths" }, +crate::registry::FieldInfo { name: "weights", type_name: "Option>", description: "Weights" }, +crate::registry::FieldInfo { name: "deadlines", type_name: "Vec", description: "Deadlines" }, +crate::registry::FieldInfo { name: "bound", type_name: "i64", description: "Decision objective bound" }, +], + decode: |_, indices: Vec| indices +); + +#[cfg(feature = "example-db")] +pub(crate) fn decision_canonical_rule_example_specs( +) -> Vec { + vec![crate::example_db::specs::RuleExampleSpec { + id: "decision_sequencing_to_minimize_tardy_task_weight_to_sequencing_to_minimize_tardy_task_weight", + build: || { + let source = crate::models::decision::Decision::new(SequencingToMinimizeTardyTaskWeight::new( + vec![3, 2, 4, 1, 2], + vec![5, 3, 7, 2, 4], + vec![6, 4, 10, 2, 8], + ), 3); + let witness = serde_json::json!(vec![3, 0, 4, 2, 1]); + crate::example_db::specs::rule_example_with_witness::<_, SequencingToMinimizeTardyTaskWeight>( + source, + crate::export::SolutionPair { source_config: witness.clone(), target_config: witness }, + ) + }, + }] +} diff --git a/src/models/misc/stacker_crane.rs b/src/models/misc/stacker_crane.rs index a04445f40..3d8744b5d 100644 --- a/src/models/misc/stacker_crane.rs +++ b/src/models/misc/stacker_crane.rs @@ -427,3 +427,48 @@ pub(crate) fn canonical_model_example_specs() -> Vec", description: "Required directed arcs." }, + crate::registry::FieldInfo { name: "graph", type_name: "Vec<(usize,usize)>", description: "Undirected connector edges." }, + crate::registry::FieldInfo { name: "num_vertices", type_name: "usize", description: "Vertex count, needed to preserve isolated vertices." }, + crate::registry::FieldInfo { name: "arc_lengths", type_name: "Vec", description: "Required-arc lengths; defaults to one per arc." }, + crate::registry::FieldInfo { name: "edge_lengths", type_name: "Vec", description: "Connector-edge lengths; defaults to one per edge." }, + crate::registry::FieldInfo { name: "bound", type_name: "i64", description: "Decision objective bound" }, + ], + decode: |_, indices: Vec| indices +); + +#[cfg(feature = "example-db")] +pub(crate) fn decision_canonical_rule_example_specs( +) -> Vec { + vec![crate::example_db::specs::RuleExampleSpec { + id: "decision_stacker_crane_to_stacker_crane", + build: || { + let source = crate::models::decision::Decision::new( + StackerCrane::new( + 6, + vec![(0, 4), (2, 5), (5, 1), (3, 0), (4, 3)], + vec![(0, 1), (1, 2), (2, 3), (3, 5), (4, 5), (0, 3), (1, 5)], + vec![3, 4, 2, 5, 3], + vec![2, 1, 3, 2, 1, 4, 3], + ), + 20, + ); + let witness = serde_json::json!(vec![0, 2, 1, 4, 3]); + crate::example_db::specs::rule_example_with_witness::<_, StackerCrane>( + source, + crate::export::SolutionPair { + source_config: witness.clone(), + target_config: witness, + }, + ) + }, + }] +} diff --git a/src/models/set/exact_cover_by_3_sets.rs b/src/models/set/exact_cover_by_3_sets.rs index 2512a7c27..9db5cb5e9 100644 --- a/src/models/set/exact_cover_by_3_sets.rs +++ b/src/models/set/exact_cover_by_3_sets.rs @@ -69,28 +69,8 @@ struct ExactCoverBy3SetsCreateSpec { impl TryFrom for ExactCoverBy3Sets { type Error = crate::registry::ConstructionError; - fn try_from(mut spec: ExactCoverBy3SetsCreateSpec) -> Result { - if !spec.universe_size.is_multiple_of(3) { - return Err("universe_size must be divisible by 3".into()); - } - for (index, subset) in spec.subsets.iter_mut().enumerate() { - if subset[0] == subset[1] || subset[0] == subset[2] || subset[1] == subset[2] { - return Err(format!("subset {index} contains duplicate elements").into()); - } - if let Some(&element) = subset - .iter() - .find(|&&element| element >= spec.universe_size) - { - return Err( - format!("subset {index} contains out-of-range element {element}").into(), - ); - } - subset.sort(); - } - Ok(Self { - universe_size: spec.universe_size, - subsets: spec.subsets, - }) + fn try_from(spec: ExactCoverBy3SetsCreateSpec) -> Result { + Self::try_new(spec.universe_size, spec.subsets) } } diff --git a/src/rules/acyclicpartition_ilp.rs b/src/rules/acyclicpartition_ilp.rs index 7877c3fc7..b5a058256 100644 --- a/src/rules/acyclicpartition_ilp.rs +++ b/src/rules/acyclicpartition_ilp.rs @@ -9,6 +9,8 @@ use crate::models::graph::AcyclicPartition; use crate::reduction; use crate::rules::ilp_helpers::mccormick_product; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; #[derive(Debug, Clone)] pub struct ReductionAcyclicPartitionToILP { @@ -25,10 +27,32 @@ impl ReductionResult for ReductionAcyclicPartitionToILP { } /// One-hot decode: for each vertex v, output the unique c with x_{v,c} = 1. - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionAcyclicPartitionToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok(crate::rules::ilp_helpers::one_hot_decode_rows( target_solution, self.n, @@ -149,7 +173,13 @@ pub(crate) fn canonical_rule_example_specs() -> Vec>( source, SolutionPair { diff --git a/src/rules/balancedcompletebipartitesubgraph_ilp.rs b/src/rules/balancedcompletebipartitesubgraph_ilp.rs index 008bf47b0..07c1dfa9e 100644 --- a/src/rules/balancedcompletebipartitesubgraph_ilp.rs +++ b/src/rules/balancedcompletebipartitesubgraph_ilp.rs @@ -8,6 +8,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::BalancedCompleteBipartiteSubgraph; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use std::collections::HashSet; #[derive(Debug, Clone)] @@ -24,10 +26,32 @@ impl ReductionResult for ReductionBCBSToILP { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionBCBSToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok(target_solution[..self.num_vertices] .iter() .map(|&value| value == 1) diff --git a/src/rules/bicliquecover_bmf.rs b/src/rules/bicliquecover_bmf.rs index 75a20dba8..53774e380 100644 --- a/src/rules/bicliquecover_bmf.rs +++ b/src/rules/bicliquecover_bmf.rs @@ -16,6 +16,8 @@ use crate::models::graph::BicliqueCover; use crate::reduction; use crate::rules::bmf_bicliquecover::config_bmf_to_bc; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; /// Result of reducing BicliqueCover to BMF. #[derive(Debug, Clone)] @@ -36,10 +38,32 @@ impl ReductionResult for ReductionBicliqueCoverToBMF { /// Map a BMF config (B row-major, C row-major) to a BicliqueCover /// config (vertex-major) via the inverse transpose. - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionBicliqueCoverToBMF { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok(config_bmf_to_bc(target_solution, self.m, self.n, self.k)) } } diff --git a/src/rules/biconnectivityaugmentation_ilp.rs b/src/rules/biconnectivityaugmentation_ilp.rs index 009c94c47..bdda0dbef 100644 --- a/src/rules/biconnectivityaugmentation_ilp.rs +++ b/src/rules/biconnectivityaugmentation_ilp.rs @@ -8,6 +8,8 @@ use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense use crate::models::graph::BiconnectivityAugmentation; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; #[derive(Debug, Clone)] @@ -54,10 +56,32 @@ impl ReductionResult for ReductionBiconnAugToILP { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionBiconnAugToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok(target_solution[..self.num_candidates] .iter() .map(|&value| value == 1) @@ -241,7 +265,13 @@ pub(crate) fn canonical_rule_example_specs() -> Vec>( source, SolutionPair { diff --git a/src/rules/binpacking_ilp.rs b/src/rules/binpacking_ilp.rs index 5e96b3559..3a40b4029 100644 --- a/src/rules/binpacking_ilp.rs +++ b/src/rules/binpacking_ilp.rs @@ -11,6 +11,8 @@ use crate::models::misc::BinPacking; use crate::reduction; use crate::rules::ilp_helpers::one_hot_decode_rows; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; /// Result of reducing BinPacking to ILP. /// @@ -37,10 +39,32 @@ impl ReductionResult for ReductionBPToILP { /// Extract solution from ILP back to BinPacking. /// /// For each item i, find the unique bin j where x_{ij} = 1. - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionBPToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok(one_hot_decode_rows(target_solution, self.n, self.n, 0)) } } diff --git a/src/rules/bmf_bicliquecover.rs b/src/rules/bmf_bicliquecover.rs index 7c88d5fa2..d188af4c1 100644 --- a/src/rules/bmf_bicliquecover.rs +++ b/src/rules/bmf_bicliquecover.rs @@ -11,13 +11,15 @@ //! //! Variable-layout mapping: BMF stores `B` row-major followed by `C` //! row-major, while BicliqueCover stores vertex memberships vertex-major. -//! `extract_solution` transposes the right-vertex half so the extracted +//! `recover_result` transposes the right-vertex half so the extracted //! BMF config matches `B` and `C`. use crate::models::algebraic::BMF; use crate::models::graph::BicliqueCover; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::topology::BipartiteGraph; /// Convert one vertex-membership row per biclique into BMF factors. @@ -82,10 +84,32 @@ impl ReductionResult for ReductionBMFToBicliqueCover { } /// Map a BicliqueCover config (vertex-major) back to a BMF config (B row-major, then C row-major). - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionBMFToBicliqueCover { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok(config_bc_to_bmf(target_solution, self.m, self.n, self.k)) } } diff --git a/src/rules/bmf_ilp.rs b/src/rules/bmf_ilp.rs index 33e03f5a2..1f93e9292 100644 --- a/src/rules/bmf_ilp.rs +++ b/src/rules/bmf_ilp.rs @@ -8,6 +8,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, BMF, ILP}; use crate::reduction; use crate::rules::ilp_helpers::mccormick_product; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; #[derive(Debug, Clone)] pub struct ReductionBMFToILP { @@ -25,10 +27,32 @@ impl ReductionResult for ReductionBMFToILP { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionBMFToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { let b = (0..self.m) .map(|i| { (0..self.k) diff --git a/src/rules/bottlenecktravelingsalesman_ilp.rs b/src/rules/bottlenecktravelingsalesman_ilp.rs index 2ba4659dc..93ad39051 100644 --- a/src/rules/bottlenecktravelingsalesman_ilp.rs +++ b/src/rules/bottlenecktravelingsalesman_ilp.rs @@ -4,6 +4,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::BottleneckTravelingSalesman; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::topology::Graph; /// A tour is encoded by positions and distinct directed uses of source edges. @@ -23,10 +25,32 @@ impl ReductionResult for ReductionBTSPToILP { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionBTSPToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { let n = self.num_vertices; Ok((0..self.num_edges) .map(|edge| { diff --git a/src/rules/boundedcomponentspanningforest_ilp.rs b/src/rules/boundedcomponentspanningforest_ilp.rs index bc90dda01..db31d3ad0 100644 --- a/src/rules/boundedcomponentspanningforest_ilp.rs +++ b/src/rules/boundedcomponentspanningforest_ilp.rs @@ -9,6 +9,8 @@ use crate::models::graph::BoundedComponentSpanningForest; use crate::reduction; use crate::rules::ilp_helpers::one_hot_decode_rows; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; #[derive(Debug, Clone)] @@ -27,10 +29,32 @@ impl ReductionResult for ReductionBCSFToILP { } /// One-hot decode: for each vertex v, output the unique component c with x_{v,c} = 1. - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionBCSFToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok(one_hot_decode_rows(target_solution, self.n, self.k, 0)) } } @@ -202,7 +226,13 @@ pub(crate) fn canonical_rule_example_specs() -> Vec>( source, SolutionPair { diff --git a/src/rules/capacityassignment_ilp.rs b/src/rules/capacityassignment_ilp.rs index e95e31566..d44f48109 100644 --- a/src/rules/capacityassignment_ilp.rs +++ b/src/rules/capacityassignment_ilp.rs @@ -11,6 +11,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::CapacityAssignment; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; /// Result of reducing CapacityAssignment to ILP. /// @@ -34,10 +36,32 @@ impl ReductionResult for ReductionCAToILP { } /// Extract solution: for each link l, find the unique capacity c where x_{l,c} = 1. - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionCAToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok(crate::rules::ilp_helpers::one_hot_decode_rows( target_solution, self.num_links, diff --git a/src/rules/circuit_ilp.rs b/src/rules/circuit_ilp.rs index a86bda996..0b83cc5e2 100644 --- a/src/rules/circuit_ilp.rs +++ b/src/rules/circuit_ilp.rs @@ -18,6 +18,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::formula::{BooleanExpr, BooleanOp, CircuitSAT}; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use std::collections::HashMap; /// Result of reducing CircuitSAT to ILP. @@ -36,10 +38,32 @@ impl ReductionResult for ReductionCircuitToILP { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionCircuitToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok({ self.source_variables .iter() diff --git a/src/rules/circuit_sat.rs b/src/rules/circuit_sat.rs index 93a36c07a..53a4b9a3d 100644 --- a/src/rules/circuit_sat.rs +++ b/src/rules/circuit_sat.rs @@ -6,6 +6,8 @@ use crate::models::formula::{ use crate::reduction; use crate::rules::sat_helpers::SatVariableAllocator; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use std::collections::HashMap; #[derive(Debug, Clone, PartialEq, Eq)] @@ -289,10 +291,32 @@ impl ReductionResult for ReductionCircuitSATToSAT { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionCircuitSATToSAT { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok(target_solution[..self.source_var_count].to_vec()) } } @@ -348,7 +372,17 @@ pub(crate) fn canonical_rule_example_specs() -> Vec Satisfiability example must be satisfiable"); crate::example_db::specs::assemble_rule_example( diff --git a/src/rules/circuit_spinglass.rs b/src/rules/circuit_spinglass.rs index 0bc7c3997..264dc49c6 100644 --- a/src/rules/circuit_spinglass.rs +++ b/src/rules/circuit_spinglass.rs @@ -6,10 +6,13 @@ //! Each logic gate is encoded as a SpinGlass Hamiltonian where the ground //! states correspond to valid input/output combinations. +use crate::models::decision::Decision; use crate::models::formula::{Assignment, BooleanExpr, BooleanOp, CircuitSAT}; use crate::models::graph::SpinGlass; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::topology::SimpleGraph; use crate::types::WeightElement; use num_traits::Zero; @@ -209,27 +212,47 @@ where #[derive(Debug, Clone)] pub struct ReductionCircuitToSG { /// The target SpinGlass problem. - target: SpinGlass, + target: Decision>, /// Mapping from source variable names to spin indices. variable_map: HashMap, /// Source variable names in order. source_variables: Vec, - /// Sum of the individual gate and equality ground energies. - zero_penalty_energy: i64, } impl ReductionResult for ReductionCircuitToSG { type Source = CircuitSAT; - type Target = SpinGlass; + type Target = Decision>; fn target_problem(&self) -> &Self::Target { &self.target } - fn extract_solution( + fn recover_result( + &self, + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionCircuitToSG { + fn map_solution( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok(self .source_variables .iter() @@ -238,19 +261,6 @@ impl ReductionResult for ReductionCircuitToSG { } } -impl crate::rules::AggregateReductionResult for ReductionCircuitToSG { - type Source = CircuitSAT; - type Target = SpinGlass; - - fn target_problem(&self) -> &Self::Target { - &self.target - } - - fn extract_value(&self, value: crate::types::Min) -> crate::types::Or { - crate::types::Or(value.0 == Some(self.zero_penalty_energy)) - } -} - /// Builder for constructing the combined SpinGlass from circuit gadgets. struct SpinGlassBuilder { /// Current number of spins. @@ -483,13 +493,12 @@ fn process_assignment( } #[reduction( - aggregate = custom, transform = upper_bound { num_spins = "num_variables + 3 * num_expression_nodes", num_interactions = "6 * num_expression_nodes + num_assignment_outputs", } )] -impl ReduceTo> for CircuitSAT { +impl ReduceTo>> for CircuitSAT { type Result = ReductionCircuitToSG; fn reduce_to(&self) -> Result { @@ -498,23 +507,19 @@ impl ReduceTo> for CircuitSAT { // Process each assignment in the circuit for assignment in &self.circuit().assignments { process_assignment(assignment, &mut builder).map_err( - crate::rules::ReductionError::construction::< - CircuitSAT, - SpinGlass, - >, + >>>::target_construction, )?; } let (target, variable_map, zero_penalty_energy) = builder.build().map_err( - crate::rules::ReductionError::construction::>, + >>>::target_construction, )?; let source_variables = self.variable_names().to_vec(); Ok(ReductionCircuitToSG { - target, + target: Decision::new(target, zero_penalty_energy), variable_map, source_variables, - zero_penalty_energy, }) } } @@ -553,7 +558,10 @@ pub(crate) fn canonical_rule_example_specs() -> Vec>( + crate::example_db::specs::rule_example_with_witness::< + _, + Decision>, + >( full_adder_circuit_sat(), SolutionPair { source_config: serde_json::json!(vec![ diff --git a/src/rules/closeststring_ilp.rs b/src/rules/closeststring_ilp.rs index 97565041f..7165627c6 100644 --- a/src/rules/closeststring_ilp.rs +++ b/src/rules/closeststring_ilp.rs @@ -25,6 +25,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::ClosestString; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; /// Result of reducing ClosestString to ILP. /// @@ -51,10 +53,32 @@ impl ReductionResult for ReductionClosestStringToILP { /// /// For every position `j`, choose the unique alphabet symbol `a` with /// `x_{j, a} = 1`. - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionClosestStringToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok(crate::rules::ilp_helpers::one_hot_decode_rows( target_solution, self.string_length, diff --git a/src/rules/closestsubstring_ilp.rs b/src/rules/closestsubstring_ilp.rs index de280add8..2dd7a9fdc 100644 --- a/src/rules/closestsubstring_ilp.rs +++ b/src/rules/closestsubstring_ilp.rs @@ -33,6 +33,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::ClosestSubstring; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; /// Result of reducing ClosestSubstring to ILP. /// @@ -71,10 +73,32 @@ impl ReductionResult for ReductionClosestSubstringToILP { /// are per-string window starts. For each center position `r`, we pick the /// unique alphabet symbol `a` with `x_{r, a} = 1`; for each input string /// `s_i`, we pick the unique window start `p` with `y_{i, p} = 1`. - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionClosestSubstringToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { let q = self.alphabet_size; let ell = self.substring_length; let y_base = q * ell; diff --git a/src/rules/closestvectorproblem_qubo.rs b/src/rules/closestvectorproblem_qubo.rs index a5f16848f..1092b0d30 100644 --- a/src/rules/closestvectorproblem_qubo.rs +++ b/src/rules/closestvectorproblem_qubo.rs @@ -9,6 +9,8 @@ use crate::export::SolutionPair; use crate::models::algebraic::{ClosestVectorProblem, QUBO}; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use num_bigint::BigInt; use num_traits::Zero; @@ -37,10 +39,32 @@ impl ReductionResult for ReductionCVPToQUBO { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionCVPToQUBO { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { self.encodings .iter() .map(|encoding| { diff --git a/src/rules/clustering_ilp.rs b/src/rules/clustering_ilp.rs index 2e09cd1ed..ac8c020ef 100644 --- a/src/rules/clustering_ilp.rs +++ b/src/rules/clustering_ilp.rs @@ -9,6 +9,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::Clustering; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; /// Result of reducing Clustering to ILP. #[derive(Debug, Clone)] @@ -26,10 +28,32 @@ impl ReductionResult for ReductionClusteringToILP { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionClusteringToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok(crate::rules::ilp_helpers::one_hot_decode_rows( target_solution, self.num_elements, diff --git a/src/rules/coloring_ilp.rs b/src/rules/coloring_ilp.rs index 9473b5fd5..cba925a47 100644 --- a/src/rules/coloring_ilp.rs +++ b/src/rules/coloring_ilp.rs @@ -12,6 +12,8 @@ use crate::models::graph::KColoring; use crate::reduction; use crate::rules::ilp_helpers::one_hot_decode_rows; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; use crate::variant::{KValue, K1, K2, K3, K4, KN}; @@ -44,10 +46,35 @@ where /// /// The ILP solution has num_vertices * K binary variables. /// For each vertex, we find which color has value 1. - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionKColoringToILP +where + G: Graph + crate::variant::VariantParam, +{ + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok(one_hot_decode_rows( target_solution, self.num_vertices, diff --git a/src/rules/coloring_qubo.rs b/src/rules/coloring_qubo.rs index 012a1d77b..ca6f63300 100644 --- a/src/rules/coloring_qubo.rs +++ b/src/rules/coloring_qubo.rs @@ -9,25 +9,27 @@ //! QUBO has n*K variables. use crate::models::algebraic::QUBO; +use crate::models::decision::Decision; use crate::models::graph::KColoring; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; use crate::variant::{KValue, K2, K3, KN}; /// Result of reducing KColoring to QUBO. #[derive(Debug, Clone)] pub struct ReductionKColoringToQUBO { - target: QUBO, + target: Decision>, num_vertices: usize, num_colors: usize, - feasible_energy: i64, _phantom: std::marker::PhantomData, } impl ReductionResult for ReductionKColoringToQUBO { type Source = KColoring; - type Target = QUBO; + type Target = Decision>; fn target_problem(&self) -> &Self::Target { &self.target @@ -35,12 +37,34 @@ impl ReductionResult for ReductionKColoringToQUBO { /// Decode a target witness at `feasible_energy` into a proper coloring. /// At that energy all nonnegative penalties vanish, including one-hot. - /// An optimum above the threshold means the source is uncolorable and - /// is interpreted through `extract_value` before witness extraction. - fn extract_solution( + /// The target decision bound selects exactly zero-penalty colorings. + /// An uncolorable source produces an infeasible decision target. + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionKColoringToQUBO { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok((0..self.num_vertices) .map(|vertex| { (0..self.num_colors) @@ -51,28 +75,16 @@ impl ReductionResult for ReductionKColoringToQUBO { } } -impl crate::rules::AggregateReductionResult for ReductionKColoringToQUBO { - type Source = KColoring; - type Target = QUBO; - - fn target_problem(&self) -> &Self::Target { - &self.target - } - - fn extract_value(&self, value: crate::types::Min) -> crate::types::Or { - crate::types::Or(value.0 == Some(self.feasible_energy)) - } -} - /// Check dimensions and the omitted constant before allocating the matrix. fn coloring_qubo_parameters( n: usize, k: usize, ) -> Result<(usize, i64, i64), crate::rules::ReductionError> { let overflow = |operation| { - crate::rules::ReductionError::integer_overflow::, QUBO>( - operation, - ) + crate::rules::ReductionError::integer_overflow::< + KColoring, + Decision>, + >(operation) }; let nq = n .checked_mul(k) @@ -99,9 +111,10 @@ fn reduce_kcoloring_to_qubo( let n = problem.graph().num_vertices(); let edges = problem.graph().edges(); let overflow = |operation| { - crate::rules::ReductionError::integer_overflow::, QUBO>( - operation, - ) + crate::rules::ReductionError::integer_overflow::< + KColoring, + Decision>, + >(operation) }; let (nq, penalty, feasible_energy) = coloring_qubo_parameters::(n, k)?; @@ -159,26 +172,28 @@ fn reduce_kcoloring_to_qubo( } Ok(ReductionKColoringToQUBO { - target: QUBO::from_rows(matrix).map_err(|message| { - crate::rules::ReductionError::construction::, QUBO>( - message, - ) - })?, + target: Decision::new( + QUBO::from_rows(matrix).map_err( + crate::rules::ReductionError::construction::< + KColoring, + Decision>, + >, + )?, + feasible_energy, + ), num_vertices: n, num_colors: k, - feasible_energy, _phantom: std::marker::PhantomData, }) } // Register only the KN variant in the reduction graph #[reduction( - aggregate = custom, transform = exact { num_vars = "num_vertices * num_colors", } )] -impl ReduceTo> for KColoring { +impl ReduceTo>> for KColoring { type Result = ReductionKColoringToQUBO; fn reduce_to(&self) -> Result { @@ -189,7 +204,7 @@ impl ReduceTo> for KColoring { // Additional concrete impls for tests (not registered in reduction graph) macro_rules! impl_kcoloring_to_qubo { ($($ktype:ty),+) => {$( - impl ReduceTo> for KColoring<$ktype, SimpleGraph> { + impl ReduceTo>> for KColoring<$ktype, SimpleGraph> { type Result = ReductionKColoringToQUBO<$ktype>; fn reduce_to(&self) -> Result { reduce_kcoloring_to_qubo(self) @@ -210,7 +225,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec::with_k(SimpleGraph::new(n, edges), 3); - crate::example_db::specs::rule_example_with_witness::<_, QUBO>( + crate::example_db::specs::rule_example_with_witness::<_, Decision>>( source, SolutionPair { source_config: serde_json::json!(vec![1, 2, 2, 1, 0]), diff --git a/src/rules/consecutiveblockminimization_ilp.rs b/src/rules/consecutiveblockminimization_ilp.rs index 40780bec4..a94e53aee 100644 --- a/src/rules/consecutiveblockminimization_ilp.rs +++ b/src/rules/consecutiveblockminimization_ilp.rs @@ -9,6 +9,8 @@ use crate::models::algebraic::{ use crate::reduction; use crate::rules::ilp_helpers::{one_hot_assignment_constraints, one_hot_decode}; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; #[derive(Debug, Clone)] pub struct ReductionCBMToILP { @@ -24,10 +26,32 @@ impl ReductionResult for ReductionCBMToILP { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionCBMToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok(one_hot_decode( target_solution, self.num_cols, diff --git a/src/rules/consecutiveonesmatrixaugmentation_ilp.rs b/src/rules/consecutiveonesmatrixaugmentation_ilp.rs index bafcf8a29..120aa1a7c 100644 --- a/src/rules/consecutiveonesmatrixaugmentation_ilp.rs +++ b/src/rules/consecutiveonesmatrixaugmentation_ilp.rs @@ -10,6 +10,8 @@ use crate::models::algebraic::{ use crate::reduction; use crate::rules::ilp_helpers::{one_hot_assignment_constraints, one_hot_decode}; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; #[derive(Debug, Clone)] pub struct ReductionCOMAToILP { @@ -25,10 +27,32 @@ impl ReductionResult for ReductionCOMAToILP { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionCOMAToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok(one_hot_decode( target_solution, self.num_cols, @@ -207,7 +231,14 @@ pub(crate) fn canonical_rule_example_specs() -> Vec>( source, SolutionPair { diff --git a/src/rules/consecutiveonessubmatrix_ilp.rs b/src/rules/consecutiveonessubmatrix_ilp.rs index 1d0128c6f..c457af937 100644 --- a/src/rules/consecutiveonessubmatrix_ilp.rs +++ b/src/rules/consecutiveonessubmatrix_ilp.rs @@ -7,6 +7,8 @@ use crate::models::algebraic::{ConsecutiveOnesSubmatrix, LinearConstraint, ObjectiveSense, ILP}; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; #[derive(Debug, Clone)] pub struct ReductionCOSToILP { @@ -22,10 +24,32 @@ impl ReductionResult for ReductionCOSToILP { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionCOSToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok({ // Output the selection bits s_c (first num_cols variables) target_solution[..self.num_cols] @@ -222,7 +246,14 @@ pub(crate) fn canonical_rule_example_specs() -> Vec>( source, SolutionPair { diff --git a/src/rules/consistencyofdatabasefrequencytables_ilp.rs b/src/rules/consistencyofdatabasefrequencytables_ilp.rs index 5662aa61d..1872c47a7 100644 --- a/src/rules/consistencyofdatabasefrequencytables_ilp.rs +++ b/src/rules/consistencyofdatabasefrequencytables_ilp.rs @@ -11,6 +11,8 @@ use crate::models::misc::ConsistencyOfDatabaseFrequencyTables; use crate::reduction; use crate::rules::ilp_helpers::mccormick_product; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; /// Result of reducing ConsistencyOfDatabaseFrequencyTables to ILP. #[derive(Debug, Clone)] @@ -91,10 +93,32 @@ impl ReductionResult for ReductionCDFTToILP { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionCDFTToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok({ let mut source_solution = Vec::with_capacity(self.source.num_assignment_variables()); for object in 0..self.source.num_objects() { diff --git a/src/rules/decisionmaximumindependentset_integralflowbundles.rs b/src/rules/decisionmaximumindependentset_integralflowbundles.rs index 13fd0419a..3147eb2c9 100644 --- a/src/rules/decisionmaximumindependentset_integralflowbundles.rs +++ b/src/rules/decisionmaximumindependentset_integralflowbundles.rs @@ -9,6 +9,8 @@ use crate::models::decision::Decision; use crate::models::graph::{IntegralFlowBundles, MaximumIndependentSet}; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; use crate::types::One; @@ -27,10 +29,32 @@ impl ReductionResult for ReductionDecisionMISToIFB { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionDecisionMISToIFB { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok((0..self.num_source_vertices) .map(|i| target_solution[2 * i + 1] == 1) .collect()) @@ -135,7 +159,14 @@ pub(crate) fn canonical_rule_example_specs() -> Vec, + target: Decision>, source_num_vertices: usize, - threshold: i64, } impl ReductionResult for ReductionDecisionMinimumDominatingSetToMinimumSumMulticenter { type Source = Decision>; - type Target = MinimumSumMulticenter; + type Target = Decision>; fn target_problem(&self) -> &Self::Target { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { - // Original vertices precede the auxiliary isolated vertices. - Ok(target_solution[..self.source_num_vertices].to_vec()) + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } } } -impl crate::rules::AggregateReductionResult - for ReductionDecisionMinimumDominatingSetToMinimumSumMulticenter -{ - type Source = Decision>; - type Target = MinimumSumMulticenter; - - fn target_problem(&self) -> &Self::Target { - &self.target - } - - fn extract_value(&self, target_value: Min) -> Or { - Or(target_value.0 == Some(self.threshold)) +impl ReductionDecisionMinimumDominatingSetToMinimumSumMulticenter { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { + // Original vertices precede the auxiliary isolated vertices. + Ok(target_solution[..self.source_num_vertices].to_vec()) } } #[reduction( - aggregate = custom, transform = upper_bound { num_vertices = "num_vertices + 2", num_edges = "num_edges" } )] -impl ReduceTo> +impl ReduceTo>> for Decision> { type Result = ReductionDecisionMinimumDominatingSetToMinimumSumMulticenter; @@ -73,9 +80,8 @@ impl ReduceTo> ); Ok( ReductionDecisionMinimumDominatingSetToMinimumSumMulticenter { - target, + target: Decision::new(target, threshold), source_num_vertices: n, - threshold, }, ) } @@ -88,7 +94,7 @@ fn multicenter_parameters( bound: i64, ) -> Result<(usize, usize, i64), crate::rules::ReductionError> { type Source = Decision>; - type Target = MinimumSumMulticenter; + type Target = Decision>; let overflow = || { crate::rules::ReductionError::integer_overflow::( "encoding multicenter construction parameters", @@ -117,7 +123,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec, + Decision>, >( Decision::new( MinimumDominatingSet::new( diff --git a/src/rules/decisionminimumdominatingset_minmaxmulticenter.rs b/src/rules/decisionminimumdominatingset_minmaxmulticenter.rs index 8980e3bbb..ef7dc2606 100644 --- a/src/rules/decisionminimumdominatingset_minmaxmulticenter.rs +++ b/src/rules/decisionminimumdominatingset_minmaxmulticenter.rs @@ -8,55 +8,63 @@ use crate::models::decision::Decision; use crate::models::graph::{MinMaxMulticenter, MinimumDominatingSet}; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; -use crate::types::{Min, One, Or}; +use crate::types::One; /// The source vertices precede the two mandatory auxiliary centers. #[derive(Debug, Clone)] pub struct ReductionDecisionMinimumDominatingSetToMinMaxMulticenter { - target: MinMaxMulticenter, + target: Decision>, source_num_vertices: usize, } impl ReductionResult for ReductionDecisionMinimumDominatingSetToMinMaxMulticenter { type Source = Decision>; - type Target = MinMaxMulticenter; + type Target = Decision>; fn target_problem(&self) -> &Self::Target { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { - Ok(target_solution[..self.source_num_vertices].to_vec()) + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } } } -impl crate::rules::AggregateReductionResult - for ReductionDecisionMinimumDominatingSetToMinMaxMulticenter -{ - type Source = Decision>; - type Target = MinMaxMulticenter; - - fn target_problem(&self) -> &Self::Target { - &self.target - } - - fn extract_value(&self, target_value: Min) -> Or { - Or(target_value.0.is_some_and(|radius| radius <= 1)) +impl ReductionDecisionMinimumDominatingSetToMinMaxMulticenter { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { + Ok(target_solution[..self.source_num_vertices].to_vec()) } } #[reduction( - aggregate = custom, transform = exact { num_vertices = "num_vertices + 2", num_edges = "num_edges", } )] -impl ReduceTo> +impl ReduceTo>> for Decision> { type Result = ReductionDecisionMinimumDominatingSetToMinMaxMulticenter; @@ -72,7 +80,7 @@ impl ReduceTo> centers, ); Ok(ReductionDecisionMinimumDominatingSetToMinMaxMulticenter { - target, + target: Decision::new(target, 1), source_num_vertices: n, }) } @@ -84,7 +92,7 @@ fn multicenter_parameters( bound: i64, ) -> Result<(usize, usize), crate::rules::ReductionError> { type Source = Decision>; - type Target = MinMaxMulticenter; + type Target = Decision>; let overflow = || { crate::rules::ReductionError::integer_overflow::( "encoding min-max multicenter parameters", @@ -108,7 +116,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec, + Decision>, >( Decision::new( MinimumDominatingSet::new( diff --git a/src/rules/decisionminimumvertexcover_hamiltoniancircuit.rs b/src/rules/decisionminimumvertexcover_hamiltoniancircuit.rs index d6d677fde..1fac88148 100644 --- a/src/rules/decisionminimumvertexcover_hamiltoniancircuit.rs +++ b/src/rules/decisionminimumvertexcover_hamiltoniancircuit.rs @@ -7,6 +7,8 @@ use crate::models::decision::Decision; use crate::models::graph::{HamiltonianCircuit, MinimumVertexCover}; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; use crate::types::One; use std::collections::BTreeSet; @@ -242,10 +244,32 @@ impl ReductionResult for ReductionDecisionMinimumVertexCoverToHamiltonianCircuit &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionDecisionMinimumVertexCoverToHamiltonianCircuit { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok({ match &self.construction { ConstructionKind::Fixed { source_cover } => source_cover.clone(), diff --git a/src/rules/directedhamiltonianpath_ilp.rs b/src/rules/directedhamiltonianpath_ilp.rs index d4f8cd4b9..da61f518d 100644 --- a/src/rules/directedhamiltonianpath_ilp.rs +++ b/src/rules/directedhamiltonianpath_ilp.rs @@ -11,6 +11,8 @@ use crate::models::graph::DirectedHamiltonianPath; use crate::reduction; use crate::rules::ilp_helpers::{one_hot_assignment_constraints, one_hot_decode}; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; /// Result of reducing DirectedHamiltonianPath to ILP. /// @@ -30,10 +32,32 @@ impl ReductionResult for ReductionDirectedHamiltonianPathToILP { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionDirectedHamiltonianPathToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok({ let n = self.num_vertices; // Decode one-hot assignment: permutation[k] = v where x_{v,k} = 1 diff --git a/src/rules/directedtwocommodityintegralflow_ilp.rs b/src/rules/directedtwocommodityintegralflow_ilp.rs index 22194abbf..8d1472158 100644 --- a/src/rules/directedtwocommodityintegralflow_ilp.rs +++ b/src/rules/directedtwocommodityintegralflow_ilp.rs @@ -16,6 +16,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::DirectedTwoCommodityIntegralFlow; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; /// Result of reducing DirectedTwoCommodityIntegralFlow to `ILP`. /// @@ -37,10 +39,32 @@ impl ReductionResult for ReductionD2CIFToILP { } /// Extract flow solution: all 2*|A| variables directly encode the flow. - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionD2CIFToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { crate::rules::ilp_helpers::decode_usize_values(&target_solution[..2 * self.num_arcs]) } } diff --git a/src/rules/disjointconnectingpaths_ilp.rs b/src/rules/disjointconnectingpaths_ilp.rs index 72c8dd44f..20b641317 100644 --- a/src/rules/disjointconnectingpaths_ilp.rs +++ b/src/rules/disjointconnectingpaths_ilp.rs @@ -7,6 +7,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::DisjointConnectingPaths; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::topology::SimpleGraph; use std::collections::VecDeque; @@ -36,10 +38,32 @@ impl ReductionResult for ReductionDCPToILP { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionDCPToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { let mut result = vec![false; self.edges.len()]; for (k, &(source, sink)) in self.terminal_pairs.iter().enumerate() { let offset = k * self.num_edge_vars_per_commodity; diff --git a/src/rules/ensemblecomputation_ilp.rs b/src/rules/ensemblecomputation_ilp.rs index 5c342c690..2524ea5e4 100644 --- a/src/rules/ensemblecomputation_ilp.rs +++ b/src/rules/ensemblecomputation_ilp.rs @@ -4,6 +4,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::EnsembleComputation; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; #[derive(Debug, Clone)] pub struct ReductionEnsembleComputationToILP { @@ -38,10 +40,32 @@ impl ReductionResult for ReductionEnsembleComputationToILP { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionEnsembleComputationToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { let mut config = Vec::with_capacity(2 * self.budget); for step in 0..self.budget { let active = target_solution[self.activity_base + step]; diff --git a/src/rules/eulerianpath_ilp.rs b/src/rules/eulerianpath_ilp.rs index 63008d824..51a3e0697 100644 --- a/src/rules/eulerianpath_ilp.rs +++ b/src/rules/eulerianpath_ilp.rs @@ -27,6 +27,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::EulerianPath; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; /// Result of reducing EulerianPath to `ILP`. /// @@ -69,10 +71,32 @@ impl ReductionResult for ReductionEulerianPathToILP { /// Reads the unique active start arc (`s_a = 1`) and walks the active /// successor relation (`y_{a,b} = 1`) one step at a time, producing an arc /// permutation of length `m` under the target path constraints. - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionEulerianPathToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok({ let m = self.num_arcs; if m == 0 { diff --git a/src/rules/exactcoverby3sets_algebraicequationsovergf2.rs b/src/rules/exactcoverby3sets_algebraicequationsovergf2.rs index 22ee0d325..7d4e9cf0d 100644 --- a/src/rules/exactcoverby3sets_algebraicequationsovergf2.rs +++ b/src/rules/exactcoverby3sets_algebraicequationsovergf2.rs @@ -4,6 +4,8 @@ use crate::models::algebraic::AlgebraicEquationsOverGF2; use crate::models::set::ExactCoverBy3Sets; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; #[derive(Debug, Clone)] pub struct ReductionX3CToAlgebraicEquationsOverGF2 { @@ -18,11 +20,18 @@ impl ReductionResult for ReductionX3CToAlgebraicEquationsOverGF2 { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { - Ok(target_solution.to_vec()) + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => Ok(SolveOutcome::optimal(source, solution)?), + SolveOutcome::Feasible { solution, .. } => { + Ok(SolveOutcome::feasible(source, solution)?) + } + } } } diff --git a/src/rules/exactcoverby3sets_boundeddiameterspanningtree.rs b/src/rules/exactcoverby3sets_boundeddiameterspanningtree.rs index f09cd0fdf..60a51ba95 100644 --- a/src/rules/exactcoverby3sets_boundeddiameterspanningtree.rs +++ b/src/rules/exactcoverby3sets_boundeddiameterspanningtree.rs @@ -35,6 +35,8 @@ use crate::models::graph::BoundedDiameterSpanningTree; use crate::models::set::ExactCoverBy3Sets; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::topology::SimpleGraph; use std::collections::HashSet; @@ -94,10 +96,32 @@ impl ReductionResult for ReductionX3CToBoundedDiameterSpanningTree { /// 2..2+m (right after the forced-center path edges). For a YES-instance, /// the optimal target witness selects exactly q of these edges, which /// correspond to the q chosen subsets. - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionX3CToBoundedDiameterSpanningTree { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok({ let m = self.source_num_subsets; let root_to_set_offset = 2; @@ -151,12 +175,12 @@ impl ReduceTo> for ExactCoverBy3Se edges.push((0, s_index(i))); weights.push(2); } - // Invariant: extract_solution reads edges[2..2 + m] to recover the + // Invariant: the reverse mapping reads edges[2..2 + m] to recover the // selected subsets. If this loop is ever reordered or moved, the // extractor must be updated to match. debug_assert!( (0..m).all(|i| edges[2 + i] == (0, s_index(i))), - "root-to-set edges must occupy indices 2..2+m for extract_solution to work" + "root-to-set edges must occupy indices 2..2+m for solution recovery to work" ); // Set-to-element edges. Subsets are already sorted in `ExactCoverBy3Sets::new`. diff --git a/src/rules/exactcoverby3sets_ilp.rs b/src/rules/exactcoverby3sets_ilp.rs index 2c5169224..1b6796cea 100644 --- a/src/rules/exactcoverby3sets_ilp.rs +++ b/src/rules/exactcoverby3sets_ilp.rs @@ -7,6 +7,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::set::ExactCoverBy3Sets; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; #[derive(Debug, Clone)] pub struct ReductionX3CToILP { @@ -21,10 +23,32 @@ impl ReductionResult for ReductionX3CToILP { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionX3CToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok(target_solution.iter().map(|&value| value == 1).collect()) } } diff --git a/src/rules/exactcoverby3sets_maximumsetpacking.rs b/src/rules/exactcoverby3sets_maximumsetpacking.rs index cfe4556ee..aaee60a94 100644 --- a/src/rules/exactcoverby3sets_maximumsetpacking.rs +++ b/src/rules/exactcoverby3sets_maximumsetpacking.rs @@ -8,6 +8,9 @@ use crate::models::set::{ExactCoverBy3Sets, MaximumSetPacking}; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; +use crate::traits::Problem; use crate::types::One; /// Result of reducing ExactCoverBy3Sets to MaximumSetPacking. @@ -29,10 +32,48 @@ impl ReductionResult for ReductionXC3SToMaximumSetPacking { /// The configuration is identity (same binary selection vector). /// A packing of q disjoint 3-sets over a 3q-element universe is necessarily /// an exact cover, so no additional checking is needed. - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + let evaluation = source.evaluate(&solution)?; + if evaluation.0 { + Ok(SolveOutcome::Optimal { + solution, + evaluation, + }) + } else { + Ok(SolveOutcome::Infeasible) + } + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + let evaluation = source.evaluate(&solution)?; + if evaluation.0 { + Ok(SolveOutcome::Feasible { + solution, + evaluation, + }) + } else { + Err(crate::rules::ExtractionError::InsufficientSolutionQuality) + } + } + } + } +} + +impl ReductionXC3SToMaximumSetPacking { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok(target_solution.to_vec()) } } diff --git a/src/rules/exactcoverby3sets_minimumaxiomset.rs b/src/rules/exactcoverby3sets_minimumaxiomset.rs index b0c481aeb..5b6be1107 100644 --- a/src/rules/exactcoverby3sets_minimumaxiomset.rs +++ b/src/rules/exactcoverby3sets_minimumaxiomset.rs @@ -7,6 +7,9 @@ use crate::models::misc::MinimumAxiomSet; use crate::models::set::ExactCoverBy3Sets; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; +use crate::traits::Problem; /// Result of reducing ExactCoverBy3Sets to MinimumAxiomSet. #[derive(Debug, Clone)] @@ -27,12 +30,51 @@ impl ReductionResult for ReductionXC3SToMinimumAxiomSet { /// Extract the chosen source subsets from the set-sentence coordinates. /// /// For YES-instances, every optimal target witness of value q consists only of - /// q set-sentences, which form an exact cover. For NO-instances, the extracted - /// vector may be non-satisfying, which is expected for an `Or -> Min` rule. - fn extract_solution( + /// q set-sentences, which form an exact cover. If a target optimum does not + /// decode to an exact cover, the source result is `Infeasible`. Without + /// optimality, failure to decode a cover is insufficient evidence of NO. + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + let evaluation = source.evaluate(&solution)?; + if evaluation.0 { + Ok(SolveOutcome::Optimal { + solution, + evaluation, + }) + } else { + Ok(SolveOutcome::Infeasible) + } + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + let evaluation = source.evaluate(&solution)?; + if evaluation.0 { + Ok(SolveOutcome::Feasible { + solution, + evaluation, + }) + } else { + Err(crate::rules::ExtractionError::InsufficientSolutionQuality) + } + } + } + } +} + +impl ReductionXC3SToMinimumAxiomSet { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok({ let set_offset = self.source_universe_size; (0..self.source_num_subsets) diff --git a/src/rules/exactcoverby3sets_minimumfaultdetectiontestset.rs b/src/rules/exactcoverby3sets_minimumfaultdetectiontestset.rs index 4c80f5d04..704ceacb4 100644 --- a/src/rules/exactcoverby3sets_minimumfaultdetectiontestset.rs +++ b/src/rules/exactcoverby3sets_minimumfaultdetectiontestset.rs @@ -9,6 +9,9 @@ use crate::models::misc::MinimumFaultDetectionTestSet; use crate::models::set::ExactCoverBy3Sets; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; +use crate::traits::Problem; /// Result of reducing ExactCoverBy3Sets to MinimumFaultDetectionTestSet. #[derive(Debug, Clone)] @@ -24,10 +27,48 @@ impl ReductionResult for ReductionXC3SToMinimumFaultDetectionTestSet { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + let evaluation = source.evaluate(&solution)?; + if evaluation.0 { + Ok(SolveOutcome::Optimal { + solution, + evaluation, + }) + } else { + Ok(SolveOutcome::Infeasible) + } + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + let evaluation = source.evaluate(&solution)?; + if evaluation.0 { + Ok(SolveOutcome::Feasible { + solution, + evaluation, + }) + } else { + Err(crate::rules::ExtractionError::InsufficientSolutionQuality) + } + } + } + } +} + +impl ReductionXC3SToMinimumFaultDetectionTestSet { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok(target_solution.iter().map(|row| row[0]).collect()) } } diff --git a/src/rules/exactcoverby3sets_staffscheduling.rs b/src/rules/exactcoverby3sets_staffscheduling.rs index 980578d21..40c221a93 100644 --- a/src/rules/exactcoverby3sets_staffscheduling.rs +++ b/src/rules/exactcoverby3sets_staffscheduling.rs @@ -14,6 +14,8 @@ use crate::models::misc::StaffScheduling; use crate::models::set::ExactCoverBy3Sets; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; /// Result of reducing ExactCoverBy3Sets to StaffScheduling. #[derive(Debug, Clone)] @@ -33,10 +35,32 @@ impl ReductionResult for ReductionXC3SToStaffScheduling { /// /// StaffScheduling config[j] = number of workers assigned to schedule j. /// XC3S config[j] = 1 if subset j is selected, 0 otherwise. - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionXC3SToStaffScheduling { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok(target_solution.iter().map(|&count| count > 0).collect()) } } diff --git a/src/rules/exactcoverby3sets_subsetproduct.rs b/src/rules/exactcoverby3sets_subsetproduct.rs index 3027d4baa..cf0dfa008 100644 --- a/src/rules/exactcoverby3sets_subsetproduct.rs +++ b/src/rules/exactcoverby3sets_subsetproduct.rs @@ -10,6 +10,8 @@ use crate::models::misc::SubsetProduct; use crate::models::set::ExactCoverBy3Sets; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use num_bigint::BigUint; use num_traits::One; @@ -26,11 +28,18 @@ impl ReductionResult for ReductionX3CToSubsetProduct { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { - Ok(target_solution.to_vec()) + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => Ok(SolveOutcome::optimal(source, solution)?), + SolveOutcome::Feasible { solution, .. } => { + Ok(SolveOutcome::feasible(source, solution)?) + } + } } } diff --git a/src/rules/expectedretrievalcost_ilp.rs b/src/rules/expectedretrievalcost_ilp.rs index f6968422d..b6a6931db 100644 --- a/src/rules/expectedretrievalcost_ilp.rs +++ b/src/rules/expectedretrievalcost_ilp.rs @@ -19,6 +19,8 @@ use crate::models::misc::ExpectedRetrievalCost; use crate::reduction; use crate::rules::ilp_helpers::{mccormick_product, one_hot_decode_rows}; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; /// Result of reducing ExpectedRetrievalCost to ILP. /// @@ -54,10 +56,32 @@ impl ReductionResult for ReductionERCToILP { } /// Extract solution: for each record r, find the unique sector s where x_{r,s} = 1. - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionERCToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok(one_hot_decode_rows( target_solution, self.num_records, diff --git a/src/rules/factoring_circuit.rs b/src/rules/factoring_circuit.rs index f62be444c..147c6f9d0 100644 --- a/src/rules/factoring_circuit.rs +++ b/src/rules/factoring_circuit.rs @@ -11,6 +11,8 @@ use crate::models::formula::{Assignment, BooleanExpr, Circuit, CircuitSAT}; use crate::models::misc::Factoring; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use num_bigint::BigUint; use num_traits::{One, Zero}; /// Result of reducing Factoring to CircuitSAT. @@ -43,10 +45,32 @@ impl ReductionResult for ReductionFactoringToCircuit { /// Extract a Factoring solution from a CircuitSAT solution. /// /// Returns the decoded factors in ascending order. - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionFactoringToCircuit { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok({ let var_names = self.target.variable_names(); diff --git a/src/rules/factoring_ilp.rs b/src/rules/factoring_ilp.rs index 10c3006be..ea99ebe09 100644 --- a/src/rules/factoring_ilp.rs +++ b/src/rules/factoring_ilp.rs @@ -24,6 +24,8 @@ use crate::models::misc::Factoring; use crate::reduction; use crate::rules::ilp_helpers::mccormick_product; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use std::cmp::min; /// Result of reducing Factoring to ILP. @@ -76,10 +78,32 @@ impl ReductionResult for ReductionFactoringToILP { /// The first m variables are p_i (first factor bits). /// The next n variables are q_j (second factor bits). /// Returns the decoded factors in ascending order. - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionFactoringToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok({ // Extract p bits (first factor) let p = (0..self.m) diff --git a/src/rules/feasibleregisterassignment_ilp.rs b/src/rules/feasibleregisterassignment_ilp.rs index d43e3a889..4de2b6e5f 100644 --- a/src/rules/feasibleregisterassignment_ilp.rs +++ b/src/rules/feasibleregisterassignment_ilp.rs @@ -14,6 +14,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::FeasibleRegisterAssignment; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; #[derive(Debug, Clone)] pub struct ReductionFeasibleRegisterAssignmentToILP { @@ -29,10 +31,32 @@ impl ReductionResult for ReductionFeasibleRegisterAssignmentToILP { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionFeasibleRegisterAssignmentToILP { + pub(crate) fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { crate::rules::ilp_helpers::decode_usize_values(&target_solution[..self.num_vertices]) } } diff --git a/src/rules/flowshopscheduling_ilp.rs b/src/rules/flowshopscheduling_ilp.rs index 334c4fec1..72744f075 100644 --- a/src/rules/flowshopscheduling_ilp.rs +++ b/src/rules/flowshopscheduling_ilp.rs @@ -9,6 +9,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::FlowShopScheduling; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; /// Result of reducing FlowShopScheduling to `ILP`. /// @@ -35,10 +37,32 @@ impl ReductionResult for ReductionFSSToILP { } /// Extract solution by sorting jobs by final-machine completion time C_{j,m-1}. - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionFSSToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok({ let n = self.num_jobs; let m = self.num_machines; diff --git a/src/rules/graph.rs b/src/rules/graph.rs index 4a302bb4c..b09ac6873 100644 --- a/src/rules/graph.rs +++ b/src/rules/graph.rs @@ -11,10 +11,9 @@ //! - JSON export for documentation and visualization use crate::rules::registry::{ - AggregateReduceFn, EdgeCapabilities, ExecutedStep, ParameterContractError, ReduceFn, - ReductionEntry, ReductionParameterContract, + EdgeCapabilities, ExecutedStep, ParameterContractError, ReduceFn, ReductionEntry, + ReductionParameterContract, }; -use crate::rules::traits::{DynAggregateReductionResult, DynReductionResult}; use crate::types::ProblemParameters; use petgraph::algo::all_simple_paths; use petgraph::graph::{DiGraph, EdgeIndex, NodeIndex}; @@ -42,13 +41,12 @@ pub struct ReductionEdgeInfo { pub(crate) struct ReductionEdgeData { pub parameter_contract: Result, pub reduce_fn: Option, - pub reduce_aggregate_fn: Option, pub turing: bool, } impl ReductionEdgeData { fn capabilities(&self) -> EdgeCapabilities { - EdgeCapabilities::from_executors(self.reduce_fn, self.reduce_aggregate_fn, self.turing) + EdgeCapabilities::from_executors(self.reduce_fn, self.turing) } } @@ -120,8 +118,6 @@ pub(crate) struct EdgeJson { pub(crate) doc_path: String, /// Whether the edge supports witness/config workflows. pub(crate) witness: bool, - /// Whether the edge supports aggregate/value workflows. - pub(crate) aggregate: bool, /// Whether the edge is a Turing (multi-query) reduction. pub(crate) turing: bool, } @@ -329,7 +325,6 @@ pub enum TraversalFlow { #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ReductionMode { Witness, - Aggregate, /// Multi-query (Turing) reductions: solving the source requires multiple /// adaptive queries to the target (e.g., binary search over a bound). Turing, @@ -453,7 +448,7 @@ impl ReductionGraph { ReductionEdgeData { parameter_contract, reduce_fn: entry.reduce_fn, - reduce_aggregate_fn: entry.reduce_aggregate_fn, + turing: entry.turing, }, ); @@ -496,7 +491,6 @@ impl ReductionGraph { fn edge_supports_mode(edge: &ReductionEdgeData, mode: ReductionMode) -> bool { match mode { ReductionMode::Witness => edge.reduce_fn.is_some(), - ReductionMode::Aggregate => edge.reduce_aggregate_fn.is_some(), ReductionMode::Turing => edge.turing, } } @@ -1406,7 +1400,6 @@ impl ReductionGraph { parameter_contract_error, doc_path, witness: capabilities.witness, - aggregate: capabilities.aggregate, turing: capabilities.turing, }); } @@ -1527,18 +1520,21 @@ pub struct MatchedEntry { pub parameter_contract: Result, } -/// Apply already-constructed witness mappings in reverse order. -fn map_solution<'a>( - steps: impl DoubleEndedIterator, - target_solution: &dyn Any, -) -> crate::rules::ExtractionResult> { - let mut steps = steps.rev(); - let first = steps.next().expect("reduction path has no steps"); - let mut solution = first.extract_solution_dyn(target_solution)?; - for step in steps { - solution = step.extract_solution_dyn(solution.as_ref())?; - } - Ok(solution) +/// Recover each intermediate result with its corresponding source instance. +fn recover_steps( + steps: &[ExecutedStep], + source: &dyn Any, + mut target: crate::solvers::ErasedOutcome, +) -> crate::rules::ExtractionResult { + for index in (0..steps.len()).rev() { + let input = if index == 0 { + source + } else { + steps[index - 1].witness.target_problem_any() + }; + target = steps[index].witness.recover_result_dyn(input, target)?; + } + Ok(target) } /// A composed reduction chain produced by [`ReductionGraph::reduce_along_path`]. @@ -1584,88 +1580,50 @@ impl ReductionChain { .expect("ReductionChain target type mismatch") } - /// Extract a solution from target space back to source space. - pub fn extract_solution( - &self, - target_solution: &T, - ) -> crate::rules::ExtractionResult { - let solution = map_solution( - self.steps.iter().map(|step| step.witness.as_ref()), - target_solution, - )?; - solution - .downcast::() - .map(|solution| *solution) - .map_err(|_| crate::rules::ExtractionError::invalid("source solution type mismatch")) - } - - /// Extract a JSON target witness into a JSON source witness. - pub fn extract_solution_json( + /// Recover a typed result through every executed step. + pub fn recover_result< + S: crate::traits::Problem + 'static, + T: crate::traits::Problem + 'static, + >( &self, - target_solution: serde_json::Value, - ) -> crate::rules::ExtractionResult { - let last = self.steps.last().expect("ReductionChain has no steps"); - let solution = last.witness.target_solution_from_json(target_solution)?; - let solution = map_solution( - self.steps.iter().map(|step| step.witness.as_ref()), - solution.as_ref(), - )?; - self.steps[0] - .witness - .source_solution_json(solution.as_ref()) - } -} - -/// A composed aggregate reduction chain produced by -/// [`ReductionGraph::reduce_aggregate_along_path`]. -pub struct AggregateReductionChain { - steps: Vec>, -} - -impl AggregateReductionChain { - /// Get the final target problem as a type-erased reference. - pub fn target_problem_any(&self) -> &dyn Any { - self.steps - .last() - .expect("AggregateReductionChain has no steps") - .target_problem_any() + source: &S, + target: crate::solvers::ProblemOutcome, + ) -> crate::rules::ExtractionResult> + where + S::Solution: 'static, + S::Value: 'static, + T::Solution: 'static, + T::Value: 'static, + { + crate::solvers::downcast_outcome(recover_steps( + &self.steps, + source, + crate::solvers::erase_outcome(target), + )?) } - /// Get a typed reference to the final target problem. - /// - /// Panics if the actual target type does not match `T`. - pub fn target_problem(&self) -> &T { - self.target_problem_any() - .downcast_ref::() - .expect("AggregateReductionChain target type mismatch") + pub(crate) fn recover_erased( + &self, + source: &dyn Any, + target: crate::solvers::ErasedOutcome, + ) -> crate::rules::ExtractionResult { + recover_steps(&self.steps, source, target) } - /// Extract an aggregate value from target space back to source space. - pub fn extract_value_dyn(&self, target_value: serde_json::Value) -> serde_json::Value { - self.steps - .iter() - .rev() - .fold(target_value, |value, step| step.extract_value_dyn(value)) + /// JSON transport for the same complete-result recovery used by typed callers. + pub fn recover_result_json( + &self, + source: &dyn Any, + target: crate::solvers::SolveOutcome, + ) -> crate::rules::ExtractionResult { + let last = self.steps.last().expect("ReductionChain has no steps"); + let target = last.witness.target_result_from_json(target)?; + let source = recover_steps(&self.steps, source, target)?; + self.steps[0].witness.source_result_json(source) } } impl ReductionGraph { - fn execute_aggregate_edge( - &self, - edge_idx: EdgeIndex, - input: &dyn Any, - ) -> Result>, crate::rules::ReductionError> { - let edge = &self.graph[edge_idx]; - if !Self::edge_supports_mode(edge, ReductionMode::Aggregate) { - return Ok(None); - } - - let Some(reduce) = edge.reduce_aggregate_fn else { - return Ok(None); - }; - reduce(input).map(Some) - } - /// Execute a reduction path on a source problem instance. /// /// Looks up each edge's `reduce_fn`, chains them, and returns the @@ -1679,7 +1637,8 @@ impl ReductionGraph { /// return Err("path is not witness-executable".into()); /// }; /// let target: &QUBO = chain.target_problem(); - /// let source_solution = chain.extract_solution(&target_solution); + /// let target_result = SolveOutcome::optimal(target, target_solution)?; + /// let source_result = chain.recover_result::>(&source_problem, target_result)?; /// ``` pub fn reduce_along_path( &self, @@ -1711,48 +1670,6 @@ impl ReductionGraph { } Ok(Some(ReductionChain::execute(source, &edge_fns)?)) } - - /// Execute an aggregate-value reduction path on a source problem instance. - pub fn reduce_aggregate_along_path( - &self, - path: &ReductionPath, - source: &dyn Any, - ) -> Result, crate::rules::ReductionError> { - if path.steps.len() < 2 { - return Ok(None); - } - - let mut edge_indices = Vec::new(); - for window in path.steps.windows(2) { - let Some(src) = self.lookup_node(&window[0].name, &window[0].variant) else { - return Ok(None); - }; - let Some(dst) = self.lookup_node(&window[1].name, &window[1].variant) else { - return Ok(None); - }; - let Some(edge_idx) = self.graph.find_edge(src, dst) else { - return Ok(None); - }; - edge_indices.push(edge_idx); - } - - let mut steps: Vec> = Vec::new(); - let Some(step) = self.execute_aggregate_edge(edge_indices[0], source)? else { - return Ok(None); - }; - steps.push(step); - for &edge_idx in &edge_indices[1..] { - let step = { - let prev_target = steps.last().unwrap().target_problem_any(); - let Some(step) = self.execute_aggregate_edge(edge_idx, prev_target)? else { - return Ok(None); - }; - step - }; - steps.push(step); - } - Ok(Some(AggregateReductionChain { steps })) - } } /// A concrete reduction path whose reductions have already been executed. @@ -1791,19 +1708,26 @@ impl ExecutedPath { .collect() } - /// Extract a solution from target space back to source space. - pub fn extract_solution( + /// Recover a typed result through the shared executed prefix. + pub fn recover_result< + S: crate::traits::Problem + 'static, + T: crate::traits::Problem + 'static, + >( &self, - target_solution: &T, - ) -> crate::rules::ExtractionResult { - let solution = map_solution( - self.steps.iter().map(|step| step.witness.as_ref()), - target_solution, - )?; - solution - .downcast::() - .map(|solution| *solution) - .map_err(|_| crate::rules::ExtractionError::invalid("source solution type mismatch")) + source: &S, + target: crate::solvers::ProblemOutcome, + ) -> crate::rules::ExtractionResult> + where + S::Solution: 'static, + S::Value: 'static, + T::Solution: 'static, + T::Value: 'static, + { + crate::solvers::downcast_outcome(recover_steps( + &self.steps, + source, + crate::solvers::erase_outcome(target), + )?) } } diff --git a/src/rules/graphpartitioning_ilp.rs b/src/rules/graphpartitioning_ilp.rs index f23d9bbb7..5e539440d 100644 --- a/src/rules/graphpartitioning_ilp.rs +++ b/src/rules/graphpartitioning_ilp.rs @@ -9,6 +9,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::GraphPartitioning; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; /// Result of reducing GraphPartitioning to ILP. @@ -30,10 +32,32 @@ impl ReductionResult for ReductionGraphPartitioningToILP { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionGraphPartitioningToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok(target_solution[..self.num_vertices] .iter() .map(|&value| value == 1) diff --git a/src/rules/graphpartitioning_maxcut.rs b/src/rules/graphpartitioning_maxcut.rs index 007ded318..b0ef48569 100644 --- a/src/rules/graphpartitioning_maxcut.rs +++ b/src/rules/graphpartitioning_maxcut.rs @@ -3,6 +3,8 @@ use crate::models::graph::{GraphPartitioning, MaxCut}; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; /// Result of reducing GraphPartitioning to MaxCut. @@ -22,11 +24,21 @@ impl ReductionResult for ReductionGPToMaxCut { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { - Ok(target_solution.to_vec()) + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + if !source.num_vertices().is_multiple_of(2) { + return Ok(SolveOutcome::Infeasible); + } + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => Ok(SolveOutcome::optimal(source, solution)?), + SolveOutcome::Feasible { .. } => { + Err(crate::rules::ExtractionError::InsufficientSolutionQuality) + } + } } } diff --git a/src/rules/graphpartitioning_qubo.rs b/src/rules/graphpartitioning_qubo.rs index 6d2ae01e1..0b7e740cb 100644 --- a/src/rules/graphpartitioning_qubo.rs +++ b/src/rules/graphpartitioning_qubo.rs @@ -8,6 +8,8 @@ use crate::models::algebraic::QUBO; use crate::models::graph::GraphPartitioning; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; /// Result of reducing GraphPartitioning to QUBO. @@ -24,11 +26,21 @@ impl ReductionResult for ReductionGraphPartitioningToQUBO { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { - Ok(target_solution.to_vec()) + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + if !source.num_vertices().is_multiple_of(2) { + return Ok(SolveOutcome::Infeasible); + } + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => Ok(SolveOutcome::optimal(source, solution)?), + SolveOutcome::Feasible { .. } => { + Err(crate::rules::ExtractionError::InsufficientSolutionQuality) + } + } } } diff --git a/src/rules/hamiltoniancircuit_biconnectivityaugmentation.rs b/src/rules/hamiltoniancircuit_biconnectivityaugmentation.rs index 1b1a7d4f2..2ef296bfe 100644 --- a/src/rules/hamiltoniancircuit_biconnectivityaugmentation.rs +++ b/src/rules/hamiltoniancircuit_biconnectivityaugmentation.rs @@ -24,6 +24,8 @@ use crate::models::graph::{BiconnectivityAugmentation, HamiltonianCircuit}; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; /// Result of reducing HamiltonianCircuit to BiconnectivityAugmentation. @@ -47,10 +49,32 @@ impl ReductionResult for ReductionHamiltonianCircuitToBiconnectivityAugmentation &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionHamiltonianCircuitToBiconnectivityAugmentation { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok({ let n = self.num_vertices; // Collect selected edges (those with config value 1) diff --git a/src/rules/hamiltoniancircuit_bottlenecktravelingsalesman.rs b/src/rules/hamiltoniancircuit_bottlenecktravelingsalesman.rs index 81056862d..926f5a242 100644 --- a/src/rules/hamiltoniancircuit_bottlenecktravelingsalesman.rs +++ b/src/rules/hamiltoniancircuit_bottlenecktravelingsalesman.rs @@ -7,7 +7,10 @@ use crate::models::graph::{BottleneckTravelingSalesman, HamiltonianCircuit}; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; +use crate::traits::Problem; /// Result of reducing HamiltonianCircuit to BottleneckTravelingSalesman. #[derive(Debug, Clone)] @@ -23,10 +26,48 @@ impl ReductionResult for ReductionHamiltonianCircuitToBottleneckTravelingSalesma &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + let evaluation = source.evaluate(&solution)?; + if evaluation.0 { + Ok(SolveOutcome::Optimal { + solution, + evaluation, + }) + } else { + Ok(SolveOutcome::Infeasible) + } + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + let evaluation = source.evaluate(&solution)?; + if evaluation.0 { + Ok(SolveOutcome::Feasible { + solution, + evaluation, + }) + } else { + Err(crate::rules::ExtractionError::InsufficientSolutionQuality) + } + } + } + } +} + +impl ReductionHamiltonianCircuitToBottleneckTravelingSalesman { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok(crate::rules::graph_helpers::edges_to_cycle_order( self.target.graph(), target_solution, diff --git a/src/rules/hamiltoniancircuit_hamiltonianpath.rs b/src/rules/hamiltoniancircuit_hamiltonianpath.rs index 85826ae97..78123c127 100644 --- a/src/rules/hamiltoniancircuit_hamiltonianpath.rs +++ b/src/rules/hamiltoniancircuit_hamiltonianpath.rs @@ -15,6 +15,8 @@ use crate::models::graph::{HamiltonianCircuit, HamiltonianPath}; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; /// Result of reducing HamiltonianCircuit to HamiltonianPath. @@ -36,10 +38,32 @@ impl ReductionResult for ReductionHamiltonianCircuitToHamiltonianPath { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionHamiltonianCircuitToHamiltonianPath { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok({ let n = self.num_original_vertices; if n == 0 { diff --git a/src/rules/hamiltoniancircuit_longestcircuit.rs b/src/rules/hamiltoniancircuit_longestcircuit.rs index faa29346c..5c7f9d37d 100644 --- a/src/rules/hamiltoniancircuit_longestcircuit.rs +++ b/src/rules/hamiltoniancircuit_longestcircuit.rs @@ -4,68 +4,83 @@ //! with unit edge weights. A Hamiltonian circuit exists iff the optimal circuit //! length equals |V|. +use crate::models::decision::Decision; use crate::models::graph::{HamiltonianCircuit, LongestCircuit}; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; /// Result of reducing HamiltonianCircuit to LongestCircuit. #[derive(Debug, Clone)] pub struct ReductionHamiltonianCircuitToLongestCircuit { - target: LongestCircuit, + target: Decision>, } impl ReductionResult for ReductionHamiltonianCircuitToLongestCircuit { type Source = HamiltonianCircuit; - type Target = LongestCircuit; + type Target = Decision>; fn target_problem(&self) -> &Self::Target { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { - Ok(crate::rules::graph_helpers::edges_to_cycle_order( - self.target.graph(), - target_solution, - )) + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } } } -impl crate::rules::AggregateReductionResult for ReductionHamiltonianCircuitToLongestCircuit { - type Source = HamiltonianCircuit; - type Target = LongestCircuit; - - fn target_problem(&self) -> &Self::Target { - &self.target - } - - fn extract_value(&self, target_value: crate::types::Max) -> crate::types::Or { - crate::types::Or( - target_value - .0 - .is_some_and(|length| usize::try_from(length) == Ok(self.target.num_vertices())), - ) +impl ReductionHamiltonianCircuitToLongestCircuit { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { + Ok(crate::rules::graph_helpers::edges_to_cycle_order( + self.target.inner().graph(), + target_solution, + )) } } #[reduction( - aggregate = custom, transform = exact { num_vertices = "num_vertices", num_edges = "num_edges", } )] -impl ReduceTo> for HamiltonianCircuit { +impl ReduceTo>> for HamiltonianCircuit { type Result = ReductionHamiltonianCircuitToLongestCircuit; fn reduce_to(&self) -> Result { let n = self.num_vertices(); let edges = self.graph().edges(); let target = LongestCircuit::new(SimpleGraph::new(n, edges), vec![1i64; self.num_edges()]); - Ok(ReductionHamiltonianCircuitToLongestCircuit { target }) + Ok(ReductionHamiltonianCircuitToLongestCircuit { + target: Decision::new( + target, + >>>::exact_i64( + n, + "encoding the circuit bound", + )?, + ), + }) } } @@ -77,7 +92,10 @@ pub(crate) fn canonical_rule_example_specs() -> Vec>( + crate::example_db::specs::rule_example_with_witness::< + _, + Decision>, + >( source, SolutionPair { source_config: serde_json::json!(vec![0, 1, 2, 3]), diff --git a/src/rules/hamiltoniancircuit_quadraticassignment.rs b/src/rules/hamiltoniancircuit_quadraticassignment.rs index a9cf105fd..a9ca7bf52 100644 --- a/src/rules/hamiltoniancircuit_quadraticassignment.rs +++ b/src/rules/hamiltoniancircuit_quadraticassignment.rs @@ -6,55 +6,66 @@ //! than three vertices map to a fixed positive-cost instance. use crate::models::algebraic::QuadraticAssignment; +use crate::models::decision::Decision; use crate::models::graph::HamiltonianCircuit; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; /// Result of reducing HamiltonianCircuit to QuadraticAssignment. #[derive(Debug, Clone)] pub struct ReductionHamiltonianCircuitToQuadraticAssignment { - target: QuadraticAssignment, + target: Decision, } impl ReductionResult for ReductionHamiltonianCircuitToQuadraticAssignment { type Source = HamiltonianCircuit; - type Target = QuadraticAssignment; + type Target = Decision; fn target_problem(&self) -> &Self::Target { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { - // Zero cost makes this permutation itself a Hamiltonian circuit. - Ok(target_solution.to_vec()) + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } } } -impl crate::rules::AggregateReductionResult for ReductionHamiltonianCircuitToQuadraticAssignment { - type Source = HamiltonianCircuit; - type Target = QuadraticAssignment; - - fn target_problem(&self) -> &Self::Target { - &self.target - } - - fn extract_value(&self, target_value: crate::types::Min) -> crate::types::Or { - crate::types::Or(target_value == crate::types::Min(Some(0))) +impl ReductionHamiltonianCircuitToQuadraticAssignment { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { + // Zero cost makes this permutation itself a Hamiltonian circuit. + Ok(target_solution.to_vec()) } } #[reduction( - aggregate = custom, transform = upper_bound { num_facilities = "num_vertices + 3", num_locations = "num_vertices + 3", } )] -impl ReduceTo for HamiltonianCircuit { +impl ReduceTo> for HamiltonianCircuit { type Result = ReductionHamiltonianCircuitToQuadraticAssignment; fn reduce_to(&self) -> Result { @@ -74,7 +85,9 @@ impl ReduceTo for HamiltonianCircuit { .collect(); let target = QuadraticAssignment::new(cost_matrix, distance_matrix); - Ok(ReductionHamiltonianCircuitToQuadraticAssignment { target }) + Ok(ReductionHamiltonianCircuitToQuadraticAssignment { + target: Decision::new(target, 0), + }) } } @@ -86,7 +99,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec( + crate::example_db::specs::rule_example_with_witness::<_, Decision>( source, SolutionPair { source_config: serde_json::json!(vec![0, 1, 2, 3]), diff --git a/src/rules/hamiltoniancircuit_ruralpostman.rs b/src/rules/hamiltoniancircuit_ruralpostman.rs index 8b81f2b5c..ba90b8d1f 100644 --- a/src/rules/hamiltoniancircuit_ruralpostman.rs +++ b/src/rules/hamiltoniancircuit_ruralpostman.rs @@ -23,15 +23,18 @@ //! b-vertices and a-vertices does not admit a perfect matching corresponding //! to a Hamiltonian circuit), so cost > 2n. +use crate::models::decision::Decision; use crate::models::graph::{HamiltonianCircuit, RuralPostman}; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; /// Result of reducing HamiltonianCircuit to RuralPostman. #[derive(Debug, Clone)] pub struct ReductionHamiltonianCircuitToRuralPostman { - target: RuralPostman, + target: Decision>, /// Number of vertices in the original graph. n: usize, /// Edges of the original graph (for solution extraction). @@ -40,16 +43,38 @@ pub struct ReductionHamiltonianCircuitToRuralPostman { impl ReductionResult for ReductionHamiltonianCircuitToRuralPostman { type Source = HamiltonianCircuit; - type Target = RuralPostman; + type Target = Decision>; fn target_problem(&self) -> &Self::Target { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionHamiltonianCircuitToRuralPostman { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok({ // The target solution is edge multiplicities. // Required edges are indices 0..n (the {v_i^a, v_i^b} edges). @@ -96,28 +121,14 @@ impl ReductionResult for ReductionHamiltonianCircuitToRuralPostman { } } -impl crate::rules::AggregateReductionResult for ReductionHamiltonianCircuitToRuralPostman { - type Source = HamiltonianCircuit; - type Target = RuralPostman; - - fn target_problem(&self) -> &Self::Target { - &self.target - } - - fn extract_value(&self, value: crate::types::Min) -> crate::types::Or { - crate::types::Or(self.n >= 3 && value.0 == Some(2 * self.n as i64)) - } -} - #[reduction( - aggregate = custom, transform = exact { num_vertices = "2 * num_vertices", num_edges = "num_vertices + 2 * num_edges", num_required_edges = "num_vertices", } )] -impl ReduceTo> for HamiltonianCircuit { +impl ReduceTo>> for HamiltonianCircuit { type Result = ReductionHamiltonianCircuitToRuralPostman; fn reduce_to(&self) -> Result { @@ -152,7 +163,17 @@ impl ReduceTo> for HamiltonianCircuit>>>::exact_i64( + 2 * n, + "encoding the route bound", + )? + }, + ), n, source_edges, }) @@ -176,7 +197,10 @@ pub(crate) fn canonical_rule_example_specs() -> Vec0: bwd edge of source edge 2=(0,2), idx=8 // Required edges all have multiplicity 1. // target_config = [1, 1, 1, 1, 0, 1, 0, 0, 1] - crate::example_db::specs::rule_example_with_witness::<_, RuralPostman>( + crate::example_db::specs::rule_example_with_witness::< + _, + Decision>, + >( source, SolutionPair { source_config: serde_json::json!(vec![0, 1, 2]), diff --git a/src/rules/hamiltoniancircuit_stackercrane.rs b/src/rules/hamiltoniancircuit_stackercrane.rs index 34f232865..b89cd5a28 100644 --- a/src/rules/hamiltoniancircuit_stackercrane.rs +++ b/src/rules/hamiltoniancircuit_stackercrane.rs @@ -12,62 +12,68 @@ //! paths cost strictly more than single-hop ones. Only permutations attaining //! this lower bound certify a Hamiltonian circuit. +use crate::models::decision::Decision; use crate::models::graph::HamiltonianCircuit; use crate::models::misc::StackerCrane; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; /// Result of reducing HamiltonianCircuit to StackerCrane. #[derive(Debug, Clone)] pub struct ReductionHamiltonianCircuitToStackerCrane { - target: StackerCrane, + target: Decision, } impl ReductionResult for ReductionHamiltonianCircuitToStackerCrane { type Source = HamiltonianCircuit; - type Target = StackerCrane; + type Target = Decision; fn target_problem(&self) -> &Self::Target { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { - // Service arc i corresponds to source vertex i. - Ok(target_solution.to_vec()) + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } } } -impl crate::rules::AggregateReductionResult for ReductionHamiltonianCircuitToStackerCrane { - type Source = HamiltonianCircuit; - type Target = StackerCrane; - - fn target_problem(&self) -> &Self::Target { - &self.target - } - - fn extract_value(&self, value: crate::types::Min) -> crate::types::Or { - crate::types::Or( - self.target.num_arcs() >= 3 - && value - .0 - .is_some_and(|cost| usize::try_from(cost) == Ok(self.target.num_vertices())), - ) +impl ReductionHamiltonianCircuitToStackerCrane { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { + // Service arc i corresponds to source vertex i. + Ok(target_solution.to_vec()) } } #[reduction( - aggregate = custom, transform = exact { num_vertices = "2 * num_vertices", num_arcs = "num_vertices", num_edges = "2 * num_edges", } )] -impl ReduceTo for HamiltonianCircuit { +impl ReduceTo> for HamiltonianCircuit { type Result = ReductionHamiltonianCircuitToStackerCrane; fn reduce_to(&self) -> Result { @@ -96,9 +102,21 @@ impl ReduceTo for HamiltonianCircuit { let target = StackerCrane::try_new(target_num_vertices, arcs, edges, arc_lengths, edge_lengths) - .map_err(>::target_construction)?; - - Ok(ReductionHamiltonianCircuitToStackerCrane { target }) + .map_err(>>::target_construction)?; + + Ok(ReductionHamiltonianCircuitToStackerCrane { + target: Decision::new( + target, + if n < 3 { + -1 + } else { + >>::exact_i64( + target_num_vertices, + "encoding the route bound", + )? + }, + ), + }) } } @@ -109,7 +127,7 @@ fn split_graph_dimensions( ) -> Result<(usize, usize), crate::rules::ReductionError> { type Source = HamiltonianCircuit; let overflow = || { - crate::rules::ReductionError::integer_overflow::( + crate::rules::ReductionError::integer_overflow::>( "encoding split graph dimensions and route costs", ) }; @@ -118,7 +136,10 @@ fn split_graph_dimensions( // A shortest connector is simple and has at most 2n-1 unit steps. // The n services therefore cost at most n * (1 + (2n-1)). let cost_bound = n.checked_mul(vertices).ok_or_else(overflow)?; - >::exact_i64(cost_bound, "bounding split graph route costs")?; + >>::exact_i64( + cost_bound, + "bounding split graph route costs", + )?; Ok((vertices, edges)) } @@ -130,7 +151,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec( + crate::example_db::specs::rule_example_with_witness::<_, Decision>( source, SolutionPair { source_config: serde_json::json!(vec![0, 1, 2, 3]), diff --git a/src/rules/hamiltoniancircuit_strongconnectivityaugmentation.rs b/src/rules/hamiltoniancircuit_strongconnectivityaugmentation.rs index 9fac217bc..1f028bda9 100644 --- a/src/rules/hamiltoniancircuit_strongconnectivityaugmentation.rs +++ b/src/rules/hamiltoniancircuit_strongconnectivityaugmentation.rs @@ -10,6 +10,8 @@ use crate::models::graph::{HamiltonianCircuit, StrongConnectivityAugmentation}; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::topology::{DirectedGraph, Graph, SimpleGraph}; /// Result of reducing HamiltonianCircuit to StrongConnectivityAugmentation. @@ -27,10 +29,32 @@ impl ReductionResult for ReductionHamiltonianCircuitToStrongConnectivityAugmenta &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionHamiltonianCircuitToStrongConnectivityAugmentation { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok({ let n = self.n; // Build directed adjacency from selected arcs. diff --git a/src/rules/hamiltoniancircuit_travelingsalesman.rs b/src/rules/hamiltoniancircuit_travelingsalesman.rs index f12128789..78e2f11e3 100644 --- a/src/rules/hamiltoniancircuit_travelingsalesman.rs +++ b/src/rules/hamiltoniancircuit_travelingsalesman.rs @@ -7,7 +7,10 @@ use crate::models::graph::{HamiltonianCircuit, TravelingSalesman}; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; +use crate::traits::Problem; /// Result of reducing HamiltonianCircuit to TravelingSalesman. #[derive(Debug, Clone)] @@ -23,10 +26,48 @@ impl ReductionResult for ReductionHamiltonianCircuitToTravelingSalesman { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + let evaluation = source.evaluate(&solution)?; + if evaluation.0 { + Ok(SolveOutcome::Optimal { + solution, + evaluation, + }) + } else { + Ok(SolveOutcome::Infeasible) + } + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + let evaluation = source.evaluate(&solution)?; + if evaluation.0 { + Ok(SolveOutcome::Feasible { + solution, + evaluation, + }) + } else { + Err(crate::rules::ExtractionError::InsufficientSolutionQuality) + } + } + } + } +} + +impl ReductionHamiltonianCircuitToTravelingSalesman { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok(crate::rules::graph_helpers::edges_to_cycle_order( self.target.graph(), target_solution, diff --git a/src/rules/hamiltonianpath_degreeconstrainedspanningtree.rs b/src/rules/hamiltonianpath_degreeconstrainedspanningtree.rs index fd12968b1..79a13e39a 100644 --- a/src/rules/hamiltonianpath_degreeconstrainedspanningtree.rs +++ b/src/rules/hamiltonianpath_degreeconstrainedspanningtree.rs @@ -5,6 +5,8 @@ use crate::models::graph::{DegreeConstrainedSpanningTree, HamiltonianPath}; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; /// Result of reducing HamiltonianPath to DegreeConstrainedSpanningTree. @@ -21,10 +23,32 @@ impl ReductionResult for ReductionHamiltonianPathToDegreeConstrainedSpanningTree &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionHamiltonianPathToDegreeConstrainedSpanningTree { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok(extract_hamiltonian_order( self.target.graph(), target_solution, diff --git a/src/rules/hamiltonianpath_ilp.rs b/src/rules/hamiltonianpath_ilp.rs index e9001806c..3a5d9cccc 100644 --- a/src/rules/hamiltonianpath_ilp.rs +++ b/src/rules/hamiltonianpath_ilp.rs @@ -13,6 +13,8 @@ use crate::rules::ilp_helpers::{ mccormick_product, one_hot_assignment_constraints, one_hot_decode, }; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; /// Result of reducing HamiltonianPath to ILP. @@ -35,10 +37,32 @@ impl ReductionResult for ReductionHamiltonianPathToILP { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionHamiltonianPathToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok(one_hot_decode( target_solution, self.num_vertices, diff --git a/src/rules/hamiltonianpath_isomorphicspanningtree.rs b/src/rules/hamiltonianpath_isomorphicspanningtree.rs index dd7802124..fd05fb863 100644 --- a/src/rules/hamiltonianpath_isomorphicspanningtree.rs +++ b/src/rules/hamiltonianpath_isomorphicspanningtree.rs @@ -7,6 +7,8 @@ use crate::models::graph::{HamiltonianPath, IsomorphicSpanningTree}; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::topology::SimpleGraph; /// Result of reducing HamiltonianPath to IsomorphicSpanningTree. @@ -28,11 +30,18 @@ impl ReductionResult for ReductionHPToIST { /// The IST config maps tree vertex i to graph vertex config[i]. Since the /// tree is P_n (path 0-1-2-...-n-1), this mapping directly gives the /// vertex ordering of the Hamiltonian path. - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { - Ok(target_solution.to_vec()) + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => Ok(SolveOutcome::optimal(source, solution)?), + SolveOutcome::Feasible { solution, .. } => { + Ok(SolveOutcome::feasible(source, solution)?) + } + } } } diff --git a/src/rules/hamiltonianpathbetweentwovertices_longestpath.rs b/src/rules/hamiltonianpathbetweentwovertices_longestpath.rs index f4d773f77..46a5b1c3d 100644 --- a/src/rules/hamiltonianpathbetweentwovertices_longestpath.rs +++ b/src/rules/hamiltonianpathbetweentwovertices_longestpath.rs @@ -5,21 +5,24 @@ //! source/target vertices, the longest path of length n-1 exactly corresponds //! to a Hamiltonian s-t path. +use crate::models::decision::Decision; use crate::models::graph::{HamiltonianPathBetweenTwoVertices, LongestPath}; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; use crate::types::One; /// Result of reducing HamiltonianPathBetweenTwoVertices to LongestPath. #[derive(Debug, Clone)] pub struct ReductionHPBTVToLP { - target: LongestPath, + target: Decision>, } impl ReductionResult for ReductionHPBTVToLP { type Source = HamiltonianPathBetweenTwoVertices; - type Target = LongestPath; + type Target = Decision>; fn target_problem(&self) -> &Self::Target { &self.target @@ -29,12 +32,37 @@ impl ReductionResult for ReductionHPBTVToLP { /// /// The target solution is a binary vector over edges. We walk the selected /// edges from the source vertex to reconstruct the vertex ordering. - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { - let mut adjacency = vec![Vec::new(); self.target.num_vertices()]; - for (&selected, (u, v)) in target_solution.iter().zip(self.target.graph().edges()) { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionHPBTVToLP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { + let mut adjacency = vec![Vec::new(); self.target.inner().num_vertices()]; + for (&selected, (u, v)) in target_solution + .iter() + .zip(self.target.inner().graph().edges()) + { if selected { adjacency[u].push(v); adjacency[v].push(u); @@ -44,9 +72,9 @@ impl ReductionResult for ReductionHPBTVToLP { // Target feasibility guarantees a single simple path with these endpoints. // Its certified n-1 edges visit every vertex; walking away from the // previous vertex terminates at the target without repetitions. - let mut current = self.target.source_vertex(); + let mut current = self.target.inner().source_vertex(); let mut previous = None; - let mut path = Vec::with_capacity(self.target.num_vertices()); + let mut path = Vec::with_capacity(self.target.inner().num_vertices()); path.push(current); while let Some(&next) = adjacency[current] .iter() @@ -60,31 +88,14 @@ impl ReductionResult for ReductionHPBTVToLP { } } -impl crate::rules::AggregateReductionResult for ReductionHPBTVToLP { - type Source = HamiltonianPathBetweenTwoVertices; - type Target = LongestPath; - - fn target_problem(&self) -> &Self::Target { - &self.target - } - - fn extract_value(&self, value: crate::types::Max) -> crate::types::Or { - // The source requires distinct valid endpoints, hence at least two vertices. - crate::types::Or( - value.0.is_some_and(|length| { - usize::try_from(length) == Ok(self.target.num_vertices() - 1) - }), - ) - } -} - #[reduction( - aggregate = custom, transform = exact { num_vertices = "num_vertices", num_edges = "num_edges", })] -impl ReduceTo> for HamiltonianPathBetweenTwoVertices { +impl ReduceTo>> + for HamiltonianPathBetweenTwoVertices +{ type Result = ReductionHPBTVToLP; fn reduce_to(&self) -> Result { @@ -99,7 +110,15 @@ impl ReduceTo> for HamiltonianPathBetweenTwoVertic self.target_vertex(), ); - Ok(ReductionHPBTVToLP { target }) + Ok(ReductionHPBTVToLP { + target: Decision::new( + target, + >>>::exact_i64( + self.num_vertices() - 1, + "encoding the path bound", + )?, + ), + }) } } @@ -116,7 +135,10 @@ pub(crate) fn canonical_rule_example_specs() -> Vec>( + crate::example_db::specs::rule_example_with_witness::< + _, + Decision>, + >( source, SolutionPair { source_config: serde_json::json!(vec![0, 1, 2, 3, 4]), diff --git a/src/rules/highlyconnecteddeletion_ilp.rs b/src/rules/highlyconnecteddeletion_ilp.rs index 0d72c1f87..8d5c0ec1b 100644 --- a/src/rules/highlyconnecteddeletion_ilp.rs +++ b/src/rules/highlyconnecteddeletion_ilp.rs @@ -29,6 +29,8 @@ use crate::models::graph::highly_connected_deletion::{induced_edge_count, is_fea use crate::models::graph::HighlyConnectedDeletion; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; /// Result of reducing HighlyConnectedDeletion to ILP. @@ -60,10 +62,32 @@ impl ReductionResult for ReductionHighlyConnectedDeletionToILP { /// 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( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionHighlyConnectedDeletionToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::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 { diff --git a/src/rules/ilp_bool_ilp_i64.rs b/src/rules/ilp_bool_ilp_i64.rs index bb2a8f9fa..8bb5d289b 100644 --- a/src/rules/ilp_bool_ilp_i64.rs +++ b/src/rules/ilp_bool_ilp_i64.rs @@ -7,6 +7,8 @@ use crate::models::algebraic::ILP; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; #[derive(Debug, Clone)] pub struct ReductionBinaryILPToIntILP { @@ -21,11 +23,18 @@ impl ReductionResult for ReductionBinaryILPToIntILP { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { - Ok(target_solution.to_vec()) + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => Ok(SolveOutcome::optimal(source, solution)?), + SolveOutcome::Feasible { solution, .. } => { + Ok(SolveOutcome::feasible(source, solution)?) + } + } } } diff --git a/src/rules/ilp_i64_ilp_bool.rs b/src/rules/ilp_i64_ilp_bool.rs index 2d53911cf..bf21b37f6 100644 --- a/src/rules/ilp_i64_ilp_bool.rs +++ b/src/rules/ilp_i64_ilp_bool.rs @@ -4,6 +4,8 @@ use crate::models::algebraic::{Comparison, LinearConstraint, ILP}; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; use crate::rules::ReductionError; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; #[derive(Debug, Clone)] struct VarEncoding { @@ -80,10 +82,32 @@ impl ReductionResult for ReductionIntILPToBinaryILP { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionIntILPToBinaryILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok(self .encodings .iter() diff --git a/src/rules/ilp_qubo.rs b/src/rules/ilp_qubo.rs index f6dd95dcd..1117b913c 100644 --- a/src/rules/ilp_qubo.rs +++ b/src/rules/ilp_qubo.rs @@ -14,6 +14,8 @@ use crate::models::algebraic::{Comparison, ObjectiveSense, ILP, QUBO}; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; /// Result of reducing binary ILP to QUBO. #[derive(Debug, Clone)] @@ -35,10 +37,44 @@ impl ReductionResult for ReductionILPToQUBO { } /// Extract only the original variables (discard slack). - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { + solution, + evaluation, + } => { + if !self.map_value(evaluation).is_valid() { + return Ok(SolveOutcome::Infeasible); + } + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { + solution, + evaluation, + } => { + if !self.map_value(evaluation).is_valid() { + return Err(crate::rules::ExtractionError::InsufficientSolutionQuality); + } + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionILPToQUBO { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok(target_solution[..self.num_original_vars] .iter() .map(|&value| i64::from(value)) @@ -46,15 +82,8 @@ impl ReductionResult for ReductionILPToQUBO { } } -impl crate::rules::AggregateReductionResult for ReductionILPToQUBO { - type Source = ILP; - type Target = QUBO; - - fn target_problem(&self) -> &Self::Target { - &self.target - } - - fn extract_value(&self, value: crate::types::Min) -> crate::types::Extremum { +impl ReductionILPToQUBO { + fn map_value(&self, value: crate::types::Min) -> crate::types::Extremum { let objective = value .0 .filter(|&energy| { @@ -71,7 +100,6 @@ impl crate::rules::AggregateReductionResult for ReductionILPToQUBO { } #[reduction( - aggregate = custom, transform = unavailable { num_vars = "the slack-bit count depends on coefficient magnitudes and right-hand sides absent from the registered source parameters vector", } diff --git a/src/rules/integerknapsack_ilp.rs b/src/rules/integerknapsack_ilp.rs index e74dddbdc..9f2151de0 100644 --- a/src/rules/integerknapsack_ilp.rs +++ b/src/rules/integerknapsack_ilp.rs @@ -8,6 +8,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::set::IntegerKnapsack; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; #[derive(Debug, Clone)] pub struct ReductionIntegerKnapsackToILP { @@ -22,10 +24,32 @@ impl ReductionResult for ReductionIntegerKnapsackToILP { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionIntegerKnapsackToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { crate::rules::ilp_helpers::decode_usize_values(target_solution) } } diff --git a/src/rules/integralflowbundles_ilp.rs b/src/rules/integralflowbundles_ilp.rs index 33122f747..e75a9af2a 100644 --- a/src/rules/integralflowbundles_ilp.rs +++ b/src/rules/integralflowbundles_ilp.rs @@ -8,6 +8,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::IntegralFlowBundles; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; /// Result of reducing IntegralFlowBundles to ILP. #[derive(Debug, Clone)] @@ -23,10 +25,32 @@ impl ReductionResult for ReductionIFBToILP { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionIFBToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { crate::rules::ilp_helpers::decode_usize_values(target_solution) } } diff --git a/src/rules/integralflowhomologousarcs_ilp.rs b/src/rules/integralflowhomologousarcs_ilp.rs index 5ef8ec0ef..ebe49d6bc 100644 --- a/src/rules/integralflowhomologousarcs_ilp.rs +++ b/src/rules/integralflowhomologousarcs_ilp.rs @@ -7,6 +7,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::IntegralFlowHomologousArcs; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; /// Result of reducing IntegralFlowHomologousArcs to ILP. #[derive(Debug, Clone)] @@ -22,10 +24,32 @@ impl ReductionResult for ReductionIFHAToILP { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionIFHAToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { crate::rules::ilp_helpers::decode_usize_values(target_solution) } } diff --git a/src/rules/integralflowwithmultipliers_ilp.rs b/src/rules/integralflowwithmultipliers_ilp.rs index 4d8b18294..2895519bd 100644 --- a/src/rules/integralflowwithmultipliers_ilp.rs +++ b/src/rules/integralflowwithmultipliers_ilp.rs @@ -7,6 +7,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::IntegralFlowWithMultipliers; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; /// Result of reducing IntegralFlowWithMultipliers to ILP. #[derive(Debug, Clone)] @@ -22,10 +24,32 @@ impl ReductionResult for ReductionIFWMToILP { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionIFWMToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { crate::rules::ilp_helpers::decode_usize_values(target_solution) } } diff --git a/src/rules/isomorphicspanningtree_ilp.rs b/src/rules/isomorphicspanningtree_ilp.rs index ee9aa2d04..7220e6010 100644 --- a/src/rules/isomorphicspanningtree_ilp.rs +++ b/src/rules/isomorphicspanningtree_ilp.rs @@ -7,6 +7,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::IsomorphicSpanningTree; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; #[derive(Debug, Clone)] @@ -24,10 +26,32 @@ impl ReductionResult for ReductionISTToILP { } /// For each tree vertex u, output the unique graph vertex v with x_{u,v} = 1. - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionISTToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok(crate::rules::ilp_helpers::one_hot_decode_rows( target_solution, self.n, diff --git a/src/rules/kclique_balancedcompletebipartitesubgraph.rs b/src/rules/kclique_balancedcompletebipartitesubgraph.rs index 7dfc7af23..dcdc10542 100644 --- a/src/rules/kclique_balancedcompletebipartitesubgraph.rs +++ b/src/rules/kclique_balancedcompletebipartitesubgraph.rs @@ -8,6 +8,8 @@ use crate::models::graph::{BalancedCompleteBipartiteSubgraph, KClique}; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::topology::{BipartiteGraph, Graph, SimpleGraph}; /// Result of reducing KClique to BalancedCompleteBipartiteSubgraph. @@ -34,10 +36,32 @@ impl ReductionResult for ReductionKCliqueToBCBS { /// The k-clique is S = {v in V : v not in A'}, i.e., the original vertices /// NOT selected on the left side. For each original vertex v (0..n-1), /// the source selection is the negation of the target's left-side selection. - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionKCliqueToBCBS { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok({ (0..self.num_original_vertices) .map(|v| !target_solution[v]) diff --git a/src/rules/kclique_conjunctivebooleanquery.rs b/src/rules/kclique_conjunctivebooleanquery.rs index c8f1241ae..cef642bb0 100644 --- a/src/rules/kclique_conjunctivebooleanquery.rs +++ b/src/rules/kclique_conjunctivebooleanquery.rs @@ -12,6 +12,8 @@ use crate::models::graph::KClique; use crate::models::misc::{CbqRelation, ConjunctiveBooleanQuery, QueryArg}; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; /// Result of reducing KClique to ConjunctiveBooleanQuery. @@ -34,10 +36,32 @@ impl ReductionResult for ReductionKCliqueToCBQ { /// CBQ config: vec of length k, each value is a domain element (vertex index). /// KClique config: binary vec of length n; set config[v]=1 for each v in /// the CBQ assignment. - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionKCliqueToCBQ { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok(KClique::::config_from_vertices( self.num_vertices, target_solution, diff --git a/src/rules/kclique_ilp.rs b/src/rules/kclique_ilp.rs index 3c7377041..0fa879525 100644 --- a/src/rules/kclique_ilp.rs +++ b/src/rules/kclique_ilp.rs @@ -13,6 +13,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::KClique; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; /// Result of reducing KClique to ILP. @@ -39,10 +41,32 @@ impl ReductionResult for ReductionKCliqueToILP { /// /// Since the mapping is 1:1 (each vertex maps to one binary variable), /// the solution extraction is simply copying the configuration. - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionKCliqueToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok(target_solution.iter().map(|&value| value == 1).collect()) } } diff --git a/src/rules/kclique_subgraphisomorphism.rs b/src/rules/kclique_subgraphisomorphism.rs index 775fb94b8..02e8a032f 100644 --- a/src/rules/kclique_subgraphisomorphism.rs +++ b/src/rules/kclique_subgraphisomorphism.rs @@ -8,6 +8,8 @@ use crate::models::graph::{KClique, SubgraphIsomorphism}; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::topology::SimpleGraph; /// Result of reducing KClique to SubgraphIsomorphism. @@ -34,10 +36,32 @@ impl ReductionResult for ReductionKCliqueToSubIso { /// The SubgraphIsomorphism config maps each pattern vertex (0..k-1) to a /// host vertex. We create a binary vector of length n and set positions /// f(0), f(1), ..., f(k-1) to 1. - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionKCliqueToSubIso { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok(KClique::::config_from_vertices( self.num_source_vertices, target_solution, diff --git a/src/rules/kcoloring_bicliquecover.rs b/src/rules/kcoloring_bicliquecover.rs index c4391c0e6..90f791d25 100644 --- a/src/rules/kcoloring_bicliquecover.rs +++ b/src/rules/kcoloring_bicliquecover.rs @@ -32,6 +32,8 @@ use crate::models::graph::{BicliqueCover, KColoring}; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::topology::{BipartiteGraph, Graph, SimpleGraph}; use crate::variant::KN; use std::collections::BTreeSet; @@ -40,7 +42,7 @@ use std::collections::BTreeSet; #[derive(Debug, Clone)] pub struct ReductionKColoringToBicliqueCover { target: BicliqueCover, - /// Number of source vertices `n`. Stored so `extract_solution` can locate + /// Number of source vertices `n`. Stored so `recover_result` can locate /// the diagonal indices of each source vertex without re-reading the /// reduction parameters. num_vertices: usize, @@ -65,10 +67,32 @@ impl ReductionResult for ReductionKColoringToBicliqueCover { /// cover yields at most `q` such distinct bicliques, so the result is a /// proper `q`-coloring of the source. /// - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionKColoringToBicliqueCover { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok({ let n = self.num_vertices; let k = self.target.k(); diff --git a/src/rules/kcoloring_casts.rs b/src/rules/kcoloring_casts.rs index 15b848cee..8af0d2143 100644 --- a/src/rules/kcoloring_casts.rs +++ b/src/rules/kcoloring_casts.rs @@ -9,6 +9,6 @@ impl_variant_reduction!( KColoring, => , fields: [num_vertices, num_edges, num_colors], - aggregate: identity, + |src| KColoring::with_k(src.graph().clone(), src.num_colors()) ); diff --git a/src/rules/kcoloring_clustering.rs b/src/rules/kcoloring_clustering.rs index 40ddffec3..8726d488d 100644 --- a/src/rules/kcoloring_clustering.rs +++ b/src/rules/kcoloring_clustering.rs @@ -8,6 +8,8 @@ use crate::models::graph::KColoring; use crate::models::misc::Clustering; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; use crate::variant::K3; @@ -28,10 +30,32 @@ impl ReductionResult for ReductionKColoringToClustering { /// Cluster labels are color labels. The empty-graph corner case uses one /// dummy target element because Clustering forbids empty instances. - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionKColoringToClustering { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok(target_solution[..self.source_num_vertices].to_vec()) } } diff --git a/src/rules/kcoloring_partitionintocliques.rs b/src/rules/kcoloring_partitionintocliques.rs index b1656eace..5b678a80b 100644 --- a/src/rules/kcoloring_partitionintocliques.rs +++ b/src/rules/kcoloring_partitionintocliques.rs @@ -7,6 +7,8 @@ use crate::models::graph::{KColoring, PartitionIntoCliques}; use crate::reduction; use crate::rules::graph_helpers::complement_edges; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; use crate::variant::KN; @@ -25,11 +27,18 @@ impl ReductionResult for ReductionKColoringToPartitionIntoCliques { } /// Solution extraction is the identity: color classes become clique classes. - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { - Ok(target_solution.to_vec()) + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => Ok(SolveOutcome::optimal(source, solution)?), + SolveOutcome::Feasible { solution, .. } => { + Ok(SolveOutcome::feasible(source, solution)?) + } + } } } diff --git a/src/rules/kcoloring_twodimensionalconsecutivesets.rs b/src/rules/kcoloring_twodimensionalconsecutivesets.rs index 5e1b715f0..8e35fe1e4 100644 --- a/src/rules/kcoloring_twodimensionalconsecutivesets.rs +++ b/src/rules/kcoloring_twodimensionalconsecutivesets.rs @@ -16,6 +16,8 @@ use crate::models::graph::KColoring; use crate::models::set::TwoDimensionalConsecutiveSets; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; use crate::variant::K3; @@ -41,10 +43,32 @@ impl ReductionResult for ReductionKColoringToTDCS { /// The first `num_vertices` symbols correspond to graph vertices, /// so their group assignments directly give a valid 3-coloring /// (after remapping to colors 0, 1, 2). - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionKColoringToTDCS { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok({ // The target solution is config[symbol] = group_index. // Vertex symbols are indices 0..num_vertices. diff --git a/src/rules/knapsack_ilp.rs b/src/rules/knapsack_ilp.rs index a45de39ce..51514e5e1 100644 --- a/src/rules/knapsack_ilp.rs +++ b/src/rules/knapsack_ilp.rs @@ -9,6 +9,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::Knapsack; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; /// Result of reducing Knapsack to ILP. #[derive(Debug, Clone)] @@ -24,10 +26,32 @@ impl ReductionResult for ReductionKnapsackToILP { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionKnapsackToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok(target_solution.iter().map(|&value| value == 1).collect()) } } diff --git a/src/rules/knapsack_qubo.rs b/src/rules/knapsack_qubo.rs index d2c29d291..0a8926710 100644 --- a/src/rules/knapsack_qubo.rs +++ b/src/rules/knapsack_qubo.rs @@ -14,6 +14,8 @@ use crate::models::algebraic::QUBO; use crate::models::misc::Knapsack; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; fn overflow(operation: &'static str) -> crate::rules::ReductionError { crate::rules::ReductionError::integer_overflow::>(operation) @@ -34,10 +36,31 @@ impl ReductionResult for ReductionKnapsackToQUBO { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { .. } => { + Err(crate::rules::ExtractionError::InsufficientSolutionQuality) + } + } + } +} + +impl ReductionKnapsackToQUBO { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok(target_solution[..self.num_items].to_vec()) } } diff --git a/src/rules/ksatisfiability_acyclicpartition.rs b/src/rules/ksatisfiability_acyclicpartition.rs index 9f1d71490..f3af8049a 100644 --- a/src/rules/ksatisfiability_acyclicpartition.rs +++ b/src/rules/ksatisfiability_acyclicpartition.rs @@ -10,6 +10,8 @@ use crate::models::graph::{AcyclicPartition, KClique}; use crate::reduction; use crate::rules::ksatisfiability_kclique::Reduction3SATToKClique; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::topology::{DirectedGraph, Graph, SimpleGraph}; use crate::variant::K3; @@ -29,16 +31,38 @@ impl ReductionResult for Reduction3SATToAcyclicPartition { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl Reduction3SATToAcyclicPartition { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { let source_label = target_solution[self.source_vertex]; let selected = target_solution[..self.sat_to_clique.target_problem().num_vertices()] .iter() .map(|&label| label == source_label) .collect(); - self.sat_to_clique.extract_solution(&selected) + self.sat_to_clique.map_solution(&selected) } } diff --git a/src/rules/ksatisfiability_bicliquecover.rs b/src/rules/ksatisfiability_bicliquecover.rs index 297321a6a..a165ce657 100644 --- a/src/rules/ksatisfiability_bicliquecover.rs +++ b/src/rules/ksatisfiability_bicliquecover.rs @@ -49,6 +49,8 @@ use crate::models::formula::KSatisfiability; use crate::models::graph::BicliqueCover; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::topology::BipartiteGraph; use crate::variant::K3; use std::collections::BTreeSet; @@ -90,10 +92,32 @@ impl ReductionResult for ReductionKSatisfiabilityToBicliqueCover { /// Its left crown memberships give the normalized truth assignment. /// Map appearing variables back to their original indices and assign false /// to variables absent from the formula. - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionKSatisfiabilityToBicliqueCover { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { // Variables absent from every clause may be assigned false. // This also defines the inverse map for the empty-formula YES target. let mut source_assignment = vec![false; self.source_num_vars]; diff --git a/src/rules/ksatisfiability_casts.rs b/src/rules/ksatisfiability_casts.rs index 659c4d5b9..d965c2619 100644 --- a/src/rules/ksatisfiability_casts.rs +++ b/src/rules/ksatisfiability_casts.rs @@ -8,7 +8,7 @@ impl_variant_reduction!( KSatisfiability, => , fields: [num_vars, num_clauses, num_literals], - aggregate: identity, + |src| KSatisfiability::new_allow_less(src.num_vars(), src.clauses().to_vec()) ); @@ -16,6 +16,6 @@ impl_variant_reduction!( KSatisfiability, => , fields: [num_vars, num_clauses, num_literals], - aggregate: identity, + |src| KSatisfiability::new_allow_less(src.num_vars(), src.clauses().to_vec()) ); diff --git a/src/rules/ksatisfiability_cyclicordering.rs b/src/rules/ksatisfiability_cyclicordering.rs index 7a43dc164..2186d3b18 100644 --- a/src/rules/ksatisfiability_cyclicordering.rs +++ b/src/rules/ksatisfiability_cyclicordering.rs @@ -21,6 +21,8 @@ use crate::models::misc::CyclicOrdering; use crate::reduction; use crate::rules::sat_helpers::SatVariableAllocator; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::variant::K3; use std::collections::BTreeSet; @@ -39,10 +41,32 @@ impl ReductionResult for Reduction3SATToCyclicOrdering { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl Reduction3SATToCyclicOrdering { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { let mut assignment = vec![false; self.source_num_vars]; for (compact, &original) in self.source_variables.iter().enumerate() { let (alpha, beta, gamma) = variable_triple(compact); diff --git a/src/rules/ksatisfiability_decisionminimumvertexcover.rs b/src/rules/ksatisfiability_decisionminimumvertexcover.rs index 3bb329f47..d05c6f51b 100644 --- a/src/rules/ksatisfiability_decisionminimumvertexcover.rs +++ b/src/rules/ksatisfiability_decisionminimumvertexcover.rs @@ -1,23 +1,31 @@ -//! Reduction from KSatisfiability (3-SAT) to Decision Minimum Vertex Cover. +//! Reduction from KSatisfiability (3-SAT) to Decision. //! -//! This wraps the classical Garey & Johnson Theorem 3.3 construction in the -//! `Decision>` wrapper, with threshold -//! `k = n + 2m` for `n` variables and `m` clauses. +//! Classical Garey & Johnson reduction (Theorem 3.3). For each variable u_i, +//! add two vertices {u_i, not-u_i} connected by a truth-setting edge. For each +//! clause c_j, add 3 vertices forming a satisfaction-testing triangle. For each +//! literal l_k in clause c_j, add a communication edge from the triangle vertex +//! j_k to the literal vertex l_k. +//! +//! The resulting graph has a vertex cover of size n + 2m if and only if the +//! 3-SAT formula is satisfiable (n = num_vars, m = num_clauses). +//! +//! Reference: Garey & Johnson, "Computers and Intractability", 1979, Theorem 3.3 use crate::models::decision::Decision; use crate::models::formula::KSatisfiability; use crate::models::graph::MinimumVertexCover; use crate::reduction; -use crate::rules::ksatisfiability_minimumvertexcover::Reduction3SATToMVC; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::topology::SimpleGraph; use crate::variant::K3; -/// Result of reducing KSatisfiability to Decision>. +/// Result of reducing KSatisfiability to Decision. #[derive(Debug, Clone)] pub struct Reduction3SATToDecisionMVC { target: Decision>, - base_reduction: Reduction3SATToMVC, + source_num_vars: usize, } impl ReductionResult for Reduction3SATToDecisionMVC { @@ -28,11 +36,42 @@ impl ReductionResult for Reduction3SATToDecisionMVC { &self.target } - fn extract_solution( + /// Extract a SAT assignment from a vertex cover solution. + /// + /// Vertex layout: indices 0..2n are literal vertices (even = positive, + /// odd = negated). For variable i, vertex 2*i is u_i and vertex 2*i+1 + /// is not-u_i. A cover meeting the n + 2m bound contains exactly one of these two + /// for each variable. If u_i is in the cover, set x_i = 1; + /// if not-u_i is in the cover, set x_i = 0. + fn recover_result( + &self, + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl Reduction3SATToDecisionMVC { + fn map_solution( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { - self.base_reduction.extract_solution(target_solution) + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { + Ok((0..self.source_num_vars) + .map(|i| target_solution[2 * i]) + .collect()) } } @@ -46,13 +85,11 @@ impl ReduceTo>> for KSatisfiabilit type Result = Reduction3SATToDecisionMVC; fn reduce_to(&self) -> Result { - let base_reduction = as ReduceTo< - MinimumVertexCover, - >>::reduce_to(self)?; - let bound = self - .num_clauses() + let n = self.num_vars(); + let m = self.num_clauses(); + let target_bound = m .checked_mul(2) - .and_then(|value| value.checked_add(self.num_vars())) + .and_then(|value| value.checked_add(n)) .and_then(|value| i64::try_from(value).ok()) .ok_or_else(|| { crate::rules::ReductionError::integer_overflow::< @@ -60,11 +97,44 @@ impl ReduceTo>> for KSatisfiabilit Decision>, >("computing the target cover bound") })?; - let target = Decision::new(base_reduction.target_problem().clone(), bound); + let total_vertices = 2 * n + 3 * m; + let mut edges: Vec<(usize, usize)> = Vec::with_capacity(n + 6 * m); + + // Step 1: Truth-setting components. + // For each variable i, add edge (2*i, 2*i+1) connecting u_i and not-u_i. + for i in 0..n { + edges.push((2 * i, 2 * i + 1)); + } + + // Step 2: Satisfaction-testing components (triangles) and communication edges. + // For each clause j, triangle vertices are at indices 2*n + 3*j, 2*n + 3*j + 1, 2*n + 3*j + 2. + for (j, clause) in self.clauses().iter().enumerate() { + let base = 2 * n + 3 * j; + + // Triangle edges within clause j + edges.push((base, base + 1)); + edges.push((base + 1, base + 2)); + edges.push((base, base + 2)); + + // Communication edges: connect triangle vertex k to the literal vertex + for (k, &lit) in clause.literals.iter().enumerate() { + let var_idx = lit.unsigned_abs() as usize - 1; // 0-indexed variable + let literal_vertex = if lit > 0 { + 2 * var_idx // positive literal vertex + } else { + 2 * var_idx + 1 // negated literal vertex + }; + edges.push((base + k, literal_vertex)); + } + } + + let graph = SimpleGraph::new(total_vertices, edges); + let weights = vec![1i64; total_vertices]; + let target = MinimumVertexCover::new(graph, weights); Ok(Reduction3SATToDecisionMVC { - target, - base_reduction, + target: Decision::new(target, target_bound), + source_num_vars: n, }) } } @@ -90,7 +160,15 @@ pub(crate) fn canonical_rule_example_specs() -> Vec( source, SolutionPair { + // x1=0, x2=0, x3=1 satisfies both clauses source_config: serde_json::json!(vec![false, false, true]), + // Literal vertices: u1(0), ~u1(1), u2(2), ~u2(3), u3(4), ~u3(5) + // Clause 0 triangle: v6, v7, v8 (literals x1, x2, x3) + // Clause 1 triangle: v9, v10, v11 (literals ~x1, ~x2, x3) + // VC: from truth-setting, pick ~u1(1), ~u2(3), u3(4) + // Clause 0: u1,u2 not in cover -> pick v6,v7; u3 in cover -> v8 free + // Clause 1: ~u1,~u2,u3 all in cover -> pick any 2: v9,v10 + // Total cover size = 3 + 2 + 2 = 7 = n + 2m target_config: serde_json::json!(vec![ false, true, false, true, true, false, true, true, false, true, true, false ]), diff --git a/src/rules/ksatisfiability_directedtwocommodityintegralflow.rs b/src/rules/ksatisfiability_directedtwocommodityintegralflow.rs index 2f5b282ef..a91db59fb 100644 --- a/src/rules/ksatisfiability_directedtwocommodityintegralflow.rs +++ b/src/rules/ksatisfiability_directedtwocommodityintegralflow.rs @@ -11,6 +11,8 @@ use crate::models::formula::KSatisfiability; use crate::models::graph::DirectedTwoCommodityIntegralFlow; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::topology::DirectedGraph; use crate::variant::K3; @@ -171,10 +173,32 @@ impl ReductionResult for Reduction3SATToDirectedTwoCommodityIntegralFlow { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl Reduction3SATToDirectedTwoCommodityIntegralFlow { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok({ self.variable_paths .iter() diff --git a/src/rules/ksatisfiability_feasibleregisterassignment.rs b/src/rules/ksatisfiability_feasibleregisterassignment.rs index 5d9861aaf..e0e5f8c16 100644 --- a/src/rules/ksatisfiability_feasibleregisterassignment.rs +++ b/src/rules/ksatisfiability_feasibleregisterassignment.rs @@ -16,6 +16,8 @@ use crate::models::formula::KSatisfiability; use crate::models::misc::FeasibleRegisterAssignment; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::variant::K3; use std::collections::BTreeSet; @@ -74,10 +76,32 @@ impl ReductionResult for Reduction3SATToFeasibleRegisterAssignment { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl Reduction3SATToFeasibleRegisterAssignment { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { let mut assignment = vec![false; self.num_vars]; let compact_vars = self.source_variables.len(); for (compact, &original) in self.source_variables.iter().enumerate() { @@ -246,8 +270,22 @@ pub(crate) fn canonical_rule_example_specs() -> Vec::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl Reduction3SATToKClique { + pub(crate) fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { // Variables absent from the selected literals are free; choose false. let mut assignment = vec![false; self.source_num_vars]; for (&selected, &(variable, positive)) in target_solution[..self.literal_assignments.len()] diff --git a/src/rules/ksatisfiability_kernel.rs b/src/rules/ksatisfiability_kernel.rs index c79e71fce..2600b7395 100644 --- a/src/rules/ksatisfiability_kernel.rs +++ b/src/rules/ksatisfiability_kernel.rs @@ -9,6 +9,8 @@ use crate::models::formula::KSatisfiability; use crate::models::graph::Kernel; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::topology::DirectedGraph; use crate::variant::K3; use std::collections::BTreeSet; @@ -29,10 +31,32 @@ impl ReductionResult for Reduction3SatToKernel { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl Reduction3SatToKernel { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { let mut assignment = vec![false; self.source_num_vars]; for (compact, &original) in self.source_variables.iter().enumerate() { assignment[original] = target_solution[2 * compact]; diff --git a/src/rules/ksatisfiability_minimumvertexcover.rs b/src/rules/ksatisfiability_minimumvertexcover.rs deleted file mode 100644 index b3af90d00..000000000 --- a/src/rules/ksatisfiability_minimumvertexcover.rs +++ /dev/null @@ -1,179 +0,0 @@ -//! Reduction from KSatisfiability (3-SAT) to MinimumVertexCover. -//! -//! Classical Garey & Johnson reduction (Theorem 3.3). For each variable u_i, -//! add two vertices {u_i, not-u_i} connected by a truth-setting edge. For each -//! clause c_j, add 3 vertices forming a satisfaction-testing triangle. For each -//! literal l_k in clause c_j, add a communication edge from the triangle vertex -//! j_k to the literal vertex l_k. -//! -//! The resulting graph has a vertex cover of size n + 2m if and only if the -//! 3-SAT formula is satisfiable (n = num_vars, m = num_clauses). -//! -//! Reference: Garey & Johnson, "Computers and Intractability", 1979, Theorem 3.3 - -use crate::models::formula::KSatisfiability; -use crate::models::graph::MinimumVertexCover; -use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; -use crate::topology::SimpleGraph; -use crate::variant::K3; - -/// Result of reducing KSatisfiability to MinimumVertexCover. -#[derive(Debug, Clone)] -pub struct Reduction3SATToMVC { - target: MinimumVertexCover, - source_num_vars: usize, - target_bound: i64, -} - -impl ReductionResult for Reduction3SATToMVC { - type Source = KSatisfiability; - type Target = MinimumVertexCover; - - fn target_problem(&self) -> &Self::Target { - &self.target - } - - /// Extract a SAT assignment from a vertex cover solution. - /// - /// Vertex layout: indices 0..2n are literal vertices (even = positive, - /// odd = negated). For variable i, vertex 2*i is u_i and vertex 2*i+1 - /// is not-u_i. A cover meeting the n + 2m bound contains exactly one of these two - /// for each variable. If u_i is in the cover, set x_i = 1; - /// if not-u_i is in the cover, set x_i = 0. - fn extract_solution( - &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { - Ok({ - (0..self.source_num_vars) - .map(|i| { - // u_i is at index 2*i, not-u_i is at index 2*i+1 - target_solution[2 * i] - }) - .collect() - }) - } -} - -impl crate::rules::AggregateReductionResult for Reduction3SATToMVC { - type Source = KSatisfiability; - type Target = MinimumVertexCover; - - fn target_problem(&self) -> &Self::Target { - &self.target - } - - fn extract_value(&self, value: crate::types::Min) -> crate::types::Or { - crate::types::Or(value.0.is_some_and(|cost| cost <= self.target_bound)) - } -} - -#[reduction( - aggregate = custom, - transform = exact { - num_vertices = "2 * num_vars + 3 * num_clauses", - num_edges = "num_vars + 6 * num_clauses", - } -)] -impl ReduceTo> for KSatisfiability { - type Result = Reduction3SATToMVC; - - fn reduce_to(&self) -> Result { - let n = self.num_vars(); - let m = self.num_clauses(); - let target_bound = m - .checked_mul(2) - .and_then(|value| value.checked_add(n)) - .and_then(|value| i64::try_from(value).ok()) - .ok_or_else(|| { - crate::rules::ReductionError::integer_overflow::< - KSatisfiability, - MinimumVertexCover, - >("computing the target cover bound") - })?; - let total_vertices = 2 * n + 3 * m; - let mut edges: Vec<(usize, usize)> = Vec::with_capacity(n + 6 * m); - - // Step 1: Truth-setting components. - // For each variable i, add edge (2*i, 2*i+1) connecting u_i and not-u_i. - for i in 0..n { - edges.push((2 * i, 2 * i + 1)); - } - - // Step 2: Satisfaction-testing components (triangles) and communication edges. - // For each clause j, triangle vertices are at indices 2*n + 3*j, 2*n + 3*j + 1, 2*n + 3*j + 2. - for (j, clause) in self.clauses().iter().enumerate() { - let base = 2 * n + 3 * j; - - // Triangle edges within clause j - edges.push((base, base + 1)); - edges.push((base + 1, base + 2)); - edges.push((base, base + 2)); - - // Communication edges: connect triangle vertex k to the literal vertex - for (k, &lit) in clause.literals.iter().enumerate() { - let var_idx = lit.unsigned_abs() as usize - 1; // 0-indexed variable - let literal_vertex = if lit > 0 { - 2 * var_idx // positive literal vertex - } else { - 2 * var_idx + 1 // negated literal vertex - }; - edges.push((base + k, literal_vertex)); - } - } - - let graph = SimpleGraph::new(total_vertices, edges); - let weights = vec![1i64; total_vertices]; - let target = MinimumVertexCover::new(graph, weights); - - Ok(Reduction3SATToMVC { - target, - source_num_vars: n, - target_bound, - }) - } -} - -#[cfg(feature = "example-db")] -pub(crate) fn canonical_rule_example_specs() -> Vec { - use crate::export::SolutionPair; - use crate::models::formula::CNFClause; - - vec![crate::example_db::specs::RuleExampleSpec { - id: "ksatisfiability_to_minimumvertexcover", - build: || { - let source = KSatisfiability::::new( - 3, - vec![ - CNFClause::new(vec![1, 2, 3]), - CNFClause::new(vec![-1, -2, 3]), - ], - ); - crate::example_db::specs::rule_example_with_witness::< - _, - MinimumVertexCover, - >( - source, - SolutionPair { - // x1=0, x2=0, x3=1 satisfies both clauses - source_config: serde_json::json!(vec![false, false, true]), - // Literal vertices: u1(0), ~u1(1), u2(2), ~u2(3), u3(4), ~u3(5) - // Clause 0 triangle: v6, v7, v8 (literals x1, x2, x3) - // Clause 1 triangle: v9, v10, v11 (literals ~x1, ~x2, x3) - // VC: from truth-setting, pick ~u1(1), ~u2(3), u3(4) - // Clause 0: u1,u2 not in cover -> pick v6,v7; u3 in cover -> v8 free - // Clause 1: ~u1,~u2,u3 all in cover -> pick any 2: v9,v10 - // Total cover size = 3 + 2 + 2 = 7 = n + 2m - target_config: serde_json::json!(vec![ - false, true, false, true, true, false, true, true, false, true, true, false - ]), - }, - ) - }, - }] -} - -#[cfg(test)] -#[path = "../unit_tests/rules/ksatisfiability_minimumvertexcover.rs"] -mod tests; diff --git a/src/rules/ksatisfiability_monochromatictriangle.rs b/src/rules/ksatisfiability_monochromatictriangle.rs index 9ffe3f0e4..28e945dff 100644 --- a/src/rules/ksatisfiability_monochromatictriangle.rs +++ b/src/rules/ksatisfiability_monochromatictriangle.rs @@ -14,6 +14,8 @@ use crate::reduction; use crate::rules::sat_helpers::SatVariableAllocator; use crate::rules::satisfiability_naesatisfiability::ReductionSATToNAESAT; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::topology::SimpleGraph; use crate::variant::K3; @@ -51,16 +53,38 @@ impl ReductionResult for Reduction3SATToMonochromaticTriangle { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl Reduction3SATToMonochromaticTriangle { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { let nae_solution = (0..self.nae_reduction.target_problem().num_vars()) .map(|index| target_solution[2 * index]) .collect(); // Reuse the formal SAT -> NAE extraction (including sentinel // normalization); no assignment search or speculative complement. - self.nae_reduction.extract_solution(&nae_solution) + self.nae_reduction.map_solution(&nae_solution) } } @@ -176,7 +200,14 @@ pub(crate) fn canonical_rule_example_specs() -> Vec::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl Reduction3SATToOneInThreeSAT { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { let mut assignment = vec![false; self.source_num_vars]; for (compact, &original) in self.source_variables.iter().enumerate() { assignment[original] = target_solution[compact]; diff --git a/src/rules/ksatisfiability_preemptivescheduling.rs b/src/rules/ksatisfiability_preemptivescheduling.rs index dd378edad..1eb3156fd 100644 --- a/src/rules/ksatisfiability_preemptivescheduling.rs +++ b/src/rules/ksatisfiability_preemptivescheduling.rs @@ -13,6 +13,9 @@ use crate::models::formula::KSatisfiability; use crate::models::misc::PreemptiveScheduling; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; +use crate::traits::Problem; use crate::variant::K3; #[derive(Debug, Clone)] @@ -335,10 +338,48 @@ impl ReductionResult for Reduction3SATToPreemptiveScheduling { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + let evaluation = source.evaluate(&solution)?; + if evaluation.0 { + Ok(SolveOutcome::Optimal { + solution, + evaluation, + }) + } else { + Ok(SolveOutcome::Infeasible) + } + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + let evaluation = source.evaluate(&solution)?; + if evaluation.0 { + Ok(SolveOutcome::Feasible { + solution, + evaluation, + }) + } else { + Err(crate::rules::ExtractionError::InsufficientSolutionQuality) + } + } + } + } +} + +impl Reduction3SATToPreemptiveScheduling { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok({ let d_max = self.target.d_max(); self.positive_start_jobs diff --git a/src/rules/ksatisfiability_quadraticcongruences.rs b/src/rules/ksatisfiability_quadraticcongruences.rs index f1ac10d28..d393171f3 100644 --- a/src/rules/ksatisfiability_quadraticcongruences.rs +++ b/src/rules/ksatisfiability_quadraticcongruences.rs @@ -6,6 +6,8 @@ //! slack equation. Squaring uses twice the linear modulus, and extraction //! orients every sign by the distinguished odd coordinate. +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use std::collections::{BTreeMap, BTreeSet}; use crate::models::algebraic::QuadraticCongruences; @@ -36,10 +38,32 @@ impl ReductionResult for Reduction3SATToQuadraticCongruences { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl Reduction3SATToQuadraticCongruences { + pub(crate) fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { // Validation gives 0 < x <= H. Each prime power divides exactly one // of H-x and H+x. The coordinate zero sign chooses x or -x so that // the odd linear target, rather than its negative, is recovered. diff --git a/src/rules/ksatisfiability_quadraticdiophantineequations.rs b/src/rules/ksatisfiability_quadraticdiophantineequations.rs index f6e8b9cb8..29650df7c 100644 --- a/src/rules/ksatisfiability_quadraticdiophantineequations.rs +++ b/src/rules/ksatisfiability_quadraticdiophantineequations.rs @@ -9,6 +9,8 @@ use crate::models::formula::KSatisfiability; use crate::reduction; use crate::rules::ksatisfiability_quadraticcongruences::Reduction3SATToQuadraticCongruences; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::variant::K3; use num_bigint::BigUint; use num_traits::One; @@ -28,14 +30,33 @@ impl ReductionResult for Reduction3SATToQuadraticDiophantineEquations { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { - Ok({ - self.congruence_reduction - .extract_solution(target_solution)? - }) + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl Reduction3SATToQuadraticDiophantineEquations { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { + self.congruence_reduction.map_solution(target_solution) } } diff --git a/src/rules/ksatisfiability_qubo.rs b/src/rules/ksatisfiability_qubo.rs index 47adbae75..e2022805b 100644 --- a/src/rules/ksatisfiability_qubo.rs +++ b/src/rules/ksatisfiability_qubo.rs @@ -13,30 +13,54 @@ //! CNFClause uses 1-indexed signed integers: positive = variable, negative = negated. use crate::models::algebraic::QUBO; +use crate::models::decision::Decision; use crate::models::formula::KSatisfiability; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::variant::{K2, K3}; /// Result of reducing KSatisfiability to QUBO. #[derive(Debug, Clone)] pub struct ReductionKSatToQUBO { - target: QUBO, + target: Decision>, source_num_vars: usize, - zero_penalty_energy: i64, } impl ReductionResult for ReductionKSatToQUBO { type Source = KSatisfiability; - type Target = QUBO; + type Target = Decision>; fn target_problem(&self) -> &Self::Target { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionKSatToQUBO { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok(target_solution[..self.source_num_vars].to_vec()) } } @@ -44,23 +68,44 @@ impl ReductionResult for ReductionKSatToQUBO { /// Result of reducing `KSatisfiability` to QUBO. #[derive(Debug, Clone)] pub struct Reduction3SATToQUBO { - target: QUBO, + target: Decision>, source_num_vars: usize, - zero_penalty_energy: i64, } impl ReductionResult for Reduction3SATToQUBO { type Source = KSatisfiability; - type Target = QUBO; + type Target = Decision>; fn target_problem(&self) -> &Self::Target { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl Reduction3SATToQUBO { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok(target_solution[..self.source_num_vars].to_vec()) } } @@ -312,83 +357,60 @@ fn build_qubo_matrix( Ok((matrix, constant)) } -impl crate::rules::AggregateReductionResult for ReductionKSatToQUBO { - type Source = KSatisfiability; - type Target = QUBO; - fn target_problem(&self) -> &Self::Target { - &self.target - } - fn extract_value(&self, value: crate::types::Min) -> crate::types::Or { - crate::types::Or(value.0 == Some(self.zero_penalty_energy)) - } -} - -impl crate::rules::AggregateReductionResult for Reduction3SATToQUBO { - type Source = KSatisfiability; - type Target = QUBO; - fn target_problem(&self) -> &Self::Target { - &self.target - } - fn extract_value(&self, value: crate::types::Min) -> crate::types::Or { - crate::types::Or(value.0 == Some(self.zero_penalty_energy)) - } -} - #[reduction( - aggregate = custom, transform = exact { num_vars = "num_vars", } )] -impl ReduceTo> for KSatisfiability { +impl ReduceTo>> for KSatisfiability { type Result = ReductionKSatToQUBO; fn reduce_to(&self) -> Result { let n = self.num_vars(); - let (matrix, constant) = build_qubo_matrix(n, self.clauses(), 0).map_err(|operation| { - crate::rules::ReductionError::integer_overflow::, QUBO>( - operation, - ) - })?; + let (matrix, constant) = + build_qubo_matrix(n, self.clauses(), 0).map_err(|operation| { + crate::rules::ReductionError::integer_overflow::< + KSatisfiability, + Decision>, + >(operation) + })?; Ok(ReductionKSatToQUBO { - target: QUBO::from_rows(matrix).map_err(|message| { - crate::rules::ReductionError::construction::, QUBO>( - message, - ) - })?, + target: Decision::new( + QUBO::from_rows(matrix) + .map_err(>>>::target_construction)?, + -constant, + ), source_num_vars: n, - zero_penalty_energy: -constant, }) } } #[reduction( - aggregate = custom, transform = exact { num_vars = "num_vars + num_clauses", } )] -impl ReduceTo> for KSatisfiability { +impl ReduceTo>> for KSatisfiability { type Result = Reduction3SATToQUBO; fn reduce_to(&self) -> Result { let n = self.num_vars(); let (matrix, constant) = build_qubo_matrix(n, self.clauses(), self.num_clauses()).map_err(|operation| { - crate::rules::ReductionError::integer_overflow::, QUBO>( - operation, - ) + crate::rules::ReductionError::integer_overflow::< + KSatisfiability, + Decision>, + >(operation) })?; Ok(Reduction3SATToQUBO { - target: QUBO::from_rows(matrix).map_err(|message| { - crate::rules::ReductionError::construction::, QUBO>( - message, - ) - })?, + target: Decision::new( + QUBO::from_rows(matrix) + .map_err(>>>::target_construction)?, + -constant, + ), source_num_vars: n, - zero_penalty_energy: -constant, }) } } @@ -412,7 +434,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec>( + crate::example_db::specs::rule_example_with_witness::<_, Decision>>( source, SolutionPair { source_config: serde_json::json!(vec![false, true, false, true]), @@ -436,7 +458,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec>( + crate::example_db::specs::rule_example_with_witness::<_, Decision>>( source, SolutionPair { source_config: serde_json::json!(vec![false, false, false, false, false]), diff --git a/src/rules/ksatisfiability_registersufficiency.rs b/src/rules/ksatisfiability_registersufficiency.rs index 9bed847ed..7177a80b7 100644 --- a/src/rules/ksatisfiability_registersufficiency.rs +++ b/src/rules/ksatisfiability_registersufficiency.rs @@ -13,6 +13,8 @@ use crate::models::formula::KSatisfiability; use crate::models::misc::RegisterSufficiency; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::variant::K3; use std::collections::BTreeSet; @@ -292,10 +294,32 @@ impl ReductionResult for Reduction3SATToRegisterSufficiency { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl Reduction3SATToRegisterSufficiency { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { let mut assignment = vec![false; self.source_num_vars]; let Some(layout) = &self.layout else { // Only the empty-conjunction target has a feasible witness here. @@ -520,7 +544,15 @@ pub(crate) fn canonical_rule_example_specs() -> Vec::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl Reduction3SATToSimultaneousIncongruences { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok({ let x = *target_solution as u64; self.variable_primes diff --git a/src/rules/ksatisfiability_subsetsum.rs b/src/rules/ksatisfiability_subsetsum.rs index cb780b17a..3cb4cc35c 100644 --- a/src/rules/ksatisfiability_subsetsum.rs +++ b/src/rules/ksatisfiability_subsetsum.rs @@ -16,6 +16,8 @@ use crate::models::formula::KSatisfiability; use crate::models::misc::SubsetSum; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::variant::K3; use num_bigint::BigUint; use num_traits::Zero; @@ -35,10 +37,32 @@ impl ReductionResult for Reduction3SATToSubsetSum { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl Reduction3SATToSubsetSum { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok({ // Variable integers are the first 2n elements in 0-based indexing: // for variable i (0 <= i < n), y_i is stored at index 2*i and z_i at index 2*i + 1. diff --git a/src/rules/ksatisfiability_timetabledesign.rs b/src/rules/ksatisfiability_timetabledesign.rs index 30846529f..0f6620d30 100644 --- a/src/rules/ksatisfiability_timetabledesign.rs +++ b/src/rules/ksatisfiability_timetabledesign.rs @@ -25,6 +25,8 @@ use crate::models::misc::TimetableDesign; use crate::reduction; use crate::rules::sat_helpers::SatVariableAllocator; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::variant::K3; use std::collections::VecDeque; @@ -744,10 +746,32 @@ impl ReductionResult for Reduction3SATToTimetableDesign { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl Reduction3SATToTimetableDesign { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok({ let num_periods = self.target.num_periods(); diff --git a/src/rules/lengthboundeddisjointpaths_ilp.rs b/src/rules/lengthboundeddisjointpaths_ilp.rs index 404f32270..0fd1ac4de 100644 --- a/src/rules/lengthboundeddisjointpaths_ilp.rs +++ b/src/rules/lengthboundeddisjointpaths_ilp.rs @@ -7,6 +7,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::LengthBoundedDisjointPaths; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; use std::collections::VecDeque; @@ -36,10 +38,32 @@ impl ReductionResult for ReductionLBDPToILP { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionLBDPToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { let m = self.edges.len(); let flow_vars_per_k = 2 * m; let activation_offset = self.num_paths * flow_vars_per_k; diff --git a/src/rules/longestcircuit_ilp.rs b/src/rules/longestcircuit_ilp.rs index d21f3812a..82c470c0a 100644 --- a/src/rules/longestcircuit_ilp.rs +++ b/src/rules/longestcircuit_ilp.rs @@ -12,6 +12,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::LongestCircuit; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; /// Result of reducing LongestCircuit to ILP. @@ -36,10 +38,32 @@ impl ReductionResult for ReductionLongestCircuitToILP { } /// Extract: output the binary edge-selection vector (y_e). - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionLongestCircuitToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok(target_solution[..self.num_edges] .iter() .map(|&value| value == 1) diff --git a/src/rules/longestcommonsubsequence_ilp.rs b/src/rules/longestcommonsubsequence_ilp.rs index 362ecfdee..ea4895526 100644 --- a/src/rules/longestcommonsubsequence_ilp.rs +++ b/src/rules/longestcommonsubsequence_ilp.rs @@ -14,6 +14,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::LongestCommonSubsequence; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; /// Result of reducing LongestCommonSubsequence to ILP. #[derive(Debug, Clone)] @@ -31,10 +33,32 @@ impl ReductionResult for ReductionLCSToILP { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionLCSToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok(crate::rules::ilp_helpers::one_hot_decode_rows( target_solution, self.max_length, diff --git a/src/rules/longestcommonsubsequence_maximumindependentset.rs b/src/rules/longestcommonsubsequence_maximumindependentset.rs index 980fe3657..08190daa6 100644 --- a/src/rules/longestcommonsubsequence_maximumindependentset.rs +++ b/src/rules/longestcommonsubsequence_maximumindependentset.rs @@ -14,6 +14,8 @@ use crate::models::graph::MaximumIndependentSet; use crate::models::misc::LongestCommonSubsequence; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::topology::SimpleGraph; use crate::types::One; @@ -46,10 +48,32 @@ impl ReductionResult for ReductionLCSToIS { /// /// Selected vertices correspond to match nodes. Sort by position in /// the first string to get the subsequence order, then pad to `max_length`. - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionLCSToIS { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok({ // Collect selected match nodes with their characters let mut selected: Vec<(usize, usize)> = target_solution diff --git a/src/rules/longestpath_ilp.rs b/src/rules/longestpath_ilp.rs index 83e18cbe2..cf31f4f82 100644 --- a/src/rules/longestpath_ilp.rs +++ b/src/rules/longestpath_ilp.rs @@ -9,6 +9,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::LongestPath; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; #[derive(Debug, Clone)] @@ -31,10 +33,32 @@ impl ReductionResult for ReductionLongestPathToILP { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionLongestPathToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok({ (0..self.num_edges) .map(|edge_idx| { diff --git a/src/rules/maxcut_minimumcutintoboundedsets.rs b/src/rules/maxcut_minimumcutintoboundedsets.rs index 0e50c80aa..964f6a8e4 100644 --- a/src/rules/maxcut_minimumcutintoboundedsets.rs +++ b/src/rules/maxcut_minimumcutintoboundedsets.rs @@ -10,6 +10,8 @@ use crate::models::graph::{MaxCut, MinimumCutIntoBoundedSets}; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; /// Result of reducing MaxCut to MinimumCutIntoBoundedSets. @@ -30,10 +32,32 @@ impl ReductionResult for ReductionMaxCutToMinCutBounded { /// Extract the source solution from the target balanced bisection. /// Take only the first `original_n` vertex assignments. - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionMaxCutToMinCutBounded { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok(target_solution[..self.original_n].to_vec()) } } diff --git a/src/rules/maxcut_minimummatrixcover.rs b/src/rules/maxcut_minimummatrixcover.rs index aa817ecba..f354156ae 100644 --- a/src/rules/maxcut_minimummatrixcover.rs +++ b/src/rules/maxcut_minimummatrixcover.rs @@ -24,6 +24,8 @@ use crate::models::algebraic::MinimumMatrixCover; use crate::models::graph::MaxCut; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; /// Result of reducing MaxCut to MinimumMatrixCover. @@ -48,11 +50,18 @@ impl ReductionResult for ReductionMaxCutToMMC { /// vertex `i` in `S`. The complementary assignment is equally optimal /// because the quadratic form (and the cut) is invariant under /// `f -> -f`. - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { - Ok(target_solution.to_vec()) + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => Ok(SolveOutcome::optimal(source, solution)?), + SolveOutcome::Feasible { solution, .. } => { + Ok(SolveOutcome::feasible(source, solution)?) + } + } } } diff --git a/src/rules/maximalis_ilp.rs b/src/rules/maximalis_ilp.rs index 8496d67b0..935043426 100644 --- a/src/rules/maximalis_ilp.rs +++ b/src/rules/maximalis_ilp.rs @@ -7,6 +7,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::MaximalIS; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; #[derive(Debug, Clone)] @@ -22,10 +24,32 @@ impl ReductionResult for ReductionMxISToILP { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionMxISToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok(target_solution.iter().map(|&value| value == 1).collect()) } } diff --git a/src/rules/maximum2satisfiability_ilp.rs b/src/rules/maximum2satisfiability_ilp.rs index 76fbc49cc..2165cec4b 100644 --- a/src/rules/maximum2satisfiability_ilp.rs +++ b/src/rules/maximum2satisfiability_ilp.rs @@ -11,6 +11,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::formula::Maximum2Satisfiability; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; /// Result of reducing Maximum2Satisfiability to ILP. #[derive(Debug, Clone)] @@ -27,10 +29,32 @@ impl ReductionResult for ReductionMaximum2SatisfiabilityToILP { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionMaximum2SatisfiabilityToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok(target_solution[..self.num_vars] .iter() .map(|&value| value == 1) diff --git a/src/rules/maximum2satisfiability_maxcut.rs b/src/rules/maximum2satisfiability_maxcut.rs index 59d3c047d..1d7b4597c 100644 --- a/src/rules/maximum2satisfiability_maxcut.rs +++ b/src/rules/maximum2satisfiability_maxcut.rs @@ -15,6 +15,8 @@ use crate::models::formula::Maximum2Satisfiability; use crate::models::graph::MaxCut; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::topology::SimpleGraph; use std::collections::BTreeMap; @@ -33,10 +35,32 @@ impl ReductionResult for ReductionMaximum2SatisfiabilityToMaxCut { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionMaximum2SatisfiabilityToMaxCut { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok({ let reference_side = target_solution[0]; (0..self.source_num_vars) diff --git a/src/rules/maximumclique_ilp.rs b/src/rules/maximumclique_ilp.rs index 60f2f9af3..0cbd42b38 100644 --- a/src/rules/maximumclique_ilp.rs +++ b/src/rules/maximumclique_ilp.rs @@ -10,6 +10,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::MaximumClique; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; /// Result of reducing MaximumClique to ILP. @@ -35,10 +37,32 @@ impl ReductionResult for ReductionCliqueToILP { /// /// Since the mapping is 1:1 (each vertex maps to one binary variable), /// the solution extraction is simply copying the configuration. - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionCliqueToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok(target_solution.iter().map(|&value| value == 1).collect()) } } diff --git a/src/rules/maximumclique_maximumindependentset.rs b/src/rules/maximumclique_maximumindependentset.rs index 63c8534e1..43c1ab1d1 100644 --- a/src/rules/maximumclique_maximumindependentset.rs +++ b/src/rules/maximumclique_maximumindependentset.rs @@ -6,6 +6,8 @@ use crate::models::graph::{MaximumClique, MaximumIndependentSet}; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; use crate::types::{One, WeightElement}; @@ -28,11 +30,18 @@ where /// Solution extraction: identity mapping. /// A clique in G is an independent set in the complement, so the configuration is the same. - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { - Ok(target_solution.to_vec()) + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => Ok(SolveOutcome::optimal(source, solution)?), + SolveOutcome::Feasible { solution, .. } => { + Ok(SolveOutcome::feasible(source, solution)?) + } + } } } diff --git a/src/rules/maximumcokplex_ilp.rs b/src/rules/maximumcokplex_ilp.rs index 5c8521c92..a9c7af796 100644 --- a/src/rules/maximumcokplex_ilp.rs +++ b/src/rules/maximumcokplex_ilp.rs @@ -9,6 +9,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::MaximumCoKPlex; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; use crate::types::{One, WeightElement}; use crate::variant::{VariantParam, KN}; @@ -31,10 +33,35 @@ where &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionCoKPlexToILP +where + W: WeightElement + VariantParam, +{ + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok(target_solution.iter().map(|&value| value == 1).collect()) } } diff --git a/src/rules/maximumcommonedgesubgraph_ilp.rs b/src/rules/maximumcommonedgesubgraph_ilp.rs index d01b63d53..04ac7e19f 100644 --- a/src/rules/maximumcommonedgesubgraph_ilp.rs +++ b/src/rules/maximumcommonedgesubgraph_ilp.rs @@ -17,6 +17,8 @@ use crate::models::graph::MaximumCommonEdgeSubgraph; use crate::reduction; use crate::rules::ilp_helpers::mccormick_product; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; /// Result of reducing MaximumCommonEdgeSubgraph to ILP. /// @@ -43,10 +45,32 @@ impl ReductionResult for ReductionMCESToILP { /// Extract: for each source vertex `u`, output the unique target vertex /// `p` with `x_(u,p) = 1`, or the sentinel `n2` ("bottom") when no /// mapping variable is selected. - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionMCESToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { let n2 = self.num_vertices_2; Ok((0..self.num_vertices_1) .map(|vertex| { diff --git a/src/rules/maximumcontactmapoverlap_ilp.rs b/src/rules/maximumcontactmapoverlap_ilp.rs index 150309bfa..a56ddf299 100644 --- a/src/rules/maximumcontactmapoverlap_ilp.rs +++ b/src/rules/maximumcontactmapoverlap_ilp.rs @@ -18,6 +18,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::MaximumContactMapOverlap; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; /// Result of reducing MaximumContactMapOverlap to ILP. /// @@ -46,10 +48,32 @@ impl ReductionResult for ReductionCMOToILP { /// For each source residue `i in V_1`, find the unique `j` with /// `x_(i,j) = 1` and encode it as `j + 1` (CMO's `bot` is `0`); if no /// `x_(i,*)` is selected, the residue is left unmatched (`0`). - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionCMOToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { let n2 = self.num_vertices_2; Ok((0..self.num_vertices_1) .map(|residue| { diff --git a/src/rules/maximumdomaticnumber_ilp.rs b/src/rules/maximumdomaticnumber_ilp.rs index a4e092a3a..b84960e24 100644 --- a/src/rules/maximumdomaticnumber_ilp.rs +++ b/src/rules/maximumdomaticnumber_ilp.rs @@ -12,6 +12,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::MaximumDomaticNumber; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; /// Result of reducing MaximumDomaticNumber to ILP. @@ -36,10 +38,32 @@ impl ReductionResult for ReductionDomaticNumberToILP { /// Extract solution from ILP back to MaximumDomaticNumber. /// /// For each vertex v, find the set index i where x_{v,i} = 1. - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionDomaticNumberToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok({ let n = self.n; let mut config = vec![0; n]; @@ -96,7 +120,7 @@ impl ReduceTo> for MaximumDomaticNumber { // Linking constraints: x_{v,i} <= y_i for each v, i // Forces y_i = 1 whenever any vertex is assigned to set i, - // ensuring extract_solution always yields a valid partition. + // ensuring recovery always yields a valid partition. for v in 0..n { for i in 0..n { constraints.push(LinearConstraint::le( diff --git a/src/rules/maximumedgeweightedkclique_ilp.rs b/src/rules/maximumedgeweightedkclique_ilp.rs index 64389a0a9..1bfc5b9e8 100644 --- a/src/rules/maximumedgeweightedkclique_ilp.rs +++ b/src/rules/maximumedgeweightedkclique_ilp.rs @@ -27,6 +27,8 @@ use crate::models::graph::MaximumEdgeWeightedKClique; use crate::reduction; use crate::rules::ilp_helpers::mccormick_product; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::topology::Graph; use crate::variant::VariantParam; @@ -58,10 +60,35 @@ where /// Extract: take the first `num_vertices` entries of the ILP solution. /// They are exactly the binary `x_v` selection variables. - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionMaximumEdgeWeightedKCliqueToILP +where + W: ILPCoefficient + VariantParam, +{ + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok(target_solution[..self.num_vertices] .iter() .map(|&value| value == 1) diff --git a/src/rules/maximumindependentset_casts.rs b/src/rules/maximumindependentset_casts.rs index ff7d07906..6f84b7b0f 100644 --- a/src/rules/maximumindependentset_casts.rs +++ b/src/rules/maximumindependentset_casts.rs @@ -11,7 +11,7 @@ impl_variant_reduction!( MaximumIndependentSet, => , fields: [num_vertices, num_edges], - aggregate: identity, + |src| MaximumIndependentSet::new( src.graph().try_to_unit_disk_graph().map_err( crate::rules::ReductionError::construction::< @@ -26,7 +26,7 @@ impl_variant_reduction!( MaximumIndependentSet, => , fields: [num_vertices, num_edges], - aggregate: identity, + |src| MaximumIndependentSet::new( src.graph().try_to_unit_disk_graph().map_err( crate::rules::ReductionError::construction::< @@ -41,7 +41,7 @@ impl_variant_reduction!( MaximumIndependentSet, => , fields: [num_vertices, num_edges], - aggregate: identity, + |src| MaximumIndependentSet::new( SimpleGraph::new(src.num_vertices(), Graph::edges(src.graph())), src.weights().to_vec()) @@ -52,7 +52,7 @@ impl_variant_reduction!( MaximumIndependentSet, => , fields: [num_vertices, num_edges], - aggregate: identity, + |src| MaximumIndependentSet::new( src.graph().try_to_unit_disk_graph().map_err( crate::rules::ReductionError::construction::< @@ -67,7 +67,7 @@ impl_variant_reduction!( MaximumIndependentSet, => , fields: [num_vertices, num_edges], - aggregate: identity, + |src| MaximumIndependentSet::new( SimpleGraph::new(src.num_vertices(), Graph::edges(src.graph())), src.weights().to_vec()) @@ -78,7 +78,7 @@ impl_variant_reduction!( MaximumIndependentSet, => , fields: [num_vertices, num_edges], - aggregate: identity, + |src| MaximumIndependentSet::new( src.graph().clone(), vec![1_i64; src.num_vertices()]) ); @@ -120,7 +120,7 @@ impl_variant_reduction!( MaximumIndependentSet, => , fields: [num_vertices, num_edges], - aggregate: identity, + |src| MaximumIndependentSet::new( src.graph().clone(), vec![1_i64; src.num_vertices()]) ); @@ -129,7 +129,7 @@ impl_variant_reduction!( MaximumIndependentSet, => , fields: [num_vertices, num_edges], - aggregate: identity, + |src| MaximumIndependentSet::new( src.graph().clone(), vec![1_i64; src.num_vertices()]) ); diff --git a/src/rules/maximumindependentset_gridgraph.rs b/src/rules/maximumindependentset_gridgraph.rs index d09644424..ec75d4497 100644 --- a/src/rules/maximumindependentset_gridgraph.rs +++ b/src/rules/maximumindependentset_gridgraph.rs @@ -7,6 +7,8 @@ use crate::models::graph::MaximumIndependentSet; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; use crate::rules::unitdiskmapping::ksg; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::topology::{Graph, KingsSubgraph, SimpleGraph}; use crate::types::One; @@ -25,10 +27,31 @@ impl ReductionResult for ReductionISSimpleOneToGridOne { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { .. } => { + Err(crate::rules::ExtractionError::InsufficientSolutionQuality) + } + } + } +} + +impl ReductionISSimpleOneToGridOne { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { let encoded = crate::config::bits_to_config(target_solution); let mapped = self.mapping_result.map_config_back(&encoded)?; Ok(crate::config::config_to_bits(&mapped)) diff --git a/src/rules/maximumindependentset_maximumclique.rs b/src/rules/maximumindependentset_maximumclique.rs index d8d21dda3..c0327321d 100644 --- a/src/rules/maximumindependentset_maximumclique.rs +++ b/src/rules/maximumindependentset_maximumclique.rs @@ -6,6 +6,8 @@ use crate::models::graph::{MaximumClique, MaximumIndependentSet}; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; use crate::types::{One, WeightElement}; @@ -28,11 +30,18 @@ where /// Solution extraction: identity mapping. /// A vertex selected in the clique (target) is also selected in the independent set (source). - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { - Ok(target_solution.to_vec()) + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => Ok(SolveOutcome::optimal(source, solution)?), + SolveOutcome::Feasible { solution, .. } => { + Ok(SolveOutcome::feasible(source, solution)?) + } + } } } diff --git a/src/rules/maximumindependentset_maximumsetpacking.rs b/src/rules/maximumindependentset_maximumsetpacking.rs index 1e657ce60..2de124b64 100644 --- a/src/rules/maximumindependentset_maximumsetpacking.rs +++ b/src/rules/maximumindependentset_maximumsetpacking.rs @@ -7,6 +7,8 @@ use crate::models::graph::MaximumIndependentSet; use crate::models::set::MaximumSetPacking; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; use crate::types::{One, WeightElement}; use std::collections::HashSet; @@ -29,11 +31,18 @@ where } /// Solutions map directly: vertex selection = set selection. - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { - Ok(target_solution.to_vec()) + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => Ok(SolveOutcome::optimal(source, solution)?), + SolveOutcome::Feasible { solution, .. } => { + Ok(SolveOutcome::feasible(source, solution)?) + } + } } } @@ -89,11 +98,18 @@ where } /// Solutions map directly. - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { - Ok(target_solution.to_vec()) + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => Ok(SolveOutcome::optimal(source, solution)?), + SolveOutcome::Feasible { solution, .. } => { + Ok(SolveOutcome::feasible(source, solution)?) + } + } } } diff --git a/src/rules/maximumindependentset_triangular.rs b/src/rules/maximumindependentset_triangular.rs index 2dec63e97..4f234a01f 100644 --- a/src/rules/maximumindependentset_triangular.rs +++ b/src/rules/maximumindependentset_triangular.rs @@ -9,6 +9,8 @@ use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; use crate::rules::unitdiskmapping::ksg; use crate::rules::unitdiskmapping::triangular; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph, TriangularSubgraph}; use crate::types::One; @@ -27,10 +29,31 @@ impl ReductionResult for ReductionISSimpleToTriangular { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { .. } => { + Err(crate::rules::ExtractionError::InsufficientSolutionQuality) + } + } + } +} + +impl ReductionISSimpleToTriangular { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { let encoded = crate::config::bits_to_config(target_solution); let mapped = triangular::map_config_back(&self.mapping_result, &encoded)?; Ok(crate::config::config_to_bits(&mapped)) diff --git a/src/rules/maximumleafspanningtree_ilp.rs b/src/rules/maximumleafspanningtree_ilp.rs index 21502bd13..69253298a 100644 --- a/src/rules/maximumleafspanningtree_ilp.rs +++ b/src/rules/maximumleafspanningtree_ilp.rs @@ -22,6 +22,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::MaximumLeafSpanningTree; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; /// Result of reducing MaximumLeafSpanningTree to ILP. @@ -39,10 +41,32 @@ impl ReductionResult for ReductionMaximumLeafSpanningTreeToILP { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionMaximumLeafSpanningTreeToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok({ // First m variables are edge selectors target_solution[..self.num_edges] diff --git a/src/rules/maximumlikelihoodranking_ilp.rs b/src/rules/maximumlikelihoodranking_ilp.rs index ffa7518e9..a1fdc2957 100644 --- a/src/rules/maximumlikelihoodranking_ilp.rs +++ b/src/rules/maximumlikelihoodranking_ilp.rs @@ -14,6 +14,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::MaximumLikelihoodRanking; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; /// Result of reducing MaximumLikelihoodRanking to ILP. #[derive(Debug, Clone)] @@ -39,10 +41,32 @@ impl ReductionResult for ReductionMaximumLikelihoodRankingToILP { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionMaximumLikelihoodRankingToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok({ let n = self.n; if n == 0 { diff --git a/src/rules/maximummatching_ilp.rs b/src/rules/maximummatching_ilp.rs index 7a5b6af52..5f613c00c 100644 --- a/src/rules/maximummatching_ilp.rs +++ b/src/rules/maximummatching_ilp.rs @@ -10,6 +10,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::MaximumMatching; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; /// Result of reducing MaximumMatching to ILP. @@ -35,10 +37,32 @@ impl ReductionResult for ReductionMatchingToILP { /// /// Since the mapping is 1:1 (each edge maps to one binary variable), /// the solution extraction is simply copying the configuration. - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionMatchingToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok(target_solution.iter().map(|&value| value == 1).collect()) } } diff --git a/src/rules/maximummatching_maximumsetpacking.rs b/src/rules/maximummatching_maximumsetpacking.rs index 6eb6c97a4..34c37b5e3 100644 --- a/src/rules/maximummatching_maximumsetpacking.rs +++ b/src/rules/maximummatching_maximumsetpacking.rs @@ -7,6 +7,8 @@ use crate::models::graph::MaximumMatching; use crate::models::set::MaximumSetPacking; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; use crate::types::WeightElement; @@ -30,11 +32,18 @@ where } /// Solutions map directly: edge i in MaximumMatching = set i in MaximumSetPacking. - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { - Ok(target_solution.to_vec()) + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => Ok(SolveOutcome::optimal(source, solution)?), + SolveOutcome::Feasible { solution, .. } => { + Ok(SolveOutcome::feasible(source, solution)?) + } + } } } diff --git a/src/rules/maximumsetpacking_casts.rs b/src/rules/maximumsetpacking_casts.rs index 04314c4df..461ce38b0 100644 --- a/src/rules/maximumsetpacking_casts.rs +++ b/src/rules/maximumsetpacking_casts.rs @@ -9,7 +9,7 @@ impl_variant_reduction!( MaximumSetPacking, => , fields: [num_sets, universe_size], - aggregate: identity, + |src| MaximumSetPacking::with_weights( src.sets().to_vec(), vec![1_i64; src.num_sets()]) diff --git a/src/rules/maximumsetpacking_ilp.rs b/src/rules/maximumsetpacking_ilp.rs index d600aef91..0ccf2813f 100644 --- a/src/rules/maximumsetpacking_ilp.rs +++ b/src/rules/maximumsetpacking_ilp.rs @@ -9,6 +9,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::set::MaximumSetPacking; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; /// Result of reducing MaximumSetPacking to ILP. /// @@ -29,10 +31,32 @@ impl ReductionResult for ReductionSPToILP { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionSPToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok(target_solution.iter().map(|&value| value == 1).collect()) } } diff --git a/src/rules/maximumsetpacking_qubo.rs b/src/rules/maximumsetpacking_qubo.rs index 24028e8fd..72706f98a 100644 --- a/src/rules/maximumsetpacking_qubo.rs +++ b/src/rules/maximumsetpacking_qubo.rs @@ -15,6 +15,8 @@ use crate::models::algebraic::QUBO; use crate::models::set::MaximumSetPacking; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; /// Result of reducing `MaximumSetPacking` to `QUBO`. #[derive(Debug, Clone)] @@ -30,11 +32,18 @@ impl ReductionResult for ReductionSPToQUBO { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { - Ok(target_solution.to_vec()) + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => Ok(SolveOutcome::optimal(source, solution)?), + SolveOutcome::Feasible { .. } => { + Err(crate::rules::ExtractionError::InsufficientSolutionQuality) + } + } } } diff --git a/src/rules/minimumcapacitatedspanningtree_ilp.rs b/src/rules/minimumcapacitatedspanningtree_ilp.rs index cbc01dd4a..5bc77d66c 100644 --- a/src/rules/minimumcapacitatedspanningtree_ilp.rs +++ b/src/rules/minimumcapacitatedspanningtree_ilp.rs @@ -28,6 +28,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::MinimumCapacitatedSpanningTree; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; use crate::types::WeightElement; @@ -46,10 +48,32 @@ impl ReductionResult for ReductionMinimumCapacitatedSpanningTreeToILP { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionMinimumCapacitatedSpanningTreeToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok({ // First m variables are edge selectors target_solution[..self.num_edges] diff --git a/src/rules/minimumcostmaximumflow_minimumcostcirculation.rs b/src/rules/minimumcostmaximumflow_minimumcostcirculation.rs index 4c04d4e76..44a987531 100644 --- a/src/rules/minimumcostmaximumflow_minimumcostcirculation.rs +++ b/src/rules/minimumcostmaximumflow_minimumcostcirculation.rs @@ -18,6 +18,8 @@ use crate::models::graph::{MinimumCostCirculation, MinimumCostMaximumFlow}; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::topology::DirectedGraph; /// Result of reducing MinimumCostMaximumFlow to MinimumCostCirculation. @@ -43,10 +45,32 @@ impl ReductionResult for ReductionMCMFToMCC { /// Extract the source flow by discarding the return arc: the first /// `num_original_arcs` entries of the circulation are exactly the /// flow values on the original arcs. - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionMCMFToMCC { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok(target_solution[..self.num_original_arcs].to_vec()) } } diff --git a/src/rules/minimumcoveringbycliques_ilp.rs b/src/rules/minimumcoveringbycliques_ilp.rs index 7fe76bc6d..0edaa3019 100644 --- a/src/rules/minimumcoveringbycliques_ilp.rs +++ b/src/rules/minimumcoveringbycliques_ilp.rs @@ -22,6 +22,8 @@ use crate::models::graph::MinimumCoveringByCliques; use crate::reduction; use crate::rules::ilp_helpers::mccormick_product; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; #[derive(Debug, Clone)] @@ -39,10 +41,32 @@ impl ReductionResult for ReductionMinimumCoveringByCliquesToILP { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionMinimumCoveringByCliquesToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok((0..self.num_edges) .flat_map(|edge| { (0..self.num_edges) diff --git a/src/rules/minimumcoveringbycliques_minimumintersectiongraphbasis.rs b/src/rules/minimumcoveringbycliques_minimumintersectiongraphbasis.rs index fe6a20594..6e79165de 100644 --- a/src/rules/minimumcoveringbycliques_minimumintersectiongraphbasis.rs +++ b/src/rules/minimumcoveringbycliques_minimumintersectiongraphbasis.rs @@ -7,6 +7,8 @@ use crate::models::graph::{MinimumCoveringByCliques, MinimumIntersectionGraphBasis}; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; use std::collections::BTreeMap; @@ -64,10 +66,32 @@ impl ReductionResult for ReductionMinimumCoveringByCliquesToMinimumIntersectionG &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionMinimumCoveringByCliquesToMinimumIntersectionGraphBasis { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok(extract_edge_clique_cover( self.target.graph(), target_solution, diff --git a/src/rules/minimumcutintoboundedsets_ilp.rs b/src/rules/minimumcutintoboundedsets_ilp.rs index 3a52a6520..fe8c59536 100644 --- a/src/rules/minimumcutintoboundedsets_ilp.rs +++ b/src/rules/minimumcutintoboundedsets_ilp.rs @@ -10,6 +10,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::MinimumCutIntoBoundedSets; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; #[derive(Debug, Clone)] @@ -26,10 +28,32 @@ impl ReductionResult for ReductionMinCutBSToILP { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionMinCutBSToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok(target_solution[..self.num_vertices] .iter() .map(|&value| value == 1) diff --git a/src/rules/minimumdiscreteplanarinversekinematics_qubo.rs b/src/rules/minimumdiscreteplanarinversekinematics_qubo.rs index 198214b0d..a5edce462 100644 --- a/src/rules/minimumdiscreteplanarinversekinematics_qubo.rs +++ b/src/rules/minimumdiscreteplanarinversekinematics_qubo.rs @@ -12,6 +12,8 @@ use crate::models::algebraic::QUBO; use crate::models::misc::MinimumDiscretePlanarInverseKinematics; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; fn block_offsets(block_sizes: &[usize]) -> Vec { let mut offsets = Vec::with_capacity(block_sizes.len()); @@ -43,10 +45,44 @@ impl ReductionResult for ReductionMinimumDiscretePlanarInverseKinematicsToQUBO { /// Decode a qualifying optimum after the energy relation establishes source /// feasibility. Such an optimum is one-hot and obeys every allowed pair. - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { + solution, + evaluation, + } => { + if !self.map_value(evaluation).is_valid() { + return Ok(SolveOutcome::Infeasible); + } + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { + solution, + evaluation, + } => { + if !self.map_value(evaluation).is_valid() { + return Err(crate::rules::ExtractionError::InsufficientSolutionQuality); + } + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionMinimumDiscretePlanarInverseKinematicsToQUBO { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok(self .block_offsets .iter() @@ -61,17 +97,8 @@ impl ReductionResult for ReductionMinimumDiscretePlanarInverseKinematicsToQUBO { } } -impl crate::rules::AggregateReductionResult - for ReductionMinimumDiscretePlanarInverseKinematicsToQUBO -{ - type Source = MinimumDiscretePlanarInverseKinematics; - type Target = QUBO; - - fn target_problem(&self) -> &Self::Target { - &self.target - } - - fn extract_value(&self, value: crate::types::Min) -> crate::types::Min { +impl ReductionMinimumDiscretePlanarInverseKinematicsToQUBO { + fn map_value(&self, value: crate::types::Min) -> crate::types::Min { crate::types::Min( value .0 @@ -81,7 +108,7 @@ impl crate::rules::AggregateReductionResult } } -#[reduction(aggregate = custom, transform = exact { +#[reduction(transform = exact { num_vars = "num_orientation_samples", })] impl ReduceTo> for MinimumDiscretePlanarInverseKinematics { diff --git a/src/rules/minimumdominatingset_ilp.rs b/src/rules/minimumdominatingset_ilp.rs index 49b9c517a..a43157ce1 100644 --- a/src/rules/minimumdominatingset_ilp.rs +++ b/src/rules/minimumdominatingset_ilp.rs @@ -10,6 +10,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::MinimumDominatingSet; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; /// Result of reducing MinimumDominatingSet to ILP. @@ -36,10 +38,32 @@ impl ReductionResult for ReductionDSToILP { /// /// Since the mapping is 1:1 (each vertex maps to one binary variable), /// the solution extraction is simply copying the configuration. - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionDSToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok(target_solution.iter().map(|&value| value == 1).collect()) } } diff --git a/src/rules/minimumedgecostflow_ilp.rs b/src/rules/minimumedgecostflow_ilp.rs index 4b55f0b60..f2f80b945 100644 --- a/src/rules/minimumedgecostflow_ilp.rs +++ b/src/rules/minimumedgecostflow_ilp.rs @@ -22,6 +22,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::MinimumEdgeCostFlow; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; /// Result of reducing MinimumEdgeCostFlow to `ILP`. /// @@ -43,10 +45,32 @@ impl ReductionResult for ReductionMECFToILP { } /// Extract flow solution: first m variables are the flow values. - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionMECFToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { crate::rules::ilp_helpers::decode_usize_values(&target_solution[..self.num_edges]) } } diff --git a/src/rules/minimumexternalmacrodatacompression_ilp.rs b/src/rules/minimumexternalmacrodatacompression_ilp.rs index 19b7e911c..aee0f9685 100644 --- a/src/rules/minimumexternalmacrodatacompression_ilp.rs +++ b/src/rules/minimumexternalmacrodatacompression_ilp.rs @@ -17,6 +17,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::MinimumExternalMacroDataCompression; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; /// Index layout for ILP variables. #[derive(Debug, Clone)] @@ -107,7 +109,7 @@ pub struct ReductionEMDCToILP { target: ILP, /// Variable layout for solution extraction. layout: VarLayout, - /// The source string (needed for extract_solution). + /// The source string (needed for solution recovery). source_string: Vec, /// Alphabet size. alphabet_size: usize, @@ -121,10 +123,32 @@ impl ReductionResult for ReductionEMDCToILP { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionEMDCToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok({ let n = self.layout.n; let k = self.alphabet_size; @@ -327,7 +351,7 @@ impl ReduceTo> for MinimumExternalMacroDataCompression { // 6. Literal matching: lit[i] can only be active if position i exists // (this is always true for i < n, so no constraint needed). // But we do need: if lit[i] = 1, the literal is s[i], which is automatic - // in the extract_solution. No additional constraint needed because the + // in solution recovery. No additional constraint needed because the // objective already penalizes literals. // Objective: minimize sum d_used[j] + sum lit[i] + h * sum ptr[i][l][d_start] @@ -378,7 +402,14 @@ pub(crate) fn canonical_rule_example_specs() -> Vec`. @@ -27,10 +29,32 @@ impl ReductionResult for ReductionMFDTSToILP { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionMFDTSToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok((0..self.num_inputs) .map(|input| { (0..self.num_outputs) diff --git a/src/rules/minimumfeedbackarcset_ilp.rs b/src/rules/minimumfeedbackarcset_ilp.rs index 6b6d1d932..c6449d9e5 100644 --- a/src/rules/minimumfeedbackarcset_ilp.rs +++ b/src/rules/minimumfeedbackarcset_ilp.rs @@ -13,6 +13,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::MinimumFeedbackArcSet; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; /// Result of reducing MinimumFeedbackArcSet to ILP. /// @@ -41,10 +43,32 @@ impl ReductionResult for ReductionFASToILP { /// /// The first m variables of the ILP solution are the binary y_a values, /// which directly correspond to the FAS configuration (1 = removed). - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionFASToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok(target_solution[..self.num_arcs] .iter() .map(|&value| value == 1) diff --git a/src/rules/minimumfeedbackvertexset_ilp.rs b/src/rules/minimumfeedbackvertexset_ilp.rs index 8c07f9793..be58bb3d2 100644 --- a/src/rules/minimumfeedbackvertexset_ilp.rs +++ b/src/rules/minimumfeedbackvertexset_ilp.rs @@ -10,6 +10,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::MinimumFeedbackVertexSet; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; /// Result of reducing MinimumFeedbackVertexSet to ILP. /// @@ -38,10 +40,32 @@ impl ReductionResult for ReductionMFVSToILP { /// /// The first n variables of the ILP solution are the binary x_i values, /// which directly correspond to the FVS configuration (1 = removed). - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionMFVSToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok(target_solution[..self.num_vertices] .iter() .map(|&value| value == 1) diff --git a/src/rules/minimumfeedbackvertexset_minimumcodegenerationunlimitedregisters.rs b/src/rules/minimumfeedbackvertexset_minimumcodegenerationunlimitedregisters.rs index 8d88975e0..cdf595399 100644 --- a/src/rules/minimumfeedbackvertexset_minimumcodegenerationunlimitedregisters.rs +++ b/src/rules/minimumfeedbackvertexset_minimumcodegenerationunlimitedregisters.rs @@ -10,6 +10,8 @@ use crate::models::graph::MinimumFeedbackVertexSet; use crate::models::misc::MinimumCodeGenerationUnlimitedRegisters; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::types::One; /// Result of the unit-weight FVS to code-generation reduction. @@ -30,10 +32,32 @@ impl ReductionResult for ReductionFVSToCodeGen { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionFVSToCodeGen { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok(self .chain_start .iter() @@ -131,7 +155,14 @@ pub(crate) fn canonical_rule_example_specs() -> Vec::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionMGBToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok(crate::rules::ilp_helpers::one_hot_decode_rows( target_solution, self.num_vertices, diff --git a/src/rules/minimumhittingset_ilp.rs b/src/rules/minimumhittingset_ilp.rs index 03bc54555..1856ee278 100644 --- a/src/rules/minimumhittingset_ilp.rs +++ b/src/rules/minimumhittingset_ilp.rs @@ -7,6 +7,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::set::MinimumHittingSet; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; #[derive(Debug, Clone)] pub struct ReductionHSToILP { @@ -21,10 +23,32 @@ impl ReductionResult for ReductionHSToILP { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionHSToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok(target_solution.iter().map(|&value| value == 1).collect()) } } diff --git a/src/rules/minimuminternalmacrodatacompression_ilp.rs b/src/rules/minimuminternalmacrodatacompression_ilp.rs index 6008e3f77..89dc5af41 100644 --- a/src/rules/minimuminternalmacrodatacompression_ilp.rs +++ b/src/rules/minimuminternalmacrodatacompression_ilp.rs @@ -19,6 +19,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::MinimumInternalMacroDataCompression; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; /// Index layout for ILP variables. #[derive(Debug, Clone)] @@ -95,10 +97,32 @@ impl ReductionResult for ReductionIMDCToILP { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionIMDCToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok({ let n = self.layout.n; let k = self.alphabet_size; @@ -298,7 +322,14 @@ pub(crate) fn canonical_rule_example_specs() -> Vec>( source, diff --git a/src/rules/minimummatrixcover_ilp.rs b/src/rules/minimummatrixcover_ilp.rs index c39aadd19..2d3d4874b 100644 --- a/src/rules/minimummatrixcover_ilp.rs +++ b/src/rules/minimummatrixcover_ilp.rs @@ -12,6 +12,8 @@ use crate::models::algebraic::{ObjectiveSense, ILP}; use crate::reduction; use crate::rules::ilp_helpers::mccormick_product; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; /// Result of reducing MinimumMatrixCover to ILP. #[derive(Debug, Clone)] @@ -28,10 +30,32 @@ impl ReductionResult for ReductionMinimumMatrixCoverToILP { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionMinimumMatrixCoverToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok({ // First n variables are the sign variables x_0,...,x_{n-1} target_solution[..self.n] diff --git a/src/rules/minimummaximalmatching_ilp.rs b/src/rules/minimummaximalmatching_ilp.rs index 410a9bf42..b70a1de86 100644 --- a/src/rules/minimummaximalmatching_ilp.rs +++ b/src/rules/minimummaximalmatching_ilp.rs @@ -11,6 +11,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::MinimumMaximalMatching; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; /// Result of reducing MinimumMaximalMatching to ILP. @@ -38,10 +40,32 @@ impl ReductionResult for ReductionMMMToILP { /// /// Since the mapping is 1:1 (each edge maps to one binary variable), /// the solution extraction is simply copying the configuration. - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionMMMToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok(target_solution.iter().map(|&value| value == 1).collect()) } } diff --git a/src/rules/minimummaximalmatching_maximumachromaticnumber.rs b/src/rules/minimummaximalmatching_maximumachromaticnumber.rs index ca773ac50..0b4bd6889 100644 --- a/src/rules/minimummaximalmatching_maximumachromaticnumber.rs +++ b/src/rules/minimummaximalmatching_maximumachromaticnumber.rs @@ -10,13 +10,15 @@ use crate::models::graph::{MaximumAchromaticNumber, MinimumMaximalMatching}; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::topology::{BipartiteGraph, Graph, SimpleGraph}; /// Result of reducing `MinimumMaximalMatching` to /// `MaximumAchromaticNumber`. /// /// Stores the target problem along with the source edge list (in unified vertex -/// coordinates) so that `extract_solution` can map a target coloring back to a +/// coordinates) so that `recover_result` can map a target coloring back to a /// maximal matching of the source graph. #[derive(Debug, Clone)] pub struct ReductionMMMToAchromatic { @@ -42,10 +44,32 @@ impl ReductionResult for ReductionMMMToAchromatic { /// size 2, i.e., a source edge. A source edge `(u, v)` belongs to the /// extracted matching iff `u` and `v` share a color, which we detect in a /// single pass over `source_edges`. - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionMMMToAchromatic { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok({ self.source_edges .iter() diff --git a/src/rules/minimummaximalmatching_minimummatrixdomination.rs b/src/rules/minimummaximalmatching_minimummatrixdomination.rs index 25749ece1..325044a91 100644 --- a/src/rules/minimummaximalmatching_minimummatrixdomination.rs +++ b/src/rules/minimummaximalmatching_minimummatrixdomination.rs @@ -37,6 +37,8 @@ use crate::models::algebraic::MinimumMatrixDomination; use crate::models::graph::MinimumMaximalMatching; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::topology::{BipartiteGraph, Graph}; /// Result of reducing `MinimumMaximalMatching` to @@ -44,7 +46,7 @@ use crate::topology::{BipartiteGraph, Graph}; /// /// Holds the constructed target matrix-domination instance together with a copy /// of the source bipartite-matching problem. The source copy is used by -/// `extract_solution` to perform the Yannakakis-Gavril conversion from an edge +/// `recover_result` to perform the Yannakakis-Gavril conversion from an edge /// dominating set to an equally-sized maximal matching. #[derive(Debug, Clone)] pub struct ReductionMMMToMatrixDomination { @@ -92,10 +94,32 @@ impl ReductionResult for ReductionMMMToMatrixDomination { /// undominated edge, for a total of `O(|F|^3)` time. The result is a /// matching that is an EDS, i.e. an independent EDS, which is precisely a /// maximal matching. - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionMMMToMatrixDomination { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok({ let graph = self.source.graph(); let edges = graph.edges(); diff --git a/src/rules/minimummetricdimension_ilp.rs b/src/rules/minimummetricdimension_ilp.rs index ea01ef90e..0c21edc4e 100644 --- a/src/rules/minimummetricdimension_ilp.rs +++ b/src/rules/minimummetricdimension_ilp.rs @@ -12,6 +12,8 @@ use crate::models::graph::minimum_metric_dimension::bfs_distances; use crate::models::graph::MinimumMetricDimension; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; /// Result of reducing MinimumMetricDimension to ILP. @@ -38,10 +40,32 @@ impl ReductionResult for ReductionMDToILP { /// /// Since the mapping is 1:1 (each vertex maps to one binary variable), /// the solution extraction is simply copying the configuration. - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionMDToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok(target_solution.iter().map(|&value| value == 1).collect()) } } diff --git a/src/rules/minimummultiwaycut_ilp.rs b/src/rules/minimummultiwaycut_ilp.rs index ce9f18853..14e1bcca4 100644 --- a/src/rules/minimummultiwaycut_ilp.rs +++ b/src/rules/minimummultiwaycut_ilp.rs @@ -10,6 +10,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::MinimumMultiwayCut; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; /// Result of reducing MinimumMultiwayCut to ILP. @@ -42,10 +44,32 @@ impl ReductionResult for ReductionMMCToILP { /// Extract solution from ILP back to MinimumMultiwayCut. /// /// For each edge e, source config[e] = target_solution[k*n + e] (the x_e variable). - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionMMCToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok({ let offset = self.k * self.n; (0..self.m) diff --git a/src/rules/minimummultiwaycut_qubo.rs b/src/rules/minimummultiwaycut_qubo.rs index faf2c054a..96d6a39e9 100644 --- a/src/rules/minimummultiwaycut_qubo.rs +++ b/src/rules/minimummultiwaycut_qubo.rs @@ -16,6 +16,8 @@ use crate::models::algebraic::QUBO; use crate::models::graph::MinimumMultiwayCut; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; /// Result of reducing MinimumMultiwayCut to QUBO. @@ -39,10 +41,31 @@ impl ReductionResult for ReductionMinimumMultiwayCutToQUBO { /// Map an optimal target assignment to an optimal edge deletion set. /// The penalty guarantees one-hot, terminal-pinned assignments at every /// optimum. All source instances are feasible by deleting every edge. - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { .. } => { + Err(crate::rules::ExtractionError::InsufficientSolutionQuality) + } + } + } +} + +impl ReductionMinimumMultiwayCutToQUBO { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { let k = self.num_terminals; let assignments: Vec = (0..self.num_vertices) .map(|vertex| { diff --git a/src/rules/minimumsetcovering_ilp.rs b/src/rules/minimumsetcovering_ilp.rs index ae0aa423c..e4712fc06 100644 --- a/src/rules/minimumsetcovering_ilp.rs +++ b/src/rules/minimumsetcovering_ilp.rs @@ -9,6 +9,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::set::MinimumSetCovering; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; /// Result of reducing MinimumSetCovering to ILP. /// @@ -33,10 +35,32 @@ impl ReductionResult for ReductionSCToILP { /// /// Since the mapping is 1:1 (each set maps to one binary variable), /// the solution extraction is simply copying the configuration. - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionSCToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok(target_solution.iter().map(|&value| value == 1).collect()) } } diff --git a/src/rules/minimumsummulticenter_ilp.rs b/src/rules/minimumsummulticenter_ilp.rs index 817beae0b..898691161 100644 --- a/src/rules/minimumsummulticenter_ilp.rs +++ b/src/rules/minimumsummulticenter_ilp.rs @@ -24,6 +24,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::MinimumSumMulticenter; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; /// Result of reducing MinimumSumMulticenter to ILP. @@ -41,10 +43,32 @@ impl ReductionResult for ReductionMSMCToILP { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionMSMCToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok(target_solution[..self.num_vertices] .iter() .map(|&value| value == 1) diff --git a/src/rules/minimumtardinesssequencing_ilp.rs b/src/rules/minimumtardinesssequencing_ilp.rs index 7621f3ee2..5d6d0da93 100644 --- a/src/rules/minimumtardinesssequencing_ilp.rs +++ b/src/rules/minimumtardinesssequencing_ilp.rs @@ -9,6 +9,8 @@ use crate::models::misc::MinimumTardinessSequencing; use crate::reduction; use crate::rules::ilp_helpers::one_hot_decode; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::types::One; /// Result of reducing MinimumTardinessSequencing to `ILP`. @@ -26,10 +28,32 @@ impl ReductionResult for ReductionMTSToILP { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionMTSToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok({ let n = self.num_tasks; @@ -53,10 +77,32 @@ impl ReductionResult for ReductionMTSWeightedToILP { &self.target } - fn extract_solution( + fn recover_result( + &self, + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionMTSWeightedToILP { + fn map_solution( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok({ let n = self.num_tasks; diff --git a/src/rules/minimumvertexcover_comparativecontainment.rs b/src/rules/minimumvertexcover_comparativecontainment.rs index eb43d1596..f5bae21b1 100644 --- a/src/rules/minimumvertexcover_comparativecontainment.rs +++ b/src/rules/minimumvertexcover_comparativecontainment.rs @@ -14,6 +14,8 @@ use crate::models::graph::MinimumVertexCover; use crate::models::set::ComparativeContainment; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; /// Identity witness map for the signed-weight containment construction. @@ -30,11 +32,18 @@ impl ReductionResult for ReductionDecisionMVCToComparativeContainment { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { - Ok(target_solution.clone()) + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => Ok(SolveOutcome::optimal(source, solution)?), + SolveOutcome::Feasible { solution, .. } => { + Ok(SolveOutcome::feasible(source, solution)?) + } + } } } diff --git a/src/rules/minimumvertexcover_ensemblecomputation.rs b/src/rules/minimumvertexcover_ensemblecomputation.rs index 61b937335..b0f3e8e88 100644 --- a/src/rules/minimumvertexcover_ensemblecomputation.rs +++ b/src/rules/minimumvertexcover_ensemblecomputation.rs @@ -16,6 +16,8 @@ use crate::models::graph::MinimumVertexCover; use crate::models::misc::EnsembleComputation; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; use crate::types::One; @@ -40,10 +42,32 @@ impl ReductionResult for ReductionVCToEC { /// its edge. An L-step program yields at most L minus the number of /// distinct required triples; loops instead require endpoint pairs. /// This applies to arbitrary programs, without a normal-form assumption. - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionVCToEC { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { let value = crate::traits::Problem::evaluate(self.target_problem(), target_solution)?; // Evaluation supplies the meaningful prefix, which the mapping needs. // The target witness premise already guarantees a feasible program. diff --git a/src/rules/minimumvertexcover_longestcommonsubsequence.rs b/src/rules/minimumvertexcover_longestcommonsubsequence.rs index 74de6eb2e..080d67bea 100644 --- a/src/rules/minimumvertexcover_longestcommonsubsequence.rs +++ b/src/rules/minimumvertexcover_longestcommonsubsequence.rs @@ -4,6 +4,8 @@ use crate::models::graph::MinimumVertexCover; use crate::models::misc::LongestCommonSubsequence; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; use crate::types::One; @@ -21,10 +23,32 @@ impl ReductionResult for ReductionVCToLCS { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionVCToLCS { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok({ let mut cover = vec![true; self.num_vertices]; for &symbol in target_solution { diff --git a/src/rules/minimumvertexcover_maximumindependentset.rs b/src/rules/minimumvertexcover_maximumindependentset.rs index c9922103d..b636a6289 100644 --- a/src/rules/minimumvertexcover_maximumindependentset.rs +++ b/src/rules/minimumvertexcover_maximumindependentset.rs @@ -5,6 +5,8 @@ use crate::models::graph::{MaximumIndependentSet, MinimumVertexCover}; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; use crate::types::WeightElement; @@ -27,10 +29,35 @@ where /// Solution extraction: complement the configuration. /// If v is in the independent set (1), it's NOT in the vertex cover (0). - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionISToVC +where + W: WeightElement + crate::variant::VariantParam, +{ + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok(target_solution.iter().map(|&x| !x).collect()) } } @@ -71,10 +98,35 @@ where } /// Solution extraction: complement the configuration. - fn extract_solution( + fn recover_result( + &self, + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionVCToIS +where + W: WeightElement + crate::variant::VariantParam, +{ + fn map_solution( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok(target_solution.iter().map(|&x| !x).collect()) } } diff --git a/src/rules/minimumvertexcover_minimumfeedbackarcset.rs b/src/rules/minimumvertexcover_minimumfeedbackarcset.rs index 4318b5d67..06134a3f4 100644 --- a/src/rules/minimumvertexcover_minimumfeedbackarcset.rs +++ b/src/rules/minimumvertexcover_minimumfeedbackarcset.rs @@ -11,6 +11,8 @@ use crate::models::graph::{MinimumFeedbackArcSet, MinimumVertexCover}; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::topology::{DirectedGraph, Graph, SimpleGraph}; /// Result of reducing MinimumVertexCover to MinimumFeedbackArcSet. @@ -31,10 +33,32 @@ impl ReductionResult for ReductionVCToFAS { /// Extract solution: internal arcs are at positions 0..n in the FAS config. /// If internal arc i is in the FAS (config[i] = 1), vertex i is in the cover. - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionVCToFAS { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok(target_solution[..self.num_source_vertices].to_vec()) } } @@ -120,7 +144,14 @@ pub(crate) fn canonical_rule_example_specs() -> Vec>( source, diff --git a/src/rules/minimumvertexcover_minimumfeedbackvertexset.rs b/src/rules/minimumvertexcover_minimumfeedbackvertexset.rs index 9c4cbd3e7..26158e30b 100644 --- a/src/rules/minimumvertexcover_minimumfeedbackvertexset.rs +++ b/src/rules/minimumvertexcover_minimumfeedbackvertexset.rs @@ -6,6 +6,8 @@ use crate::models::graph::{MinimumFeedbackVertexSet, MinimumVertexCover}; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::topology::{DirectedGraph, Graph, SimpleGraph}; use crate::types::WeightElement; @@ -26,11 +28,18 @@ where &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { - Ok(target_solution.to_vec()) + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => Ok(SolveOutcome::optimal(source, solution)?), + SolveOutcome::Feasible { solution, .. } => { + Ok(SolveOutcome::feasible(source, solution)?) + } + } } } diff --git a/src/rules/minimumvertexcover_minimumhittingset.rs b/src/rules/minimumvertexcover_minimumhittingset.rs index a0841c4f9..54b72d457 100644 --- a/src/rules/minimumvertexcover_minimumhittingset.rs +++ b/src/rules/minimumvertexcover_minimumhittingset.rs @@ -7,6 +7,8 @@ use crate::models::graph::MinimumVertexCover; use crate::models::set::MinimumHittingSet; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; use crate::types::One; @@ -26,11 +28,18 @@ impl ReductionResult for ReductionVCToHS { /// Solution extraction: variables correspond 1:1. /// Element i in the hitting set corresponds to vertex i in the vertex cover. - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { - Ok(target_solution.to_vec()) + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => Ok(SolveOutcome::optimal(source, solution)?), + SolveOutcome::Feasible { solution, .. } => { + Ok(SolveOutcome::feasible(source, solution)?) + } + } } } diff --git a/src/rules/minimumvertexcover_minimummaximalmatching.rs b/src/rules/minimumvertexcover_minimummaximalmatching.rs index 06819a13b..3d2f015cd 100644 --- a/src/rules/minimumvertexcover_minimummaximalmatching.rs +++ b/src/rules/minimumvertexcover_minimummaximalmatching.rs @@ -30,7 +30,6 @@ inventory::submit! { }, module_path: module_path!(), reduce_fn: None, - reduce_aggregate_fn: None, turing: false, } } diff --git a/src/rules/minimumvertexcover_minimumsetcovering.rs b/src/rules/minimumvertexcover_minimumsetcovering.rs index 048e4e31d..c7dddd51b 100644 --- a/src/rules/minimumvertexcover_minimumsetcovering.rs +++ b/src/rules/minimumvertexcover_minimumsetcovering.rs @@ -7,6 +7,8 @@ use crate::models::graph::MinimumVertexCover; use crate::models::set::MinimumSetCovering; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; use crate::types::WeightElement; @@ -29,11 +31,18 @@ where /// Solution extraction: variables correspond 1:1. /// Vertex i in VC corresponds to set i in SC. - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { - Ok(target_solution.to_vec()) + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => Ok(SolveOutcome::optimal(source, solution)?), + SolveOutcome::Feasible { solution, .. } => { + Ok(SolveOutcome::feasible(source, solution)?) + } + } } } diff --git a/src/rules/minimumvertexcover_minimumweightandorgraph.rs b/src/rules/minimumvertexcover_minimumweightandorgraph.rs index f462f28f6..ae00726b7 100644 --- a/src/rules/minimumvertexcover_minimumweightandorgraph.rs +++ b/src/rules/minimumvertexcover_minimumweightandorgraph.rs @@ -4,6 +4,8 @@ use crate::models::graph::MinimumVertexCover; use crate::models::misc::MinimumWeightAndOrGraph; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::topology::Graph; use crate::topology::SimpleGraph; @@ -23,10 +25,32 @@ impl ReductionResult for ReductionVCToAndOrGraph { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionVCToAndOrGraph { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok({ (0..self.num_source_vertices) .map(|j| target_solution[self.sink_arc_start + j]) diff --git a/src/rules/minimumweightdecoding_ilp.rs b/src/rules/minimumweightdecoding_ilp.rs index ce5ec087e..49d5c7977 100644 --- a/src/rules/minimumweightdecoding_ilp.rs +++ b/src/rules/minimumweightdecoding_ilp.rs @@ -19,6 +19,8 @@ use crate::models::algebraic::MinimumWeightDecoding; use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; /// Result of reducing MinimumWeightDecoding to `ILP`. /// @@ -40,10 +42,32 @@ impl ReductionResult for ReductionMinimumWeightDecodingToILP { } /// Extract the source solution: first m variables are the binary x_j values. - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionMinimumWeightDecodingToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok(target_solution[..self.num_cols] .iter() .map(|&value| value == 1) diff --git a/src/rules/minmaxmulticenter_ilp.rs b/src/rules/minmaxmulticenter_ilp.rs index 14580acdf..de26ef4ed 100644 --- a/src/rules/minmaxmulticenter_ilp.rs +++ b/src/rules/minmaxmulticenter_ilp.rs @@ -28,6 +28,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::MinMaxMulticenter; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; /// Result of reducing MinMaxMulticenter to ILP. @@ -45,10 +47,32 @@ impl ReductionResult for ReductionMMCToILP { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionMMCToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok(target_solution[..self.num_vertices] .iter() .map(|&value| value == 1) diff --git a/src/rules/mixedchinesepostman_ilp.rs b/src/rules/mixedchinesepostman_ilp.rs index fc0b840a0..cb3810a5f 100644 --- a/src/rules/mixedchinesepostman_ilp.rs +++ b/src/rules/mixedchinesepostman_ilp.rs @@ -9,6 +9,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::MixedChinesePostman; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::types::WeightElement; /// Result of reducing MixedChinesePostman to ILP. @@ -26,10 +28,32 @@ impl ReductionResult for ReductionMCPToILP { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionMCPToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok({ // Return the orientation bits d_k in source edge order target_solution[..self.num_undirected_edges] diff --git a/src/rules/mod.rs b/src/rules/mod.rs index 4b7faa052..ba3fa3acc 100644 --- a/src/rules/mod.rs +++ b/src/rules/mod.rs @@ -64,7 +64,6 @@ pub(crate) mod ksatisfiability_directedtwocommodityintegralflow; pub(crate) mod ksatisfiability_feasibleregisterassignment; pub(crate) mod ksatisfiability_kclique; pub(crate) mod ksatisfiability_kernel; -pub(crate) mod ksatisfiability_minimumvertexcover; pub(crate) mod ksatisfiability_monochromatictriangle; pub(crate) mod ksatisfiability_oneinthreesatisfiability; pub(crate) mod ksatisfiability_preemptivescheduling; @@ -284,14 +283,14 @@ pub(crate) mod undirectedtwocommodityintegralflow_ilp; #[cfg(test)] pub(crate) use graph::ReductionEdgeData; pub use graph::{ - AggregateReductionChain, ExecutePathsError, ExecutedPath, NeighborInfo, NeighborTree, - PathParameterError, ReductionChain, ReductionEdgeInfo, ReductionGraph, ReductionMode, - ReductionPath, ReductionStep, TraversalFlow, + ExecutePathsError, ExecutedPath, NeighborInfo, NeighborTree, PathParameterError, + ReductionChain, ReductionEdgeInfo, ReductionGraph, ReductionMode, ReductionPath, ReductionStep, + TraversalFlow, }; pub(crate) use traits::DynReductionResult; pub use traits::{ - AggregateReductionResult, ExtractionError, ExtractionResult, ReduceTo, ReduceToAggregate, - ReductionError, ReductionResult, VariantReductionResult, + ExtractionError, ExtractionResult, ReduceTo, ReductionError, ReductionResult, + VariantReductionResult, }; #[cfg(feature = "example-db")] @@ -346,7 +345,6 @@ pub(crate) fn canonical_rule_example_specs() -> Vec Vec => < $($dst_param:ty),+ >, fields: [$($field:ident),+], - $(aggregate: $aggregate:ident,)? |$src:ident| $body:expr) => { #[$crate::reduction( transform = exact { $($field = $field),+ } - $(, aggregate = $aggregate)? )] impl $crate::rules::ReduceTo<$problem<$($dst_param),+>> for $problem<$($src_param),+> diff --git a/src/rules/monochromatictriangle_ilp.rs b/src/rules/monochromatictriangle_ilp.rs index 6ec1a2a6f..d81e8dbe1 100644 --- a/src/rules/monochromatictriangle_ilp.rs +++ b/src/rules/monochromatictriangle_ilp.rs @@ -8,6 +8,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::MonochromaticTriangle; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; use std::collections::HashMap; @@ -25,10 +27,32 @@ impl ReductionResult for ReductionMonochromaticTriangleToILP { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionMonochromaticTriangleToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok(target_solution.iter().map(|&value| value == 1).collect()) } } diff --git a/src/rules/multiplechoicebranching_ilp.rs b/src/rules/multiplechoicebranching_ilp.rs index 206a551d8..4ae6ad2bb 100644 --- a/src/rules/multiplechoicebranching_ilp.rs +++ b/src/rules/multiplechoicebranching_ilp.rs @@ -4,6 +4,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::MultipleChoiceBranching; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; #[derive(Debug, Clone)] pub struct ReductionMultipleChoiceBranchingToILP { @@ -19,10 +21,32 @@ impl ReductionResult for ReductionMultipleChoiceBranchingToILP { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionMultipleChoiceBranchingToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok(target_solution[..self.num_arcs] .iter() .map(|&selected| selected == 1) diff --git a/src/rules/multiplecopyfileallocation_ilp.rs b/src/rules/multiplecopyfileallocation_ilp.rs index 4c9ce90c8..df2107ed9 100644 --- a/src/rules/multiplecopyfileallocation_ilp.rs +++ b/src/rules/multiplecopyfileallocation_ilp.rs @@ -18,6 +18,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::MultipleCopyFileAllocation; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; use std::collections::VecDeque; @@ -36,10 +38,32 @@ impl ReductionResult for ReductionMCFAToILP { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionMCFAToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok(target_solution[..self.num_vertices] .iter() .map(|&value| value == 1) diff --git a/src/rules/multiprocessorscheduling_ilp.rs b/src/rules/multiprocessorscheduling_ilp.rs index 2e8136b60..fa9c2084d 100644 --- a/src/rules/multiprocessorscheduling_ilp.rs +++ b/src/rules/multiprocessorscheduling_ilp.rs @@ -10,6 +10,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::MultiprocessorScheduling; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; /// Result of reducing MultiprocessorScheduling to ILP. /// @@ -33,10 +35,32 @@ impl ReductionResult for ReductionMSToILP { } /// Extract solution: for each task j, find the unique processor p where x_{j,p} = 1. - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionMSToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok(crate::rules::ilp_helpers::one_hot_decode_rows( target_solution, self.num_tasks, diff --git a/src/rules/naesatisfiability_ilp.rs b/src/rules/naesatisfiability_ilp.rs index 2a91a142b..af7631ada 100644 --- a/src/rules/naesatisfiability_ilp.rs +++ b/src/rules/naesatisfiability_ilp.rs @@ -12,6 +12,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::formula::NAESatisfiability; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; #[derive(Debug, Clone)] pub struct ReductionNAESATToILP { @@ -26,10 +28,32 @@ impl ReductionResult for ReductionNAESATToILP { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionNAESATToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok(target_solution.iter().map(|&value| value == 1).collect()) } } diff --git a/src/rules/naesatisfiability_maxcut.rs b/src/rules/naesatisfiability_maxcut.rs index 5ed460e9f..a4c49e590 100644 --- a/src/rules/naesatisfiability_maxcut.rs +++ b/src/rules/naesatisfiability_maxcut.rs @@ -11,23 +11,25 @@ //! Section 4, arXiv:1512.03127. The triangle construction is the classical //! NAE-3SAT to MaxCut reduction (Garey and Johnson, ND16). +use crate::models::decision::Decision; use crate::models::formula::NAESatisfiability; use crate::models::graph::MaxCut; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::topology::SimpleGraph; /// Result of reducing NAESatisfiability to MaxCut. #[derive(Debug, Clone)] pub struct ReductionNAESATToMaxCut { - target: MaxCut, + target: Decision>, source_num_vars: usize, - feasible_cut: i64, } impl ReductionResult for ReductionNAESATToMaxCut { type Source = NAESatisfiability; - type Target = MaxCut; + type Target = Decision>; fn target_problem(&self) -> &Self::Target { &self.target @@ -38,10 +40,32 @@ impl ReductionResult for ReductionNAESATToMaxCut { /// Variable x_i is assigned based on vertex 2*i: if it is in set 0 /// (config[2*i] == 0), set x_i = false (config value 0); if in set 1, /// set x_i = true (config value 1). - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionNAESATToMaxCut { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok({ (0..self.source_num_vars) .map(|i| target_solution[2 * i]) @@ -50,28 +74,16 @@ impl ReductionResult for ReductionNAESATToMaxCut { } } -impl crate::rules::AggregateReductionResult for ReductionNAESATToMaxCut { - type Source = NAESatisfiability; - type Target = MaxCut; - - fn target_problem(&self) -> &Self::Target { - &self.target - } - - fn extract_value(&self, value: crate::types::Max) -> crate::types::Or { - crate::types::Or(value.0 == Some(self.feasible_cut)) - } -} - /// Dimensions, variable-edge weight, and certificate for legal clause lengths. fn nae_maxcut_parameters( n: usize, lengths: impl ExactSizeIterator, ) -> Result<(usize, usize, i64, i64), crate::rules::ReductionError> { let overflow = |operation| { - crate::rules::ReductionError::integer_overflow::>( - operation, - ) + crate::rules::ReductionError::integer_overflow::< + NAESatisfiability, + Decision>, + >(operation) }; let weight = i64::try_from(lengths.len()) .ok() @@ -136,13 +148,12 @@ fn nae_maxcut_parameters( } #[reduction( - aggregate = custom, transform = upper_bound { num_vertices = "2 * (num_vars + num_literals - 2 * num_clauses)", num_edges = "num_vars + 4 * num_literals - 7 * num_clauses", } )] -impl ReduceTo> for NAESatisfiability { +impl ReduceTo>> for NAESatisfiability { type Result = ReductionNAESATToMaxCut; fn reduce_to(&self) -> Result { @@ -165,7 +176,7 @@ impl ReduceTo> for NAESatisfiability { let index = usize::try_from(literal.unsigned_abs()).map_err(|_| { crate::rules::ReductionError::integer_overflow::< NAESatisfiability, - MaxCut, + Decision>, >("converting a literal index") })? - 1; // Validated literals are in 1..=n, and 2*total_variables was checked. @@ -192,9 +203,11 @@ impl ReduceTo> for NAESatisfiability { } Ok(ReductionNAESATToMaxCut { - target: MaxCut::new(SimpleGraph::new(total_vertices, edges), weights), + target: Decision::new( + MaxCut::new(SimpleGraph::new(total_vertices, edges), weights), + feasible_cut, + ), source_num_vars: self.num_vars(), - feasible_cut, }) } } @@ -218,7 +231,10 @@ pub(crate) fn canonical_rule_example_specs() -> Vec>( + crate::example_db::specs::rule_example_with_witness::< + _, + Decision>, + >( source, SolutionPair { // x1=T(1), x2=F(0), x3=T(1) diff --git a/src/rules/naesatisfiability_partitionintoperfectmatchings.rs b/src/rules/naesatisfiability_partitionintoperfectmatchings.rs index d56cc6b35..202d93223 100644 --- a/src/rules/naesatisfiability_partitionintoperfectmatchings.rs +++ b/src/rules/naesatisfiability_partitionintoperfectmatchings.rs @@ -8,6 +8,8 @@ use crate::models::formula::NAESatisfiability; use crate::models::graph::PartitionIntoPerfectMatchings; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::topology::SimpleGraph; #[derive(Debug, Clone, Copy)] @@ -65,10 +67,32 @@ impl ReductionResult for ReductionNAESATToPartitionIntoPerfectMatchings { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionNAESATToPartitionIntoPerfectMatchings { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok({ self.layout .variables diff --git a/src/rules/naesatisfiability_setsplitting.rs b/src/rules/naesatisfiability_setsplitting.rs index 284a9cac4..c78b8e738 100644 --- a/src/rules/naesatisfiability_setsplitting.rs +++ b/src/rules/naesatisfiability_setsplitting.rs @@ -10,6 +10,8 @@ use crate::models::formula::NAESatisfiability; use crate::models::set::SetSplitting; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; #[derive(Debug, Clone)] pub struct ReductionNAESATToSetSplitting { @@ -25,10 +27,32 @@ impl ReductionResult for ReductionNAESATToSetSplitting { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionNAESATToSetSplitting { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok(target_solution[..self.num_source_variables].to_vec()) } } diff --git a/src/rules/numerical3dimensionalmatching_numericalmatchingwithtargetsums.rs b/src/rules/numerical3dimensionalmatching_numericalmatchingwithtargetsums.rs index 9f8478b8c..0b1bf1cf4 100644 --- a/src/rules/numerical3dimensionalmatching_numericalmatchingwithtargetsums.rs +++ b/src/rules/numerical3dimensionalmatching_numericalmatchingwithtargetsums.rs @@ -8,6 +8,8 @@ use crate::models::misc::{Numerical3DimensionalMatching, NumericalMatchingWithTargetSums}; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; /// Result of reducing Numerical3DimensionalMatching to NumericalMatchingWithTargetSums. #[derive(Debug, Clone)] @@ -23,10 +25,32 @@ impl ReductionResult for ReductionN3DMToNMTS { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionN3DMToNMTS { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok({ let mut pairs: Vec<_> = target_solution .iter() diff --git a/src/rules/numericalmatchingwithtargetsums_ilp.rs b/src/rules/numericalmatchingwithtargetsums_ilp.rs index a4cfe5092..d08724e25 100644 --- a/src/rules/numericalmatchingwithtargetsums_ilp.rs +++ b/src/rules/numericalmatchingwithtargetsums_ilp.rs @@ -15,6 +15,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::NumericalMatchingWithTargetSums; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; /// A compatible triple (i, j, k) where s(x_i) + s(y_j) = B_k. #[derive(Debug, Clone)] @@ -44,10 +46,32 @@ impl ReductionResult for ReductionNMTSToILP { } /// Extract solution: for each x_i find the y_j it is paired with. - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionNMTSToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok({ let mut assignment = vec![0usize; self.m]; for (var_idx, triple) in self.triples.iter().enumerate() { diff --git a/src/rules/openshopscheduling_ilp.rs b/src/rules/openshopscheduling_ilp.rs index 28c2416fc..086305ec7 100644 --- a/src/rules/openshopscheduling_ilp.rs +++ b/src/rules/openshopscheduling_ilp.rs @@ -30,6 +30,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::OpenShopScheduling; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; /// Result of reducing OpenShopScheduling to `ILP`. /// @@ -87,10 +89,32 @@ impl ReductionResult for ReductionOSSToILP { } /// Extract the job-major operation start times from the ILP solution. - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionOSSToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { let start = self.num_order_vars; let end = start + self.num_jobs * self.num_machines; crate::rules::ilp_helpers::decode_usize_values(&target_solution[start..end]) diff --git a/src/rules/optimallineararrangement_consecutiveonesmatrixaugmentation.rs b/src/rules/optimallineararrangement_consecutiveonesmatrixaugmentation.rs index ab1433533..5faa66243 100644 --- a/src/rules/optimallineararrangement_consecutiveonesmatrixaugmentation.rs +++ b/src/rules/optimallineararrangement_consecutiveonesmatrixaugmentation.rs @@ -9,6 +9,8 @@ use crate::models::decision::Decision; use crate::models::graph::OptimalLinearArrangement; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; /// The target incidence matrix, or a fixed infeasible matrix when the source @@ -26,10 +28,32 @@ impl ReductionResult for ReductionOptimalLinearArrangementToConsecutiveOnesMatri &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionOptimalLinearArrangementToConsecutiveOnesMatrixAugmentation { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { // Validation establishes a permutation within the augmentation budget. // The NO sentinel has no such certificate; all remaining columns are // source vertices, including the empty permutation for an empty graph. diff --git a/src/rules/optimallineararrangement_ilp.rs b/src/rules/optimallineararrangement_ilp.rs index 02d0f2e53..bc7d4002e 100644 --- a/src/rules/optimallineararrangement_ilp.rs +++ b/src/rules/optimallineararrangement_ilp.rs @@ -11,6 +11,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::OptimalLinearArrangement; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; /// Result of reducing OptimalLinearArrangement to ILP. @@ -34,10 +36,32 @@ impl ReductionResult for ReductionOLAToILP { } /// Extract: for each vertex v, output its position p (the unique p with x_{v,p} = 1). - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionOLAToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok(crate::rules::ilp_helpers::one_hot_decode_rows( target_solution, self.num_vertices, diff --git a/src/rules/optimallineararrangement_sequencingtominimizeweightedcompletiontime.rs b/src/rules/optimallineararrangement_sequencingtominimizeweightedcompletiontime.rs index 0c8223680..b7894b1ac 100644 --- a/src/rules/optimallineararrangement_sequencingtominimizeweightedcompletiontime.rs +++ b/src/rules/optimallineararrangement_sequencingtominimizeweightedcompletiontime.rs @@ -15,6 +15,8 @@ use crate::models::graph::OptimalLinearArrangement; use crate::models::misc::SequencingToMinimizeWeightedCompletionTime; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; /// Result of reducing OptimalLinearArrangement to SequencingToMinimizeWeightedCompletionTime. @@ -32,10 +34,32 @@ impl ReductionResult for ReductionOLAToSequencingToMinimizeWeightedCompletionTim &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionOLAToSequencingToMinimizeWeightedCompletionTime { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok({ let mut arrangement = vec![0usize; self.num_vertices]; let mut next_position = 0usize; @@ -128,7 +152,14 @@ pub(crate) fn canonical_rule_example_specs() -> Vec::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionOptimumCommunicationSpanningTreeToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok(target_solution[..self.num_edges] .iter() .map(|&value| value == 1) diff --git a/src/rules/paintshop_ilp.rs b/src/rules/paintshop_ilp.rs index df7af6521..58268ae37 100644 --- a/src/rules/paintshop_ilp.rs +++ b/src/rules/paintshop_ilp.rs @@ -8,6 +8,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::PaintShop; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; #[derive(Debug, Clone)] pub struct ReductionPaintShopToILP { @@ -24,10 +26,32 @@ impl ReductionResult for ReductionPaintShopToILP { } /// Extract first-occurrence color bits (x_i) from ILP solution. - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionPaintShopToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok(target_solution[..self.num_cars] .iter() .map(|&value| value == 1) @@ -124,7 +148,14 @@ pub(crate) fn canonical_rule_example_specs() -> Vec>( source, SolutionPair { diff --git a/src/rules/paintshop_qubo.rs b/src/rules/paintshop_qubo.rs index 920b65fab..3e6ea0eff 100644 --- a/src/rules/paintshop_qubo.rs +++ b/src/rules/paintshop_qubo.rs @@ -11,6 +11,8 @@ use crate::models::algebraic::QUBO; use crate::models::misc::PaintShop; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; /// Result of reducing PaintShop to QUBO. #[derive(Debug, Clone)] @@ -28,11 +30,18 @@ impl ReductionResult for ReductionPaintShopToQUBO { /// The QUBO solution maps directly back: car i's first occurrence gets /// color x_i, second gets 1 - x_i. - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { - Ok(target_solution.to_vec()) + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => Ok(SolveOutcome::optimal(source, solution)?), + SolveOutcome::Feasible { solution, .. } => { + Ok(SolveOutcome::feasible(source, solution)?) + } + } } } diff --git a/src/rules/partiallyorderedknapsack_ilp.rs b/src/rules/partiallyorderedknapsack_ilp.rs index 56c5b3c08..8c939b71d 100644 --- a/src/rules/partiallyorderedknapsack_ilp.rs +++ b/src/rules/partiallyorderedknapsack_ilp.rs @@ -7,6 +7,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::PartiallyOrderedKnapsack; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; #[derive(Debug, Clone)] pub struct ReductionPOKToILP { @@ -21,10 +23,32 @@ impl ReductionResult for ReductionPOKToILP { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionPOKToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok(target_solution.iter().map(|&value| value == 1).collect()) } } diff --git a/src/rules/partition_binpacking.rs b/src/rules/partition_binpacking.rs index 639e603ec..217e38236 100644 --- a/src/rules/partition_binpacking.rs +++ b/src/rules/partition_binpacking.rs @@ -15,6 +15,9 @@ use crate::models::misc::{BinPacking, Partition}; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; +use crate::traits::Problem; /// Result of reducing Partition to BinPacking. #[derive(Debug, Clone)] @@ -30,10 +33,48 @@ impl ReductionResult for ReductionPartitionToBinPacking { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + let evaluation = source.evaluate(&solution)?; + if evaluation.0 { + Ok(SolveOutcome::Optimal { + solution, + evaluation, + }) + } else { + Ok(SolveOutcome::Infeasible) + } + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + let evaluation = source.evaluate(&solution)?; + if evaluation.0 { + Ok(SolveOutcome::Feasible { + solution, + evaluation, + }) + } else { + Err(crate::rules::ExtractionError::InsufficientSolutionQuality) + } + } + } + } +} + +impl ReductionPartitionToBinPacking { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok({ // BinPacking may use any bin indices (0..n-1). Remap the two distinct // bins used in a 2-bin packing to Partition's {0, 1} assignment. diff --git a/src/rules/partition_cosineproductintegration.rs b/src/rules/partition_cosineproductintegration.rs index 3cea2660d..c1abec040 100644 --- a/src/rules/partition_cosineproductintegration.rs +++ b/src/rules/partition_cosineproductintegration.rs @@ -13,6 +13,8 @@ use crate::models::misc::{CosineProductIntegration, Partition}; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; /// Result of reducing Partition to CosineProductIntegration. #[derive(Debug, Clone)] @@ -28,11 +30,18 @@ impl ReductionResult for ReductionPartitionToCPI { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { - Ok(target_solution.to_vec()) + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => Ok(SolveOutcome::optimal(source, solution)?), + SolveOutcome::Feasible { solution, .. } => { + Ok(SolveOutcome::feasible(source, solution)?) + } + } } } diff --git a/src/rules/partition_integralflowwithmultipliers.rs b/src/rules/partition_integralflowwithmultipliers.rs index 2bae8498b..a35fa854d 100644 --- a/src/rules/partition_integralflowwithmultipliers.rs +++ b/src/rules/partition_integralflowwithmultipliers.rs @@ -9,6 +9,8 @@ use crate::models::graph::IntegralFlowWithMultipliers; use crate::models::misc::Partition; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::topology::DirectedGraph; /// Result of reducing Partition to IntegralFlowWithMultipliers. @@ -26,10 +28,32 @@ impl ReductionResult for ReductionPartitionToIntegralFlowWithMultipliers { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionPartitionToIntegralFlowWithMultipliers { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok({ target_solution[..self.item_arc_count] .iter() diff --git a/src/rules/partition_knapsack.rs b/src/rules/partition_knapsack.rs index 4692151b1..96a4461b8 100644 --- a/src/rules/partition_knapsack.rs +++ b/src/rules/partition_knapsack.rs @@ -3,6 +3,9 @@ use crate::models::misc::{Knapsack, Partition}; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; +use crate::traits::Problem; /// Result of reducing Partition to Knapsack. #[derive(Debug, Clone)] @@ -18,10 +21,48 @@ impl ReductionResult for ReductionPartitionToKnapsack { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + let evaluation = source.evaluate(&solution)?; + if evaluation.0 { + Ok(SolveOutcome::Optimal { + solution, + evaluation, + }) + } else { + Ok(SolveOutcome::Infeasible) + } + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + let evaluation = source.evaluate(&solution)?; + if evaluation.0 { + Ok(SolveOutcome::Feasible { + solution, + evaluation, + }) + } else { + Err(crate::rules::ExtractionError::InsufficientSolutionQuality) + } + } + } + } +} + +impl ReductionPartitionToKnapsack { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok(target_solution.to_vec()) } } diff --git a/src/rules/partition_multiprocessorscheduling.rs b/src/rules/partition_multiprocessorscheduling.rs index ddccfaafb..89335301d 100644 --- a/src/rules/partition_multiprocessorscheduling.rs +++ b/src/rules/partition_multiprocessorscheduling.rs @@ -15,6 +15,8 @@ use crate::models::misc::{MultiprocessorScheduling, Partition}; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; /// Result of reducing Partition to MultiprocessorScheduling. #[derive(Debug, Clone)] @@ -32,10 +34,32 @@ impl ReductionResult for ReductionPartitionToMPS { /// Solution extraction: identity mapping. /// Partition config (0/1 for subset) maps directly to processor assignment (0/1). - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionPartitionToMPS { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok(target_solution .iter() .map(|&processor| processor == 1) diff --git a/src/rules/partition_openshopscheduling.rs b/src/rules/partition_openshopscheduling.rs index a660b5dc2..d75241746 100644 --- a/src/rules/partition_openshopscheduling.rs +++ b/src/rules/partition_openshopscheduling.rs @@ -1,72 +1,75 @@ //! Reduction from Partition to Open Shop Scheduling. +use crate::models::decision::Decision; use crate::models::misc::{OpenShopScheduling, Partition}; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; #[derive(Debug, Clone)] pub struct ReductionPartitionToOpenShopScheduling { - target: OpenShopScheduling, - feasible_makespan: i64, + target: Decision, } impl ReductionResult for ReductionPartitionToOpenShopScheduling { type Source = Partition; - type Target = OpenShopScheduling; + type Target = Decision; fn target_problem(&self) -> &Self::Target { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { - Ok({ - let num_elements = self.target.num_jobs() - 1; - let mut source_config = vec![false; num_elements]; - let m = self.target.num_machines(); - let start_times = target_solution - .chunks_exact(m) - .map(|times| times.iter().map(|&time| time as i64).collect::>()) - .collect::>(); - let special_job = num_elements; - let half_sum = self.target.processing_times()[special_job][0]; - - // Find the middle machine where the special job starts at half_sum - let middle_machine: usize = (0..m) - .filter(|&machine| start_times[special_job][machine] == half_sum) - .sum(); - let pivot = start_times[special_job][middle_machine]; - - for (job, slot) in source_config.iter_mut().enumerate() { - let completion = start_times[job][middle_machine] - + self.target.processing_times()[job][middle_machine]; - if completion <= pivot { - *slot = true; - } + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) } - - source_config - }) + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } } } -impl crate::rules::AggregateReductionResult for ReductionPartitionToOpenShopScheduling { - type Source = Partition; - type Target = OpenShopScheduling; - - fn target_problem(&self) -> &Self::Target { - &self.target - } - - fn extract_value(&self, value: crate::types::Min) -> crate::types::Or { - crate::types::Or(value.0 == Some(self.feasible_makespan)) +impl ReductionPartitionToOpenShopScheduling { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { + let target = self.target.inner(); + let num_elements = target.num_jobs() - 1; + let mut source_config = vec![false; num_elements]; + let m = target.num_machines(); + let special_job = num_elements; + let half_sum = target.processing_times()[special_job][0]; + + // Find the middle machine where the special job starts at half_sum. + let middle_machine: usize = (0..m) + .filter(|&machine| target_solution[special_job * m + machine] as i64 == half_sum) + .sum(); + let pivot = target_solution[special_job * m + middle_machine] as i64; + + for (job, slot) in source_config.iter_mut().enumerate() { + let completion = target_solution[job * m + middle_machine] as i64 + + target.processing_times()[job][middle_machine]; + *slot = completion <= pivot; + } + + Ok(source_config) } } #[reduction( - aggregate = custom, transform = exact { num_jobs = "num_elements + 1", num_machines = "3", @@ -75,7 +78,7 @@ impl crate::rules::AggregateReductionResult for ReductionPartitionToOpenShopSche schedule_horizon = "depends on the numeric partition sizes, which are not represented by source size parameters", } )] -impl ReduceTo for Partition { +impl ReduceTo> for Partition { type Result = ReductionPartitionToOpenShopScheduling; fn reduce_to(&self) -> Result { @@ -85,12 +88,11 @@ impl ReduceTo for Partition { processing_times.push(vec![half_sum; 3]); let target = OpenShopScheduling::try_new(3, processing_times) - .map_err(>::target_construction)?; + .map_err(>>::target_construction)?; // The validated nonnegative schedule horizon includes these three terms. let feasible_makespan = 3 * half_sum; Ok(ReductionPartitionToOpenShopScheduling { - target, - feasible_makespan, + target: Decision::new(target, feasible_makespan), }) } } @@ -102,7 +104,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec( + crate::example_db::specs::rule_example_with_witness::<_, Decision>( Partition::new(vec![1, 2, 3]).unwrap(), SolutionPair { source_config: serde_json::json!(vec![true, true, false]), diff --git a/src/rules/partition_productionplanning.rs b/src/rules/partition_productionplanning.rs index f9c77435a..4996231ce 100644 --- a/src/rules/partition_productionplanning.rs +++ b/src/rules/partition_productionplanning.rs @@ -3,6 +3,8 @@ use crate::models::misc::{Partition, ProductionPlanning}; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; #[derive(Debug, Clone)] pub struct ReductionPartitionToProductionPlanning { @@ -17,10 +19,32 @@ impl ReductionResult for ReductionPartitionToProductionPlanning { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionPartitionToProductionPlanning { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok(target_solution[..self.target.num_periods() - 1] .iter() .map(|&production| production > 0) diff --git a/src/rules/partition_sequencingtominimizetardytaskweight.rs b/src/rules/partition_sequencingtominimizetardytaskweight.rs index b13ef760d..764011009 100644 --- a/src/rules/partition_sequencingtominimizetardytaskweight.rs +++ b/src/rules/partition_sequencingtominimizetardytaskweight.rs @@ -1,34 +1,59 @@ //! Reduction from Partition to Sequencing to Minimize Tardy Task Weight. +use crate::models::decision::Decision; use crate::models::misc::{Partition, SequencingToMinimizeTardyTaskWeight}; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; /// Result of reducing Partition to SequencingToMinimizeTardyTaskWeight. #[derive(Debug, Clone)] pub struct ReductionPartitionToSequencingToMinimizeTardyTaskWeight { - target: SequencingToMinimizeTardyTaskWeight, + target: Decision, } impl ReductionResult for ReductionPartitionToSequencingToMinimizeTardyTaskWeight { type Source = Partition; - type Target = SequencingToMinimizeTardyTaskWeight; + type Target = Decision; fn target_problem(&self) -> &Self::Target { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionPartitionToSequencingToMinimizeTardyTaskWeight { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok({ - let mut source_config = vec![true; self.target.num_tasks()]; + let mut source_config = vec![true; self.target.inner().num_tasks()]; let mut completion_time = 0i64; for &task in target_solution { - completion_time += self.target.lengths()[task]; - if completion_time <= self.target.deadlines()[task] { + completion_time += self.target.inner().lengths()[task]; + if completion_time <= self.target.inner().deadlines()[task] { source_config[task] = false; } } @@ -38,28 +63,11 @@ impl ReductionResult for ReductionPartitionToSequencingToMinimizeTardyTaskWeight } } -impl crate::rules::AggregateReductionResult - for ReductionPartitionToSequencingToMinimizeTardyTaskWeight -{ - type Source = Partition; - type Target = SequencingToMinimizeTardyTaskWeight; - - fn target_problem(&self) -> &Self::Target { - &self.target - } - - fn extract_value(&self, value: crate::types::Min) -> crate::types::Or { - // The source is nonempty, so the common deadline always exists. - crate::types::Or(value.0 == Some(self.target.deadlines()[0])) - } -} - #[reduction( - aggregate = custom, transform = exact { num_tasks = "num_elements", })] -impl ReduceTo for Partition { +impl ReduceTo> for Partition { type Result = ReductionPartitionToSequencingToMinimizeTardyTaskWeight; fn reduce_to(&self) -> Result { @@ -69,7 +77,10 @@ impl ReduceTo for Partition { let deadlines = vec![common_deadline; self.num_elements()]; Ok(ReductionPartitionToSequencingToMinimizeTardyTaskWeight { - target: SequencingToMinimizeTardyTaskWeight::new(lengths, weights, deadlines), + target: Decision::new( + SequencingToMinimizeTardyTaskWeight::new(lengths, weights, deadlines), + common_deadline, + ), }) } } @@ -83,7 +94,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec, >( Partition::new(vec![3, 1, 1, 2, 2, 1]).unwrap(), SolutionPair { diff --git a/src/rules/partition_subsetsum.rs b/src/rules/partition_subsetsum.rs index 358f39ac9..f411ac7fe 100644 --- a/src/rules/partition_subsetsum.rs +++ b/src/rules/partition_subsetsum.rs @@ -7,6 +7,8 @@ use crate::models::misc::{Partition, SubsetSum}; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use num_bigint::{BigUint, ToBigUint}; /// Result of reducing Partition to SubsetSum. @@ -23,11 +25,18 @@ impl ReductionResult for ReductionPartitionToSubsetSum { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { - Ok(target_solution.to_vec()) + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => Ok(SolveOutcome::optimal(source, solution)?), + SolveOutcome::Feasible { solution, .. } => { + Ok(SolveOutcome::feasible(source, solution)?) + } + } } } diff --git a/src/rules/partition_sumofsquarespartition.rs b/src/rules/partition_sumofsquarespartition.rs index 6aa16d8c7..ebd3c468b 100644 --- a/src/rules/partition_sumofsquarespartition.rs +++ b/src/rules/partition_sumofsquarespartition.rs @@ -7,11 +7,9 @@ //! instance is YES iff the optimal target witness is a balanced split, in //! which case `Partition::evaluate(extracted_witness) = Or(true)`. //! -//! The target `SumOfSquaresPartition` model has no `J` bound field — it is a -//! pure minimisation (`Value = Min`). We therefore implement the rule in -//! the witness-style form used by `partition_multiprocessorscheduling.rs`: -//! the optimal target witness directly recovers the source YES/NO answer via -//! `source.evaluate(extract_solution(target_witness))`. +//! Recovery uses this optimum correspondence: a balanced optimum yields a source +//! solution; an unbalanced optimum proves source infeasibility. An unbalanced +//! candidate without optimality returns `InsufficientSolutionQuality`. //! //! Solution extraction is the identity (group assignment in the target is the //! subset assignment in the source). Small inputs with `|A| < 2` use a @@ -23,6 +21,9 @@ use crate::models::misc::{Partition, SumOfSquaresPartition}; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; +use crate::traits::Problem; /// Result of reducing Partition to SumOfSquaresPartition. #[derive(Debug, Clone)] @@ -45,10 +46,48 @@ impl ReductionResult for ReductionPartitionToSumOfSquaresPartition { /// Solution extraction preserves the source elements. The sentinel target /// appends elements, so only the prefix corresponding to actual source /// elements is mapped back. - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + let evaluation = source.evaluate(&solution)?; + if evaluation.0 { + Ok(SolveOutcome::Optimal { + solution, + evaluation, + }) + } else { + Ok(SolveOutcome::Infeasible) + } + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + let evaluation = source.evaluate(&solution)?; + if evaluation.0 { + Ok(SolveOutcome::Feasible { + solution, + evaluation, + }) + } else { + Err(crate::rules::ExtractionError::InsufficientSolutionQuality) + } + } + } + } +} + +impl ReductionPartitionToSumOfSquaresPartition { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok(target_solution[..self.source_n] .iter() .map(|&group| group == 1) diff --git a/src/rules/partitionintocliques_ilp.rs b/src/rules/partitionintocliques_ilp.rs index 00cd30881..4d9329a2b 100644 --- a/src/rules/partitionintocliques_ilp.rs +++ b/src/rules/partitionintocliques_ilp.rs @@ -4,6 +4,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::PartitionIntoCliques; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; #[derive(Debug, Clone)] @@ -21,10 +23,32 @@ impl ReductionResult for ReductionPartitionIntoCliquesToILP { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionPartitionIntoCliquesToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok(crate::rules::ilp_helpers::one_hot_decode_rows( target_solution, self.num_vertices, diff --git a/src/rules/partitionintocliques_minimumcoveringbycliques.rs b/src/rules/partitionintocliques_minimumcoveringbycliques.rs index 143e9ef2c..7d7e50862 100644 --- a/src/rules/partitionintocliques_minimumcoveringbycliques.rs +++ b/src/rules/partitionintocliques_minimumcoveringbycliques.rs @@ -8,11 +8,13 @@ //! where q counts distinct directed non-loop adjacencies. Each side includes //! a private vertex, so its forced clique exists even for an empty source. +use crate::models::decision::Decision; use crate::models::graph::{MinimumCoveringByCliques, PartitionIntoCliques}; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; -use crate::types::{Min, OptimizationValue, Or}; use std::collections::BTreeMap; #[derive(Debug, Clone)] @@ -87,7 +89,7 @@ impl OrlinLayout { let overflow = |operation: &str| { crate::rules::ReductionError::integer_overflow::< PartitionIntoCliques, - MinimumCoveringByCliques, + Decision>, >(operation) }; // The two sides each have n+q+1 vertices, including their private @@ -106,10 +108,9 @@ impl OrlinLayout { .and_then(|s| s.checked_add(n)) .and_then(|s| q.checked_mul(4).and_then(|cross| s.checked_add(cross))) .ok_or_else(|| overflow("counting target edges"))?; - as ReduceTo>>::exact_i64( - target_edges, - "representing every target cover value", - )?; + as ReduceTo< + Decision>, + >>::exact_i64(target_edges, "representing every target cover value")?; Ok((target_vertices, target_edges)) } } @@ -132,7 +133,7 @@ fn target_clique_bound( .ok_or_else(|| { crate::rules::ReductionError::integer_overflow::< PartitionIntoCliques, - MinimumCoveringByCliques, + Decision>, >("computing target clique bound") }) } @@ -140,26 +141,48 @@ fn target_clique_bound( /// Result of reducing PartitionIntoCliques to MinimumCoveringByCliques. #[derive(Debug, Clone)] pub struct ReductionPartitionIntoCliquesToMinimumCoveringByCliques { - target: MinimumCoveringByCliques, + target: Decision>, num_source_vertices: usize, - target_bound: i64, } impl ReductionResult for ReductionPartitionIntoCliquesToMinimumCoveringByCliques { type Source = PartitionIntoCliques; - type Target = MinimumCoveringByCliques; + type Target = Decision>; fn target_problem(&self) -> &Self::Target { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionPartitionIntoCliquesToMinimumCoveringByCliques { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { let n = self.num_source_vertices; let mut matching_labels: Vec<_> = self .target + .inner() .graph() .edges() .into_iter() @@ -186,29 +209,15 @@ impl ReductionResult for ReductionPartitionIntoCliquesToMinimumCoveringByCliques } } -impl crate::rules::AggregateReductionResult - for ReductionPartitionIntoCliquesToMinimumCoveringByCliques -{ - type Source = PartitionIntoCliques; - type Target = MinimumCoveringByCliques; - - fn target_problem(&self) -> &Self::Target { - &self.target - } - - fn extract_value(&self, target_value: Min) -> Or { - Or(Min::meets_bound(&target_value, &self.target_bound)) - } -} - #[reduction( - aggregate = custom, transform = upper_bound { num_vertices = "2 * num_vertices + 4 * num_edges + 4", num_edges = "(num_vertices + 2 * num_edges)^2 + 4 * num_vertices + 14 * num_edges + 2", } )] -impl ReduceTo> for PartitionIntoCliques { +impl ReduceTo>> + for PartitionIntoCliques +{ type Result = ReductionPartitionIntoCliquesToMinimumCoveringByCliques; fn reduce_to(&self) -> Result { @@ -216,14 +225,16 @@ impl ReduceTo> for PartitionIntoCliques>>::exact_i64( - self.num_cliques().min(n), - "converting effective clique bound", - )?; - let directed_pairs = >>::exact_i64( - q, - "converting gadget count", - )?; + let source_bound = + >>>::exact_i64( + self.num_cliques().min(n), + "converting effective clique bound", + )?; + let directed_pairs = + >>>::exact_i64( + q, + "converting gadget count", + )?; let target_bound = target_clique_bound(source_bound, directed_pairs)?; let left_vertices = layout.left_vertices(); let right_vertices = layout.right_vertices(); @@ -258,9 +269,8 @@ impl ReduceTo> for PartitionIntoCliques Vec>::reduce_to(&source) - .expect("reduction should succeed"); + let reduction = + ReduceTo::>>::reduce_to(&source) + .expect("reduction should succeed"); let layout = OrlinLayout::new(source.graph()); let target_config = edge_labels_from_clique_cover( - reduction.target_problem().graph(), + reduction.target_problem().inner().graph(), &[ vec![layout.x(0), layout.x(1), layout.y(0), layout.y(1)], vec![layout.x(2), layout.y(2)], @@ -323,7 +334,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec, + Decision>, >( source, SolutionPair { diff --git a/src/rules/partitionintopathsoflength2_boundedcomponentspanningforest.rs b/src/rules/partitionintopathsoflength2_boundedcomponentspanningforest.rs index 869e18d90..1a1e74a6b 100644 --- a/src/rules/partitionintopathsoflength2_boundedcomponentspanningforest.rs +++ b/src/rules/partitionintopathsoflength2_boundedcomponentspanningforest.rs @@ -13,6 +13,8 @@ use crate::models::graph::{BoundedComponentSpanningForest, PartitionIntoPathsOfLength2}; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; /// Result of reducing PartitionIntoPathsOfLength2 to BoundedComponentSpanningForest. @@ -33,11 +35,18 @@ impl ReductionResult for ReductionPPL2ToBCSF { /// /// Both problems use the same vertex-to-group assignment encoding, /// so the solution mapping is identity. - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { - Ok(target_solution.to_vec()) + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => Ok(SolveOutcome::optimal(source, solution)?), + SolveOutcome::Feasible { solution, .. } => { + Ok(SolveOutcome::feasible(source, solution)?) + } + } } } diff --git a/src/rules/partitionintopathsoflength2_ilp.rs b/src/rules/partitionintopathsoflength2_ilp.rs index fe713d7ce..8c2d70b0f 100644 --- a/src/rules/partitionintopathsoflength2_ilp.rs +++ b/src/rules/partitionintopathsoflength2_ilp.rs @@ -21,6 +21,8 @@ use crate::models::graph::PartitionIntoPathsOfLength2; use crate::reduction; use crate::rules::ilp_helpers::mccormick_product; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; /// Result of reducing PartitionIntoPathsOfLength2 to ILP. @@ -44,10 +46,32 @@ impl ReductionResult for ReductionPIPL2ToILP { } /// Extract solution: for each vertex v, find the unique group g where x_{v,g} = 1. - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionPIPL2ToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok(crate::rules::ilp_helpers::one_hot_decode_rows( target_solution, self.num_vertices, diff --git a/src/rules/partitionintotriangles_ilp.rs b/src/rules/partitionintotriangles_ilp.rs index 82e21297d..72567f3c7 100644 --- a/src/rules/partitionintotriangles_ilp.rs +++ b/src/rules/partitionintotriangles_ilp.rs @@ -13,6 +13,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::PartitionIntoTriangles; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; /// Result of reducing PartitionIntoTriangles to ILP. @@ -37,10 +39,32 @@ impl ReductionResult for ReductionPITToILP { } /// Extract solution: for each vertex v, find the unique group g where x_{v,g} = 1. - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionPITToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok(crate::rules::ilp_helpers::one_hot_decode_rows( target_solution, self.num_vertices, diff --git a/src/rules/pathconstrainednetworkflow_ilp.rs b/src/rules/pathconstrainednetworkflow_ilp.rs index d7868e606..e0e1ff096 100644 --- a/src/rules/pathconstrainednetworkflow_ilp.rs +++ b/src/rules/pathconstrainednetworkflow_ilp.rs @@ -7,6 +7,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::PathConstrainedNetworkFlow; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; /// Result of reducing PathConstrainedNetworkFlow to ILP. #[derive(Debug, Clone)] @@ -22,10 +24,32 @@ impl ReductionResult for ReductionPCNFToILP { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionPCNFToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { crate::rules::ilp_helpers::decode_usize_values(target_solution) } } diff --git a/src/rules/precedenceconstrainedscheduling_ilp.rs b/src/rules/precedenceconstrainedscheduling_ilp.rs index a837400c1..6d414cf67 100644 --- a/src/rules/precedenceconstrainedscheduling_ilp.rs +++ b/src/rules/precedenceconstrainedscheduling_ilp.rs @@ -14,6 +14,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::PrecedenceConstrainedScheduling; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; /// Result of reducing PrecedenceConstrainedScheduling to `ILP`. /// @@ -38,10 +40,32 @@ impl ReductionResult for ReductionPCSToILP { /// /// For each task j, find the time slot t where x_{j,t} = 1. /// Returns the time slot for each task (matching the `dims()` encoding of PCS). - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionPCSToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok(crate::rules::ilp_helpers::one_hot_decode_rows( target_solution, self.num_tasks, diff --git a/src/rules/preemptivescheduling_ilp.rs b/src/rules/preemptivescheduling_ilp.rs index 0d390f9f3..07b496635 100644 --- a/src/rules/preemptivescheduling_ilp.rs +++ b/src/rules/preemptivescheduling_ilp.rs @@ -25,6 +25,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::PreemptiveScheduling; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; /// Result of reducing PreemptiveScheduling to `ILP`. /// @@ -51,10 +53,32 @@ 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}`. - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionPSToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok((0..self.num_tasks) .map(|task| { (0..self.d_max) diff --git a/src/rules/prizecollectingsteinerforest_steinertree.rs b/src/rules/prizecollectingsteinerforest_steinertree.rs index 8447b4fd6..ee06eaf0c 100644 --- a/src/rules/prizecollectingsteinerforest_steinertree.rs +++ b/src/rules/prizecollectingsteinerforest_steinertree.rs @@ -36,6 +36,8 @@ use crate::models::graph::{PrizeCollectingSteinerForest, SteinerTree}; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; /// Result of reducing PCSF to SteinerTree. @@ -71,10 +73,32 @@ impl ReductionResult for ReductionPCSFToSteinerTree { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionPCSFToSteinerTree { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok({ let n = self.num_source_vertices; let m = self.num_source_edges; diff --git a/src/rules/quadraticassignment_ilp.rs b/src/rules/quadraticassignment_ilp.rs index af1b433cf..37101a692 100644 --- a/src/rules/quadraticassignment_ilp.rs +++ b/src/rules/quadraticassignment_ilp.rs @@ -12,6 +12,8 @@ use crate::models::algebraic::{ObjectiveSense, ILP}; use crate::reduction; use crate::rules::ilp_helpers::{mccormick_product, one_hot_assignment_constraints}; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; /// Result of reducing QuadraticAssignment to ILP. /// @@ -34,10 +36,32 @@ impl ReductionResult for ReductionQAPToILP { } /// Extract: for each facility i, output the unique location p with x_{i,p} = 1. - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionQAPToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok(crate::rules::ilp_helpers::one_hot_decode_rows( target_solution, self.num_facilities, diff --git a/src/rules/qubo_ilp.rs b/src/rules/qubo_ilp.rs index 9d2f6e1e0..638667784 100644 --- a/src/rules/qubo_ilp.rs +++ b/src/rules/qubo_ilp.rs @@ -18,6 +18,8 @@ use crate::models::algebraic::{ILPCoefficient, ObjectiveSense, ILP, QUBO}; use crate::reduction; use crate::rules::ilp_helpers::mccormick_product; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; /// Result of reducing QUBO to ILP. #[derive(Debug, Clone)] @@ -37,10 +39,35 @@ where &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionQUBOToILP +where + C: ILPCoefficient + crate::variant::VariantParam, +{ + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok(target_solution[..self.num_original] .iter() .map(|&value| value == 1) diff --git a/src/rules/rectilinearpicturecompression_ilp.rs b/src/rules/rectilinearpicturecompression_ilp.rs index d5be34842..ac4d931aa 100644 --- a/src/rules/rectilinearpicturecompression_ilp.rs +++ b/src/rules/rectilinearpicturecompression_ilp.rs @@ -7,6 +7,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::RectilinearPictureCompression; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; #[derive(Debug, Clone)] pub struct ReductionRPCToILP { @@ -21,10 +23,32 @@ impl ReductionResult for ReductionRPCToILP { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionRPCToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok(target_solution.iter().map(|&value| value == 1).collect()) } } diff --git a/src/rules/registersufficiency_ilp.rs b/src/rules/registersufficiency_ilp.rs index 24b00c96d..77dfa62af 100644 --- a/src/rules/registersufficiency_ilp.rs +++ b/src/rules/registersufficiency_ilp.rs @@ -11,6 +11,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::RegisterSufficiency; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; #[derive(Debug, Clone)] pub struct ReductionRegisterSufficiencyToILP { @@ -26,10 +28,32 @@ impl ReductionResult for ReductionRegisterSufficiencyToILP { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionRegisterSufficiencyToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { crate::rules::ilp_helpers::decode_usize_values(&target_solution[..self.num_vertices]) } } diff --git a/src/rules/registry.rs b/src/rules/registry.rs index 7f0b7bc1a..4e5bd9240 100644 --- a/src/rules/registry.rs +++ b/src/rules/registry.rs @@ -2,7 +2,7 @@ use crate::expr::Expr; use crate::parameters::{ParameterRelation, ParameterTransform, ParameterTransformError}; -use crate::rules::traits::{DynAggregateReductionResult, DynReductionResult}; +use crate::rules::traits::DynReductionResult; use std::any::Any; use std::collections::HashSet; @@ -132,32 +132,19 @@ impl From for ParameterContractError { } } -/// Interpret an accepted target optimum using this execution's mathematical relation. -pub type InterpretOptimum = dyn Fn(&dyn Any) -> crate::rules::ExtractionResult; - -/// One executed witness reduction, with optional value mapping over the same state. +/// One executed reduction, shared by all paths using this prefix. #[derive(Clone)] pub struct ExecutedStep { - /// Target access and witness recovery for this execution. pub witness: std::rc::Rc, - /// Value recovery sharing the witness result allocation, when supported. - pub aggregate: Option>, - /// Solver completion only: whether the mapped optimum supplies a source witness. - pub interpret_optimum: Option>, } /// Witness/config reduction executor stored in the inventory. pub type ReduceFn = fn(&dyn Any) -> Result; -/// Aggregate/value reduction executor stored in the inventory. -pub type AggregateReduceFn = - fn(&dyn Any) -> Result, crate::rules::ReductionError>; - /// Execution capabilities carried by a reduction edge. #[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub struct EdgeCapabilities { pub witness: bool, - pub aggregate: bool, /// Turing (multi-query) reduction: solving the source requires multiple /// adaptive queries to the target (e.g., binary search over a decision bound). #[serde(default)] @@ -165,14 +152,9 @@ pub struct EdgeCapabilities { } impl EdgeCapabilities { - pub(crate) const fn from_executors( - reduce_fn: Option, - reduce_aggregate_fn: Option, - turing: bool, - ) -> Self { + pub(crate) const fn from_executors(reduce_fn: Option, turing: bool) -> Self { Self { witness: reduce_fn.is_some(), - aggregate: reduce_aggregate_fn.is_some(), turing, } } @@ -197,12 +179,6 @@ pub struct ReductionEntry { /// Takes a `&dyn Any` (must be `&SourceType`), calls `ReduceTo::reduce_to()`, /// and returns one `ExecutedStep` sharing the result, or the edge's `ReductionError`. pub reduce_fn: Option, - /// Type-erased aggregate reduction executor. - /// Takes a `&dyn Any` (must be `&SourceType`), calls - /// `ReduceToAggregate::reduce_to_aggregate()`, and returns either a boxed - /// `DynAggregateReductionResult` or the edge's `ReductionError`. - pub reduce_aggregate_fn: Option, - /// Whether this is a Turing (multi-query) reduction. pub turing: bool, } @@ -224,7 +200,7 @@ impl ReductionEntry { /// Return the modes backed by this entry's executors. pub fn capabilities(&self) -> EdgeCapabilities { - EdgeCapabilities::from_executors(self.reduce_fn, self.reduce_aggregate_fn, self.turing) + EdgeCapabilities::from_executors(self.reduce_fn, self.turing) } /// Check if this reduction involves only the base (unweighted) variants. diff --git a/src/rules/resourceconstrainedscheduling_ilp.rs b/src/rules/resourceconstrainedscheduling_ilp.rs index 9bbbcea69..9ebf01c5f 100644 --- a/src/rules/resourceconstrainedscheduling_ilp.rs +++ b/src/rules/resourceconstrainedscheduling_ilp.rs @@ -8,6 +8,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::ResourceConstrainedScheduling; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; /// Result of reducing ResourceConstrainedScheduling to `ILP`. /// @@ -29,10 +31,32 @@ impl ReductionResult for ReductionRCSToILP { } /// Extract: for each task j, find the unique slot t with x_{j,t} = 1. - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionRCSToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok(crate::rules::ilp_helpers::one_hot_decode_rows( target_solution, self.num_tasks, diff --git a/src/rules/rootedtreearrangement_rootedtreestorageassignment.rs b/src/rules/rootedtreearrangement_rootedtreestorageassignment.rs index a08051394..9a4534166 100644 --- a/src/rules/rootedtreearrangement_rootedtreestorageassignment.rs +++ b/src/rules/rootedtreearrangement_rootedtreestorageassignment.rs @@ -13,6 +13,8 @@ use crate::models::graph::RootedTreeArrangement; use crate::models::set::RootedTreeStorageAssignment; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; /// Result of reducing RootedTreeArrangement to RootedTreeStorageAssignment. @@ -36,10 +38,32 @@ impl ReductionResult for ReductionRootedTreeArrangementToRootedTreeStorageAssign /// The target config is a parent array defining a rooted tree on X = V. /// The source config is [parent_array | identity_mapping] since X = V /// means the mapping f is the identity. - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionRootedTreeArrangementToRootedTreeStorageAssignment { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok({ let n = self.num_vertices; // target_solution is the parent array of the rooted tree on X = V diff --git a/src/rules/rootedtreestorageassignment_ilp.rs b/src/rules/rootedtreestorageassignment_ilp.rs index 46d67fcb8..63985e29b 100644 --- a/src/rules/rootedtreestorageassignment_ilp.rs +++ b/src/rules/rootedtreestorageassignment_ilp.rs @@ -9,6 +9,8 @@ use crate::models::set::RootedTreeStorageAssignment; use crate::reduction; use crate::rules::ilp_helpers::{mccormick_product, one_hot_decode_rows}; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; // Index helpers @@ -72,10 +74,32 @@ impl ReductionResult for ReductionRTSAToILP { } /// Decode parent array from one-hot parent indicators p_{v,u}. - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionRTSAToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok(one_hot_decode_rows(target_solution, self.n, self.n, 0)) } } @@ -404,7 +428,14 @@ pub(crate) fn canonical_rule_example_specs() -> Vec>( source, SolutionPair { diff --git a/src/rules/ruralpostman_ilp.rs b/src/rules/ruralpostman_ilp.rs index 0441e8bde..6ef6a505c 100644 --- a/src/rules/ruralpostman_ilp.rs +++ b/src/rules/ruralpostman_ilp.rs @@ -8,6 +8,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::RuralPostman; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; use crate::types::WeightElement; @@ -26,10 +28,32 @@ impl ReductionResult for ReductionRPToILP { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionRPToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { if self.target.num_vars() == 0 { Ok(vec![0; self.num_edges]) } else { diff --git a/src/rules/sat_circuitsat.rs b/src/rules/sat_circuitsat.rs index e18f6ca6e..5baa3ff09 100644 --- a/src/rules/sat_circuitsat.rs +++ b/src/rules/sat_circuitsat.rs @@ -7,6 +7,8 @@ use crate::models::formula::Satisfiability; use crate::models::formula::{Assignment, BooleanExpr, Circuit, CircuitSAT}; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use std::collections::HashSet; /// Result of reducing SAT to CircuitSAT. @@ -25,10 +27,32 @@ impl ReductionResult for ReductionSATToCircuit { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionSATToCircuit { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok({ self.source_var_indices .iter() diff --git a/src/rules/sat_coloring.rs b/src/rules/sat_coloring.rs index 4fb262177..754ffd30b 100644 --- a/src/rules/sat_coloring.rs +++ b/src/rules/sat_coloring.rs @@ -13,6 +13,8 @@ use crate::models::graph::KColoring; use crate::reduction; use crate::rules::sat_maximumindependentset::BoolVar; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::topology::SimpleGraph; use crate::variant::K3; use std::collections::HashMap; @@ -240,10 +242,32 @@ impl ReductionResult for ReductionSATToColoring { /// /// For each variable, we check if its positive literal vertex has TRUE color (0). /// If so, the variable is assigned true (1); otherwise false (0). - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionSATToColoring { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok({ // First determine which color is TRUE, FALSE, and AUX // Vertices 0, 1, 2 are TRUE, FALSE, AUX respectively diff --git a/src/rules/sat_ksat.rs b/src/rules/sat_ksat.rs index 9d27c416f..76ee4d55f 100644 --- a/src/rules/sat_ksat.rs +++ b/src/rules/sat_ksat.rs @@ -10,6 +10,8 @@ use crate::models::formula::{CNFClause, KSatisfiability, Satisfiability}; use crate::reduction; use crate::rules::sat_helpers::SatVariableAllocator; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::variant::{KValue, K2, K3, KN}; /// Result of reducing general SAT to K-SAT. @@ -32,10 +34,32 @@ impl ReductionResult for ReductionSATToKSAT { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionSATToKSAT { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok({ // Only return the original variables, discarding ancillas target_solution[..self.source_num_vars].to_vec() @@ -180,10 +204,32 @@ impl ReductionResult for ReductionKSATToSAT { &self.target } - fn extract_solution( + fn recover_result( + &self, + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionKSATToSAT { + fn map_solution( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok({ // Direct mapping - no transformation needed target_solution.to_vec() diff --git a/src/rules/sat_maximumindependentset.rs b/src/rules/sat_maximumindependentset.rs index 4e9d647f9..0d4c7d65c 100644 --- a/src/rules/sat_maximumindependentset.rs +++ b/src/rules/sat_maximumindependentset.rs @@ -8,12 +8,15 @@ //! A satisfying assignment corresponds to an independent set of size = num_clauses, //! where we pick exactly one literal from each clause. +use crate::models::decision::Decision; use crate::models::formula::Satisfiability; use crate::models::graph::MaximumIndependentSet; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::topology::SimpleGraph; -use crate::types::{Max, One, Or}; +use crate::types::One; /// A literal in the SAT problem, representing a variable or its negation. #[derive(Debug, Clone, PartialEq, Eq)] @@ -54,20 +57,18 @@ impl BoolVar { #[derive(Debug, Clone)] pub struct ReductionSATToIS { /// The target MaximumIndependentSet problem. - target: MaximumIndependentSet, + target: Decision>, /// Mapping from vertex index to the literal it represents. literals: Vec, /// The number of variables in the source SAT problem. num_source_variables: usize, /// The number of clauses in the source SAT problem. num_clauses: usize, - /// Exact independent-set cardinality certifying satisfiability. - target_size: i64, } impl ReductionResult for ReductionSATToIS { type Source = Satisfiability; - type Target = MaximumIndependentSet; + type Target = Decision>; fn target_problem(&self) -> &Self::Target { &self.target @@ -78,10 +79,32 @@ impl ReductionResult for ReductionSATToIS { /// For each selected vertex (representing a literal), we set the corresponding /// variable to make that literal true. Variables not covered by any selected /// literal default to false. - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionSATToIS { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { let mut assignment = vec![false; self.num_source_variables]; for (literal, &selected) in self.literals.iter().zip(target_solution) { if selected { @@ -92,19 +115,6 @@ impl ReductionResult for ReductionSATToIS { } } -impl crate::rules::AggregateReductionResult for ReductionSATToIS { - type Source = Satisfiability; - type Target = MaximumIndependentSet; - - fn target_problem(&self) -> &Self::Target { - &self.target - } - - fn extract_value(&self, target_value: Max) -> Or { - Or(target_value == Max(Some(self.target_size))) - } -} - impl ReductionSATToIS { /// Get the number of clauses in the source SAT problem. pub fn num_clauses(&self) -> usize { @@ -118,20 +128,20 @@ impl ReductionSATToIS { } #[reduction( - aggregate = custom, transform = upper_bound { num_vertices = "num_literals", num_edges = "num_literals^2", } )] -impl ReduceTo> for Satisfiability { +impl ReduceTo>> for Satisfiability { type Result = ReductionSATToIS; fn reduce_to(&self) -> Result { - let target_size = >>::exact_i64( - self.num_clauses(), - "representing the satisfying independent-set cardinality", - )?; + let target_size = + >>>::exact_i64( + self.num_clauses(), + "representing the satisfying independent-set cardinality", + )?; let mut literals: Vec = Vec::new(); let mut edges: Vec<(usize, usize)> = Vec::new(); @@ -171,11 +181,10 @@ impl ReduceTo> for Satisfiability { ); Ok(ReductionSATToIS { - target, + target: Decision::new(target, target_size), literals, num_source_variables: self.num_vars(), num_clauses: self.num_clauses(), - target_size, }) } } @@ -205,7 +214,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec, + Decision>, >( sat_seven_clause_example(), SolutionPair { diff --git a/src/rules/sat_minimumdominatingset.rs b/src/rules/sat_minimumdominatingset.rs index 0897b13d7..cc94c8109 100644 --- a/src/rules/sat_minimumdominatingset.rs +++ b/src/rules/sat_minimumdominatingset.rs @@ -14,13 +14,15 @@ //! - Selecting the negative literal vertex means the variable is false //! - Selecting the dummy vertex means the variable may be assigned either value +use crate::models::decision::Decision; use crate::models::formula::Satisfiability; use crate::models::graph::MinimumDominatingSet; use crate::reduction; use crate::rules::sat_maximumindependentset::BoolVar; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::topology::SimpleGraph; -use crate::types::{Min, Or}; use std::collections::BTreeMap; /// Result of reducing Satisfiability to MinimumDominatingSet. @@ -32,20 +34,18 @@ use std::collections::BTreeMap; #[derive(Debug, Clone)] pub struct ReductionSATToDS { /// The target MinimumDominatingSet problem. - target: MinimumDominatingSet, + target: Decision>, /// The number of variables in the source SAT problem. num_literals: usize, /// The number of clauses in the source SAT problem. num_clauses: usize, /// Original variable indices mapped to dense triangle indices. variables: BTreeMap, - /// Exact minimum size certifying satisfiability. - target_size: i64, } impl ReductionResult for ReductionSATToDS { type Source = Satisfiability; - type Target = MinimumDominatingSet; + type Target = Decision>; fn target_problem(&self) -> &Self::Target { &self.target @@ -58,10 +58,32 @@ impl ReductionResult for ReductionSATToDS { /// that original variable true. Negative, dummy and absent variables decode /// to false. The finite size certificate proves every clause is dominated /// by a selected literal and no clause vertex is selected. - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionSATToDS { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { let mut assignment = vec![false; self.num_literals]; for (&variable, &gadget) in &self.variables { assignment[variable] = target_solution[3 * gadget]; @@ -71,19 +93,6 @@ impl ReductionResult for ReductionSATToDS { } } -impl crate::rules::AggregateReductionResult for ReductionSATToDS { - type Source = Satisfiability; - type Target = MinimumDominatingSet; - - fn target_problem(&self) -> &Self::Target { - &self.target - } - - fn extract_value(&self, target_value: Min) -> Or { - Or(target_value == Min(Some(self.target_size))) - } -} - impl ReductionSATToDS { /// Compute the graph dimensions and exact certificate before allocation. fn target_dimensions( @@ -96,20 +105,21 @@ impl ReductionSATToDS { .ok_or_else(|| { crate::rules::ReductionError::integer_overflow::< Satisfiability, - MinimumDominatingSet, + Decision>, >("counting dominating-set vertices") })?; // All vertices may be selected, so every count up to this total must // fit the target objective, not only the optimum certificate. - >>::exact_i64( + >>>::exact_i64( num_vertices, "representing all dominating-set weights", )?; - let target_size = - >>::exact_i64( - num_variables, - "representing the satisfying dominating-set cardinality", - )?; + let target_size = >, + >>::exact_i64( + num_variables, + "representing the satisfying dominating-set cardinality", + )?; Ok((num_vertices, target_size)) } @@ -125,13 +135,12 @@ impl ReductionSATToDS { } #[reduction( - aggregate = custom, transform = upper_bound { num_vertices = "3 * num_vars + num_clauses", num_edges = "3 * num_vars + num_literals", } )] -impl ReduceTo> for Satisfiability { +impl ReduceTo>> for Satisfiability { type Result = ReductionSATToDS; fn reduce_to(&self) -> Result { @@ -187,11 +196,10 @@ impl ReduceTo> for Satisfiability { ); Ok(ReductionSATToDS { - target, + target: Decision::new(target, target_size), num_literals: self.num_vars(), num_clauses, variables, - target_size, }) } } @@ -218,7 +226,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec, + Decision>, >( source, SolutionPair { diff --git a/src/rules/satisfiability_integralflowhomologousarcs.rs b/src/rules/satisfiability_integralflowhomologousarcs.rs index c8b3484f3..72c221f45 100644 --- a/src/rules/satisfiability_integralflowhomologousarcs.rs +++ b/src/rules/satisfiability_integralflowhomologousarcs.rs @@ -10,6 +10,8 @@ use crate::models::formula::Satisfiability; use crate::models::graph::IntegralFlowHomologousArcs; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::topology::DirectedGraph; #[derive(Debug, Clone)] @@ -102,10 +104,32 @@ impl ReductionResult for ReductionSATToIntegralFlowHomologousArcs { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionSATToIntegralFlowHomologousArcs { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok({ self.variable_paths .iter() diff --git a/src/rules/satisfiability_maximum2satisfiability.rs b/src/rules/satisfiability_maximum2satisfiability.rs index e477085c1..936274d5d 100644 --- a/src/rules/satisfiability_maximum2satisfiability.rs +++ b/src/rules/satisfiability_maximum2satisfiability.rs @@ -1,45 +1,55 @@ //! Reduction from Satisfiability to Maximum 2-Satisfiability. +use crate::models::decision::Decision; use crate::models::formula::{CNFClause, Maximum2Satisfiability, Satisfiability}; use crate::reduction; use crate::rules::sat_helpers::SatVariableAllocator; use crate::rules::traits::{ReduceTo, ReductionResult}; -use crate::types::{Max, Or}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; /// Result of reducing SAT to MAX-2-SAT. #[derive(Debug, Clone)] pub struct ReductionSatisfiabilityToMaximum2Satisfiability { - target: Maximum2Satisfiability, + target: Decision, source_num_vars: usize, - target_score: i64, } impl ReductionResult for ReductionSatisfiabilityToMaximum2Satisfiability { type Source = Satisfiability; - type Target = Maximum2Satisfiability; + type Target = Decision; fn target_problem(&self) -> &Self::Target { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { - Ok(target_solution[..self.source_num_vars].to_vec()) + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } } } -impl crate::rules::AggregateReductionResult for ReductionSatisfiabilityToMaximum2Satisfiability { - type Source = Satisfiability; - type Target = Maximum2Satisfiability; - - fn target_problem(&self) -> &Self::Target { - &self.target - } - - fn extract_value(&self, value: Max) -> Or { - Or(value == Max(Some(self.target_score))) +impl ReductionSatisfiabilityToMaximum2Satisfiability { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { + Ok(target_solution[..self.source_num_vars].to_vec()) } } @@ -112,13 +122,12 @@ fn add_gjs_gadget(clause: &CNFClause, w: i64, target_clauses: &mut Vec for Satisfiability { +impl ReduceTo> for Satisfiability { type Result = ReductionSatisfiabilityToMaximum2Satisfiability; fn reduce_to(&self) -> Result { @@ -126,29 +135,22 @@ impl ReduceTo for Satisfiability { let mut variables = SatVariableAllocator::new("Satisfiability -> Maximum2Satisfiability", self.num_vars()) .map_err( - crate::rules::ReductionError::construction::< - Satisfiability, - Maximum2Satisfiability, - >, + >>::target_construction, )?; for clause in self.clauses() { add_normalized_clause(clause, &mut variables, &mut normalized).map_err( - crate::rules::ReductionError::construction::< - Satisfiability, - Maximum2Satisfiability, - >, + >>::target_construction, )?; } - let capacity = - normalized.len().checked_mul(10).ok_or_else(|| { - crate::rules::ReductionError::integer_overflow::< - Satisfiability, - Maximum2Satisfiability, - >("computing the target clause count") - })?; - let clause_count = >::exact_i64( + let capacity = normalized.len().checked_mul(10).ok_or_else(|| { + crate::rules::ReductionError::integer_overflow::< + Satisfiability, + Decision, + >("computing the target clause count") + })?; + let clause_count = >>::exact_i64( capacity, "representing every satisfied-clause count", )?; @@ -158,23 +160,18 @@ impl ReduceTo for Satisfiability { let target_score = (clause_count / 10) * 7; let mut target_clauses = Vec::with_capacity(capacity); for clause in &normalized { - let w = - variables.allocate().map_err( - crate::rules::ReductionError::construction::< - Satisfiability, - Maximum2Satisfiability, - >, - )?; + let w = variables.allocate().map_err( + >>::target_construction, + )?; add_gjs_gadget(clause, w, &mut target_clauses); } let target = Maximum2Satisfiability::try_new(variables.num_vars(), target_clauses) - .map_err(>::target_construction)?; + .map_err(>>::target_construction)?; Ok(ReductionSatisfiabilityToMaximum2Satisfiability { - target, + target: Decision::new(target, target_score), source_num_vars: self.num_vars(), - target_score, }) } } @@ -190,7 +187,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec( + crate::example_db::specs::rule_example_with_witness::<_, Decision>( source, SolutionPair { source_config: serde_json::json!(vec![true, true, true]), diff --git a/src/rules/satisfiability_naesatisfiability.rs b/src/rules/satisfiability_naesatisfiability.rs index fd3543ed9..177bbd548 100644 --- a/src/rules/satisfiability_naesatisfiability.rs +++ b/src/rules/satisfiability_naesatisfiability.rs @@ -11,6 +11,8 @@ use crate::models::formula::{CNFClause, NAESatisfiability, Satisfiability}; use crate::reduction; use crate::rules::sat_helpers::SatVariableAllocator; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; /// Result of reducing Satisfiability to NAE-Satisfiability. #[derive(Debug, Clone)] @@ -29,10 +31,32 @@ impl ReductionResult for ReductionSATToNAESAT { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionSATToNAESAT { + pub(crate) fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { let n = self.source_num_vars; let sentinel = target_solution[n]; Ok(target_solution[..n] diff --git a/src/rules/satisfiability_nontautology.rs b/src/rules/satisfiability_nontautology.rs index d932cc773..1983c6da8 100644 --- a/src/rules/satisfiability_nontautology.rs +++ b/src/rules/satisfiability_nontautology.rs @@ -6,6 +6,8 @@ use crate::models::formula::{NonTautology, Satisfiability}; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; /// Result of reducing SAT to NonTautology. #[derive(Debug, Clone)] @@ -21,11 +23,18 @@ impl ReductionResult for ReductionSATToNonTautology { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { - Ok(target_solution.to_vec()) + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => Ok(SolveOutcome::optimal(source, solution)?), + SolveOutcome::Feasible { solution, .. } => { + Ok(SolveOutcome::feasible(source, solution)?) + } + } } } diff --git a/src/rules/schedulingtominimizeweightedcompletiontime_ilp.rs b/src/rules/schedulingtominimizeweightedcompletiontime_ilp.rs index 17e80dfa3..5d238488f 100644 --- a/src/rules/schedulingtominimizeweightedcompletiontime_ilp.rs +++ b/src/rules/schedulingtominimizeweightedcompletiontime_ilp.rs @@ -10,6 +10,8 @@ use crate::models::misc::SchedulingToMinimizeWeightedCompletionTime; use crate::reduction; use crate::rules::ilp_helpers::one_hot_decode_rows; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; /// Result of reducing SchedulingToMinimizeWeightedCompletionTime to ILP. /// @@ -52,10 +54,32 @@ impl ReductionResult for ReductionSMWCTToILP { } /// Extract solution: for each task, find the processor with x_{t,p} = 1. - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionSMWCTToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok(one_hot_decode_rows( target_solution, self.num_tasks, diff --git a/src/rules/schedulingwithindividualdeadlines_ilp.rs b/src/rules/schedulingwithindividualdeadlines_ilp.rs index 2be00352f..a9fff7fab 100644 --- a/src/rules/schedulingwithindividualdeadlines_ilp.rs +++ b/src/rules/schedulingwithindividualdeadlines_ilp.rs @@ -16,6 +16,8 @@ use crate::models::misc::SchedulingWithIndividualDeadlines; use crate::reduction; use crate::rules::ilp_helpers::one_hot_decode_rows; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; /// Result of reducing SchedulingWithIndividualDeadlines to `ILP`. /// @@ -39,10 +41,32 @@ impl ReductionResult for ReductionSWIDToILP { /// Extract schedule from ILP solution. /// /// For each task j, find the time slot t where x_{j,t} = 1. - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionSWIDToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok(one_hot_decode_rows( target_solution, self.num_tasks, diff --git a/src/rules/sequencingtominimizemaximumcumulativecost_ilp.rs b/src/rules/sequencingtominimizemaximumcumulativecost_ilp.rs index f350cd09e..08823d93c 100644 --- a/src/rules/sequencingtominimizemaximumcumulativecost_ilp.rs +++ b/src/rules/sequencingtominimizemaximumcumulativecost_ilp.rs @@ -9,6 +9,8 @@ use crate::models::misc::SequencingToMinimizeMaximumCumulativeCost; use crate::reduction; use crate::rules::ilp_helpers::one_hot_decode; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; /// Result of reducing SequencingToMinimizeMaximumCumulativeCost to `ILP`. /// @@ -31,10 +33,32 @@ impl ReductionResult for ReductionSTMMCCToILP { } /// Extract: decode position assignment → permutation → Lehmer code. - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionSTMMCCToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok({ let n = self.num_tasks; diff --git a/src/rules/sequencingtominimizetardytaskweight_ilp.rs b/src/rules/sequencingtominimizetardytaskweight_ilp.rs index 69413f461..b2b06fb99 100644 --- a/src/rules/sequencingtominimizetardytaskweight_ilp.rs +++ b/src/rules/sequencingtominimizetardytaskweight_ilp.rs @@ -9,6 +9,8 @@ use crate::models::misc::SequencingToMinimizeTardyTaskWeight; use crate::reduction; use crate::rules::ilp_helpers::one_hot_decode; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; /// Result of reducing SequencingToMinimizeTardyTaskWeight to `ILP`. #[derive(Debug, Clone)] @@ -25,10 +27,32 @@ impl ReductionResult for ReductionSTMTTWToILP { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionSTMTTWToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok({ let n = self.num_tasks; // Decode the n*n block of x_{j,p} variables into a schedule permutation. diff --git a/src/rules/sequencingtominimizeweightedcompletiontime_ilp.rs b/src/rules/sequencingtominimizeweightedcompletiontime_ilp.rs index dd9978ac5..e3617b856 100644 --- a/src/rules/sequencingtominimizeweightedcompletiontime_ilp.rs +++ b/src/rules/sequencingtominimizeweightedcompletiontime_ilp.rs @@ -9,6 +9,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::SequencingToMinimizeWeightedCompletionTime; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; #[derive(Debug, Clone)] pub struct ReductionSTMWCTToILP { @@ -37,10 +39,32 @@ impl ReductionResult for ReductionSTMWCTToILP { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionSTMWCTToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok({ let mut schedule: Vec = (0..self.num_tasks).collect(); schedule.sort_by_key(|&task| (target_solution[task], task)); diff --git a/src/rules/sequencingtominimizeweightedtardiness_ilp.rs b/src/rules/sequencingtominimizeweightedtardiness_ilp.rs index b9f9174ec..a4a86ff47 100644 --- a/src/rules/sequencingtominimizeweightedtardiness_ilp.rs +++ b/src/rules/sequencingtominimizeweightedtardiness_ilp.rs @@ -8,6 +8,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::SequencingToMinimizeWeightedTardiness; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; /// Result of reducing SequencingToMinimizeWeightedTardiness to `ILP`. /// @@ -33,10 +35,32 @@ impl ReductionResult for ReductionSTMWTToILP { } /// Extract by sorting jobs by completion time C_j. - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionSTMWTToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok({ let n = self.num_tasks; let c_offset = self.num_order_vars; diff --git a/src/rules/sequencingwithdeadlinesandsetuptimes_ilp.rs b/src/rules/sequencingwithdeadlinesandsetuptimes_ilp.rs index 6a7a15b7c..ab5726d01 100644 --- a/src/rules/sequencingwithdeadlinesandsetuptimes_ilp.rs +++ b/src/rules/sequencingwithdeadlinesandsetuptimes_ilp.rs @@ -20,6 +20,8 @@ use crate::models::misc::SequencingWithDeadlinesAndSetUpTimes; use crate::reduction; use crate::rules::ilp_helpers::one_hot_decode; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; /// Result of reducing SequencingWithDeadlinesAndSetUpTimes to `ILP`. #[derive(Debug, Clone)] @@ -36,10 +38,32 @@ impl ReductionResult for ReductionSWDSTToILP { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionSWDSTToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok({ let n = self.num_tasks; // x_{j,p} occupies the first n*n variables: decode the permutation. diff --git a/src/rules/sequencingwithinintervals_ilp.rs b/src/rules/sequencingwithinintervals_ilp.rs index 8235c9af4..20355e37a 100644 --- a/src/rules/sequencingwithinintervals_ilp.rs +++ b/src/rules/sequencingwithinintervals_ilp.rs @@ -19,6 +19,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::SequencingWithinIntervals; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; /// Result of reducing SequencingWithinIntervals to `ILP`. /// @@ -43,10 +45,32 @@ impl ReductionResult for ReductionSWIToILP { /// /// For each task j, find the offset k where x_{j,k} = 1. /// Returns config[j] = k (start time offset from release time). - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionSWIToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok(self .task_layout .iter() @@ -165,7 +189,14 @@ pub(crate) fn canonical_rule_example_specs() -> Vec>( source, SolutionPair { diff --git a/src/rules/sequencingwithreleasetimesanddeadlines_ilp.rs b/src/rules/sequencingwithreleasetimesanddeadlines_ilp.rs index d47e56d21..d12e06e68 100644 --- a/src/rules/sequencingwithreleasetimesanddeadlines_ilp.rs +++ b/src/rules/sequencingwithreleasetimesanddeadlines_ilp.rs @@ -8,6 +8,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::SequencingWithReleaseTimesAndDeadlines; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; /// Result of reducing SequencingWithReleaseTimesAndDeadlines to `ILP`. /// @@ -29,10 +31,32 @@ impl ReductionResult for ReductionSWRTDToILP { } /// Extract by reading each task's start time and sorting tasks by start time. - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionSWRTDToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok({ let n = self.num_tasks; let horizon = self.time_horizon; diff --git a/src/rules/setsplitting_betweenness.rs b/src/rules/setsplitting_betweenness.rs index 5f5c9ff5e..b48c69045 100644 --- a/src/rules/setsplitting_betweenness.rs +++ b/src/rules/setsplitting_betweenness.rs @@ -11,6 +11,8 @@ use crate::models::misc::Betweenness; use crate::models::set::SetSplitting; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; /// Result of reducing SetSplitting to Betweenness. #[derive(Debug, Clone)] @@ -28,10 +30,32 @@ impl ReductionResult for ReductionSetSplittingToBetweenness { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionSetSplittingToBetweenness { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { let pole_position = target_solution[self.pole]; Ok(target_solution[..self.source_universe_size] .iter() diff --git a/src/rules/setsplitting_ilp.rs b/src/rules/setsplitting_ilp.rs index 682e944c0..23ebce3fd 100644 --- a/src/rules/setsplitting_ilp.rs +++ b/src/rules/setsplitting_ilp.rs @@ -13,6 +13,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::set::SetSplitting; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; /// Result of reducing SetSplitting to ILP. #[derive(Debug, Clone)] @@ -28,10 +30,32 @@ impl ReductionResult for ReductionSetSplittingToILP { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionSetSplittingToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok(target_solution.iter().map(|&value| value == 1).collect()) } } diff --git a/src/rules/shortestcommonsupersequence_ilp.rs b/src/rules/shortestcommonsupersequence_ilp.rs index ef65f31b4..aca74e33a 100644 --- a/src/rules/shortestcommonsupersequence_ilp.rs +++ b/src/rules/shortestcommonsupersequence_ilp.rs @@ -9,6 +9,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::ShortestCommonSupersequence; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; #[derive(Debug, Clone)] pub struct ReductionSCSToILP { @@ -27,10 +29,32 @@ impl ReductionResult for ReductionSCSToILP { /// At each position p, output the unique symbol a with x_{p,a} = 1. /// Uses alphabet_size + 1 symbols (last = padding). - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionSCSToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok(crate::rules::ilp_helpers::one_hot_decode_rows( target_solution, self.max_length, @@ -163,7 +187,14 @@ pub(crate) fn canonical_rule_example_specs() -> Vec>( source, SolutionPair { diff --git a/src/rules/shortestweightconstrainedpath_ilp.rs b/src/rules/shortestweightconstrainedpath_ilp.rs index 4c3e38b68..daf30cb48 100644 --- a/src/rules/shortestweightconstrainedpath_ilp.rs +++ b/src/rules/shortestweightconstrainedpath_ilp.rs @@ -10,6 +10,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::ShortestWeightConstrainedPath; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; use crate::types::WeightElement; @@ -40,10 +42,32 @@ impl ReductionResult for ReductionSWCPToILP { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionSWCPToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok({ (0..self.num_edges) .map(|edge_idx| { diff --git a/src/rules/sparsematrixcompression_ilp.rs b/src/rules/sparsematrixcompression_ilp.rs index 086b312ef..fd256607b 100644 --- a/src/rules/sparsematrixcompression_ilp.rs +++ b/src/rules/sparsematrixcompression_ilp.rs @@ -6,6 +6,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, SparseMatrixCompression, ILP}; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; #[derive(Debug, Clone)] pub struct ReductionSMCToILP { @@ -22,10 +24,32 @@ impl ReductionResult for ReductionSMCToILP { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionSMCToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok(crate::rules::ilp_helpers::one_hot_decode_rows( target_solution, self.num_rows, @@ -129,7 +153,14 @@ pub(crate) fn canonical_rule_example_specs() -> Vec>( source, SolutionPair { diff --git a/src/rules/spinglass_maxcut.rs b/src/rules/spinglass_maxcut.rs index 6988964a0..8e56561f4 100644 --- a/src/rules/spinglass_maxcut.rs +++ b/src/rules/spinglass_maxcut.rs @@ -7,6 +7,8 @@ use crate::models::graph::MaxCut; use crate::models::graph::SpinGlass; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; use crate::types::WeightElement; use num_traits::Zero; @@ -35,10 +37,42 @@ where &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionMaxCutToSG +where + W: WeightElement + + crate::variant::VariantParam + + PartialOrd + + num_traits::Num + + num_traits::Zero + + num_traits::Bounded + + std::ops::AddAssign + + std::ops::Mul, +{ + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok(target_solution.iter().map(|&spin| spin == 1).collect()) } } @@ -119,10 +153,42 @@ where &self.target } - fn extract_solution( + fn recover_result( + &self, + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionSGToMaxCut +where + W: WeightElement + + crate::variant::VariantParam + + PartialOrd + + num_traits::Num + + num_traits::Zero + + num_traits::Bounded + + std::ops::AddAssign + + std::ops::Mul, +{ + fn map_solution( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok({ match self.ancilla { None => target_solution diff --git a/src/rules/spinglass_qubo.rs b/src/rules/spinglass_qubo.rs index 2e4cb929f..0410881f5 100644 --- a/src/rules/spinglass_qubo.rs +++ b/src/rules/spinglass_qubo.rs @@ -9,6 +9,8 @@ use crate::models::algebraic::QUBO; use crate::models::graph::SpinGlass; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::topology::SimpleGraph; /// Result of reducing QUBO to SpinGlass. @@ -26,10 +28,32 @@ impl ReductionResult for ReductionQUBOToSG { } /// Solution maps directly (same binary encoding). - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionQUBOToSG { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok(target_solution.iter().map(|&spin| spin == 1).collect()) } } @@ -114,10 +138,35 @@ where &self.target } - fn extract_solution( + fn recover_result( + &self, + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionSGToQUBO +where + W: crate::types::WeightElement + crate::types::NumericSize + crate::variant::VariantParam, +{ + fn map_solution( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok(target_solution .iter() .map(|&bit| if bit { 1 } else { -1 }) diff --git a/src/rules/stackercrane_ilp.rs b/src/rules/stackercrane_ilp.rs index 91de9aae0..b91d45c91 100644 --- a/src/rules/stackercrane_ilp.rs +++ b/src/rules/stackercrane_ilp.rs @@ -9,6 +9,8 @@ use crate::models::misc::StackerCrane; use crate::reduction; use crate::rules::ilp_helpers::{mccormick_product, one_hot_decode}; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; /// Result of reducing StackerCrane to ILP. /// @@ -31,10 +33,32 @@ impl ReductionResult for ReductionSCToILP { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionSCToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok({ // Decode the permutation: for each position p, find the arc a with x_{a,p} = 1 one_hot_decode(target_solution, self.num_arcs, self.num_arcs, 0) diff --git a/src/rules/steinertree_ilp.rs b/src/rules/steinertree_ilp.rs index c4d54b303..15104a9eb 100644 --- a/src/rules/steinertree_ilp.rs +++ b/src/rules/steinertree_ilp.rs @@ -8,6 +8,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::SteinerTree; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; /// Binary layout: m edge selectors, n vertex selectors, then 2m flow arcs @@ -26,10 +28,32 @@ impl ReductionResult for ReductionSteinerTreeToILP { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionSteinerTreeToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok(target_solution[..self.num_edges] .iter() .map(|&value| value == 1) diff --git a/src/rules/stringtostringcorrection_ilp.rs b/src/rules/stringtostringcorrection_ilp.rs index 88228982c..56ef689bc 100644 --- a/src/rules/stringtostringcorrection_ilp.rs +++ b/src/rules/stringtostringcorrection_ilp.rs @@ -8,6 +8,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::StringToStringCorrection; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; /// Result of reducing StringToStringCorrection to ILP. #[derive(Debug, Clone)] @@ -54,10 +56,32 @@ impl ReductionResult for ReductionSTSCToILP { } /// Extract operation sequence from ILP solution. - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionSTSCToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok({ let n = self.n; let k = self.bound; @@ -390,7 +414,14 @@ pub(crate) fn canonical_rule_example_specs() -> Vec>( source, SolutionPair { diff --git a/src/rules/strongconnectivityaugmentation_ilp.rs b/src/rules/strongconnectivityaugmentation_ilp.rs index ce2aad71d..77f1c56cb 100644 --- a/src/rules/strongconnectivityaugmentation_ilp.rs +++ b/src/rules/strongconnectivityaugmentation_ilp.rs @@ -8,6 +8,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::StrongConnectivityAugmentation; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; #[derive(Debug, Clone)] pub struct ReductionSCAToILP { @@ -23,10 +25,32 @@ impl ReductionResult for ReductionSCAToILP { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionSCAToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok(target_solution[..self.num_candidates] .iter() .map(|&value| value == 1) @@ -199,7 +223,13 @@ pub(crate) fn canonical_rule_example_specs() -> Vec>( source, SolutionPair { diff --git a/src/rules/subgraphisomorphism_ilp.rs b/src/rules/subgraphisomorphism_ilp.rs index 958994f47..8f699a50e 100644 --- a/src/rules/subgraphisomorphism_ilp.rs +++ b/src/rules/subgraphisomorphism_ilp.rs @@ -12,6 +12,8 @@ use crate::models::graph::SubgraphIsomorphism; use crate::reduction; use crate::rules::ilp_helpers::{one_hot_assignment_constraints, one_hot_decode_rows}; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::topology::Graph; /// Result of reducing SubgraphIsomorphism to ILP. @@ -34,10 +36,32 @@ impl ReductionResult for ReductionSubIsoToILP { } /// Extract: for each pattern vertex v, output the unique host vertex u with x_{v,u} = 1. - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionSubIsoToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok(one_hot_decode_rows( target_solution, self.num_pattern_vertices, diff --git a/src/rules/subsetsum_closestvectorproblem.rs b/src/rules/subsetsum_closestvectorproblem.rs index 630cb8def..9e6421309 100644 --- a/src/rules/subsetsum_closestvectorproblem.rs +++ b/src/rules/subsetsum_closestvectorproblem.rs @@ -1,31 +1,55 @@ //! Reduction from Subset Sum to CVP using binary carry equations. use crate::models::algebraic::ClosestVectorProblem; +use crate::models::decision::Decision; use crate::models::misc::SubsetSum; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; -use crate::types::{Min, Or}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use num_rational::BigRational; /// Result of reducing SubsetSum to ClosestVectorProblem. #[derive(Debug, Clone)] pub struct ReductionSubsetSumToClosestVectorProblem { - target: ClosestVectorProblem, + target: Decision>, num_elements: usize, } impl ReductionResult for ReductionSubsetSumToClosestVectorProblem { type Source = SubsetSum; - type Target = ClosestVectorProblem; + type Target = Decision>; fn target_problem(&self) -> &Self::Target { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionSubsetSumToClosestVectorProblem { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok(target_solution[..self.num_elements] .iter() .map(|&value| value == 1) @@ -33,19 +57,6 @@ impl ReductionResult for ReductionSubsetSumToClosestVectorProblem { } } -impl crate::rules::AggregateReductionResult for ReductionSubsetSumToClosestVectorProblem { - type Source = SubsetSum; - type Target = ClosestVectorProblem; - - fn target_problem(&self) -> &Self::Target { - &self.target - } - - fn extract_value(&self, target_value: Min) -> Or { - Or(target_value == Min(Some(BigRational::from_integer(self.num_elements.into())))) - } -} - impl ReductionSubsetSumToClosestVectorProblem { /// Check the dense representation before allocating its columns. fn dimensions( @@ -53,9 +64,10 @@ impl ReductionSubsetSumToClosestVectorProblem { bit_width: u64, ) -> Result<(usize, usize, usize), crate::rules::ReductionError> { let overflow = || { - crate::rules::ReductionError::integer_overflow::>( - "sizing the binary-carry lattice", - ) + crate::rules::ReductionError::integer_overflow::< + SubsetSum, + Decision>, + >("sizing the binary-carry lattice") }; let bits = usize::try_from(bit_width).map_err(|_| overflow())?; let carries = bits.checked_sub(1).ok_or_else(overflow)?; @@ -72,13 +84,12 @@ impl ReductionSubsetSumToClosestVectorProblem { } #[reduction( - aggregate = custom, 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", }, )] -impl ReduceTo> for SubsetSum { +impl ReduceTo>> for SubsetSum { type Result = ReductionSubsetSumToClosestVectorProblem; fn reduce_to(&self) -> Result { @@ -115,10 +126,14 @@ impl ReduceTo> for SubsetSum { for bit in 0..bits { target[rows - 1 - bit] = i64::from(self.target().bit(bit as u64)); } - let target = ClosestVectorProblem::new(basis, target) - .map_err(>>::target_construction)?; + let target = ClosestVectorProblem::new(basis, target).map_err( + >>>::target_construction, + )?; Ok(ReductionSubsetSumToClosestVectorProblem { - target, + target: Decision::new( + target, + BigRational::from_integer(self.num_elements().into()), + ), num_elements: n, }) } @@ -131,7 +146,10 @@ pub(crate) fn canonical_rule_example_specs() -> Vec>( + crate::example_db::specs::rule_example_with_witness::< + _, + Decision>, + >( SubsetSum::new(vec![3u32, 7, 1, 8], 11u32), SolutionPair { source_config: serde_json::json!(vec![true, false, false, true]), diff --git a/src/rules/subsetsum_integerexpressionmembership.rs b/src/rules/subsetsum_integerexpressionmembership.rs index a0346bd1d..f5ecfdff7 100644 --- a/src/rules/subsetsum_integerexpressionmembership.rs +++ b/src/rules/subsetsum_integerexpressionmembership.rs @@ -2,6 +2,8 @@ use crate::models::misc::SubsetSum; use crate::models::misc::{IntExpr, IntegerExpressionMembership}; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use num_traits::ToPrimitive; #[derive(Debug, Clone)] @@ -17,10 +19,32 @@ impl ReductionResult for ReductionSubsetSumToIntegerExpressionMembership { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionSubsetSumToIntegerExpressionMembership { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok({ // Union choice 0 = left = Atom(1) = exclude, choice 1 = right = Atom(s_i+1) = include. // This maps directly to SubsetSum's 0/1 include/exclude encoding. diff --git a/src/rules/subsetsum_integerknapsack.rs b/src/rules/subsetsum_integerknapsack.rs index 1a244f968..72f89dd63 100644 --- a/src/rules/subsetsum_integerknapsack.rs +++ b/src/rules/subsetsum_integerknapsack.rs @@ -41,7 +41,6 @@ inventory::submit! { }, module_path: module_path!(), reduce_fn: None, - reduce_aggregate_fn: None, turing: false, } } diff --git a/src/rules/subsetsum_partition.rs b/src/rules/subsetsum_partition.rs index 43fb710c8..efb2de6cb 100644 --- a/src/rules/subsetsum_partition.rs +++ b/src/rules/subsetsum_partition.rs @@ -3,6 +3,8 @@ use crate::models::misc::{Partition, SubsetSum}; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use num_bigint::BigUint; use num_traits::ToPrimitive; use std::cmp::Ordering; @@ -30,10 +32,32 @@ impl ReductionResult for ReductionSubsetSumToPartition { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionSubsetSumToPartition { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok({ let source_bits = &target_solution[..self.source_len]; diff --git a/src/rules/sumofsquarespartition_ilp.rs b/src/rules/sumofsquarespartition_ilp.rs index 469d3bae6..a8debff85 100644 --- a/src/rules/sumofsquarespartition_ilp.rs +++ b/src/rules/sumofsquarespartition_ilp.rs @@ -21,6 +21,8 @@ use crate::models::misc::SumOfSquaresPartition; use crate::reduction; use crate::rules::ilp_helpers::mccormick_product; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; /// Result of reducing SumOfSquaresPartition to ILP. /// @@ -57,10 +59,32 @@ impl ReductionResult for ReductionSSPToILP { } /// Extract solution: for each element i, find the unique group g where x_{i,g} = 1. - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionSSPToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok(crate::rules::ilp_helpers::one_hot_decode_rows( target_solution, self.num_elements, diff --git a/src/rules/test_helpers.rs b/src/rules/test_helpers.rs index f43dafa45..e3abf7857 100644 --- a/src/rules/test_helpers.rs +++ b/src/rules/test_helpers.rs @@ -1,6 +1,7 @@ use crate::rules::{ReductionChain, ReductionResult}; use crate::solvers::BruteForce; use crate::solvers::SolutionAggregate; +use crate::solvers::SolveOutcome; use crate::traits::Problem; use std::collections::HashSet; @@ -14,7 +15,7 @@ fn verify_optimization_round_trip( Source: Problem + 'static, Source::Solution: Eq + std::hash::Hash + std::fmt::Debug + 'static, ::Value: SolutionAggregate + std::fmt::Debug + PartialEq, - Extract: Fn(&TargetSolution) -> Source::Solution, + Extract: Fn(TargetSolution) -> Source::Solution, { assert!( !target_solutions.is_empty(), @@ -39,7 +40,7 @@ fn verify_optimization_round_trip( .expect("reference set is non-empty"), ); let extracted: HashSet = - target_solutions.iter().map(extract_solution).collect(); + target_solutions.into_iter().map(extract_solution).collect(); assert!( !extracted.is_empty(), "{context}: no extracted source solutions" @@ -67,14 +68,14 @@ fn verify_satisfaction_round_trip( Source: Problem + 'static, Source::Solution: Eq + std::hash::Hash + std::fmt::Debug + 'static, ::Value: SolutionAggregate + std::fmt::Debug, - Extract: Fn(&TargetSolution) -> Source::Solution, + Extract: Fn(TargetSolution) -> Source::Solution, { assert!( !target_solutions.is_empty(), "{context}: target solver found no {target_solution_kind} solutions" ); let extracted: HashSet = - target_solutions.iter().map(extract_solution).collect(); + target_solutions.into_iter().map(extract_solution).collect(); assert!( !extracted.is_empty(), "{context}: no extracted source solutions" @@ -113,7 +114,16 @@ pub(crate) fn assert_optimization_round_trip_from_optimization_target( verify_optimization_round_trip( source, target_solutions, - |target_solution| reduction.extract_solution(target_solution).unwrap(), + |target_solution| { + reduction + .recover_result( + source, + SolveOutcome::optimal(reduction.target_problem(), target_solution).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution") + }, "optimal", context, ); @@ -138,7 +148,16 @@ pub(crate) fn assert_optimization_round_trip_from_satisfaction_target( verify_optimization_round_trip( source, target_solutions, - |target_solution| reduction.extract_solution(target_solution).unwrap(), + |target_solution| { + reduction + .recover_result( + source, + SolveOutcome::optimal(reduction.target_problem(), target_solution).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution") + }, "satisfying", context, ); @@ -164,7 +183,12 @@ pub(crate) fn assert_optimization_round_trip_chain( target_solutions, |target_solution| { chain - .extract_solution::(target_solution) + .recover_result::( + source, + SolveOutcome::optimal(chain.target_problem::(), target_solution) + .unwrap(), + ) + .map(|outcome| outcome.into_solution().unwrap()) .unwrap() }, "optimal", @@ -191,7 +215,16 @@ pub(crate) fn assert_satisfaction_round_trip_from_optimization_target( verify_satisfaction_round_trip( source, target_solutions, - |target_solution| reduction.extract_solution(target_solution).unwrap(), + |target_solution| { + reduction + .recover_result( + source, + SolveOutcome::optimal(reduction.target_problem(), target_solution).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution") + }, "optimal", context, ); @@ -216,7 +249,16 @@ pub(crate) fn assert_satisfaction_round_trip_from_satisfaction_target( verify_satisfaction_round_trip( source, target_solutions, - |target_solution| reduction.extract_solution(target_solution).unwrap(), + |target_solution| { + reduction + .recover_result( + source, + SolveOutcome::optimal(reduction.target_problem(), target_solution).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution") + }, "satisfying", context, ); @@ -239,7 +281,14 @@ where let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + source, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(source.evaluate(&extracted).unwrap(), bf_value); } @@ -252,6 +301,7 @@ mod tests { assert_satisfaction_round_trip_from_satisfaction_target, }; use crate::rules::ReductionResult; + use crate::solvers::{ProblemOutcome, SolveOutcome}; use crate::traits::Problem; use crate::types::{Max, Or}; @@ -384,11 +434,32 @@ mod tests { &self.target } - fn extract_solution( + fn recover_result( + &self, + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } + } + + impl OptToOptReduction { + fn map_solution( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> - { + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok(target_solution.to_vec()) } } @@ -405,11 +476,32 @@ mod tests { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> - { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } + } + + impl OptToSatReduction { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok(target_solution.to_vec()) } } @@ -426,11 +518,32 @@ mod tests { &self.target } - fn extract_solution( + fn recover_result( + &self, + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } + } + + impl SatToOptReduction { + fn map_solution( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> - { + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok(target_solution.to_vec()) } } @@ -447,11 +560,32 @@ mod tests { &self.target } - fn extract_solution( + fn recover_result( + &self, + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } + } + + impl SatToSatReduction { + fn map_solution( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> - { + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok(target_solution.to_vec()) } } diff --git a/src/rules/threedimensionalmatching_ilp.rs b/src/rules/threedimensionalmatching_ilp.rs index a3dce750c..790f3968f 100644 --- a/src/rules/threedimensionalmatching_ilp.rs +++ b/src/rules/threedimensionalmatching_ilp.rs @@ -4,6 +4,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::set::ThreeDimensionalMatching; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; #[derive(Debug, Clone)] pub struct ReductionThreeDimensionalMatchingToILP { @@ -18,10 +20,32 @@ impl ReductionResult for ReductionThreeDimensionalMatchingToILP { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionThreeDimensionalMatchingToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok(target_solution.iter().map(|&value| value == 1).collect()) } } diff --git a/src/rules/threedimensionalmatching_minimumweightdecoding.rs b/src/rules/threedimensionalmatching_minimumweightdecoding.rs index 9f39d4afa..f356fbf44 100644 --- a/src/rules/threedimensionalmatching_minimumweightdecoding.rs +++ b/src/rules/threedimensionalmatching_minimumweightdecoding.rs @@ -11,9 +11,9 @@ //! //! `source.evaluate(S) == Or(true)` ⇔ `target.evaluate(x) == Min(Some(q))`, //! -//! where `S = { t_j ∈ T : x_j = 1 }`. We rely on the witness-extraction -//! route `source.evaluate(extract_solution(x))` rather than comparing the -//! optimum value directly, mirroring `partition_sumofsquarespartition.rs`. +//! where `S = { t_j ∈ T : x_j = 1 }`. Thus recovery of an optimum yields +//! either an exact matching or `Infeasible`. A candidate without optimality +//! that does not decode to a matching returns `InsufficientSolutionQuality`. //! //! **Sentinel branch.** `MinimumWeightDecoding::new` panics on zero-row or //! zero-column matrices, so degenerate inputs (`q = 0` or `T = []`) emit a @@ -25,6 +25,9 @@ use crate::models::algebraic::MinimumWeightDecoding; use crate::models::set::ThreeDimensionalMatching; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; +use crate::traits::Problem; /// Result of reducing ThreeDimensionalMatching to MinimumWeightDecoding. #[derive(Debug, Clone)] @@ -47,10 +50,48 @@ impl ReductionResult for ReductionThreeDimensionalMatchingToMinimumWeightDecodin /// The target codeword prefix is the source subset indicator over the same /// triple index set. The sentinel target appends one synthetic column, so /// an empty source maps back to the empty prefix. - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + let evaluation = source.evaluate(&solution)?; + if evaluation.0 { + Ok(SolveOutcome::Optimal { + solution, + evaluation, + }) + } else { + Ok(SolveOutcome::Infeasible) + } + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + let evaluation = source.evaluate(&solution)?; + if evaluation.0 { + Ok(SolveOutcome::Feasible { + solution, + evaluation, + }) + } else { + Err(crate::rules::ExtractionError::InsufficientSolutionQuality) + } + } + } + } +} + +impl ReductionThreeDimensionalMatchingToMinimumWeightDecoding { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok(target_solution[..self.source_num_triples].to_vec()) } } diff --git a/src/rules/threedimensionalmatching_threepartition.rs b/src/rules/threedimensionalmatching_threepartition.rs index 02201b248..7fe25a94d 100644 --- a/src/rules/threedimensionalmatching_threepartition.rs +++ b/src/rules/threedimensionalmatching_threepartition.rs @@ -9,6 +9,8 @@ use crate::models::misc::ThreePartition; use crate::models::set::ThreeDimensionalMatching; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; #[derive(Debug, Clone, Copy)] enum Step2Item { @@ -262,10 +264,32 @@ impl ReductionResult for ReductionThreeDimensionalMatchingToThreePartition { /// Reverse the 4-Partition -> 3-Partition pairing gadget, then decode the /// surviving real ABCD groups back into selected source triples. - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionThreeDimensionalMatchingToThreePartition { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { let mut groups = vec![Vec::with_capacity(3); self.target.num_groups()]; let mut positions = Vec::with_capacity(target_solution.len()); for (element, &group) in target_solution.iter().enumerate() { diff --git a/src/rules/threepartition_resourceconstrainedscheduling.rs b/src/rules/threepartition_resourceconstrainedscheduling.rs index 993b638a2..e85d008aa 100644 --- a/src/rules/threepartition_resourceconstrainedscheduling.rs +++ b/src/rules/threepartition_resourceconstrainedscheduling.rs @@ -21,6 +21,8 @@ use crate::models::misc::{ResourceConstrainedScheduling, ThreePartition}; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; /// Result of reducing ThreePartition to ResourceConstrainedScheduling. #[derive(Debug, Clone)] @@ -38,11 +40,18 @@ impl ReductionResult for ReductionThreePartitionToRCS { /// Solution extraction: identity mapping. /// ThreePartition config (group index 0..m-1) maps directly to time slot assignment. - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { - Ok(target_solution.to_vec()) + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => Ok(SolveOutcome::optimal(source, solution)?), + SolveOutcome::Feasible { solution, .. } => { + Ok(SolveOutcome::feasible(source, solution)?) + } + } } } diff --git a/src/rules/threepartition_sequencingwithreleasetimesanddeadlines.rs b/src/rules/threepartition_sequencingwithreleasetimesanddeadlines.rs index 871da9e18..b20cc2e91 100644 --- a/src/rules/threepartition_sequencingwithreleasetimesanddeadlines.rs +++ b/src/rules/threepartition_sequencingwithreleasetimesanddeadlines.rs @@ -14,6 +14,8 @@ use crate::models::misc::{SequencingWithReleaseTimesAndDeadlines, ThreePartition}; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; /// Number of element tasks (= source.num_elements() = 3m). fn num_element_tasks(source: &ThreePartition) -> usize { @@ -47,10 +49,32 @@ impl ReductionResult for ReductionThreePartitionToSRTD { /// /// Simulate the task permutation to find each task's start time, then assign each element task to its slot /// based on start_time / (B + 1). - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionThreePartitionToSRTD { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok({ // Simulate the schedule to find start times let mut current_time: i64 = 0; diff --git a/src/rules/timetabledesign_ilp.rs b/src/rules/timetabledesign_ilp.rs index 3b40bc01d..06dcf1113 100644 --- a/src/rules/timetabledesign_ilp.rs +++ b/src/rules/timetabledesign_ilp.rs @@ -8,6 +8,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::TimetableDesign; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; /// Result of reducing TimetableDesign to `ILP`. /// @@ -31,10 +33,32 @@ impl ReductionResult for ReductionTDToILP { /// Extract: direct identity mapping — the ILP variable layout matches the /// source configuration layout exactly. - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionTDToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok((0..self.num_craftsmen) .map(|craftsman| { (0..self.num_tasks) diff --git a/src/rules/traits.rs b/src/rules/traits.rs index 7fd097e97..582f388ca 100644 --- a/src/rules/traits.rs +++ b/src/rules/traits.rs @@ -1,8 +1,6 @@ //! Core traits for problem reductions. use crate::traits::Problem; -use serde::de::DeserializeOwned; -use serde::Serialize; use std::any::Any; use std::marker::PhantomData; @@ -129,6 +127,8 @@ impl ReductionError { /// Failure to map a target witness back into the source configuration space. #[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] pub enum ExtractionError { + #[error("the target result does not establish the conditions required for source recovery")] + InsufficientSolutionQuality, #[error("{0}")] InvalidTargetSolution(String), #[error("{source_problem} -> {target_problem}: {message}")] @@ -137,7 +137,7 @@ pub enum ExtractionError { target_problem: &'static str, message: String, }, - #[error("target evaluation failed during extraction: {0}")] + #[error("problem evaluation failed during recovery: {0}")] Evaluation(#[from] crate::traits::EvaluationError), } @@ -162,8 +162,7 @@ pub type ExtractionResult = std::result::Result; /// Result of reducing a source problem to a target problem. /// -/// This trait encapsulates the target problem and provides methods -/// to extract solutions back to the source problem space. +/// Stores the target and recovers complete source results using the executed mapping. pub trait ReductionResult { /// The source problem type. type Source: Problem; @@ -173,19 +172,16 @@ pub trait ReductionResult { /// Get a reference to the target problem. fn target_problem(&self) -> &Self::Target; - /// Extract a solution from target problem space to source problem space. - /// - /// # Arguments - /// * `target_solution` - A target solution satisfying this reduction's - /// mathematical premises, including optimality when required. The solver - /// or external caller establishes these premises before extraction. - /// - /// # Returns - /// The corresponding solution in the source problem space - fn extract_solution( + /// Recover the complete source result using this execution's mathematical relation. + /// `source` must be the instance used to construct this reduction result. + /// Optimal results must preserve optimality or prove source infeasibility. + /// Feasible incumbents may establish only what the rule proves; insufficient + /// witness quality is an error, never evidence of source infeasibility. + fn recover_result( &self, - target_solution: &::Solution, - ) -> ExtractionResult<::Solution>; + source: &Self::Source, + target: crate::solvers::ProblemOutcome, + ) -> ExtractionResult>; } /// Trait for problems that can be reduced to target type T. @@ -208,12 +204,10 @@ pub trait ReductionResult { /// let reduction = sat_problem.reduce_to().expect("reduction should succeed"); /// let is_problem = reduction.target_problem(); /// -/// // Solve and extract solutions -/// let solver = BruteForce::new(); -/// let solutions = solver.find_all_witnesses(is_problem).unwrap(); -/// let sat_solutions: Vec<_> = solutions.iter() -/// .map(|s| reduction.extract_solution(s)) -/// .collect(); +/// // Solve the target and recover its complete source result. +/// let solution = BruteForce::new().solve(is_problem)?.unwrap(); +/// let target_result = SolveOutcome::optimal(is_problem, solution)?; +/// let source_result = reduction.recover_result(&sat_problem, target_result)?; /// ``` pub trait ReduceTo: Problem { /// The reduction result type. @@ -239,36 +233,6 @@ pub trait ReduceTo: Problem { fn reduce_to(&self) -> Result; } -/// Result of reducing a source problem to a target problem for aggregate values. -/// -/// Unlike [`ReductionResult`], this trait maps aggregate values back from target -/// space to source space instead of mapping witness configurations. -pub trait AggregateReductionResult { - /// The source problem type. - type Source: Problem; - /// The target problem type. - type Target: Problem; - - /// Get a reference to the target problem. - fn target_problem(&self) -> &Self::Target; - - /// Extract an aggregate value from target problem space back to source space. - fn extract_value( - &self, - target_value: ::Value, - ) -> ::Value; -} - -/// Trait for problems that can be reduced to target type T for aggregate-value -/// workflows. -pub trait ReduceToAggregate: Problem { - /// The reduction result type. - type Result: AggregateReductionResult; - - /// Reduce this problem to the target problem type. - fn reduce_to_aggregate(&self) -> Result; -} - /// Reduction result for an explicit conversion between variants of one model. /// /// The target witness is also the source witness. @@ -292,22 +256,6 @@ impl ReductionResult for VariantReductionResult where S: Problem, T: Problem, - S::Solution: Clone, -{ - type Source = S; - type Target = T; - - fn target_problem(&self) -> &Self::Target { - &self.target - } - - fn extract_solution(&self, target_solution: &T::Solution) -> ExtractionResult { - Ok(target_solution.clone()) - } -} - -impl> AggregateReductionResult - for VariantReductionResult { type Source = S; type Target = T; @@ -316,105 +264,125 @@ impl> AggregateReductionResult &self.target } - fn extract_value(&self, target_value: T::Value) -> S::Value { - target_value + fn recover_result( + &self, + source: &S, + target: crate::solvers::ProblemOutcome, + ) -> ExtractionResult> { + use crate::solvers::SolveOutcome; + Ok(match target { + SolveOutcome::Optimal { solution, .. } => SolveOutcome::optimal(source, solution)?, + SolveOutcome::Feasible { solution, .. } => SolveOutcome::feasible(source, solution)?, + SolveOutcome::Infeasible => SolveOutcome::Infeasible, + }) } } -/// Type-erased reduction result for runtime-discovered paths. -/// -/// Implemented automatically for all `ReductionResult` types via blanket impl. -/// Used internally by `ReductionChain`. +/// Type erasure for executed reduction results. Mathematical recovery remains typed. pub trait DynReductionResult { - /// Get the target problem as a type-erased reference. fn target_problem_any(&self) -> &dyn Any; - /// Extract a solution from target space to source space. - fn extract_solution_dyn(&self, target_solution: &dyn Any) -> ExtractionResult>; - /// Serialize a source-space solution after the complete extraction chain. - fn source_solution_json( + fn source_solution_json(&self, solution: &dyn Any) -> ExtractionResult; + fn recover_result_dyn( + &self, + source: &dyn Any, + target: crate::solvers::ErasedOutcome, + ) -> ExtractionResult; + fn target_result_from_json( &self, - source_solution: &dyn Any, - ) -> ExtractionResult; - /// Deserialize the concrete target witness at the dynamic boundary. - fn target_solution_from_json( + target: crate::solvers::SolveOutcome, + ) -> ExtractionResult; + fn source_result_json( &self, - target_solution: serde_json::Value, - ) -> ExtractionResult>; + source: crate::solvers::ErasedOutcome, + ) -> ExtractionResult; } impl DynReductionResult for R where + R::Source: 'static, R::Target: 'static, - ::Solution: 'static, - ::Solution: serde::de::DeserializeOwned, - ::Solution: 'static, - ::Solution: serde::Serialize, + ::Solution: serde::de::DeserializeOwned + 'static, + ::Value: 'static, + ::Solution: serde::Serialize + 'static, + ::Value: std::fmt::Display + 'static, { fn target_problem_any(&self) -> &dyn Any { - self.target_problem() as &dyn Any - } - fn extract_solution_dyn(&self, target_solution: &dyn Any) -> ExtractionResult> { - let target_solution = target_solution - .downcast_ref::<::Solution>() - .ok_or_else(|| { - ExtractionError::invalid(format!( - "target solution type mismatch: expected {}", - std::any::type_name::<::Solution>() - )) - })?; - self.extract_solution(target_solution) - .map(|solution| Box::new(solution) as Box) - .map_err(|error| error.for_reduction::()) + self.target_problem() } - fn source_solution_json( - &self, - source_solution: &dyn Any, - ) -> ExtractionResult { - let source_solution = source_solution + fn source_solution_json(&self, solution: &dyn Any) -> ExtractionResult { + let solution = solution .downcast_ref::<::Solution>() .ok_or_else(|| ExtractionError::invalid("source solution type mismatch"))?; - serde_json::to_value(source_solution).map_err(|error| { - ExtractionError::invalid(format!("source solution serialization failed: {error}")) - }) + serde_json::to_value(solution).map_err(|error| ExtractionError::invalid(error.to_string())) } - fn target_solution_from_json( + fn recover_result_dyn( &self, - target_solution: serde_json::Value, - ) -> ExtractionResult> { - serde_json::from_value::<::Solution>(target_solution) - .map(|solution| Box::new(solution) as Box) - .map_err(|error| { - ExtractionError::invalid(format!("target solution deserialization failed: {error}")) - }) + source: &dyn Any, + target: crate::solvers::ErasedOutcome, + ) -> ExtractionResult { + let source = source + .downcast_ref::() + .ok_or_else(|| ExtractionError::invalid("source problem type mismatch"))?; + let target = crate::solvers::downcast_outcome(target)?; + self.recover_result(source, target) + .map(crate::solvers::erase_outcome) + .map_err(ExtractionError::for_reduction::) } -} -/// Type-erased aggregate reduction result for runtime-discovered paths. -pub trait DynAggregateReductionResult { - /// Get the target problem as a type-erased reference. - fn target_problem_any(&self) -> &dyn Any; - /// Extract an aggregate value from target space to source space. - fn extract_value_dyn(&self, target_value: serde_json::Value) -> serde_json::Value; -} - -impl DynAggregateReductionResult for R -where - R::Target: 'static, - ::Value: Serialize + DeserializeOwned, - ::Value: Serialize, -{ - fn target_problem_any(&self) -> &dyn Any { - self.target_problem() as &dyn Any + fn target_result_from_json( + &self, + target: crate::solvers::SolveOutcome, + ) -> ExtractionResult { + use crate::solvers::SolveOutcome; + // Numeric evaluation is model-owned, not parsed from a display string. + let decode = |solution| { + serde_json::from_value(solution).map_err(|error| { + ExtractionError::invalid(format!("target solution deserialization failed: {error}")) + }) + }; + let target = match target { + SolveOutcome::Optimal { solution, .. } => { + SolveOutcome::optimal(self.target_problem(), decode(solution)?)? + } + SolveOutcome::Feasible { solution, .. } => { + SolveOutcome::feasible(self.target_problem(), decode(solution)?)? + } + SolveOutcome::Infeasible => SolveOutcome::Infeasible, + }; + Ok(crate::solvers::erase_outcome(target)) } - fn extract_value_dyn(&self, target_value: serde_json::Value) -> serde_json::Value { - let target_value = serde_json::from_value(target_value) - .expect("DynAggregateReductionResult target value deserialize failed"); - let source_value = self.extract_value(target_value); - serde_json::to_value(source_value) - .expect("DynAggregateReductionResult source value serialize failed") + fn source_result_json( + &self, + source: crate::solvers::ErasedOutcome, + ) -> ExtractionResult { + use crate::solvers::SolveOutcome; + let source: crate::solvers::ProblemOutcome = + crate::solvers::downcast_outcome(source)?; + let encode = |solution| { + serde_json::to_value(solution).map_err(|error| { + ExtractionError::invalid(format!("source solution serialization failed: {error}")) + }) + }; + Ok(match source { + SolveOutcome::Optimal { + solution, + evaluation, + } => SolveOutcome::Optimal { + solution: encode(solution)?, + evaluation: evaluation.to_string(), + }, + SolveOutcome::Feasible { + solution, + evaluation, + } => SolveOutcome::Feasible { + solution: encode(solution)?, + evaluation: evaluation.to_string(), + }, + SolveOutcome::Infeasible => SolveOutcome::Infeasible, + }) } } diff --git a/src/rules/travelingsalesman_ilp.rs b/src/rules/travelingsalesman_ilp.rs index b232f15ef..55be91ad0 100644 --- a/src/rules/travelingsalesman_ilp.rs +++ b/src/rules/travelingsalesman_ilp.rs @@ -10,6 +10,8 @@ use crate::models::graph::TravelingSalesman; use crate::reduction; use crate::rules::ilp_helpers::{mccormick_product, one_hot_decode}; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; /// Result of reducing TravelingSalesman to ILP. @@ -32,10 +34,32 @@ impl ReductionResult for ReductionTSPToILP { /// Extract solution: read tour permutation from x variables, /// then map to edge selection for the source problem. - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionTSPToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok({ let n = self.num_vertices; diff --git a/src/rules/travelingsalesman_qubo.rs b/src/rules/travelingsalesman_qubo.rs index 31dbdbbbf..dbc23fa6c 100644 --- a/src/rules/travelingsalesman_qubo.rs +++ b/src/rules/travelingsalesman_qubo.rs @@ -10,6 +10,8 @@ use crate::models::algebraic::QUBO; use crate::models::graph::TravelingSalesman; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::topology::SimpleGraph; use std::collections::HashMap; @@ -35,10 +37,44 @@ impl ReductionResult for ReductionTravelingSalesmanToQUBO { /// Decode an optimum whose value relation establishes source feasibility. /// The energy gap guarantees a permutation using existing source edges. - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { + solution, + evaluation, + } => { + if !self.map_value(evaluation).is_valid() { + return Ok(SolveOutcome::Infeasible); + } + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { + solution, + evaluation, + } => { + if !self.map_value(evaluation).is_valid() { + return Err(crate::rules::ExtractionError::InsufficientSolutionQuality); + } + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionTravelingSalesmanToQUBO { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { if self.num_vertices < 3 { return Ok(self.small_optimum.as_ref().unwrap().0.clone()); } @@ -59,13 +95,8 @@ impl ReductionResult for ReductionTravelingSalesmanToQUBO { } } -impl crate::rules::AggregateReductionResult for ReductionTravelingSalesmanToQUBO { - type Source = TravelingSalesman; - type Target = QUBO; - fn target_problem(&self) -> &Self::Target { - &self.target - } - fn extract_value(&self, value: crate::types::Min) -> crate::types::Min { +impl ReductionTravelingSalesmanToQUBO { + fn map_value(&self, value: crate::types::Min) -> crate::types::Min { if self.num_vertices < 3 { return crate::types::Min( value @@ -83,7 +114,6 @@ impl crate::rules::AggregateReductionResult for ReductionTravelingSalesmanToQUBO } #[reduction( - aggregate = custom, transform = exact { num_vars = "num_vertices^2", } diff --git a/src/rules/undirectedflowlowerbounds_ilp.rs b/src/rules/undirectedflowlowerbounds_ilp.rs index ecae0d8de..bacaf2206 100644 --- a/src/rules/undirectedflowlowerbounds_ilp.rs +++ b/src/rules/undirectedflowlowerbounds_ilp.rs @@ -27,6 +27,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::UndirectedFlowLowerBounds; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::topology::Graph; /// Result of reducing UndirectedFlowLowerBounds to `ILP`. @@ -54,10 +56,32 @@ impl ReductionResult for ReductionUFLBToILP { /// The model encodes orientation as config[e] = 0 for u→v, 1 for v→u. /// The ILP uses z_e = 1 for u→v, z_e = 0 for v→u. /// So we return 1 - z_e to match the model's convention. - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionUFLBToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok({ let e = self.num_edges; target_solution[2 * e..3 * e] diff --git a/src/rules/undirectedtwocommodityintegralflow_ilp.rs b/src/rules/undirectedtwocommodityintegralflow_ilp.rs index e61c3f14e..2dddb38b2 100644 --- a/src/rules/undirectedtwocommodityintegralflow_ilp.rs +++ b/src/rules/undirectedtwocommodityintegralflow_ilp.rs @@ -28,6 +28,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::UndirectedTwoCommodityIntegralFlow; use crate::reduction; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::ProblemOutcome; +use crate::solvers::SolveOutcome; use crate::topology::Graph; /// Result of reducing UndirectedTwoCommodityIntegralFlow to `ILP`. @@ -51,10 +53,32 @@ impl ReductionResult for ReductionU2CIFToILP { } /// Extract flow solution: first 4*|E| variables are the flow values. - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::optimal(source, solution)?) + } + SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl ReductionU2CIFToILP { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { crate::rules::ilp_helpers::decode_usize_values(&target_solution[..4 * self.num_edges]) } } @@ -222,7 +246,14 @@ pub(crate) fn canonical_rule_example_specs() -> Vec>( source, SolutionPair { diff --git a/src/solvers/customized/solver.rs b/src/solvers/customized/solver.rs index 87b9a5baf..97c6c0758 100644 --- a/src/solvers/customized/solver.rs +++ b/src/solvers/customized/solver.rs @@ -103,6 +103,17 @@ register_customized_solver!( |problem| super::closest_vector_problem::solve(problem).map(Some) ); +register_customized_solver!( + crate::models::decision::Decision>, + "cvp-sphere-enumeration", + |problem: &crate::models::decision::Decision< + crate::models::algebraic::ClosestVectorProblem, + >| { + let solution = super::closest_vector_problem::solve(problem.inner())?; + Ok(problem.evaluate(&solution)?.0.then_some(solution)) + } +); + /// Solve MinimumCardinalityKey: find a minimal key with smallest cardinality. /// /// Uses iterative deepening by cardinality to guarantee the first solution diff --git a/src/solvers/mod.rs b/src/solvers/mod.rs index 47351bb48..eb26a5357 100644 --- a/src/solvers/mod.rs +++ b/src/solvers/mod.rs @@ -3,9 +3,12 @@ mod brute_force; pub(crate) mod customized; pub mod decision_search; +mod outcome; mod pipelines; mod registry; mod resolver; +pub(crate) use outcome::{downcast_outcome, erase_outcome, ErasedOutcome}; +pub use outcome::{ProblemOutcome, SolveOutcome}; pub mod ilp; @@ -16,9 +19,7 @@ pub use registry::{ brute_force_dimensions, solver_capabilities, CustomizedSolverCapability, ExactProblemKey, IlpSolverCapability, RegistryBuildError, SolverCapabilities, }; -pub use resolver::{ - complete_reduction, solve, SolveOutcome, SolveResult, SolverExecution, SolverRequest, -}; +pub use resolver::{solve, SolveResult, SolverExecution, SolverRequest}; pub use ilp::{ILPSolveError, ILPSolver}; diff --git a/src/solvers/outcome.rs b/src/solvers/outcome.rs new file mode 100644 index 000000000..e3b009f7c --- /dev/null +++ b/src/solvers/outcome.rs @@ -0,0 +1,117 @@ +//! Mathematical solve results, shared by solvers and reduction recovery. + +use crate::traits::{EvaluationError, Problem}; +use serde::{Deserialize, Serialize}; +use std::any::Any; + +/// A completed solve or a feasible incumbent whose optimality is not established. +/// Execution failures are returned separately as errors. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(tag = "status", rename_all = "snake_case")] +pub enum SolveOutcome { + Optimal { solution: S, evaluation: V }, + Feasible { solution: S, evaluation: V }, + Infeasible, +} + +/// A result retaining the model's concrete solution and value types. +pub type ProblemOutcome

= SolveOutcome<

::Solution,

::Value>; + +impl SolveOutcome { + /// Package an optimum established by the caller and evaluate its objective. + pub fn optimal>( + problem: &P, + solution: S, + ) -> Result { + let evaluation = problem.evaluate(&solution)?; + Ok(Self::Optimal { + solution, + evaluation, + }) + } + + /// Package a feasible witness established by the caller without claiming optimality. + pub fn feasible>( + problem: &P, + solution: S, + ) -> Result { + let evaluation = problem.evaluate(&solution)?; + Ok(Self::Feasible { + solution, + evaluation, + }) + } + + pub fn solution(&self) -> Option<&S> { + match self { + Self::Optimal { solution, .. } | Self::Feasible { solution, .. } => Some(solution), + Self::Infeasible => None, + } + } + + pub fn into_solution(self) -> Option { + match self { + Self::Optimal { solution, .. } | Self::Feasible { solution, .. } => Some(solution), + Self::Infeasible => None, + } + } +} + +pub(crate) type ErasedOutcome = SolveOutcome, Box>; + +pub(crate) fn erase_outcome(outcome: SolveOutcome) -> ErasedOutcome { + match outcome { + SolveOutcome::Optimal { + solution, + evaluation, + } => SolveOutcome::Optimal { + solution: Box::new(solution), + evaluation: Box::new(evaluation), + }, + SolveOutcome::Feasible { + solution, + evaluation, + } => SolveOutcome::Feasible { + solution: Box::new(solution), + evaluation: Box::new(evaluation), + }, + SolveOutcome::Infeasible => SolveOutcome::Infeasible, + } +} + +pub(crate) fn downcast_outcome( + outcome: ErasedOutcome, +) -> crate::rules::ExtractionResult> { + let convert = |solution: Box, evaluation: Box| { + let solution = *solution + .downcast::() + .map_err(|_| crate::rules::ExtractionError::invalid("result solution type mismatch"))?; + let evaluation = *evaluation.downcast::().map_err(|_| { + crate::rules::ExtractionError::invalid("result evaluation type mismatch") + })?; + Ok::<_, crate::rules::ExtractionError>((solution, evaluation)) + }; + Ok(match outcome { + SolveOutcome::Optimal { + solution, + evaluation, + } => { + let (solution, evaluation) = convert(solution, evaluation)?; + SolveOutcome::Optimal { + solution, + evaluation, + } + } + SolveOutcome::Feasible { + solution, + evaluation, + } => { + let (solution, evaluation) = convert(solution, evaluation)?; + SolveOutcome::Feasible { + solution, + evaluation, + } + } + SolveOutcome::Infeasible => SolveOutcome::Infeasible, + }) +} diff --git a/src/solvers/pipelines.rs b/src/solvers/pipelines.rs index d9674cd78..e5d154597 100644 --- a/src/solvers/pipelines.rs +++ b/src/solvers/pipelines.rs @@ -125,6 +125,7 @@ register_ilp_pipeline! { register_ilp_pipeline! { ("DecisionMinimumDominatingSet", [("graph", "SimpleGraph"), ("weight", "One")]), + ("DecisionMinimumSumMulticenter", [("graph", "SimpleGraph"), ("weight", "i64")]), ("MinimumSumMulticenter", [("graph", "SimpleGraph"), ("weight", "i64")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), } @@ -205,6 +206,7 @@ register_ilp_pipeline! { register_ilp_pipeline! { ("HamiltonianCircuit", [("graph", "SimpleGraph")]), + ("DecisionLongestCircuit", [("graph", "SimpleGraph"), ("weight", "i64")]), ("LongestCircuit", [("graph", "SimpleGraph"), ("weight", "i64")]), ("ILP", [("variable", "bool"), ("coefficient", "i64")]), } @@ -836,3 +838,70 @@ register_ilp_pipeline! { ("UndirectedTwoCommodityIntegralFlow", []), ("ILP", [("variable", "i64"), ("coefficient", "i64")]), } + +register_ilp_pipeline! { + ("DecisionLongestCircuit", [("graph", "SimpleGraph"), ("weight", "i64")]), + ("LongestCircuit", [("graph", "SimpleGraph"), ("weight", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64")]), +} + +register_ilp_pipeline! { + ("DecisionMaximum2Satisfiability", []), + ("Maximum2Satisfiability", []), + ("ILP", [("variable", "bool"), ("coefficient", "i64")]), +} + +register_ilp_pipeline! { + ("DecisionMinimumCoveringByCliques", [("graph", "SimpleGraph")]), + ("MinimumCoveringByCliques", [("graph", "SimpleGraph")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64")]), +} + +register_ilp_pipeline! { + ("DecisionMinimumSumMulticenter", [("graph", "SimpleGraph"), ("weight", "i64")]), + ("MinimumSumMulticenter", [("graph", "SimpleGraph"), ("weight", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64")]), +} + +register_ilp_pipeline! { + ("DecisionOpenShopScheduling", []), + ("OpenShopScheduling", []), + ("ILP", [("variable", "i64"), ("coefficient", "i64")]), +} + +register_ilp_pipeline! { + ("DecisionQUBO", [("weight", "i64")]), + ("QUBO", [("weight", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64")]), +} + +register_ilp_pipeline! { + ("DecisionQuadraticAssignment", []), + ("QuadraticAssignment", []), + ("ILP", [("variable", "bool"), ("coefficient", "i64")]), +} + +register_ilp_pipeline! { + ("DecisionRuralPostman", [("graph", "SimpleGraph"), ("weight", "i64")]), + ("RuralPostman", [("graph", "SimpleGraph"), ("weight", "i64")]), + ("ILP", [("variable", "i64"), ("coefficient", "i64")]), +} + +register_ilp_pipeline! { + ("DecisionSequencingToMinimizeTardyTaskWeight", []), + ("SequencingToMinimizeTardyTaskWeight", []), + ("ILP", [("variable", "bool"), ("coefficient", "i64")]), +} + +register_ilp_pipeline! { + ("DecisionSpinGlass", [("graph", "SimpleGraph"), ("weight", "i64")]), + ("SpinGlass", [("graph", "SimpleGraph"), ("weight", "i64")]), + ("QUBO", [("weight", "i64")]), + ("ILP", [("variable", "bool"), ("coefficient", "i64")]), +} + +register_ilp_pipeline! { + ("DecisionStackerCrane", []), + ("StackerCrane", []), + ("ILP", [("variable", "bool"), ("coefficient", "i64")]), +} diff --git a/src/solvers/registry.rs b/src/solvers/registry.rs index 2619ba8c5..b7a189795 100644 --- a/src/solvers/registry.rs +++ b/src/solvers/registry.rs @@ -14,11 +14,14 @@ use std::sync::OnceLock; fn solve_ilp_terminal( source: &dyn Any, adapter: &HighsAdapter, -) -> Result, super::ILPSolveError> { +) -> Result { macro_rules! dispatch { ($($v:ty, $c:ty);* $(;)?) => { $( if let Some(ilp) = source.downcast_ref::>() { - return adapter.solve(ilp).map_err(Into::into); + let solution = adapter.solve(ilp)?; + let outcome = super::SolveOutcome::optimal(ilp, solution) + .map_err(crate::rules::ExtractionError::from)?; + return Ok(super::erase_outcome(outcome)); } )* }; } @@ -145,15 +148,37 @@ impl CompiledIlpPipeline { Option<&dyn DynReductionResult>, ) -> Result, ) -> Result { - if self.reducers.is_empty() { - return finish(Box::new(solve_ilp_terminal(source, adapter)?), None); - } - - let chain = crate::rules::ReductionChain::execute(source, &self.reducers)?; - let target_solution = solve_ilp_terminal(chain.target_problem_any(), adapter)?; - let source_solution = super::resolver::complete_chain(&chain, &target_solution)? - .ok_or(super::ILPSolveError::Infeasible)?; - finish(source_solution, Some(chain.steps[0].witness.as_ref())) + let chain = if self.reducers.is_empty() { + None + } else { + Some(crate::rules::ReductionChain::execute( + source, + &self.reducers, + )?) + }; + let target_problem = chain + .as_ref() + .map_or(source, |chain| chain.target_problem_any()); + let target = match solve_ilp_terminal(target_problem, adapter) { + Ok(outcome) => outcome, + Err(super::ILPSolveError::Infeasible) => super::SolveOutcome::Infeasible, + Err(error) => return Err(error), + }; + let recovered = match &chain { + Some(chain) => chain.recover_erased(source, target)?, + None => target, + }; + let source_solution = match recovered { + super::SolveOutcome::Optimal { solution, .. } => solution, + super::SolveOutcome::Infeasible => return Err(super::ILPSolveError::Infeasible), + super::SolveOutcome::Feasible { .. } => { + return Err(crate::rules::ExtractionError::InsufficientSolutionQuality.into()) + } + }; + finish( + source_solution, + chain.as_ref().map(|chain| chain.steps[0].witness.as_ref()), + ) } pub(crate) fn solve( diff --git a/src/solvers/resolver.rs b/src/solvers/resolver.rs index eadfd5e75..73640e386 100644 --- a/src/solvers/resolver.rs +++ b/src/solvers/resolver.rs @@ -2,6 +2,7 @@ use super::registry::CompiledIlpPipeline; use super::registry::{solver_capability_registry, CustomizedSolverRegistration, ExactProblemKey}; +use super::SolveOutcome; use crate::registry::LoadedDynProblem; use serde::Serialize; @@ -19,6 +20,7 @@ pub enum SolverRequest { #[derive(Clone, Debug, PartialEq, Eq, Serialize)] #[serde(tag = "kind", rename_all = "kebab-case")] pub enum SolverExecution { + External, Customized { implementation: &'static str }, Ilp { reduction_path: Vec }, BruteForce, @@ -31,65 +33,6 @@ pub struct SolveResult { pub outcome: SolveOutcome, } -/// Semantic result of a completed solve under the selected backend's numerical contract. -#[derive(Clone, Debug, PartialEq, Eq, Serialize)] -#[serde(tag = "status", rename_all = "snake_case")] -pub enum SolveOutcome { - /// The selected backend established optimality and returned a solution. - /// ILP optimality is subject to backend numerical tolerances. - Optimal { - solution: serde_json::Value, - evaluation: String, - }, - /// The selected backend established infeasibility under its numerical contract. - Infeasible, -} - -/// Interpret aggregate outcomes before mapping each accepted target optimum. -pub(crate) fn complete_chain( - chain: &crate::rules::ReductionChain, - target_solution: &dyn std::any::Any, -) -> crate::rules::ExtractionResult>> { - let mut solution: Option> = None; - for step in chain.steps.iter().rev() { - let input = solution.as_deref().unwrap_or(target_solution); - if let Some(interpret) = &step.interpret_optimum { - if !interpret(input)? { - return Ok(None); - } - } - solution = Some(step.witness.extract_solution_dyn(input)?); - } - Ok(Some(solution.expect("reduction chain has no steps"))) -} - -/// Map a completed target solve through an executed reduction chain. -/// -/// The target outcome must come from a completed solve, not merely a feasible -/// assignment: only an accepted optimum can establish a source decision's NO. -pub fn complete_reduction( - source: &dyn crate::registry::DynProblem, - chain: &crate::rules::ReductionChain, - target: &SolveOutcome, -) -> Result { - let SolveOutcome::Optimal { solution, .. } = target else { - return Ok(SolveOutcome::Infeasible); - }; - let last = chain.steps.last().expect("reduction chain has no steps"); - let target_solution = last.witness.target_solution_from_json(solution.clone())?; - let Some(solution) = complete_chain(chain, target_solution.as_ref())? else { - return Ok(SolveOutcome::Infeasible); - }; - let solution = chain.steps[0] - .witness - .source_solution_json(solution.as_ref())?; - let (evaluation, _) = source.evaluate_dyn(&solution)?; - Ok(SolveOutcome::Optimal { - solution, - evaluation, - }) -} - fn problem_key(problem: &LoadedDynProblem) -> ExactProblemKey { ExactProblemKey::new(problem.problem_name(), problem.variant_map()) } diff --git a/src/unit_tests/example_db.rs b/src/unit_tests/example_db.rs index 37f0a3f81..715324f9a 100644 --- a/src/unit_tests/example_db.rs +++ b/src/unit_tests/example_db.rs @@ -4,6 +4,7 @@ use crate::example_db::{ use crate::export::ProblemRef; use crate::registry::load_dyn; use crate::rules::{registry::reduction_entries, ReductionGraph}; +use crate::solvers::SolveOutcome; use std::collections::{BTreeMap, BTreeSet, HashSet}; #[test] @@ -559,6 +560,9 @@ fn model_specs_are_optimal() { let loaded = load_dyn(name, &variant, spec.instance.serialize_json()).ok()?; match solve(&loaded, request).ok()?.outcome { SolveOutcome::Optimal { solution, .. } => Some(solution), + SolveOutcome::Feasible { .. } => { + panic!("exact solver returned only a feasible incumbent") + } SolveOutcome::Infeasible => None, } }; @@ -650,41 +654,19 @@ fn rule_specs_solution_pairs_are_consistent() { .into_iter() .find(|path| path.len() == 1); if witness_path.is_none() { - let has_aggregate_path = graph - .find_all_paths_mode( + assert!( + graph + .has_direct_reduction_by_name(&example.source.problem, &example.target.problem), + "No direct reduction for {label}" + ); + assert!( + !graph.has_direct_reduction_by_name_mode( &example.source.problem, - &example.source.variant, &example.target.problem, - &example.target.variant, - crate::rules::ReductionMode::Aggregate, - ) - .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}" - ); - } + crate::rules::ReductionMode::Witness + ), + "Proof-only edge unexpectedly executable for {label}" + ); } // Only do witness round-trip when a witness path exists @@ -726,7 +708,14 @@ fn rule_specs_solution_pairs_are_consistent() { // source config with the same evaluation value (witness paths only) if let Some(ref chain) = chain { let extracted = chain - .extract_solution_json(pair.target_config.clone()) + .recover_result_json( + source.as_any(), + SolveOutcome::Optimal { + solution: pair.target_config.clone(), + evaluation: target_eval.0.clone(), + }, + ) + .map(|outcome| outcome.into_solution().unwrap()) .unwrap(); let extracted_val = source .evaluate_json(&extracted) @@ -739,9 +728,36 @@ fn rule_specs_solution_pairs_are_consistent() { extracted_val, source_val, extracted, pair.source_config ); + assert_eq!( + chain + .recover_result_json(source.as_any(), SolveOutcome::Infeasible) + .unwrap(), + SolveOutcome::Infeasible, + "Rule {label}: target infeasibility must propagate" + ); + match chain.recover_result_json(source.as_any(), SolveOutcome::Feasible { + solution: pair.target_config.clone(), evaluation: target_eval.0.clone(), + }) { + Ok(SolveOutcome::Feasible { solution, evaluation }) => { + let (actual, valid) = source.evaluate_dyn(&solution).unwrap(); + assert!(valid, "Rule {label}: feasible recovery returned an invalid source witness"); + assert_eq!(evaluation, actual); + } + Err(crate::rules::ExtractionError::InsufficientSolutionQuality) => {} + result => panic!("Rule {label}: feasible recovery returned an unjustified status: {result:?}"), + } + let malformed = serde_json::json!({"invalid_solution": true}); assert!( - chain.extract_solution_json(malformed).is_err(), + chain + .recover_result_json( + source.as_any(), + SolveOutcome::Optimal { + solution: malformed, + evaluation: String::new() + } + ) + .is_err(), "Rule {label}: extraction accepted malformed target-solution JSON" ); } @@ -882,7 +898,7 @@ fn test_find_rule_example_ksatisfiability_to_minimumvertexcover() { variant: BTreeMap::from([("k".to_string(), "K3".to_string())]), }; let target = ProblemRef { - name: "MinimumVertexCover".to_string(), + name: "DecisionMinimumVertexCover".to_string(), variant: BTreeMap::from([ ("graph".to_string(), "SimpleGraph".to_string()), ("weight".to_string(), "i64".to_string()), @@ -890,7 +906,7 @@ fn test_find_rule_example_ksatisfiability_to_minimumvertexcover() { }; let example = find_rule_example(&source, &target).unwrap(); assert_eq!(example.source.problem, "KSatisfiability"); - assert_eq!(example.target.problem, "MinimumVertexCover"); + assert_eq!(example.target.problem, "DecisionMinimumVertexCover"); } #[test] @@ -966,12 +982,12 @@ fn test_find_rule_example_hamiltoniancircuit_to_stackercrane() { variant: BTreeMap::from([("graph".to_string(), "SimpleGraph".to_string())]), }; let target = ProblemRef { - name: "StackerCrane".to_string(), + name: "DecisionStackerCrane".to_string(), variant: BTreeMap::new(), }; let example = find_rule_example(&source, &target).unwrap(); assert_eq!(example.source.problem, "HamiltonianCircuit"); - assert_eq!(example.target.problem, "StackerCrane"); + assert_eq!(example.target.problem, "DecisionStackerCrane"); } #[test] @@ -981,7 +997,7 @@ fn test_find_rule_example_hamiltoniancircuit_to_ruralpostman() { variant: BTreeMap::from([("graph".to_string(), "SimpleGraph".to_string())]), }; let target = ProblemRef { - name: "RuralPostman".to_string(), + name: "DecisionRuralPostman".to_string(), variant: BTreeMap::from([ ("graph".to_string(), "SimpleGraph".to_string()), ("weight".to_string(), "i64".to_string()), @@ -989,7 +1005,7 @@ fn test_find_rule_example_hamiltoniancircuit_to_ruralpostman() { }; let example = find_rule_example(&source, &target).unwrap(); assert_eq!(example.source.problem, "HamiltonianCircuit"); - assert_eq!(example.target.problem, "RuralPostman"); + assert_eq!(example.target.problem, "DecisionRuralPostman"); } #[test] @@ -1017,12 +1033,12 @@ fn test_find_rule_example_hamiltoniancircuit_to_quadraticassignment() { variant: BTreeMap::from([("graph".to_string(), "SimpleGraph".to_string())]), }; let target = ProblemRef { - name: "QuadraticAssignment".to_string(), + name: "DecisionQuadraticAssignment".to_string(), variant: BTreeMap::new(), }; let example = find_rule_example(&source, &target).unwrap(); assert_eq!(example.source.problem, "HamiltonianCircuit"); - assert_eq!(example.target.problem, "QuadraticAssignment"); + assert_eq!(example.target.problem, "DecisionQuadraticAssignment"); } // PR #804 rules @@ -1088,7 +1104,7 @@ fn test_find_rule_example_hamiltoniancircuit_to_longestcircuit() { variant: BTreeMap::from([("graph".to_string(), "SimpleGraph".to_string())]), }; let target = ProblemRef { - name: "LongestCircuit".to_string(), + name: "DecisionLongestCircuit".to_string(), variant: BTreeMap::from([ ("graph".to_string(), "SimpleGraph".to_string()), ("weight".to_string(), "i64".to_string()), @@ -1096,7 +1112,7 @@ fn test_find_rule_example_hamiltoniancircuit_to_longestcircuit() { }; let example = find_rule_example(&source, &target).unwrap(); assert_eq!(example.source.problem, "HamiltonianCircuit"); - assert_eq!(example.target.problem, "LongestCircuit"); + assert_eq!(example.target.problem, "DecisionLongestCircuit"); } #[test] @@ -1252,7 +1268,7 @@ fn test_find_rule_example_naesatisfiability_to_maxcut() { variant: BTreeMap::new(), }; let target = ProblemRef { - name: "MaxCut".to_string(), + name: "DecisionMaxCut".to_string(), variant: BTreeMap::from([ ("graph".to_string(), "SimpleGraph".to_string()), ("weight".to_string(), "i64".to_string()), @@ -1260,7 +1276,7 @@ fn test_find_rule_example_naesatisfiability_to_maxcut() { }; let example = find_rule_example(&source, &target).unwrap(); assert_eq!(example.source.problem, "NAESatisfiability"); - assert_eq!(example.target.problem, "MaxCut"); + assert_eq!(example.target.problem, "DecisionMaxCut"); } #[test] diff --git a/src/unit_tests/models/algebraic/closest_vector_problem.rs b/src/unit_tests/models/algebraic/closest_vector_problem.rs index 5d7ad48fb..8cf23c292 100644 --- a/src/unit_tests/models/algebraic/closest_vector_problem.rs +++ b/src/unit_tests/models/algebraic/closest_vector_problem.rs @@ -1,4 +1,5 @@ use super::*; +use crate::solvers::SolveOutcome; use crate::traits::Problem; use crate::types::Min; @@ -122,7 +123,7 @@ fn test_cvp_real_target_preserves_its_stored_rational_value() { .outcome; assert_eq!( outcome, - crate::solvers::SolveOutcome::Optimal { + SolveOutcome::Optimal { solution: serde_json::json!([0]), evaluation: "Min(1/16)".into(), } diff --git a/src/unit_tests/models/decision.rs b/src/unit_tests/models/decision.rs index bffba4846..4a6dd3d23 100644 --- a/src/unit_tests/models/decision.rs +++ b/src/unit_tests/models/decision.rs @@ -1,6 +1,7 @@ use crate::models::decision::Decision; use crate::models::graph::{MaximumIndependentSet, MinimumDominatingSet, MinimumVertexCover}; use crate::solvers::BruteForce; +use crate::solvers::SolveOutcome; use crate::topology::SimpleGraph; use crate::traits::Problem; use crate::types::{One, Or}; @@ -158,38 +159,36 @@ fn construction_contract_decision_rejects_nested_persisted_shape() { #[test] fn test_decision_reduce_to_aggregate() { - use crate::rules::{AggregateReductionResult, ReduceToAggregate}; + use crate::rules::{ReduceTo, ReductionResult}; let decision = Decision::new(triangle_mvc(), 2); - let result = decision - .reduce_to_aggregate() + let result = ReduceTo::>::reduce_to(&decision) .expect("reduction should succeed"); let target = result.target_problem(); assert_eq!(target.num_vertices(), 3); let target_val = target.evaluate(&vec![true, true, false]).unwrap(); - let source_val = result.extract_value(target_val); + let source_val = result.map_value(target_val); assert_eq!(source_val, Or(true)); let target_val = target.evaluate(&vec![true, true, true]).unwrap(); - let source_val = result.extract_value(target_val); + let source_val = result.map_value(target_val); assert_eq!(source_val, Or(false)); } #[test] fn test_decision_reduce_to_aggregate_infeasible_bound() { - use crate::rules::{AggregateReductionResult, ReduceToAggregate}; + use crate::rules::{ReduceTo, ReductionResult}; let decision = Decision::new(triangle_mvc(), 1); - let result = decision - .reduce_to_aggregate() + let result = ReduceTo::>::reduce_to(&decision) .expect("reduction should succeed"); let target = result.target_problem(); for mask in 0..8 { let config = vec![mask & 0b001 != 0, mask & 0b010 != 0, mask & 0b100 != 0]; let target_val = target.evaluate(&config).unwrap(); - let source_val = result.extract_value(target_val); + let source_val = result.map_value(target_val); assert_eq!( source_val, Or(false), @@ -230,11 +229,10 @@ fn test_decision_mds_evaluate_infeasible_cost() { #[test] fn test_decision_mds_reduce_to_aggregate() { - use crate::rules::{AggregateReductionResult, ReduceToAggregate}; + use crate::rules::{ReduceTo, ReductionResult}; let decision = Decision::new(star_mds(), 1); - let result = decision - .reduce_to_aggregate() + let result = ReduceTo::>::reduce_to(&decision) .expect("reduction should succeed"); let target = result.target_problem(); assert_eq!(target.num_vertices(), 5); @@ -242,13 +240,13 @@ fn test_decision_mds_reduce_to_aggregate() { let target_val = target .evaluate(&vec![true, false, false, false, false]) .unwrap(); - let source_val = result.extract_value(target_val); + let source_val = result.map_value(target_val); assert_eq!(source_val, Or(true)); let target_val = target .evaluate(&vec![true, true, false, false, false]) .unwrap(); - let source_val = result.extract_value(target_val); + let source_val = result.map_value(target_val); assert_eq!(source_val, Or(false)); } @@ -321,31 +319,36 @@ fn test_decision_mis_unit_dynamic_identity_edges() { assert_eq!((edge.parameter_declarations_fn)().fields.len(), 2); let witness = vec![true, false, true]; let reduced = (edge.reduce_fn.unwrap())(&decision).unwrap(); - assert!(std::ptr::eq( - reduced.witness.target_problem_any(), - reduced.aggregate.as_ref().unwrap().target_problem_any(), - )); + let target = reduced + .witness + .target_problem_any() + .downcast_ref::>() + .unwrap(); assert_eq!( - *reduced - .witness - .extract_solution_dyn(&witness) - .unwrap() - .downcast::>() - .unwrap(), - witness + target.evaluate(&witness).unwrap(), + crate::types::Max(Some(2)) + ); + let target_result = SolveOutcome::optimal(target, witness.clone()).unwrap(); + let recovered = reduced + .witness + .recover_result_dyn(&decision, crate::solvers::erase_outcome(target_result)) + .unwrap(); + assert_eq!( + crate::solvers::downcast_outcome::, Or>(recovered).unwrap(), + SolveOutcome::Optimal { + solution: witness, + evaluation: Or(true) + } ); assert!(matches!( (edge.reduce_fn.unwrap())(decision.inner()), Err(crate::rules::ReductionError::SourceTypeMismatch { .. }) )); - let aggregate = (edge.reduce_aggregate_fn.unwrap())(&decision).unwrap(); - assert_eq!( - aggregate.extract_value_dyn(serde_json::json!(2)), - serde_json::json!(true) - ); assert!(matches!( - (edge.reduce_aggregate_fn.unwrap())(decision.inner()), - Err(crate::rules::ReductionError::SourceTypeMismatch { .. }) + reduced + .witness + .recover_result_dyn(decision.inner(), SolveOutcome::Infeasible), + Err(crate::rules::ExtractionError::InvalidTargetSolution(_)) )); let reverse = entries .iter() @@ -381,7 +384,7 @@ fn unit_vertex_cover_uses_registered_construction_and_solver() { #[test] fn decision_executed_result_maps_witness_and_bound_together() { - use crate::rules::{AggregateReductionResult, ReduceTo, ReductionResult}; + use crate::rules::{ReduceTo, ReductionResult}; use crate::types::Min; let witness = vec![true, true, false]; @@ -393,13 +396,63 @@ fn decision_executed_result_maps_witness_and_bound_together() { let target = ReductionResult::target_problem(&result); assert!(std::ptr::eq( target, - AggregateReductionResult::target_problem(&result), + crate::rules::ReductionResult::target_problem(&result), )); let value = target.evaluate(&witness).unwrap(); - assert_eq!(result.extract_value(value), Or(bound == 2)); - assert_eq!(result.extract_value(Min(None)), Or(false)); + assert_eq!(result.map_value(value), Or(bound == 2)); + assert_eq!(result.map_value(Min(None)), Or(false)); if bound == 2 { - assert_eq!(result.extract_solution(&witness).unwrap(), witness); + assert_eq!( + result + .recover_result( + &decision, + SolveOutcome::optimal(result.target_problem(), witness.clone()).unwrap() + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"), + witness + ); } } } + +#[test] +fn recovery_distinguishes_a_proved_no_from_an_insufficient_incumbent() { + use crate::rules::{ExtractionError, ReduceTo, ReductionResult}; + use SolveOutcome; + let source = Decision::new(triangle_mvc(), 2); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + let candidate = vec![true, true, true]; + assert!(matches!( + reduction.recover_result( + &source, + SolveOutcome::feasible(reduction.target_problem(), candidate).unwrap() + ), + Err(ExtractionError::InsufficientSolutionQuality) + )); + let optimum = vec![true, true, false]; + for outcome in [ + SolveOutcome::optimal(reduction.target_problem(), optimum.clone()).unwrap(), + SolveOutcome::feasible(reduction.target_problem(), optimum).unwrap(), + ] { + let recovered = reduction.recover_result(&source, outcome).unwrap(); + assert_eq!( + source.evaluate(recovered.solution().unwrap()).unwrap(), + Or(true) + ); + } + let no_source = Decision::new(triangle_mvc(), 1); + let no_reduction = + ReduceTo::>::reduce_to(&no_source).unwrap(); + assert_eq!( + no_reduction + .recover_result( + &no_source, + SolveOutcome::optimal(no_reduction.target_problem(), vec![true, true, false]) + .unwrap() + ) + .unwrap(), + SolveOutcome::Infeasible + ); +} diff --git a/src/unit_tests/models/graph/minimum_edge_cost_flow.rs b/src/unit_tests/models/graph/minimum_edge_cost_flow.rs index 1821d31d6..55910eae8 100644 --- a/src/unit_tests/models/graph/minimum_edge_cost_flow.rs +++ b/src/unit_tests/models/graph/minimum_edge_cost_flow.rs @@ -152,3 +152,30 @@ fn test_minimum_edge_cost_flow_all_witnesses_optimal() { assert_eq!(problem.evaluate(sol).unwrap(), Min(Some(3))); } } + +#[test] +fn deserialize_rejects_invalid_flow_network_data() { + use serde_json::json; + let valid = serde_json::to_value(MinimumEdgeCostFlow::new( + DirectedGraph::new(2, vec![(0, 1)]), + vec![1], + vec![2], + 0, + 1, + 1, + )) + .unwrap(); + for (field, value, message) in [ + ("prices", json!([]), "prices length"), + ("capacities", json!([]), "capacities length"), + ("source", json!(2), "source"), + ("sink", json!(2), "sink"), + ("sink", json!(0), "distinct"), + ("capacities", json!([-1]), "negative"), + ] { + let mut input = valid.clone(); + input[field] = value; + let error = serde_json::from_value::(input).unwrap_err(); + assert!(error.to_string().contains(message), "{field}: {error}"); + } +} diff --git a/src/unit_tests/models/misc/conjunctive_boolean_query.rs b/src/unit_tests/models/misc/conjunctive_boolean_query.rs index 7b5d9fa00..b8c670036 100644 --- a/src/unit_tests/models/misc/conjunctive_boolean_query.rs +++ b/src/unit_tests/models/misc/conjunctive_boolean_query.rs @@ -181,3 +181,87 @@ fn test_conjunctivebooleanquery_create_spec_rejects_invalid_relation_index() { assert!(result.is_err()); } + +#[test] +#[should_panic(expected = "expected arity")] +fn new_rejects_invalid_tuple_arity() { + ConjunctiveBooleanQuery::new( + 2, + vec![Relation { + arity: 1, + tuples: vec![vec![0, 1]], + }], + 1, + vec![], + ); +} + +#[test] +#[should_panic(expected = "must be < 2")] +fn new_rejects_invalid_tuple_domain() { + ConjunctiveBooleanQuery::new( + 2, + vec![Relation { + arity: 1, + tuples: vec![vec![2]], + }], + 1, + vec![], + ); +} + +#[test] +#[should_panic(expected = "relation index")] +fn new_rejects_invalid_relation_index() { + ConjunctiveBooleanQuery::new( + 2, + vec![Relation { + arity: 1, + tuples: vec![vec![0]], + }], + 1, + vec![(1, vec![QueryArg::Variable(0)])], + ); +} + +#[test] +#[should_panic(expected = "expected arity")] +fn new_rejects_invalid_argument_arity() { + ConjunctiveBooleanQuery::new( + 2, + vec![Relation { + arity: 1, + tuples: vec![vec![0]], + }], + 1, + vec![(0, vec![])], + ); +} + +#[test] +#[should_panic(expected = "num_variables")] +fn new_rejects_invalid_variable_index() { + ConjunctiveBooleanQuery::new( + 2, + vec![Relation { + arity: 1, + tuples: vec![vec![0]], + }], + 1, + vec![(0, vec![QueryArg::Variable(1)])], + ); +} + +#[test] +#[should_panic(expected = "domain_size")] +fn new_rejects_invalid_constant_domain() { + ConjunctiveBooleanQuery::new( + 2, + vec![Relation { + arity: 1, + tuples: vec![vec![0]], + }], + 1, + vec![(0, vec![QueryArg::Constant(2)])], + ); +} diff --git a/src/unit_tests/models/misc/minimum_axiom_set.rs b/src/unit_tests/models/misc/minimum_axiom_set.rs index d3b5e3bab..b05b74527 100644 --- a/src/unit_tests/models/misc/minimum_axiom_set.rs +++ b/src/unit_tests/models/misc/minimum_axiom_set.rs @@ -179,3 +179,20 @@ fn test_minimum_axiom_set_paper_example() { assert!(metric.is_valid()); assert_eq!(metric.unwrap(), 2); } + +#[test] +fn deserialize_rejects_invalid_sentence_references() { + use serde_json::json; + let valid = json!({"num_sentences": 2, "true_sentences": [0, 1], "implications": [[[0], 1]]}); + for (field, value, message) in [ + ("true_sentences", json!([2]), "True sentence index"), + ("true_sentences", json!([0, 0]), "Duplicate true sentence"), + ("implications", json!([[[2], 1]]), "antecedent"), + ("implications", json!([[[0], 2]]), "consequent"), + ] { + let mut input = valid.clone(); + input[field] = value; + let error = serde_json::from_value::(input).unwrap_err(); + assert!(error.to_string().contains(message), "{field}: {error}"); + } +} diff --git a/src/unit_tests/models/misc/minimum_code_generation_one_register.rs b/src/unit_tests/models/misc/minimum_code_generation_one_register.rs index b2f959de9..43889e36e 100644 --- a/src/unit_tests/models/misc/minimum_code_generation_one_register.rs +++ b/src/unit_tests/models/misc/minimum_code_generation_one_register.rs @@ -262,3 +262,21 @@ fn test_minimum_code_generation_one_register_lost_value() { let result = problem.simulate(&config).unwrap(); assert!(result.is_some()); } + +#[test] +fn deserialize_rejects_invalid_expression_graph() { + use serde_json::json; + let valid = json!({"num_vertices": 4, "edges": [[0, 1], [0, 2]], "num_leaves": 3}); + for (field, value, message) in [ + ("num_leaves", json!(5), "exceeds num_vertices"), + ("num_leaves", json!(2), "actual leaf count"), + ("edges", json!([[0, 4]]), "out of bounds"), + ("edges", json!([[0, 0]]), "Self-loop"), + ("edges", json!([[0, 1], [0, 2], [0, 3]]), "out-degree"), + ] { + let mut input = valid.clone(); + input[field] = value; + let error = serde_json::from_value::(input).unwrap_err(); + assert!(error.to_string().contains(message), "{field}: {error}"); + } +} diff --git a/src/unit_tests/models/misc/minimum_code_generation_unlimited_registers.rs b/src/unit_tests/models/misc/minimum_code_generation_unlimited_registers.rs index 2c9900bb2..6025d498a 100644 --- a/src/unit_tests/models/misc/minimum_code_generation_unlimited_registers.rs +++ b/src/unit_tests/models/misc/minimum_code_generation_unlimited_registers.rs @@ -210,3 +210,24 @@ fn test_minimum_code_generation_unlimited_registers_paper_example() { let witness = solver.solve(&problem).unwrap().unwrap(); assert_eq!(problem.simulate(&witness).unwrap(), Some(4)); } + +#[test] +fn deserialize_rejects_invalid_operand_arcs() { + use serde_json::json; + let valid = json!({"num_vertices": 4, "left_arcs": [[0, 1]], "right_arcs": [[0, 2]]}); + for (field, value, message) in [ + ("left_arcs", json!([[0, 4]]), "Left arc"), + ("right_arcs", json!([[4, 0]]), "Right arc"), + ("left_arcs", json!([[0, 0]]), "Self-loop"), + ("right_arcs", json!([[0, 0]]), "Self-loop"), + ("left_arcs", json!([[0, 1], [0, 3]]), "out-degree"), + ("left_arcs", json!([]), "Unary vertex"), + ("right_arcs", json!([[1, 2], [1, 3]]), "Binary vertex"), + ] { + let mut input = valid.clone(); + input[field] = value; + let error = + serde_json::from_value::(input).unwrap_err(); + assert!(error.to_string().contains(message), "{field}: {error}"); + } +} diff --git a/src/unit_tests/models/misc/optimum_communication_spanning_tree.rs b/src/unit_tests/models/misc/optimum_communication_spanning_tree.rs index f321e47ad..7523aea2a 100644 --- a/src/unit_tests/models/misc/optimum_communication_spanning_tree.rs +++ b/src/unit_tests/models/misc/optimum_communication_spanning_tree.rs @@ -221,3 +221,47 @@ fn test_ocst_canonical_example() { ); assert_eq!(spec.optimal_value, serde_json::json!(20)); } + +#[test] +fn deserialize_rejects_invalid_communication_matrices() { + use serde_json::json; + let valid = json!({"edge_weights": [[0, 1], [1, 0]], "requirements": [[0, 1], [1, 0]]}); + for (field, value, message) in [ + ("requirements", json!([[0]]), "same size"), + ( + "edge_weights", + json!([[0], [1, 0]]), + "edge_weights must be square", + ), + ( + "requirements", + json!([[0], [1, 0]]), + "requirements must be square", + ), + ( + "edge_weights", + json!([[1, 1], [1, 0]]), + "diagonal of edge_weights", + ), + ( + "requirements", + json!([[1, 1], [1, 0]]), + "diagonal of requirements", + ), + ( + "edge_weights", + json!([[0, -1], [-1, 0]]), + "edge_weights must be non-negative", + ), + ( + "requirements", + json!([[0, -1], [-1, 0]]), + "requirements must be non-negative", + ), + ] { + let mut input = valid.clone(); + input[field] = value; + let error = serde_json::from_value::(input).unwrap_err(); + assert!(error.to_string().contains(message), "{field}: {error}"); + } +} diff --git a/src/unit_tests/models/misc/timetable_design.rs b/src/unit_tests/models/misc/timetable_design.rs index a2c1322d9..969548215 100644 --- a/src/unit_tests/models/misc/timetable_design.rs +++ b/src/unit_tests/models/misc/timetable_design.rs @@ -230,3 +230,41 @@ fn test_timetable_design_paper_example_is_valid() { ); assert_eq!(spec.optimal_value, serde_json::json!(true)); } + +#[test] +#[should_panic(expected = "craftsman 0 availability")] +fn new_rejects_wrong_craftsman_period_count() { + TimetableDesign::new( + 2, + 1, + 1, + vec![vec![true]], + vec![vec![true; 2]], + vec![vec![1]], + ); +} + +#[test] +#[should_panic(expected = "task_avail has 0 rows")] +fn new_rejects_wrong_task_count() { + TimetableDesign::new(2, 1, 1, vec![vec![true; 2]], vec![], vec![vec![1]]); +} + +#[test] +#[should_panic(expected = "task 0 availability")] +fn new_rejects_wrong_task_period_count() { + TimetableDesign::new( + 2, + 1, + 1, + vec![vec![true; 2]], + vec![vec![true]], + vec![vec![1]], + ); +} + +#[test] +#[should_panic(expected = "requirements has 0 rows")] +fn new_rejects_wrong_requirement_row_count() { + TimetableDesign::new(2, 1, 1, vec![vec![true; 2]], vec![vec![true; 2]], vec![]); +} diff --git a/src/unit_tests/reduction_graph.rs b/src/unit_tests/reduction_graph.rs index 133bffee1..cb8b6a28a 100644 --- a/src/unit_tests/reduction_graph.rs +++ b/src/unit_tests/reduction_graph.rs @@ -6,6 +6,7 @@ use crate::models::formula::KSatisfiability; use crate::models::misc::Clustering; use crate::prelude::*; use crate::rules::{ReductionGraph, ReductionMode, ReductionPath, ReductionStep, TraversalFlow}; +use crate::solvers::SolveOutcome; use crate::topology::{KingsSubgraph, SimpleGraph, UnitDiskGraph}; use crate::types::ProblemParameters; use crate::variant::{K3, KN}; @@ -56,7 +57,7 @@ fn symbolic_composition_propagates_num_colors_across_multiple_edges() { variant: ReductionGraph::variant_to_map(&KColoring::::variant()), }, ReductionStep { - name: QUBO::::NAME.to_string(), + name: Decision::>::NAME.to_string(), variant: ReductionGraph::variant_to_map(&QUBO::::variant()), }, ], @@ -151,7 +152,7 @@ fn test_reduction_graph_discovers_registered_reductions() { // Specific reductions should exist assert!(graph.has_direct_reduction_by_name("MaximumIndependentSet", "MinimumVertexCover")); assert!(graph.has_direct_reduction_by_name("MaxCut", "SpinGlass")); - assert!(graph.has_direct_reduction_by_name("Satisfiability", "MaximumIndependentSet")); + assert!(graph.has_direct_reduction_by_name("Satisfiability", "DecisionMaximumIndependentSet")); } #[test] @@ -191,18 +192,20 @@ fn test_find_direct_route_by_exact_variants() { fn test_multi_step_path() { let graph = ReductionGraph::new(); - // Factoring -> CircuitSAT -> SpinGlass is a 2-step path + // Factoring -> CircuitSAT -> DecisionSpinGlass -> SpinGlass is a 3-step path let src = ReductionGraph::variant_to_map(&crate::models::misc::Factoring::variant()); let dst = ReductionGraph::variant_to_map(&SpinGlass::::variant()); let path = graph .find_all_paths("Factoring", &src, "SpinGlass", &dst) .into_iter() - .find(|path| path.type_names() == ["Factoring", "CircuitSAT", "SpinGlass"]) + .find(|path| { + path.type_names() == ["Factoring", "CircuitSAT", "DecisionSpinGlass", "SpinGlass"] + }) .expect("explicit CircuitSAT route should exist"); - assert_eq!(path.len(), 2, "Should be a 2-step path"); + assert_eq!(path.len(), 3, "Should be a 3-step path"); assert_eq!( path.type_names(), - vec!["Factoring", "CircuitSAT", "SpinGlass"] + vec!["Factoring", "CircuitSAT", "DecisionSpinGlass", "SpinGlass"] ); } @@ -227,7 +230,7 @@ fn aggregate_mode_rejects_witness_only_real_edge() { &src, "MinimumVertexCover", &dst, - ReductionMode::Aggregate + ReductionMode::Turing ) .is_empty()); } @@ -249,13 +252,13 @@ fn variant_reduction_supports_both_modes_public_api() { ReductionMode::Witness ) .is_empty()); - assert!(!graph + assert!(graph .find_all_paths_mode( "MaximumIndependentSet", &src, "MaximumIndependentSet", &dst, - ReductionMode::Aggregate + ReductionMode::Turing ) .is_empty()); } @@ -274,7 +277,7 @@ fn value_changing_variant_cast_is_not_aggregate_capable() { &src, "MaximumSetPacking", &dst, - ReductionMode::Aggregate + ReductionMode::Turing ) .is_empty()); } @@ -326,7 +329,7 @@ fn test_subsetsum_to_integerknapsack_is_proof_only() { assert!(!graph.has_direct_reduction_by_name_mode( "SubsetSum", "IntegerKnapsack", - ReductionMode::Aggregate, + ReductionMode::Turing, )); assert!(!graph.has_direct_reduction_by_name_mode( "SubsetSum", @@ -416,7 +419,9 @@ fn test_reduction_path_display() { let path = graph .find_all_paths("Factoring", &src_var, "SpinGlass", &dst_var) .into_iter() - .find(|path| path.type_names() == ["Factoring", "CircuitSAT", "SpinGlass"]) + .find(|path| { + path.type_names() == ["Factoring", "CircuitSAT", "DecisionSpinGlass", "SpinGlass"] + }) .expect("explicit CircuitSAT route"); let s = format!("{path}"); @@ -791,7 +796,7 @@ fn test_has_direct_reduction_by_name_mode() { assert!(!graph.has_direct_reduction_by_name_mode( "MaximumIndependentSet", "MinimumVertexCover", - ReductionMode::Aggregate, + ReductionMode::Turing, )); } @@ -808,7 +813,7 @@ fn test_minimumvertexcover_to_minimummaximalmatching_is_proof_only_direct_edge() assert!(!graph.has_direct_reduction_by_name_mode( "MinimumVertexCover", "MinimumMaximalMatching", - ReductionMode::Aggregate, + ReductionMode::Turing, )); assert!(!graph.has_direct_reduction_by_name_mode( "MinimumVertexCover", @@ -856,7 +861,7 @@ fn test_find_all_paths_mode_aggregate_rejects_witness_only() { &src, "MinimumVertexCover", &dst, - ReductionMode::Aggregate, + ReductionMode::Turing, ); assert!(paths.is_empty()); } @@ -868,7 +873,7 @@ fn test_decision_minimum_vertex_cover_has_both_edges() { assert!(graph.has_direct_reduction_by_name_mode( "DecisionMinimumVertexCover", "MinimumVertexCover", - ReductionMode::Aggregate, + ReductionMode::Witness, )); assert!(graph.has_direct_reduction_by_name_mode( "DecisionMinimumVertexCover", @@ -884,7 +889,7 @@ fn test_decision_minimum_dominating_set_has_both_edges() { assert!(graph.has_direct_reduction_by_name_mode( "DecisionMinimumDominatingSet", "MinimumDominatingSet", - ReductionMode::Aggregate, + ReductionMode::Witness, )); assert!(graph.has_direct_reduction_by_name_mode( "DecisionMinimumDominatingSet", @@ -900,15 +905,15 @@ fn test_decision_minimum_dominating_set_to_minmax_multicenter_has_direct_witness assert!(graph.has_direct_reduction_mode::< Decision>, - MinMaxMulticenter, + Decision>, >(ReductionMode::Witness)); assert!(graph.has_direct_reduction_mode::< Decision>, - MinMaxMulticenter, - >(ReductionMode::Aggregate)); + Decision>, + >(ReductionMode::Witness)); assert!(!graph.has_direct_reduction_mode::< Decision>, - MinMaxMulticenter, + Decision>, >(ReductionMode::Turing)); let entries = crate::rules::registry::reduction_entries(); let variant = Decision::>::variant(); @@ -916,7 +921,7 @@ fn test_decision_minimum_dominating_set_to_minmax_multicenter_has_direct_witness .iter() .find(|e| { e.source_name == "DecisionMinimumDominatingSet" - && e.target_name == "MinMaxMulticenter" + && e.target_name == "DecisionMinMaxMulticenter" && (e.source_variant_fn)() == variant && (e.target_variant_fn)() == variant }) @@ -930,10 +935,21 @@ fn test_decision_minimum_dominating_set_to_minmax_multicenter_has_direct_witness bound, ); let step = (edge.reduce_fn.unwrap())(&source).unwrap(); - assert_eq!( - step.interpret_optimum.as_ref().unwrap()(&witness).unwrap(), - expected - ); + let target = step + .witness + .target_problem_any() + .downcast_ref::>>() + .unwrap(); + let outcome = if expected { + SolveOutcome::optimal(target, witness).unwrap() + } else { + SolveOutcome::Infeasible + }; + let recovered = step + .witness + .recover_result_dyn(&source, crate::solvers::erase_outcome(outcome)) + .unwrap(); + assert_eq!(!matches!(recovered, SolveOutcome::Infeasible), expected); } } @@ -944,15 +960,15 @@ fn test_decision_minimum_dominating_set_to_minimum_sum_multicenter_has_direct_wi assert!(graph.has_direct_reduction_mode::< Decision>, - MinimumSumMulticenter, + Decision>, >(ReductionMode::Witness)); assert!(graph.has_direct_reduction_mode::< Decision>, - MinimumSumMulticenter, - >(ReductionMode::Aggregate)); + Decision>, + >(ReductionMode::Witness)); assert!(!graph.has_direct_reduction_mode::< Decision>, - MinimumSumMulticenter, + Decision>, >(ReductionMode::Turing)); } @@ -974,7 +990,7 @@ fn test_optimization_to_decision_turing_edges() { assert!(!graph.has_direct_reduction_by_name_mode( "MinimumVertexCover", "DecisionMinimumVertexCover", - ReductionMode::Aggregate, + ReductionMode::Witness, )); // MinimumDominatingSet → DecisionMinimumDominatingSet (Turing) @@ -993,10 +1009,10 @@ fn test_ksatisfiability_k3_to_decision_minimum_vertex_cover_direct_witness_edge( KSatisfiability, Decision>, >(ReductionMode::Witness)); - assert!(!graph.has_direct_reduction_mode::< + assert!(graph.has_direct_reduction_mode::< KSatisfiability, Decision>, - >(ReductionMode::Aggregate)); + >(ReductionMode::Witness)); assert!(!graph.has_direct_reduction_mode::< KSatisfiability, Decision>, @@ -1069,8 +1085,6 @@ fn test_find_paths_bounded_returns_shortest_when_truncated() { >::new( crate::models::formula::Satisfiability::new(0, vec![]) )), - aggregate: None, - interpret_optimum: None, }) } @@ -1084,7 +1098,7 @@ fn test_find_paths_bounded_returns_shortest_when_truncated() { }, ), reduce_fn: Some(reduce), - reduce_aggregate_fn: None, + turing: false, } } diff --git a/src/unit_tests/registry/variant.rs b/src/unit_tests/registry/variant.rs index 4dcbca44c..71259a675 100644 --- a/src/unit_tests/registry/variant.rs +++ b/src/unit_tests/registry/variant.rs @@ -381,6 +381,10 @@ fn unit_variants_construct_without_unit_inputs() { json!({"graph":graph,"bound":1}) } "MaxCut" => json!({"graph":[[0,1],[1,2]]}), + "DecisionLongestPath" => { + json!({"graph":[[0,1],[1,2]],"source_vertex":0,"target_vertex":2,"bound":2}) + } + "DecisionMinMaxMulticenter" => json!({"graph":[[0,1],[1,2]],"k":1,"bound":1}), "LongestPath" => json!({"graph":[[0,1],[1,2]],"source_vertex":0,"target_vertex":2}), "MinMaxMulticenter" => json!({"graph":[[0,1],[1,2]],"k":1}), "MixedChinesePostman" => json!({"graph":[[0,1],[1,2]],"arcs":[[2,0]]}), diff --git a/src/unit_tests/rules/acyclicpartition_ilp.rs b/src/unit_tests/rules/acyclicpartition_ilp.rs index bf5bec2cb..50ec5acee 100644 --- a/src/unit_tests/rules/acyclicpartition_ilp.rs +++ b/src/unit_tests/rules/acyclicpartition_ilp.rs @@ -2,6 +2,7 @@ use super::*; use crate::models::algebraic::ILP; use crate::models::graph::AcyclicPartition; use crate::rules::ReduceTo; +use crate::solvers::SolveOutcome; use crate::solvers::{BruteForce, ILPSolver}; use crate::topology::DirectedGraph; use crate::traits::Problem; @@ -32,7 +33,14 @@ fn test_acyclicpartition_to_ilp_closed_loop() { // Solve ILP let ilp_solver = ILPSolver::new(); let ilp_sol = ilp_solver.solve(ilp).expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_sol).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), ilp_sol.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!( source.evaluate(&extracted).unwrap().0, @@ -60,7 +68,14 @@ fn test_extract_solution() { let ilp = reduction.target_problem(); let solver = ILPSolver::new(); let ilp_sol = solver.solve(ilp).expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_sol).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), ilp_sol.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted.len(), 4); assert!(source.evaluate(&extracted).unwrap().0); } @@ -110,7 +125,14 @@ fn test_acyclicpartition_to_ilp_regression_direct_topological_labels() { let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("the feasible source instance must yield a feasible ILP"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!(source.evaluate(&extracted).unwrap().0); } diff --git a/src/unit_tests/rules/balancedcompletebipartitesubgraph_ilp.rs b/src/unit_tests/rules/balancedcompletebipartitesubgraph_ilp.rs index 630fb7c69..9136af53e 100644 --- a/src/unit_tests/rules/balancedcompletebipartitesubgraph_ilp.rs +++ b/src/unit_tests/rules/balancedcompletebipartitesubgraph_ilp.rs @@ -3,6 +3,7 @@ use crate::models::algebraic::ILP; use crate::models::graph::BalancedCompleteBipartiteSubgraph; use crate::rules::test_helpers::assert_bf_vs_ilp; use crate::rules::ReduceTo; +use crate::solvers::SolveOutcome; use crate::topology::BipartiteGraph; use crate::traits::Problem; @@ -57,7 +58,14 @@ fn test_extract_solution_identity() { let reduction: ReductionBCBSToILP = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let target_sol = vec![1, 1, 0, 1, 1, 0]; - let extracted = reduction.extract_solution(&target_sol).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), target_sol.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted, vec![true, true, false, true, true, false]); assert!(source.evaluate(&extracted).unwrap().0); } diff --git a/src/unit_tests/rules/bicliquecover_bmf.rs b/src/unit_tests/rules/bicliquecover_bmf.rs index 739b63df8..27450f060 100644 --- a/src/unit_tests/rules/bicliquecover_bmf.rs +++ b/src/unit_tests/rules/bicliquecover_bmf.rs @@ -3,6 +3,7 @@ use crate::models::algebraic::BMF; use crate::models::graph::BicliqueCover; use crate::rules::{ReduceTo, ReductionResult}; use crate::solvers::BruteForce; +use crate::solvers::SolveOutcome; use crate::topology::BipartiteGraph; use crate::traits::Problem; @@ -70,7 +71,14 @@ fn test_bicliquecover_to_bmf_closed_loop_full_biclique() { .solve(target) .unwrap() .expect("target must be feasible"); - let extracted = reduction.extract_solution(&target_witness).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), target_witness.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(problem.evaluate(&extracted).unwrap(), bf_source); } @@ -89,7 +97,14 @@ fn test_bicliquecover_to_bmf_closed_loop_identity_rank2() { .solve(target) .unwrap() .expect("target must be feasible"); - let extracted = reduction.extract_solution(&target_witness).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), target_witness.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(problem.evaluate(&extracted).unwrap(), bf_source); } diff --git a/src/unit_tests/rules/biconnectivityaugmentation_ilp.rs b/src/unit_tests/rules/biconnectivityaugmentation_ilp.rs index 9c429ce12..784a98e5f 100644 --- a/src/unit_tests/rules/biconnectivityaugmentation_ilp.rs +++ b/src/unit_tests/rules/biconnectivityaugmentation_ilp.rs @@ -2,8 +2,11 @@ use super::*; use crate::models::algebraic::ILP; use crate::models::graph::BiconnectivityAugmentation; use crate::rules::ReduceTo; +use crate::rules::ReductionResult; +use crate::solvers::SolveOutcome; use crate::solvers::{BruteForce, ILPSolver}; use crate::topology::SimpleGraph; +use crate::traits::EvaluationError::InvalidConfiguration; use crate::traits::Problem; fn small_instance() -> BiconnectivityAugmentation { @@ -30,7 +33,14 @@ fn test_biconnectivityaugmentation_to_ilp_closed_loop() { // Solve ILP let ilp_solver = ILPSolver::new(); let ilp_sol = ilp_solver.solve(ilp).expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_sol).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), ilp_sol.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!( source.evaluate(&extracted).unwrap().0, @@ -46,7 +56,14 @@ fn test_extract_solution() { let ilp = reduction.target_problem(); let solver = ILPSolver::new(); let ilp_sol = solver.solve(ilp).expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_sol).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), ilp_sol.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted.len(), 3); assert!(source.evaluate(&extracted).unwrap().0); } @@ -59,7 +76,14 @@ fn test_trivial_single_vertex() { let ilp = reduction.target_problem(); let solver = ILPSolver::new(); let ilp_sol = solver.solve(ilp).expect("trivial ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_sol).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), ilp_sol.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!(source.evaluate(&extracted).unwrap().0); } @@ -78,7 +102,14 @@ fn test_already_biconnected() { let ilp_sol = solver .solve(ilp) .expect("already biconnected should be solvable"); - let extracted = reduction.extract_solution(&ilp_sol).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), ilp_sol.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!(source.evaluate(&extracted).unwrap().0); } @@ -112,7 +143,7 @@ fn test_biconnectivityaugmentation_to_ilp_all_two_vertex_instances() { assert!(expected); assert!( source - .evaluate(&reduction.extract_solution(&z).unwrap()) + .evaluate(&reduction.recover_result(&source, SolveOutcome::optimal(reduction.target_problem(), z.clone()).unwrap()).unwrap().into_solution().expect("qualifying target result must recover a source solution")) .unwrap() .0 ); @@ -143,7 +174,15 @@ fn test_biconnectivityaugmentation_to_ilp_empty_negative_budget() { budget >= 0 ); if budget >= 0 { - assert!(reduction.extract_solution(&vec![]).unwrap().is_empty()); + assert!(reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), vec![].clone()).unwrap() + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution") + .is_empty()); } } } @@ -158,22 +197,34 @@ fn test_biconnectivityaugmentation_to_ilp_signed_cost_and_certificate_bounds() { let z = ILPSolver::new().solve(reduction.target_problem()).unwrap(); assert!( source - .evaluate(&reduction.extract_solution(&z).unwrap()) + .evaluate( + &reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), z.clone()).unwrap() + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution") + ) .unwrap() .0 ); - assert!( - !matches!(crate::traits::Problem::evaluate(reduction.target_problem(), &vec![0; z.len()]), Ok(value) if value.is_valid()) - ); - assert!( - !matches!(crate::traits::Problem::evaluate(reduction.target_problem(), &vec![1; z.len() + 1]), Ok(value) if value.is_valid()) - ); + assert!(!ReductionResult::target_problem(&reduction) + .evaluate(&vec![0; z.len()]) + .unwrap() + .is_valid()); + assert!(matches!( + ReductionResult::target_problem(&reduction).evaluate(&vec![1; z.len() + 1]), + Err(InvalidConfiguration(_)) + )); for value in [-1, 2] { let mut bad = z.clone(); bad[0] = value; - assert!( - !matches!(crate::traits::Problem::evaluate(reduction.target_problem(), &bad), Ok(value) if value.is_valid()) - ); + assert!(!ReductionResult::target_problem(&reduction) + .evaluate(&bad) + .unwrap() + .is_valid()); } } } diff --git a/src/unit_tests/rules/binpacking_ilp.rs b/src/unit_tests/rules/binpacking_ilp.rs index b254153d5..4101f63b2 100644 --- a/src/unit_tests/rules/binpacking_ilp.rs +++ b/src/unit_tests/rules/binpacking_ilp.rs @@ -1,4 +1,5 @@ use super::*; +use crate::solvers::SolveOutcome; use crate::solvers::{BruteForce, ILPSolver}; use crate::traits::Problem; use crate::types::Min; @@ -36,7 +37,14 @@ fn test_binpacking_to_ilp_closed_loop() { // Solve via ILP let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let ilp_obj = problem.evaluate(&extracted).unwrap(); assert_eq!(bf_obj, Min(Some(2))); @@ -55,7 +63,14 @@ fn test_single_item() { let ilp_solver = ILPSolver::new(); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!(problem.evaluate(&extracted).unwrap().is_valid()); assert_eq!(problem.evaluate(&extracted).unwrap(), Min(Some(1))); @@ -71,7 +86,14 @@ fn test_same_weight_items() { let ilp_solver = ILPSolver::new(); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!(problem.evaluate(&extracted).unwrap().is_valid()); assert_eq!(problem.evaluate(&extracted).unwrap(), Min(Some(2))); @@ -87,7 +109,14 @@ fn test_exact_fill() { let ilp_solver = ILPSolver::new(); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!(problem.evaluate(&extracted).unwrap().is_valid()); assert_eq!(problem.evaluate(&extracted).unwrap(), Min(Some(1))); @@ -109,7 +138,14 @@ fn test_solution_extraction() { ilp_solution[9] = 1; // y_0 = 1 ilp_solution[10] = 1; // y_1 = 1 - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted, vec![0, 1, 0]); assert!(problem.evaluate(&extracted).unwrap().is_valid()); } diff --git a/src/unit_tests/rules/bmf_bicliquecover.rs b/src/unit_tests/rules/bmf_bicliquecover.rs index 6ed1e3a2b..83905b6ba 100644 --- a/src/unit_tests/rules/bmf_bicliquecover.rs +++ b/src/unit_tests/rules/bmf_bicliquecover.rs @@ -3,6 +3,7 @@ use crate::models::algebraic::BMF; use crate::models::graph::BicliqueCover; use crate::rules::{ReduceTo, ReductionResult}; use crate::solvers::BruteForce; +use crate::solvers::SolveOutcome; use crate::traits::Problem; #[test] @@ -33,7 +34,14 @@ fn test_bmf_to_bicliquecover_closed_loop_all_ones() { .solve(target) .unwrap() .expect("target has feasible biclique cover"); - let extracted = reduction.extract_solution(&target_witness).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), target_witness.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(problem.evaluate(&extracted).unwrap(), bf_source); assert!(problem.is_exact(&extracted).unwrap()); @@ -54,7 +62,14 @@ fn test_bmf_to_bicliquecover_closed_loop_identity() { .solve(target) .unwrap() .expect("target has feasible biclique cover"); - let extracted = reduction.extract_solution(&target_witness).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), target_witness.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(problem.evaluate(&extracted).unwrap(), bf_source); assert!(problem.is_exact(&extracted).unwrap()); diff --git a/src/unit_tests/rules/bottlenecktravelingsalesman_ilp.rs b/src/unit_tests/rules/bottlenecktravelingsalesman_ilp.rs index 635b06813..afba4fee4 100644 --- a/src/unit_tests/rules/bottlenecktravelingsalesman_ilp.rs +++ b/src/unit_tests/rules/bottlenecktravelingsalesman_ilp.rs @@ -1,6 +1,9 @@ use super::*; +use crate::rules::ReductionResult; +use crate::solvers::SolveOutcome; use crate::solvers::{BruteForce, ILPSolver}; use crate::topology::SimpleGraph; +use crate::traits::EvaluationError::InvalidConfiguration; use crate::traits::Problem; fn k4_btsp() -> BottleneckTravelingSalesman { @@ -34,7 +37,14 @@ fn test_bottlenecktravelingsalesman_to_ilp_closed_loop() { let ilp_solution = ilp_solver .solve(reduction.target_problem()) .expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let ilp_value = problem.evaluate(&extracted).unwrap(); assert!( @@ -64,7 +74,14 @@ fn test_bottlenecktravelingsalesman_to_ilp_c4() { let ilp_solution = ilp_solver .solve(reduction.target_problem()) .expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let ilp_value = problem.evaluate(&extracted).unwrap(); assert!(ilp_value.is_valid()); @@ -80,7 +97,14 @@ fn test_solution_extraction() { let ilp_solution = ilp_solver .solve(reduction.target_problem()) .expect("solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let metric = problem.evaluate(&extracted).unwrap(); assert!(metric.is_valid()); } @@ -172,7 +196,14 @@ 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 witness = tour_witness(&source, &tour, &edge_order); - let extracted = result.extract_solution(&witness).unwrap(); + let extracted = result + .recover_result( + &source, + SolveOutcome::optimal(result.target_problem(), witness.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let expected = source.evaluate(&extracted).unwrap().unwrap(); assert_eq!( result.target_problem().evaluate(&witness).unwrap().value, @@ -182,13 +213,16 @@ fn test_bottleneck_ilp_signed_full_range_and_native_cycles() { for variable in 0..witness.len() { let mut invalid = witness.clone(); invalid[variable] = 2; - assert!( - !matches!(crate::traits::Problem::evaluate(result.target_problem(), &invalid), Ok(value) if value.is_valid()) - ); + assert!(!ReductionResult::target_problem(&result) + .evaluate(&invalid) + .unwrap() + .is_valid()); } - assert!( - !matches!(crate::traits::Problem::evaluate(result.target_problem(), &witness[..witness.len() - 1].to_vec()), Ok(value) if value.is_valid()) - ); + assert!(matches!( + ReductionResult::target_problem(&result) + .evaluate(&witness[..witness.len() - 1].to_vec()), + Err(InvalidConfiguration(_)) + )); } } @@ -199,18 +233,21 @@ fn test_bottleneck_ilp_maximum_must_be_used_and_dominate() { 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); - assert!( - !matches!(crate::traits::Problem::evaluate(result.target_problem(), &config), Ok(value) if value.is_valid()) - ); + assert!(!ReductionResult::target_problem(&result) + .evaluate(&config) + .unwrap() + .is_valid()); config[selector] = 1; // used, but lower than the maximum edge - assert!( - !matches!(crate::traits::Problem::evaluate(result.target_problem(), &config), Ok(value) if value.is_valid()) - ); + assert!(!ReductionResult::target_problem(&result) + .evaluate(&config) + .unwrap() + .is_valid()); config[selector] = 0; config[selector + 1] = 1; // unused - assert!( - !matches!(crate::traits::Problem::evaluate(result.target_problem(), &config), Ok(value) if value.is_valid()) - ); + assert!(!ReductionResult::target_problem(&result) + .evaluate(&config) + .unwrap() + .is_valid()); } #[test] diff --git a/src/unit_tests/rules/boundedcomponentspanningforest_ilp.rs b/src/unit_tests/rules/boundedcomponentspanningforest_ilp.rs index be4313293..ed41a8bab 100644 --- a/src/unit_tests/rules/boundedcomponentspanningforest_ilp.rs +++ b/src/unit_tests/rules/boundedcomponentspanningforest_ilp.rs @@ -2,6 +2,7 @@ use super::*; use crate::models::algebraic::ILP; use crate::models::graph::BoundedComponentSpanningForest; use crate::rules::ReduceTo; +use crate::solvers::SolveOutcome; use crate::solvers::{BruteForce, ILPSolver}; use crate::topology::SimpleGraph; use crate::traits::Problem; @@ -31,7 +32,14 @@ fn test_boundedcomponentspanningforest_to_ilp_closed_loop() { // Solve ILP let ilp_solver = ILPSolver::new(); let ilp_sol = ilp_solver.solve(ilp).expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_sol).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), ilp_sol.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!( source.evaluate(&extracted).unwrap().0, @@ -47,7 +55,14 @@ fn test_extract_solution() { let ilp = reduction.target_problem(); let solver = ILPSolver::new(); let ilp_sol = solver.solve(ilp).expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_sol).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), ilp_sol.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted.len(), 4); assert!(source.evaluate(&extracted).unwrap().0); } @@ -68,7 +83,14 @@ fn test_single_component() { let ilp_sol = solver .solve(ilp) .expect("single component should be solvable"); - let extracted = reduction.extract_solution(&ilp_sol).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), ilp_sol.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!(source.evaluate(&extracted).unwrap().0); } diff --git a/src/unit_tests/rules/capacityassignment_ilp.rs b/src/unit_tests/rules/capacityassignment_ilp.rs index 693805a86..30ec31ee9 100644 --- a/src/unit_tests/rules/capacityassignment_ilp.rs +++ b/src/unit_tests/rules/capacityassignment_ilp.rs @@ -1,4 +1,5 @@ use super::*; +use crate::solvers::SolveOutcome; use crate::solvers::{BruteForce, ILPSolver}; use crate::traits::Problem; use crate::types::Min; @@ -65,7 +66,14 @@ fn test_capacityassignment_to_ilp_closed_loop() { ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be feasible"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let ilp_value = problem.evaluate(&extracted).unwrap(); assert_eq!( ilp_value, bf_value, @@ -87,7 +95,14 @@ fn test_solution_extraction() { // Both links choose capacity level 1: total delay 4 + 3 <= 10. let ilp_solution = vec![0, 1, 0, 0, 1, 0]; - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted, vec![1, 1]); assert!(problem.evaluate(&extracted).unwrap().is_valid()); } @@ -106,7 +121,14 @@ fn test_capacityassignment_to_ilp_trivial() { let ilp_solver = ILPSolver::new(); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be feasible"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!(problem.evaluate(&extracted).unwrap().0.is_some()); } diff --git a/src/unit_tests/rules/circuit_ilp.rs b/src/unit_tests/rules/circuit_ilp.rs index ea48a23ba..0435d30b5 100644 --- a/src/unit_tests/rules/circuit_ilp.rs +++ b/src/unit_tests/rules/circuit_ilp.rs @@ -1,7 +1,10 @@ use super::*; use crate::models::formula::{Assignment, BooleanExpr, Circuit, CircuitSAT}; use crate::rules::test_helpers::assert_bf_vs_ilp; +use crate::rules::ReductionResult; +use crate::solvers::SolveOutcome; use crate::solvers::{BruteForce, ILPSolver}; +use crate::traits::EvaluationError::InvalidConfiguration; use crate::traits::Problem; use crate::types::Or; @@ -103,7 +106,14 @@ fn test_circuit_to_ilp_bf_vs_ilp() { let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(source.evaluate(&extracted).unwrap(), Or(true)); } @@ -140,13 +150,25 @@ fn test_circuit_ilp_native_folds_all_feasible_witnesses() { .map(|i| if (mask >> i) & 1 == 0 { 0 } else { 1 }) .collect(); if target.evaluate(&solution).unwrap().value.is_some() { - let extracted = reduction.extract_solution(&solution).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), solution.clone()) + .unwrap(), + ) + .map(|result| { + result.into_solution().expect( + "qualifying target result must recover a source solution", + ) + }) + .unwrap(); assert!(source.evaluate(&extracted).unwrap().0); actual.insert(extracted); } else { - assert!( - !matches!(crate::traits::Problem::evaluate(reduction.target_problem(), &solution), Ok(value) if value.is_valid()) - ); + assert!(!ReductionResult::target_problem(&reduction) + .evaluate(&solution) + .unwrap() + .is_valid()); } } assert_eq!(actual, expected, "{expr:?}, output={output}"); @@ -162,16 +184,30 @@ fn test_circuit_ilp_rejects_invalid_target_and_supports_empty_circuit() { BooleanExpr::constant(true), )])); let reduction = ReduceTo::>::reduce_to(&source).unwrap(); - for invalid in [vec![], vec![0], vec![0, 0], vec![2, 1], vec![1, 1, 1]] { - assert!( - !matches!(crate::traits::Problem::evaluate(reduction.target_problem(), &invalid), Ok(value) if value.is_valid()) - ); + for invalid in [vec![], vec![0], vec![1, 1, 1]] { + assert!(matches!( + ReductionResult::target_problem(&reduction).evaluate(&invalid), + Err(InvalidConfiguration(_)) + )); + } + for invalid in [vec![0, 0], vec![2, 1]] { + assert!(!ReductionResult::target_problem(&reduction) + .evaluate(&invalid) + .unwrap() + .is_valid()); } let empty = CircuitSAT::new(Circuit::new(vec![])); let reduction = ReduceTo::>::reduce_to(&empty).unwrap(); assert_eq!(reduction.target_problem().num_vars(), 0); assert_eq!( - reduction.extract_solution(&vec![]).unwrap(), + reduction + .recover_result( + &empty, + SolveOutcome::optimal(reduction.target_problem(), vec![].clone()).unwrap() + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"), Vec::::new() ); } diff --git a/src/unit_tests/rules/circuit_sat.rs b/src/unit_tests/rules/circuit_sat.rs index be07e6372..9f5168584 100644 --- a/src/unit_tests/rules/circuit_sat.rs +++ b/src/unit_tests/rules/circuit_sat.rs @@ -3,6 +3,7 @@ use crate::models::formula::{Assignment, BooleanExpr, Circuit, CircuitSAT, Satis use crate::rules::test_helpers::assert_satisfaction_round_trip_from_satisfaction_target; use crate::rules::ReduceTo; use crate::solvers::BruteForce; +use crate::solvers::SolveOutcome; use crate::traits::Problem; fn contradiction_source() -> CircuitSAT { @@ -28,7 +29,14 @@ fn test_circuitsat_to_satisfiability_closed_loop() { .solve(reduction.target_problem()) .unwrap() .expect("issue example should yield a SAT witness"); - let extracted = reduction.extract_solution(&target_solution).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), target_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted.len(), source.num_variables()); assert!(source.evaluate(&extracted).unwrap().0); } diff --git a/src/unit_tests/rules/circuit_spinglass.rs b/src/unit_tests/rules/circuit_spinglass.rs index 08cb4e8c3..9ad670180 100644 --- a/src/unit_tests/rules/circuit_spinglass.rs +++ b/src/unit_tests/rules/circuit_spinglass.rs @@ -1,8 +1,13 @@ use super::*; +use crate::models::decision::Decision; use crate::models::formula::Circuit; -use crate::rules::test_helpers::assert_satisfaction_round_trip_from_optimization_target; +use crate::rules::test_helpers::assert_satisfaction_round_trip_from_satisfaction_target; +use crate::rules::ReductionResult; use crate::solvers::BruteForce; +use crate::solvers::SolveOutcome; +use crate::traits::EvaluationError::InvalidConfiguration; use crate::traits::Problem; +use crate::types::OptimizationValue; use crate::types::{NumericSize, WeightElement}; use num_traits::Num; @@ -146,7 +151,7 @@ fn test_constant_true() { BooleanExpr::constant(true), )]); let problem = CircuitSAT::new(circuit); - let reduction = ReduceTo::>::reduce_to(&problem) + let reduction = ReduceTo::>>::reduce_to(&problem) .expect("reduction should succeed"); let sg = reduction.target_problem(); @@ -155,7 +160,16 @@ fn test_constant_true() { let extracted: Vec> = solutions .iter() - .map(|s| reduction.extract_solution(s).unwrap()) + .map(|s| { + reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), (s).clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution") + }) .collect(); // c should be 1 @@ -174,7 +188,7 @@ fn test_constant_false() { BooleanExpr::constant(false), )]); let problem = CircuitSAT::new(circuit); - let reduction = ReduceTo::>::reduce_to(&problem) + let reduction = ReduceTo::>>::reduce_to(&problem) .expect("reduction should succeed"); let sg = reduction.target_problem(); @@ -183,7 +197,16 @@ fn test_constant_false() { let extracted: Vec> = solutions .iter() - .map(|s| reduction.extract_solution(s).unwrap()) + .map(|s| { + reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), (s).clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution") + }) .collect(); // c should be 0 @@ -206,7 +229,7 @@ fn test_multi_input_and() { ]), )]); let problem = CircuitSAT::new(circuit); - let reduction = ReduceTo::>::reduce_to(&problem) + let reduction = ReduceTo::>>::reduce_to(&problem) .expect("reduction should succeed"); let sg = reduction.target_problem(); @@ -215,7 +238,16 @@ fn test_multi_input_and() { let extracted: Vec> = solutions .iter() - .map(|s| reduction.extract_solution(s).unwrap()) + .map(|s| { + reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), (s).clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution") + }) .collect(); // Variables sorted: c, x, y, z @@ -240,24 +272,24 @@ fn test_reduction_result_methods() { BooleanExpr::var("x"), )]); let problem = CircuitSAT::new(circuit); - let reduction = ReduceTo::>::reduce_to(&problem) + let reduction = ReduceTo::>>::reduce_to(&problem) .expect("reduction should succeed"); // Test target_problem and extract_solution work let sg = reduction.target_problem(); - assert!(sg.num_spins() >= 2); // At least c and x + assert!(sg.inner().num_spins() >= 2); // At least c and x } #[test] fn test_empty_circuit() { let circuit = Circuit::new(vec![]); let problem = CircuitSAT::new(circuit); - let reduction = ReduceTo::>::reduce_to(&problem) + let reduction = ReduceTo::>>::reduce_to(&problem) .expect("reduction should succeed"); let sg = reduction.target_problem(); // Empty circuit should result in empty SpinGlass - assert_eq!(sg.num_spins(), 0); + assert_eq!(sg.inner().num_spins(), 0); } #[test] @@ -267,7 +299,7 @@ fn test_solution_extraction() { BooleanExpr::and(vec![BooleanExpr::var("x"), BooleanExpr::var("y")]), )]); let problem = CircuitSAT::new(circuit); - let reduction = ReduceTo::>::reduce_to(&problem) + let reduction = ReduceTo::>>::reduce_to(&problem) .expect("reduction should succeed"); // The source variables are c, x, y (sorted) @@ -276,7 +308,7 @@ fn test_solution_extraction() { // Test extraction with a mock target solution // Need to know the mapping to construct proper test let sg = reduction.target_problem(); - assert!(sg.num_spins() >= 3); // At least c, x, y + assert!(sg.inner().num_spins() >= 3); // At least c, x, y } #[test] @@ -298,9 +330,9 @@ fn test_jl_parity_circuitsat_to_spinglass() { Assignment::new(vec!["z".to_string()], z_expr), ]); let source = CircuitSAT::new(circuit); - let result = ReduceTo::>::reduce_to(&source) + let result = ReduceTo::>>::reduce_to(&source) .expect("reduction should succeed"); - assert_satisfaction_round_trip_from_optimization_target( + assert_satisfaction_round_trip_from_satisfaction_target( &source, &result, "CircuitSAT->SpinGlass parity", @@ -309,7 +341,6 @@ fn test_jl_parity_circuitsat_to_spinglass() { #[test] fn test_circuit_spinglass_all_threshold_witnesses_native_domain() { - use crate::rules::AggregateReductionResult; use std::collections::BTreeSet; let x = BooleanExpr::var("x"); let y = BooleanExpr::var("y"); @@ -339,64 +370,102 @@ fn test_circuit_spinglass_all_threshold_witnesses_native_domain() { vec![output.into()], expr.clone(), )])); - let reduction = ReduceTo::>::reduce_to(&source).unwrap(); - let target = AggregateReductionResult::target_problem(&reduction); + let reduction = + ReduceTo::>>::reduce_to(&source).unwrap(); + let target = crate::rules::ReductionResult::target_problem(&reduction); let expected: BTreeSet<_> = BruteForce::new() .find_all_witnesses(&source) .unwrap() .into_iter() .collect(); let mut actual = BTreeSet::new(); - for mask in 0..(1usize << target.num_spins()) { - let spins = (0..target.num_spins()) + for mask in 0..(1usize << target.inner().num_spins()) { + let spins = (0..target.inner().num_spins()) .map(|i| if mask >> i & 1 == 0 { -1 } else { 1 }) .collect(); - let energy = target.evaluate(&spins).unwrap(); - assert!(energy.0.unwrap() >= reduction.zero_penalty_energy); - if reduction.extract_value(energy).0 { - let decoded = reduction.extract_solution(&spins).unwrap(); + let energy = target.inner().evaluate(&spins).unwrap(); + assert!(energy.0.unwrap() >= *ReductionResult::target_problem(&reduction).bound()); + if crate::types::Or(OptimizationValue::meets_bound( + &(energy), + crate::rules::ReductionResult::target_problem(&reduction).bound(), + )) + .0 + { + let decoded = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), spins.clone()) + .unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!(source.evaluate(&decoded).unwrap().0); actual.insert(decoded); } else { - assert!( - !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &spins), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&reduction, value); value.is_valid() }) - ); + assert!(!ReductionResult::target_problem(&reduction) + .evaluate(&spins) + .unwrap() + .is_valid()); } } assert_eq!(actual, expected, "expression {expr:?}, output {output}"); - assert!(!reduction.extract_value(crate::types::Min(None)).0); + assert!( + !crate::types::Or(OptimizationValue::meets_bound( + &(crate::types::Min(None)), + crate::rules::ReductionResult::target_problem(&reduction).bound() + )) + .0 + ); } } } #[test] fn test_circuit_spinglass_unsat_threshold_and_invalid_spins() { - use crate::rules::AggregateReductionResult; let source = CircuitSAT::new(Circuit::new(vec![Assignment::new( vec!["x".into()], BooleanExpr::not(BooleanExpr::var("x")), )])); - let reduction = ReduceTo::>::reduce_to(&source).unwrap(); - assert_eq!(reduction.zero_penalty_energy, -5); - assert!(!reduction.extract_value(crate::types::Min(Some(-3))).0); - for witness in BruteForce::new() + let reduction = ReduceTo::>>::reduce_to(&source).unwrap(); + assert_eq!(*ReductionResult::target_problem(&reduction).bound(), -5); + assert!( + !crate::types::Or(OptimizationValue::meets_bound( + &(crate::types::Min(Some(-3))), + crate::rules::ReductionResult::target_problem(&reduction).bound() + )) + .0 + ); + assert!(BruteForce::new() .find_all_witnesses(ReductionResult::target_problem(&reduction)) .unwrap() - { - assert!( - !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &witness), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&reduction, value); value.is_valid() }) - ); - } + .is_empty()); for bad in [vec![], vec![1], vec![0, 0], vec![1, 1, 1]] { - assert!( - !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &bad), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&reduction, value); value.is_valid() }) - ); + assert!(matches!( + ReductionResult::target_problem(&reduction) + .inner() + .evaluate(&bad), + Err(InvalidConfiguration(_)) + )); } let empty = CircuitSAT::new(Circuit::new(vec![])); - let reduction = ReduceTo::>::reduce_to(&empty).unwrap(); - assert!(reduction.extract_value(crate::types::Min(Some(0))).0); + let reduction = ReduceTo::>>::reduce_to(&empty).unwrap(); + assert!( + crate::types::Or(OptimizationValue::meets_bound( + &(crate::types::Min(Some(0))), + crate::rules::ReductionResult::target_problem(&reduction).bound() + )) + .0 + ); assert_eq!( - reduction.extract_solution(&vec![]).unwrap(), + reduction + .recover_result( + &empty, + SolveOutcome::optimal(reduction.target_problem(), vec![].clone()).unwrap() + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"), Vec::::new() ); } @@ -425,19 +494,24 @@ fn test_circuit_spinglass_variadic_constant_overhead() { let args = vec![BooleanExpr::constant(false); width]; let expr = BooleanExpr::xor(args); let source = CircuitSAT::new(Circuit::new(vec![Assignment::new(vec![], expr)])); - let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + let reduction = + ReduceTo::>>::reduce_to(&source).unwrap(); let target = reduction.target_problem(); let expected = if width == 0 { 1 } else { width + 2 * (width - 1) }; - assert_eq!(target.num_spins(), expected); - assert!(target.num_spins() <= source.num_variables() + 3 * source.num_expression_nodes()); + assert_eq!(target.inner().num_spins(), expected); + assert!( + target.inner().num_spins() + <= source.num_variables() + 3 * source.num_expression_nodes() + ); if width == 5 { - assert_eq!(target.num_spins(), 13); + assert_eq!(target.inner().num_spins(), 13); assert!( - target.num_spins() > source.num_variables() + 2 * source.num_expression_nodes() + target.inner().num_spins() + > source.num_variables() + 2 * source.num_expression_nodes() ); } } diff --git a/src/unit_tests/rules/closeststring_ilp.rs b/src/unit_tests/rules/closeststring_ilp.rs index 0611e5574..7c79f79a1 100644 --- a/src/unit_tests/rules/closeststring_ilp.rs +++ b/src/unit_tests/rules/closeststring_ilp.rs @@ -2,6 +2,7 @@ use super::*; use crate::models::algebraic::{ObjectiveSense, ILP}; use crate::models::misc::ClosestString; use crate::rules::test_helpers::assert_bf_vs_ilp; +use crate::solvers::SolveOutcome; use crate::solvers::{BruteForce, ILPSolver}; use crate::traits::Problem; use crate::types::Min; @@ -59,7 +60,14 @@ fn test_closeststring_to_ilp_closed_loop() { let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let extracted_value = source.evaluate(&extracted).unwrap(); // The extracted center must be syntactically valid and match the BF optimum. @@ -89,7 +97,14 @@ fn test_closeststring_to_ilp_extract_known_center() { target_solution[4] = 1; // x_{2,0} target_solution[6] = 2; // R = 2 - let extracted = reduction.extract_solution(&target_solution).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), target_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted, vec![0, 0, 0]); assert_eq!(source.evaluate(&extracted).unwrap(), Min(Some(2))); } @@ -133,7 +148,14 @@ fn test_closeststring_to_ilp_single_string_zero_radius() { let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted, vec![1, 0, 1, 1]); assert_eq!(source.evaluate(&extracted).unwrap(), Min(Some(0))); } diff --git a/src/unit_tests/rules/closestsubstring_ilp.rs b/src/unit_tests/rules/closestsubstring_ilp.rs index 5797cb02d..bf567ff91 100644 --- a/src/unit_tests/rules/closestsubstring_ilp.rs +++ b/src/unit_tests/rules/closestsubstring_ilp.rs @@ -2,6 +2,7 @@ use super::*; use crate::models::algebraic::{ObjectiveSense, ILP}; use crate::models::misc::ClosestSubstring; use crate::rules::test_helpers::assert_bf_vs_ilp; +use crate::solvers::SolveOutcome; use crate::solvers::{BruteForce, ILPSolver}; use crate::traits::Problem; use crate::types::Min; @@ -95,7 +96,14 @@ fn test_closestsubstring_to_ilp_closed_loop() { let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); // Extracted config must be syntactically valid (length ell + n = 6) and // match the brute-force optimum. @@ -129,7 +137,14 @@ fn test_closestsubstring_to_ilp_zero_radius_when_common_substring_exists() { let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let extracted_value = source.evaluate(&extracted).unwrap(); assert!(extracted_value.is_valid()); @@ -175,7 +190,14 @@ fn test_closestsubstring_to_ilp_extract_known_solution() { target_solution[6 + 6] = 1; // y_{3, 0} target_solution[ilp.num_vars() - 1] = 1; // R = 1 - let extracted = reduction.extract_solution(&target_solution).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), target_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted, vec![0, 1, 0, 0, 1, 0]); assert_eq!(source.evaluate(&extracted).unwrap(), Min(Some(1))); } diff --git a/src/unit_tests/rules/closestvectorproblem_casts.rs b/src/unit_tests/rules/closestvectorproblem_casts.rs index efd57957b..f26290257 100644 --- a/src/unit_tests/rules/closestvectorproblem_casts.rs +++ b/src/unit_tests/rules/closestvectorproblem_casts.rs @@ -1,5 +1,6 @@ use super::*; use crate::rules::{ReduceTo, ReductionError, ReductionGraph, ReductionResult}; +use crate::solvers::SolveOutcome; use crate::types::MAX_EXACT_F64_INTEGER; #[test] @@ -9,7 +10,17 @@ fn test_closestvectorproblem_i64_to_f64_closed_loop() { assert_eq!(reduction.target_problem().basis(), source.basis()); assert_eq!(reduction.target_problem().target(), &[3.0, 2.0]); - assert_eq!(reduction.extract_solution(&vec![1, 1]).unwrap(), vec![1, 1]); + assert_eq!( + reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), vec![1, 1].clone()).unwrap() + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"), + vec![1, 1] + ); } #[test] diff --git a/src/unit_tests/rules/closestvectorproblem_qubo.rs b/src/unit_tests/rules/closestvectorproblem_qubo.rs index 22de016f9..54fbfe698 100644 --- a/src/unit_tests/rules/closestvectorproblem_qubo.rs +++ b/src/unit_tests/rules/closestvectorproblem_qubo.rs @@ -1,5 +1,6 @@ use super::*; use crate::solvers::BruteForce; +use crate::solvers::SolveOutcome; use crate::traits::Problem; fn canonical_cvp() -> ClosestVectorProblem { @@ -57,7 +58,14 @@ fn test_closestvectorproblem_to_qubo_twelve_dimensional_identity() { } assert_eq!(offset, 0); } - let solution = reduction.extract_solution(&bits).unwrap(); + let solution = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), bits.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(solution, vec![1; size]); assert_eq!( source.evaluate(&solution).unwrap().0, @@ -73,7 +81,14 @@ fn test_closestvectorproblem_to_qubo_closed_loop() { .solve(reduction.target_problem()) .unwrap() .unwrap(); - let source_solution = reduction.extract_solution(&target_solution).unwrap(); + let source_solution = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), target_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(source_solution, vec![1, 1]); assert_eq!( @@ -98,14 +113,32 @@ fn test_closestvectorproblem_to_qubo_coefficients() { fn test_closestvectorproblem_to_qubo_exact_range_decoding() { let reduction = ReduceTo::>::reduce_to(&canonical_cvp()).unwrap(); assert_eq!( - reduction.extract_solution(&canonical_bits()).unwrap(), + reduction + .recover_result( + &canonical_cvp(), + SolveOutcome::optimal(reduction.target_problem(), canonical_bits().clone()) + .unwrap() + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"), 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]); + assert_eq!( + reduction + .recover_result( + &canonical_cvp(), + SolveOutcome::optimal(reduction.target_problem(), duplicate.clone()).unwrap() + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"), + vec![1, 1] + ); assert_eq!( reduction .target_problem() @@ -124,7 +157,14 @@ fn test_closestvectorproblem_to_qubo_preserves_optimum_outside_old_box() { .unwrap() .unwrap(); assert_eq!( - reduction.extract_solution(&target_solution).unwrap(), + reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), target_solution.clone()).unwrap() + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"), vec![20] ); } @@ -171,8 +211,15 @@ fn qubo_energy_matches_squared_distance_up_to_the_dropped_constant() { assert_eq!(target.num_vars(), 3); // The all-zero encoding represents x=-2, with squared distance (-4-1)^2=25. for index in 0..8 { - let bits = (0..3).map(|bit| index & (1 << bit) != 0).collect(); - let coefficient = reduction.extract_solution(&bits).unwrap(); + let bits: Vec = (0..3).map(|bit| index & (1 << bit) != 0).collect(); + let coefficient = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), bits.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let energy = target.evaluate(&bits).unwrap().unwrap(); assert_eq!( source.squared_distance(&coefficient).unwrap(), diff --git a/src/unit_tests/rules/clustering_ilp.rs b/src/unit_tests/rules/clustering_ilp.rs index ab374fa2e..40fac843a 100644 --- a/src/unit_tests/rules/clustering_ilp.rs +++ b/src/unit_tests/rules/clustering_ilp.rs @@ -2,6 +2,7 @@ use super::*; use crate::models::algebraic::{Comparison, ObjectiveSense, ILP}; use crate::rules::test_helpers::assert_bf_vs_ilp; use crate::solvers::ILPSolver; +use crate::solvers::SolveOutcome; use crate::traits::Problem; use crate::types::Or; @@ -64,8 +65,17 @@ fn test_clustering_to_ilp_solution_extraction() { ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let extracted = reduction - .extract_solution(&vec![1, 0, 1, 0, 0, 1, 0, 1]) - .unwrap(); + .recover_result( + &problem, + SolveOutcome::optimal( + reduction.target_problem(), + vec![1, 0, 1, 0, 0, 1, 0, 1].clone(), + ) + .unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted, vec![0, 0, 1, 1]); assert_eq!(problem.evaluate(&extracted).unwrap(), Or(true)); } diff --git a/src/unit_tests/rules/coloring_ilp.rs b/src/unit_tests/rules/coloring_ilp.rs index 5625dea4e..a54fda858 100644 --- a/src/unit_tests/rules/coloring_ilp.rs +++ b/src/unit_tests/rules/coloring_ilp.rs @@ -1,4 +1,5 @@ use super::*; +use crate::solvers::SolveOutcome; use crate::solvers::{BruteForce, ILPSolver}; use crate::traits::Problem; use crate::variant::{K1, K2, K3, K4, KN}; @@ -76,7 +77,14 @@ fn test_coloring_to_ilp_closed_loop() { // Solve via ILP reduction let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); // Verify the extracted solution is valid for the original problem assert!( @@ -101,7 +109,14 @@ fn test_ilp_solution_equals_brute_force_path() { // Solve via ILP let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); // Verify validity assert!( @@ -144,7 +159,14 @@ fn test_solution_extraction() { // vertex 2 has color 0 (x_{2,0} = 1) // Variables are indexed as: v0c0, v0c1, v0c2, v1c0, v1c1, v1c2, v2c0, v2c1, v2c2 let ilp_solution = vec![0, 1, 0, 0, 0, 1, 1, 0, 0]; - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted, vec![1, 2, 0]); @@ -177,7 +199,14 @@ fn test_empty_graph() { let ilp_solver = ILPSolver::new(); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!(problem.evaluate(&extracted).unwrap()); } @@ -194,7 +223,14 @@ fn test_complete_graph_k4() { let ilp_solver = ILPSolver::new(); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!(problem.evaluate(&extracted).unwrap()); @@ -235,7 +271,14 @@ fn test_bipartite_graph() { let ilp_solver = ILPSolver::new(); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!(problem.evaluate(&extracted).unwrap()); @@ -253,7 +296,14 @@ fn test_reduction_closed_loop() { let target_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("target ILP should be solvable"); - let solution = reduction.extract_solution(&target_solution).unwrap(); + let solution = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), target_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!(problem.evaluate(&solution).unwrap()); } @@ -270,7 +320,14 @@ fn test_single_vertex() { let ilp_solver = ILPSolver::new(); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted, vec![0]); } @@ -284,7 +341,14 @@ fn test_single_edge() { let ilp_solver = ILPSolver::new(); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!(problem.evaluate(&extracted).unwrap()); assert_ne!(extracted[0], extracted[1]); diff --git a/src/unit_tests/rules/coloring_qubo.rs b/src/unit_tests/rules/coloring_qubo.rs index a96906e80..54ce4b6e2 100644 --- a/src/unit_tests/rules/coloring_qubo.rs +++ b/src/unit_tests/rules/coloring_qubo.rs @@ -1,14 +1,20 @@ use super::*; +use crate::models::decision::Decision; +use crate::rules::ReductionResult; use crate::solvers::BruteForce; use crate::solvers::BruteForceProblem as _; +use crate::solvers::SolveOutcome; +use crate::traits::EvaluationError::InvalidConfiguration; use crate::traits::Problem; +use crate::types::OptimizationValue; use crate::variant::{K2, K3}; #[test] fn test_kcoloring_to_qubo_closed_loop() { // Triangle K3, 3 colors → exactly 6 valid colorings (3! permutations) let kc = KColoring::::new(SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)])); - let reduction = ReduceTo::>::reduce_to(&kc).expect("reduction should succeed"); + let reduction = + ReduceTo::>>::reduce_to(&kc).expect("reduction should succeed"); let qubo = reduction.target_problem(); let solver = BruteForce::new(); @@ -16,7 +22,14 @@ fn test_kcoloring_to_qubo_closed_loop() { // All solutions should extract to valid colorings for sol in &qubo_solutions { - let extracted = reduction.extract_solution(sol).unwrap(); + let extracted = reduction + .recover_result( + &kc, + SolveOutcome::optimal(reduction.target_problem(), (sol).clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!(kc.evaluate(&extracted).unwrap()); } @@ -28,14 +41,22 @@ fn test_kcoloring_to_qubo_closed_loop() { fn test_kcoloring_to_qubo_path() { // Path graph: 0-1-2, 2 colors let kc = KColoring::::new(SimpleGraph::new(3, vec![(0, 1), (1, 2)])); - let reduction = ReduceTo::>::reduce_to(&kc).expect("reduction should succeed"); + let reduction = + ReduceTo::>>::reduce_to(&kc).expect("reduction should succeed"); let qubo = reduction.target_problem(); let solver = BruteForce::new(); let qubo_solutions = solver.find_all_witnesses(qubo).unwrap(); for sol in &qubo_solutions { - let extracted = reduction.extract_solution(sol).unwrap(); + let extracted = reduction + .recover_result( + &kc, + SolveOutcome::optimal(reduction.target_problem(), (sol).clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!(kc.evaluate(&extracted).unwrap()); } @@ -48,14 +69,22 @@ fn test_kcoloring_to_qubo_reversed_edges() { // Edge (2, 0) triggers the idx_v < idx_u swap branch (line 104). // Path: 2-0-1 with reversed edge ordering let kc = KColoring::::new(SimpleGraph::new(3, vec![(2, 0), (0, 1)])); - let reduction = ReduceTo::>::reduce_to(&kc).expect("reduction should succeed"); + let reduction = + ReduceTo::>>::reduce_to(&kc).expect("reduction should succeed"); let qubo = reduction.target_problem(); let solver = BruteForce::new(); let qubo_solutions = solver.find_all_witnesses(qubo).unwrap(); for sol in &qubo_solutions { - let extracted = reduction.extract_solution(sol).unwrap(); + let extracted = reduction + .recover_result( + &kc, + SolveOutcome::optimal(reduction.target_problem(), (sol).clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!(kc.evaluate(&extracted).unwrap()); } @@ -66,7 +95,8 @@ fn test_kcoloring_to_qubo_reversed_edges() { #[test] fn test_kcoloring_to_qubo_sizes() { let kc = KColoring::::new(SimpleGraph::new(3, vec![(0, 1), (1, 2), (0, 2)])); - let reduction = ReduceTo::>::reduce_to(&kc).expect("reduction should succeed"); + let reduction = + ReduceTo::>>::reduce_to(&kc).expect("reduction should succeed"); // QUBO should have n*K = 3*3 = 9 variables assert_eq!(reduction.target_problem().num_variables().unwrap(), 9); @@ -74,7 +104,6 @@ fn test_kcoloring_to_qubo_sizes() { #[test] fn test_kcoloring_to_qubo_all_small_graphs_and_configurations() { - use crate::rules::AggregateReductionResult; for n in 0..=3 { let possible: Vec<_> = (0..n) .flat_map(|u| ((u + 1)..n).map(move |v| (u, v))) @@ -87,9 +116,9 @@ fn test_kcoloring_to_qubo_all_small_graphs_and_configurations() { .collect(); for k in 0..=3 { let source = KColoring::::with_k(SimpleGraph::new(n, edges.clone()), k); - let reduction = ReduceTo::>::reduce_to(&source).unwrap(); - let target = AggregateReductionResult::target_problem(&reduction); - assert_eq!(target.num_vars(), n * k); + let reduction = ReduceTo::>>::reduce_to(&source).unwrap(); + let target = crate::rules::ReductionResult::target_problem(&reduction); + assert_eq!(target.inner().num_vars(), n * k); let mut minimum = i64::MAX; let mut any_coloring = false; for bits in 0..(1usize << (n * k)) { @@ -108,34 +137,56 @@ fn test_kcoloring_to_qubo_all_small_graphs_and_configurations() { let penalty = (n + 1) as i64; let residual = 2 * counts.iter().map(|&count| (1 - count).pow(2)).sum::() + conflicts; - let value = target.evaluate(&config).unwrap(); + let value = target.inner().evaluate(&config).unwrap(); assert_eq!(value.0, Some(penalty * residual - 2 * penalty * n as i64)); minimum = minimum.min(value.0.unwrap()); let expected = residual == 0; assert_eq!( - AggregateReductionResult::extract_value(&reduction, value).0, + crate::types::Or(OptimizationValue::meets_bound( + &(value), + crate::rules::ReductionResult::target_problem(&reduction).bound() + )) + .0, expected ); if expected { - let coloring = reduction.extract_solution(&config).unwrap(); + let coloring = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), config.clone()) + .unwrap(), + ) + .map(|result| { + result.into_solution().expect( + "qualifying target result must recover a source solution", + ) + }) + .unwrap(); assert!(source.evaluate(&coloring).unwrap().0); any_coloring = true; } } assert_eq!( - AggregateReductionResult::extract_value( - &reduction, - crate::types::Min(Some(minimum)) - ) + crate::types::Or(OptimizationValue::meets_bound( + &(crate::types::Min(Some(minimum))), + crate::rules::ReductionResult::target_problem(&reduction).bound() + )) .0, any_coloring ); assert!( - !AggregateReductionResult::extract_value(&reduction, crate::types::Min(None)).0 - ); - assert!( - !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![false; n * k + 1]), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&reduction, value); value.is_valid() }) + !crate::types::Or(OptimizationValue::meets_bound( + &(crate::types::Min(None)), + crate::rules::ReductionResult::target_problem(&reduction).bound() + )) + .0 ); + assert!(matches!( + ReductionResult::target_problem(&reduction) + .inner() + .evaluate(&vec![false; n * k + 1]), + Err(InvalidConfiguration(_)) + )); } } } diff --git a/src/unit_tests/rules/consecutiveblockminimization_ilp.rs b/src/unit_tests/rules/consecutiveblockminimization_ilp.rs index f778a2f96..b8f0015e6 100644 --- a/src/unit_tests/rules/consecutiveblockminimization_ilp.rs +++ b/src/unit_tests/rules/consecutiveblockminimization_ilp.rs @@ -2,6 +2,7 @@ use super::*; use crate::models::algebraic::{ObjectiveSense, ILP}; use crate::rules::test_helpers::assert_bf_vs_ilp; use crate::rules::{ReduceTo, ReductionResult}; +use crate::solvers::SolveOutcome; use crate::solvers::{BruteForce, ILPSolver}; use crate::traits::Problem; use crate::types::Or; @@ -48,7 +49,14 @@ fn test_cbm_to_ilp_bf_vs_ilp() { let ilp_solution = ilp_solver .solve(reduction.target_problem()) .expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(problem.evaluate(&extracted).unwrap(), Or(true)); } diff --git a/src/unit_tests/rules/consecutiveonesmatrixaugmentation_ilp.rs b/src/unit_tests/rules/consecutiveonesmatrixaugmentation_ilp.rs index 1a9169afe..b00fbfc7c 100644 --- a/src/unit_tests/rules/consecutiveonesmatrixaugmentation_ilp.rs +++ b/src/unit_tests/rules/consecutiveonesmatrixaugmentation_ilp.rs @@ -1,6 +1,7 @@ use super::*; use crate::models::algebraic::{ObjectiveSense, ILP}; use crate::rules::{ReduceTo, ReductionResult}; +use crate::solvers::SolveOutcome; use crate::solvers::{BruteForce, ILPSolver}; use crate::traits::Problem; use crate::types::Or; @@ -33,7 +34,14 @@ fn test_coma_to_ilp_closed_loop() { let ilp_solution = ilp_solver .solve(reduction.target_problem()) .expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(problem.evaluate(&extracted).unwrap(), Or(true)); // Also verify that brute-force on the source agrees @@ -59,7 +67,14 @@ fn test_coma_to_ilp_bf_vs_ilp() { let ilp_solution = ilp_solver .solve(reduction.target_problem()) .expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(problem.evaluate(&extracted).unwrap(), Or(true)); } diff --git a/src/unit_tests/rules/consecutiveonessubmatrix_ilp.rs b/src/unit_tests/rules/consecutiveonessubmatrix_ilp.rs index b6a7ecf2f..135f0c650 100644 --- a/src/unit_tests/rules/consecutiveonessubmatrix_ilp.rs +++ b/src/unit_tests/rules/consecutiveonessubmatrix_ilp.rs @@ -1,6 +1,7 @@ use super::*; use crate::models::algebraic::{ObjectiveSense, ILP}; use crate::rules::{ReduceTo, ReductionResult}; +use crate::solvers::SolveOutcome; use crate::solvers::{BruteForce, ILPSolver}; use crate::traits::Problem; use crate::types::Or; @@ -43,7 +44,14 @@ fn test_cos_to_ilp_closed_loop() { let ilp_solution = ilp_solver .solve(reduction.target_problem()) .expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(problem.evaluate(&extracted).unwrap(), Or(true)); // Verify brute-force on source agrees @@ -73,7 +81,14 @@ fn test_cos_to_ilp_bf_vs_ilp() { let ilp_solution = ilp_solver .solve(reduction.target_problem()) .expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(problem.evaluate(&extracted).unwrap(), Or(true)); } @@ -93,7 +108,14 @@ fn test_cos_to_ilp_allows_zero_rows_in_selected_submatrix() { let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("a single selected column always has C1P"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(problem.evaluate(&extracted).unwrap(), Or(true)); } @@ -107,6 +129,13 @@ fn test_cos_to_ilp_trivial() { let ilp_solution = ilp_solver .solve(reduction.target_problem()) .expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(problem.evaluate(&extracted).unwrap(), Or(true)); } diff --git a/src/unit_tests/rules/consistencyofdatabasefrequencytables_ilp.rs b/src/unit_tests/rules/consistencyofdatabasefrequencytables_ilp.rs index a65b3a638..35b03d1e6 100644 --- a/src/unit_tests/rules/consistencyofdatabasefrequencytables_ilp.rs +++ b/src/unit_tests/rules/consistencyofdatabasefrequencytables_ilp.rs @@ -3,6 +3,7 @@ use crate::models::algebraic::{ObjectiveSense, ILP}; use crate::models::misc::{ConsistencyOfDatabaseFrequencyTables, FrequencyTable, KnownValue}; use crate::rules::test_helpers::assert_bf_vs_ilp; use crate::rules::{ReduceTo, ReductionResult}; +use crate::solvers::SolveOutcome; use crate::solvers::{BruteForce, ILPSolver}; use crate::traits::Problem; @@ -55,7 +56,14 @@ fn test_cdft_to_ilp_solution_encoding_round_trip() { let reduction: ReductionCDFTToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp_solution = reduction.encode_source_solution(&small_yes_witness()); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted, small_yes_witness()); } @@ -96,7 +104,14 @@ fn test_consistency_to_ilp_bf_vs_ilp() { let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!(problem.evaluate(&extracted).unwrap()); } @@ -129,7 +144,14 @@ fn test_cdft_to_ilp_issue_instance_closed_loop() { let target_solution = solver .solve(reduction.target_problem()) .expect("ILP solver should find a feasible solution for the issue instance"); - let source_solution = reduction.extract_solution(&target_solution).unwrap(); + let source_solution = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), target_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!( problem.evaluate(&source_solution).unwrap(), "extracted source solution must satisfy the original CDFT instance" @@ -142,6 +164,13 @@ fn test_cdft_to_ilp_issue_instance_encoding_round_trip() { let reduction: ReductionCDFTToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp_solution = reduction.encode_source_solution(&issue_witness()); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted, issue_witness()); } diff --git a/src/unit_tests/rules/decisionmaximumindependentset_integralflowbundles.rs b/src/unit_tests/rules/decisionmaximumindependentset_integralflowbundles.rs index 20fd7a5cf..1beb9e2f6 100644 --- a/src/unit_tests/rules/decisionmaximumindependentset_integralflowbundles.rs +++ b/src/unit_tests/rules/decisionmaximumindependentset_integralflowbundles.rs @@ -1,5 +1,8 @@ use super::*; +use crate::rules::ReductionResult; use crate::solvers::BruteForce; +use crate::solvers::SolveOutcome; +use crate::traits::EvaluationError::InvalidConfiguration; use crate::traits::Problem; use crate::types::Or; @@ -23,7 +26,17 @@ fn test_decisionmaximumindependentset_to_integralflowbundles_closed_loop() { assert_eq!(witness.is_some(), bound == 2); if let Some(witness) = witness { assert_eq!( - source.evaluate(&reduction.extract_solution(&witness).unwrap()), + source.evaluate( + &reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), witness.clone()) + .unwrap() + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution") + ), Ok(Or(true)) ); } @@ -71,7 +84,21 @@ fn test_decision_ifb_all_small_graphs_thresholds_and_binary_flows() { if target.evaluate(&flow).unwrap().0 { target_exists = true; assert_eq!( - source.evaluate(&reduction.extract_solution(&flow).unwrap()), + source.evaluate( + &reduction + .recover_result( + &source, + SolveOutcome::optimal( + reduction.target_problem(), + flow.clone() + ) + .unwrap() + ) + .map(|result| result.into_solution().expect( + "qualifying target result must recover a source solution" + )) + .unwrap() + ), Ok(Or(true)) ); } @@ -91,20 +118,31 @@ fn test_decision_ifb_loops_parallel_edges_and_invalid_witnesses() { let reduction = ReduceTo::::reduce_to(&source).unwrap(); let valid = vec![0, 0, 1, 1, 1, 1, 1, 1]; assert_eq!( - reduction.extract_solution(&valid).unwrap(), + reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), valid.clone()).unwrap() + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"), vec![false, true, true] ); + assert!(matches!( + ReductionResult::target_problem(&reduction).evaluate(&vec![]), + Err(InvalidConfiguration(_)) + )); for flow in [ - vec![], vec![usize::MAX; 8], vec![0; 8], vec![0, 0, 1, 0, 1, 1, 1, 1], // conservation vec![1, 1, 0, 0, 1, 1, 1, 1], // self-loop vec![0, 0, 2, 2, 1, 1, 1, 1], // path capacity ] { - assert!( - !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &flow), Ok(value) if { value.is_valid() }) - ); + assert!(!ReductionResult::target_problem(&reduction) + .evaluate(&flow) + .unwrap() + .is_valid()); } } diff --git a/src/unit_tests/rules/decisionminimumdominatingset_minimumsummulticenter.rs b/src/unit_tests/rules/decisionminimumdominatingset_minimumsummulticenter.rs index 41899497a..c32542944 100644 --- a/src/unit_tests/rules/decisionminimumdominatingset_minimumsummulticenter.rs +++ b/src/unit_tests/rules/decisionminimumdominatingset_minimumsummulticenter.rs @@ -2,9 +2,12 @@ use crate::models::decision::Decision; use crate::models::graph::{MinimumDominatingSet, MinimumSumMulticenter}; use crate::rules::{ReduceTo, ReductionResult}; use crate::solvers::BruteForce; +use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; +use crate::traits::EvaluationError::InvalidConfiguration; use crate::traits::Problem; -use crate::types::{Min, One, Or}; +use crate::types::One; +use crate::types::OptimizationValue; fn decision_mds( num_vertices: usize, @@ -27,22 +30,28 @@ fn test_decisionminimumdominatingset_to_minimumsummulticenter_structure() { &[(0, 1), (0, 2), (1, 3), (2, 3), (3, 4), (3, 5), (4, 5)], 2, ); - let reduction = ReduceTo::>::reduce_to(&source) - .expect("reduction should succeed"); + let reduction = + ReduceTo::>>::reduce_to(&source) + .expect("reduction should succeed"); let target = reduction.target_problem(); assert_eq!( - crate::rules::AggregateReductionResult::target_problem(&reduction).k(), - target.k() + crate::rules::ReductionResult::target_problem(&reduction) + .inner() + .k(), + target.inner().k() ); assert_eq!( - target.graph().num_vertices(), + target.inner().graph().num_vertices(), source.inner().graph().num_vertices() + 1 ); - assert_eq!(target.graph().edges(), source.inner().graph().edges()); - assert_eq!(target.vertex_weights(), vec![1i64; 7].as_slice()); - assert_eq!(target.edge_lengths(), vec![1i64; 7].as_slice()); - assert_eq!(target.k(), 3); + assert_eq!( + target.inner().graph().edges(), + source.inner().graph().edges() + ); + assert_eq!(target.inner().vertex_weights(), vec![1i64; 7].as_slice()); + assert_eq!(target.inner().edge_lengths(), vec![1i64; 7].as_slice()); + assert_eq!(target.inner().k(), 3); } #[test] @@ -52,8 +61,9 @@ fn test_decisionminimumdominatingset_to_minimumsummulticenter_closed_loop_yes_in &[(0, 1), (0, 2), (1, 3), (2, 3), (3, 4), (3, 5), (4, 5)], 2, ); - let reduction = ReduceTo::>::reduce_to(&source) - .expect("reduction should succeed"); + let reduction = + ReduceTo::>>::reduce_to(&source) + .expect("reduction should succeed"); let target = reduction.target_problem(); let target_solutions = BruteForce::new().find_all_witnesses(target).unwrap(); @@ -63,10 +73,20 @@ fn test_decisionminimumdominatingset_to_minimumsummulticenter_closed_loop_yes_in ); for target_solution in target_solutions { - assert_eq!(target.evaluate(&target_solution).unwrap().unwrap(), 4); - let extracted = reduction.extract_solution(&target_solution).unwrap(); + assert_eq!( + target.inner().evaluate(&target_solution).unwrap().unwrap(), + 4 + ); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), target_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted, target_solution[..6]); - assert_eq!(source.evaluate(&extracted).unwrap(), Or(true)); + assert_eq!(source.evaluate(&extracted).unwrap(), crate::types::Or(true)); } } @@ -77,11 +97,15 @@ fn test_decisionminimumdominatingset_to_minimumsummulticenter_closed_loop_no_ins &[(0, 1), (0, 2), (1, 3), (2, 3), (3, 4), (3, 5), (4, 5)], 1, ); - let reduction = ReduceTo::>::reduce_to(&source) - .expect("reduction should succeed"); + let reduction = + ReduceTo::>>::reduce_to(&source) + .expect("reduction should succeed"); let target = reduction.target_problem(); + assert!(BruteForce::new().solve(target).unwrap().is_none()); - let target_solutions = BruteForce::new().find_all_witnesses(target).unwrap(); + let target_solutions = BruteForce::new() + .find_all_witnesses(target.inner()) + .unwrap(); assert!( !target_solutions.is_empty(), "target should still have optimal K-center placements" @@ -90,19 +114,16 @@ fn test_decisionminimumdominatingset_to_minimumsummulticenter_closed_loop_no_ins let threshold = i64::try_from(source.inner().graph().num_vertices()).unwrap() - i64::try_from(source.k()).unwrap(); for target_solution in target_solutions { - let target_value = target.evaluate(&target_solution).unwrap().unwrap(); + let target_value = target.inner().evaluate(&target_solution).unwrap().unwrap(); assert_eq!(target_value, 6); assert!(target_value > threshold); assert_eq!( - crate::rules::AggregateReductionResult::extract_value( - &reduction, - Min(Some(target_value)) - ), - Or(false) - ); - assert!( - !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &target_solution), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&reduction, value); value.is_valid() }) + crate::types::Or(OptimizationValue::meets_bound( + &(crate::types::Min(Some(target_value))), + crate::rules::ReductionResult::target_problem(&reduction).bound() + )), + crate::types::Or(false) ); } } @@ -129,36 +150,53 @@ fn test_decisionminimumdominatingset_to_minimumsummulticenter_all_small_graphs() for bound in bounds { let source = decision_mds(n, &edges, bound); let reduction = - ReduceTo::>::reduce_to(&source) - .unwrap(); + ReduceTo::>>::reduce_to( + &source, + ) + .unwrap(); let target = reduction.target_problem(); - assert!(target.num_vertices() <= n + 2); - assert_eq!(target.num_edges(), edges.len()); + assert!(target.inner().num_vertices() <= n + 2); + assert_eq!(target.inner().num_edges(), edges.len()); let source_yes = BruteForce::new().solve(&source).unwrap().is_some(); let mut optimum = None; - for mask in 0usize..1 << target.num_vertices() { - let placement: Vec<_> = (0..target.num_vertices()) + for mask in 0usize..1 << target.inner().num_vertices() { + let placement: Vec<_> = (0..target.inner().num_vertices()) .map(|i| mask & (1 << i) != 0) .collect(); - let value = target.evaluate(&placement).unwrap(); + let value = target.inner().evaluate(&placement).unwrap(); if let Some(cost) = value.0 { optimum = Some(optimum.map_or(cost, |previous: i64| previous.min(cost))); } - let accepted = - crate::rules::AggregateReductionResult::extract_value(&reduction, value).0; + let accepted = crate::types::Or(OptimizationValue::meets_bound( + &(value), + crate::rules::ReductionResult::target_problem(&reduction).bound(), + )) + .0; if accepted { - let witness = reduction.extract_solution(&placement).unwrap(); - assert_eq!(source.evaluate(&witness).unwrap(), Or(true)); + let witness = reduction + .recover_result( + &source, + SolveOutcome::feasible(target, placement).unwrap(), + ) + .map(|outcome| outcome.into_solution().unwrap()) + .unwrap(); + assert_eq!(source.evaluate(&witness).unwrap(), crate::types::Or(true)); } } assert_eq!( - crate::rules::AggregateReductionResult::extract_value(&reduction, Min(optimum)), - Or(source_yes), + crate::types::Or(OptimizationValue::meets_bound( + &(crate::types::Min(optimum)), + crate::rules::ReductionResult::target_problem(&reduction).bound() + )), + crate::types::Or(source_yes), "n={n}, edges={edges:?}, K={bound}" ); - assert!( - !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![false; target.num_vertices() + 1]), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&reduction, value); value.is_valid() }) - ); + assert!(matches!( + ReductionResult::target_problem(&reduction) + .inner() + .evaluate(&vec![false; target.inner().num_vertices() + 1]), + Err(InvalidConfiguration(_)) + )); } } } diff --git a/src/unit_tests/rules/decisionminimumdominatingset_minmaxmulticenter.rs b/src/unit_tests/rules/decisionminimumdominatingset_minmaxmulticenter.rs index 933f762f3..4a212d982 100644 --- a/src/unit_tests/rules/decisionminimumdominatingset_minmaxmulticenter.rs +++ b/src/unit_tests/rules/decisionminimumdominatingset_minmaxmulticenter.rs @@ -1,6 +1,11 @@ use super::*; +use crate::models::decision::Decision; +use crate::rules::ReductionResult; use crate::solvers::BruteForce; +use crate::solvers::SolveOutcome; +use crate::traits::EvaluationError::InvalidConfiguration; use crate::traits::Problem; +use crate::types::OptimizationValue; fn decision_mds( n: usize, @@ -16,34 +21,57 @@ fn decision_mds( #[test] fn test_decisionminimumdominatingset_to_minmaxmulticenter_closed_loop() { let source = decision_mds(3, &[(0, 1), (1, 2)], 1); - let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + let reduction = + ReduceTo::>>::reduce_to(&source).unwrap(); let target = reduction.target_problem(); - assert_eq!(target.num_vertices(), 5); - assert_eq!(target.num_edges(), 2); - assert_eq!(target.k(), 3); - for witness in BruteForce::new().find_all_witnesses(target).unwrap() { + assert_eq!(target.inner().num_vertices(), 5); + assert_eq!(target.inner().num_edges(), 2); + assert_eq!(target.inner().k(), 3); + for witness in BruteForce::new() + .find_all_witnesses(target.inner()) + .unwrap() + { assert!(witness[3] && witness[4]); assert!( source - .evaluate(&reduction.extract_solution(&witness).unwrap()) + .evaluate( + &reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), witness.clone()) + .unwrap() + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution") + ) .unwrap() .0 ); } let source = decision_mds(4, &[(0, 1), (1, 2), (2, 3)], 1); - let reduction = ReduceTo::>::reduce_to(&source).unwrap(); - let witness = BruteForce::new() + let reduction = + ReduceTo::>>::reduce_to(&source).unwrap(); + assert!(BruteForce::new() .solve(reduction.target_problem()) .unwrap() + .is_none()); + let witness = BruteForce::new() + .solve(reduction.target_problem().inner()) + .unwrap() + .unwrap(); + let optimum = reduction + .target_problem() + .inner() + .evaluate(&witness) .unwrap(); - let optimum = reduction.target_problem().evaluate(&witness).unwrap(); - assert_eq!(optimum, Min(Some(2))); + assert_eq!(optimum, crate::types::Min(Some(2))); assert_eq!( - crate::rules::AggregateReductionResult::extract_value(&reduction, optimum), - Or(false) - ); - assert!( - !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &witness), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&reduction, value); value.is_valid() }) + crate::types::Or(OptimizationValue::meets_bound( + &(optimum), + crate::rules::ReductionResult::target_problem(&reduction).bound() + )), + crate::types::Or(false) ); } @@ -61,13 +89,14 @@ fn test_multicenter_all_small_graphs_bounds_and_placements() { for bound in [i64::MIN, -1, 0, 1, n_i64, n_i64 + 1, i64::MAX] { let source = decision_mds(n, &edges, bound); let reduction = - ReduceTo::>::reduce_to(&source).unwrap(); + ReduceTo::>>::reduce_to(&source) + .unwrap(); let target = reduction.target_problem(); - assert_eq!(target.graph().edges(), edges); - assert_eq!(target.num_vertices(), n + 2); - assert_eq!(target.vertex_weights(), vec![One; n + 2]); - assert_eq!(target.edge_lengths(), vec![One; edges.len()]); - assert!((1..=n + 2).contains(&target.k())); + assert_eq!(target.inner().graph().edges(), edges); + assert_eq!(target.inner().num_vertices(), n + 2); + assert_eq!(target.inner().vertex_weights(), vec![One; n + 2]); + assert_eq!(target.inner().edge_lengths(), vec![One; edges.len()]); + assert!((1..=n + 2).contains(&target.inner().k())); let mut source_yes = false; for mask in 0..(1usize << n) { let mut witness: Vec<_> = (0..n).map(|v| mask & (1 << v) != 0).collect(); @@ -75,38 +104,47 @@ fn test_multicenter_all_small_graphs_bounds_and_placements() { source_yes = true; let mut count = witness.iter().filter(|&&b| b).count(); for bit in &mut witness { - if !*bit && count < target.k() - 2 { + if !*bit && count < target.inner().k() - 2 { *bit = true; count += 1; } } witness.extend([true, true]); - assert!(target.evaluate(&witness).unwrap().0.is_some_and(|r| r <= 1)); + assert!(target + .inner() + .evaluate(&witness) + .unwrap() + .0 + .is_some_and(|r| r <= 1)); } } let mut optimum: Option = None; for mask in 0..(1usize << (n + 2)) { let witness: Vec<_> = (0..n + 2).map(|v| mask & (1 << v) != 0).collect(); - let radius = target.evaluate(&witness).unwrap().0; + let radius = target.inner().evaluate(&witness).unwrap().0; if let Some(r) = radius { optimum = Some(optimum.map_or(r, |old| old.min(r))); } if radius.is_some_and(|r| r <= 1) { assert!( source - .evaluate(&reduction.extract_solution(&witness).unwrap()) + .evaluate(&reduction.recover_result(&source, SolveOutcome::optimal(reduction.target_problem(), witness.clone()).unwrap()).unwrap().into_solution().expect("qualifying target result must recover a source solution")) .unwrap() .0 ); } else { - assert!( - !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &witness), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&reduction, value); value.is_valid() }) - ); + assert!(!ReductionResult::target_problem(&reduction) + .evaluate(&witness) + .unwrap() + .is_valid()); } } assert_eq!( - crate::rules::AggregateReductionResult::extract_value(&reduction, Min(optimum)), - Or(source_yes) + crate::types::Or(OptimizationValue::meets_bound( + &(crate::types::Min(optimum)), + crate::rules::ReductionResult::target_problem(&reduction).bound() + )), + crate::types::Or(source_yes) ); } } @@ -116,20 +154,34 @@ fn test_multicenter_all_small_graphs_bounds_and_placements() { #[test] fn test_multicenter_duplicate_edges_and_malformed_witness() { let source = decision_mds(3, &[(0, 0), (0, 1), (0, 1)], 2); - let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + let reduction = + ReduceTo::>>::reduce_to(&source).unwrap(); let witness = vec![true, false, true, true, true]; assert_eq!( - reduction.extract_solution(&witness).unwrap(), + reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), witness.clone()).unwrap() + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"), vec![true, false, true] ); for bad in [vec![], vec![true; 6]] { - assert!( - !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &bad), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&reduction, value); value.is_valid() }) - ); + assert!(matches!( + ReductionResult::target_problem(&reduction) + .inner() + .evaluate(&bad), + Err(InvalidConfiguration(_)) + )); } assert_eq!( - crate::rules::AggregateReductionResult::extract_value(&reduction, Min(None)), - Or(false) + crate::types::Or(OptimizationValue::meets_bound( + &(crate::types::Min(None)), + crate::rules::ReductionResult::target_problem(&reduction).bound() + )), + crate::types::Or(false) ); } diff --git a/src/unit_tests/rules/decisionminimumvertexcover_hamiltoniancircuit.rs b/src/unit_tests/rules/decisionminimumvertexcover_hamiltoniancircuit.rs index 4f61dcfd9..e128dadc6 100644 --- a/src/unit_tests/rules/decisionminimumvertexcover_hamiltoniancircuit.rs +++ b/src/unit_tests/rules/decisionminimumvertexcover_hamiltoniancircuit.rs @@ -3,6 +3,7 @@ use crate::models::decision::Decision; use crate::models::graph::{HamiltonianCircuit, MinimumVertexCover}; use crate::rules::ReduceTo; use crate::solvers::BruteForce; +use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; use crate::traits::Problem; @@ -49,7 +50,14 @@ fn test_decisionminimumvertexcover_to_hamiltoniancircuit_closed_loop() { .0 ); - let extracted = reduction.extract_solution(&target_witness).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), target_witness.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted, cover); assert!(source.evaluate(&extracted).unwrap().0); } @@ -69,7 +77,14 @@ fn test_decisionminimumvertexcover_to_hamiltoniancircuit_ignores_isolated_vertic .0 ); - let extracted = reduction.extract_solution(&target_witness).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), target_witness.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted.len(), 3); assert!(!extracted[2]); assert!(source.evaluate(&extracted).unwrap().0); @@ -90,7 +105,14 @@ fn test_decisionminimumvertexcover_to_hamiltoniancircuit_fixed_yes_when_k_covers .solve(target) .unwrap() .expect("triangle should have a Hamiltonian circuit"); - let extracted = reduction.extract_solution(&witness).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), witness.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!(source.evaluate(&extracted).unwrap().0); } @@ -129,7 +151,14 @@ fn unit_cover_bound_handles_negative_and_empty_graphs() { let witness = BruteForce::new().solve(reduction.target_problem()).unwrap(); assert_eq!(witness.is_some(), expected); if let Some(witness) = witness { - let cover = reduction.extract_solution(&witness).unwrap(); + let cover = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), witness.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!(source.evaluate(&cover).unwrap().0); } } diff --git a/src/unit_tests/rules/directedhamiltonianpath_ilp.rs b/src/unit_tests/rules/directedhamiltonianpath_ilp.rs index a0fcccd20..23a6ef217 100644 --- a/src/unit_tests/rules/directedhamiltonianpath_ilp.rs +++ b/src/unit_tests/rules/directedhamiltonianpath_ilp.rs @@ -1,5 +1,6 @@ use super::*; use crate::models::graph::DirectedHamiltonianPath; +use crate::solvers::SolveOutcome; use crate::solvers::{BruteForce, ILPSolver}; use crate::topology::DirectedGraph; use crate::traits::Problem; @@ -39,7 +40,14 @@ fn test_directedhamiltonianpath_to_ilp_closed_loop() { let ilp_solution = ilp_solver .solve(reduction.target_problem()) .expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!( problem.evaluate(&extracted).unwrap(), Or(true), @@ -72,7 +80,14 @@ fn test_directedhamiltonianpath_to_ilp_issue_example() { let ilp_solution = ilp_solver .solve(reduction.target_problem()) .expect("ILP should find a path"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!( problem.evaluate(&extracted).unwrap(), Or(true), diff --git a/src/unit_tests/rules/directedtwocommodityintegralflow_ilp.rs b/src/unit_tests/rules/directedtwocommodityintegralflow_ilp.rs index 61eb1b7a1..2f0c37c9c 100644 --- a/src/unit_tests/rules/directedtwocommodityintegralflow_ilp.rs +++ b/src/unit_tests/rules/directedtwocommodityintegralflow_ilp.rs @@ -1,5 +1,6 @@ use super::*; use crate::models::algebraic::{ObjectiveSense, ILP}; +use crate::solvers::SolveOutcome; use crate::solvers::{BruteForce, ILPSolver}; use crate::topology::DirectedGraph; use crate::traits::Problem; @@ -84,7 +85,14 @@ fn test_directedtwocommodityintegralflow_to_ilp_closed_loop() { let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be feasible"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!( problem.evaluate(&extracted).unwrap().0, @@ -132,7 +140,14 @@ fn test_directedtwocommodityintegralflow_to_ilp_extract_solution() { target_solution[8 + 3] = 1; // f2 on arc (1,3) target_solution[8 + 7] = 1; // f2 on arc (3,5) - let extracted = reduction.extract_solution(&target_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), target_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted.len(), 16); assert!( problem.evaluate(&extracted).unwrap().0, diff --git a/src/unit_tests/rules/disjointconnectingpaths_ilp.rs b/src/unit_tests/rules/disjointconnectingpaths_ilp.rs index 740f58586..9379b8cf5 100644 --- a/src/unit_tests/rules/disjointconnectingpaths_ilp.rs +++ b/src/unit_tests/rules/disjointconnectingpaths_ilp.rs @@ -3,6 +3,7 @@ use crate::models::algebraic::ILP; use crate::rules::test_helpers::assert_bf_vs_ilp; use crate::rules::ReduceTo; use crate::solvers::ILPSolver; +use crate::solvers::SolveOutcome; use crate::topology::SimpleGraph; use crate::traits::Problem; @@ -45,7 +46,14 @@ fn test_disjointconnectingpaths_to_ilp_forbids_using_another_pairs_terminal() { .is_feasible(&colliding_flow) .unwrap()); let target_solution = ILPSolver::new().solve(reduction.target_problem()).unwrap(); - let extracted = reduction.extract_solution(&target_solution).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), target_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!(source.evaluate(&extracted).unwrap().0); } @@ -64,7 +72,14 @@ fn test_disjointconnectingpaths_to_ilp_discards_disconnected_circulation() { .target_problem() .is_feasible(&target_solution) .unwrap()); - let extracted = reduction.extract_solution(&target_solution).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), target_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted, vec![true, true, false, false, false]); assert!(source.evaluate(&extracted).unwrap().0); } diff --git a/src/unit_tests/rules/ensemblecomputation_ilp.rs b/src/unit_tests/rules/ensemblecomputation_ilp.rs index 319cdca2b..7d18f85c5 100644 --- a/src/unit_tests/rules/ensemblecomputation_ilp.rs +++ b/src/unit_tests/rules/ensemblecomputation_ilp.rs @@ -3,6 +3,7 @@ use crate::models::algebraic::ILP; use crate::models::misc::EnsembleComputation; use crate::rules::ReduceTo; use crate::solvers::ILPSolver; +use crate::solvers::SolveOutcome; use crate::traits::Problem; use crate::types::Min; @@ -22,7 +23,14 @@ fn test_ensemblecomputation_to_ilp_closed_loop() { let source = feasible_instance(); 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(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), target_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(source.evaluate(&extracted).unwrap(), Min(Some(3))); } @@ -51,6 +59,13 @@ fn test_ensemblecomputation_to_ilp_empty_family() { let source = EnsembleComputation::new(1, vec![], 2); 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(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), target_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); 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 3c95e24cb..d8111a981 100644 --- a/src/unit_tests/rules/eulerianpath_ilp.rs +++ b/src/unit_tests/rules/eulerianpath_ilp.rs @@ -2,6 +2,7 @@ use super::*; use crate::models::algebraic::{ObjectiveSense, ILP}; use crate::models::graph::EulerianPath; use crate::solvers::ILPSolver; +use crate::solvers::SolveOutcome; use crate::topology::DirectedGraph; use crate::traits::Problem; use crate::types::Or; @@ -52,7 +53,14 @@ fn test_eulerianpath_to_ilp_empty_instance() { let solution = ILPSolver::new() .solve(ilp) .expect("Empty ILP should be feasible"); - let extracted = reduction.extract_solution(&solution).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted.len(), 0); assert_eq!(source.evaluate(&extracted).unwrap(), Or(true)); } @@ -66,7 +74,14 @@ fn test_eulerianpath_to_ilp_closed_loop() { let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be feasible for a YES instance"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted.len(), source.num_arcs()); assert!( @@ -105,7 +120,14 @@ fn test_eulerianpath_to_ilp_closed_circuit_with_loop() { let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be feasible for a closed Eulerian circuit"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted.len(), 3); assert!( source.is_valid_solution(&extracted), diff --git a/src/unit_tests/rules/exactcoverby3sets_algebraicequationsovergf2.rs b/src/unit_tests/rules/exactcoverby3sets_algebraicequationsovergf2.rs index 98f072d86..35b51eb54 100644 --- a/src/unit_tests/rules/exactcoverby3sets_algebraicequationsovergf2.rs +++ b/src/unit_tests/rules/exactcoverby3sets_algebraicequationsovergf2.rs @@ -2,6 +2,7 @@ use crate::models::algebraic::AlgebraicEquationsOverGF2; use crate::models::set::ExactCoverBy3Sets; use crate::rules::test_helpers::assert_satisfaction_round_trip_from_satisfaction_target; use crate::rules::{ReduceTo, ReductionResult}; +use crate::solvers::SolveOutcome; #[test] fn test_exactcoverby3sets_to_algebraicequationsovergf2_closed_loop() { @@ -49,8 +50,14 @@ fn test_exactcoverby3sets_to_algebraicequationsovergf2_extract_solution_is_ident assert_eq!( reduction - .extract_solution(&vec![true, true, false]) - .unwrap(), + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), vec![true, true, false].clone()) + .unwrap() + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"), vec![true, true, false] ); } diff --git a/src/unit_tests/rules/exactcoverby3sets_boundeddiameterspanningtree.rs b/src/unit_tests/rules/exactcoverby3sets_boundeddiameterspanningtree.rs index a1f12a328..9967a8372 100644 --- a/src/unit_tests/rules/exactcoverby3sets_boundeddiameterspanningtree.rs +++ b/src/unit_tests/rules/exactcoverby3sets_boundeddiameterspanningtree.rs @@ -1,7 +1,11 @@ use super::*; use crate::rules::test_helpers::assert_satisfaction_round_trip_from_satisfaction_target; +use crate::rules::ReductionResult; use crate::solvers::BruteForce; +use crate::solvers::SolveOutcome; use crate::topology::Graph; +use crate::traits::EvaluationError::InvalidConfiguration; +use crate::traits::Problem; /// q = 2, m = 2: X = {0..5} with C = [{0,1,2}, {3,4,5}]. /// Both subsets together form the unique exact cover. @@ -74,7 +78,14 @@ fn test_exactcoverby3sets_to_boundeddiameterspanningtree_extract_solution() { let mut target_config = vec![true; reduction.target_problem().num_edges()]; *target_config.last_mut().unwrap() = false; assert_eq!( - reduction.extract_solution(&target_config).unwrap(), + reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), target_config.clone()).unwrap() + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"), vec![true, true] ); @@ -82,15 +93,18 @@ fn test_exactcoverby3sets_to_boundeddiameterspanningtree_extract_solution() { let mut invalid = vec![false; target_config.len()]; invalid[2] = true; invalid[3] = true; - assert!( - !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &invalid), Ok(value) if { value.is_valid() }) - ); - assert!( - !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![]), Ok(value) if { value.is_valid() }) - ); - assert!( - !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![true; target_config.len()]), Ok(value) if { value.is_valid() }) - ); + assert!(!ReductionResult::target_problem(&reduction) + .evaluate(&invalid) + .unwrap() + .is_valid()); + assert!(matches!( + ReductionResult::target_problem(&reduction).evaluate(&vec![]), + Err(InvalidConfiguration(_)) + )); + assert!(!ReductionResult::target_problem(&reduction) + .evaluate(&vec![true; target_config.len()]) + .unwrap() + .is_valid()); } #[test] @@ -107,9 +121,10 @@ fn test_exactcoverby3sets_to_boundeddiameterspanningtree_no_instance() { // exist here). Equivalently, the brute-force aggregate evaluates to // Or(false). assert!(BruteForce::new().solve(target).unwrap().is_none()); - assert!( - !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![]), Ok(value) if { value.is_valid() }) - ); + assert!(!ReductionResult::target_problem(&reduction) + .evaluate(&vec![]) + .unwrap() + .is_valid()); } #[test] @@ -124,9 +139,10 @@ fn test_exactcoverby3sets_to_boundeddiameterspanningtree_universe_boundaries() { .solve(reduction.target_problem()) .unwrap() .is_none()); - assert!( - !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![]), Ok(value) if { value.is_valid() }) - ); + assert!(!ReductionResult::target_problem(&reduction) + .evaluate(&vec![]) + .unwrap() + .is_valid()); } let source = ExactCoverBy3Sets::new(0, vec![]); let reduction = @@ -137,7 +153,14 @@ fn test_exactcoverby3sets_to_boundeddiameterspanningtree_universe_boundaries() { .unwrap(); assert_eq!(witness, vec![true, true]); assert_eq!( - reduction.extract_solution(&witness).unwrap(), + reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), witness.clone()).unwrap() + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"), Vec::::new() ); } @@ -152,7 +175,14 @@ fn test_exactcoverby3sets_to_boundeddiameterspanningtree_duplicate_sets() { .unwrap(); assert!(!witnesses.is_empty()); for witness in witnesses { - let extracted = reduction.extract_solution(&witness).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), witness.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!(source.is_valid_solution(&extracted).unwrap()); assert_eq!(extracted.iter().filter(|&&x| x).count(), 1); } diff --git a/src/unit_tests/rules/exactcoverby3sets_ilp.rs b/src/unit_tests/rules/exactcoverby3sets_ilp.rs index 06de42089..606c117c0 100644 --- a/src/unit_tests/rules/exactcoverby3sets_ilp.rs +++ b/src/unit_tests/rules/exactcoverby3sets_ilp.rs @@ -1,4 +1,5 @@ use super::*; +use crate::solvers::SolveOutcome; use crate::solvers::{BruteForce, ILPSolver}; use crate::traits::Problem; use crate::types::Or; @@ -29,7 +30,14 @@ fn test_exactcoverby3sets_to_ilp_bf_vs_ilp() { assert_eq!(problem.evaluate(&bf_witness).unwrap(), Or(true)); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(problem.evaluate(&extracted).unwrap(), Or(true)); } @@ -39,7 +47,14 @@ fn test_solution_extraction() { let reduction: ReductionX3CToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp_solution = vec![1, 1]; // select both triples - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted, vec![true, true]); assert_eq!(problem.evaluate(&extracted).unwrap(), Or(true)); } diff --git a/src/unit_tests/rules/exactcoverby3sets_maximumsetpacking.rs b/src/unit_tests/rules/exactcoverby3sets_maximumsetpacking.rs index 146036ae1..005e8a653 100644 --- a/src/unit_tests/rules/exactcoverby3sets_maximumsetpacking.rs +++ b/src/unit_tests/rules/exactcoverby3sets_maximumsetpacking.rs @@ -2,6 +2,7 @@ use super::*; use crate::models::set::ExactCoverBy3Sets; use crate::rules::test_helpers::assert_satisfaction_round_trip_from_optimization_target; use crate::solvers::BruteForce; +use crate::solvers::SolveOutcome; use crate::traits::Problem; use crate::types::Max; @@ -65,7 +66,23 @@ fn test_exactcoverby3sets_to_maximumsetpacking_unsatisfiable() { assert_eq!(target.evaluate(&best).unwrap(), Max(Some(1))); // q = 2, but packing value is 1 < 2, so no exact cover exists - let extracted = reduction.extract_solution(&best).unwrap(); + let extracted = reduction.map_solution(&best).unwrap(); + assert_eq!( + reduction + .recover_result( + &source, + SolveOutcome::optimal(target, best.clone()).unwrap() + ) + .unwrap(), + SolveOutcome::Infeasible + ); + assert!(matches!( + reduction.recover_result( + &source, + SolveOutcome::feasible(target, best.clone()).unwrap() + ), + Err(crate::rules::ExtractionError::InsufficientSolutionQuality) + )); assert!(!source.evaluate(&extracted).unwrap()); } @@ -84,6 +101,13 @@ fn test_exactcoverby3sets_to_maximumsetpacking_optimal_value() { // Maximum packing: S0 + S1 = 2 disjoint sets = q assert_eq!(target.evaluate(&best).unwrap(), Max(Some(2))); - let extracted = reduction.extract_solution(&best).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), best.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!(source.evaluate(&extracted).unwrap()); } diff --git a/src/unit_tests/rules/exactcoverby3sets_minimumaxiomset.rs b/src/unit_tests/rules/exactcoverby3sets_minimumaxiomset.rs index 757f57c49..211c903b5 100644 --- a/src/unit_tests/rules/exactcoverby3sets_minimumaxiomset.rs +++ b/src/unit_tests/rules/exactcoverby3sets_minimumaxiomset.rs @@ -1,6 +1,7 @@ use super::*; use crate::rules::test_helpers::assert_satisfaction_round_trip_from_optimization_target; use crate::solvers::BruteForce; +use crate::solvers::SolveOutcome; use crate::traits::Problem; use crate::types::Min; @@ -70,7 +71,23 @@ fn test_exactcoverby3sets_to_minimumaxiomset_no_instance_gap() { .expect("expected an optimal target witness"); assert_eq!(target.evaluate(&optimal).unwrap(), Min(Some(3))); - let extracted = reduction.extract_solution(&optimal).unwrap(); + let extracted = reduction.map_solution(&optimal).unwrap(); + assert_eq!( + reduction + .recover_result( + &source, + SolveOutcome::optimal(target, optimal.clone()).unwrap() + ) + .unwrap(), + SolveOutcome::Infeasible + ); + assert!(matches!( + reduction.recover_result( + &source, + SolveOutcome::feasible(target, optimal.clone()).unwrap() + ), + Err(crate::rules::ExtractionError::InsufficientSolutionQuality) + )); assert!(!source.evaluate(&extracted).unwrap()); } @@ -81,9 +98,19 @@ fn test_extract_solution_reads_only_set_sentence_axioms() { ReduceTo::::reduce_to(&source).expect("reduction should succeed"); let extracted = reduction - .extract_solution(&vec![ - true, false, true, false, false, true, false, false, false, true, true, - ]) - .unwrap(); + .recover_result( + &source, + SolveOutcome::optimal( + reduction.target_problem(), + vec![ + true, false, true, false, false, true, false, false, false, true, true, + ] + .clone(), + ) + .unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted, vec![false, false, false, true, true]); } diff --git a/src/unit_tests/rules/exactcoverby3sets_minimumfaultdetectiontestset.rs b/src/unit_tests/rules/exactcoverby3sets_minimumfaultdetectiontestset.rs index aeb36f7a0..fca006e62 100644 --- a/src/unit_tests/rules/exactcoverby3sets_minimumfaultdetectiontestset.rs +++ b/src/unit_tests/rules/exactcoverby3sets_minimumfaultdetectiontestset.rs @@ -3,6 +3,7 @@ use crate::models::misc::MinimumFaultDetectionTestSet; use crate::models::set::ExactCoverBy3Sets; use crate::rules::test_helpers::assert_satisfaction_round_trip_from_optimization_target; use crate::solvers::BruteForce; +use crate::solvers::SolveOutcome; use crate::traits::Problem; use crate::types::Min; @@ -81,7 +82,23 @@ fn test_exactcoverby3sets_to_minimumfaultdetectiontestset_no_instance_gap() { .expect("expected an optimal target witness"); assert_eq!(target.evaluate(&best).unwrap(), Min(Some(3))); - let extracted = reduction.extract_solution(&best).unwrap(); + let extracted = reduction.map_solution(&best).unwrap(); + assert_eq!( + reduction + .recover_result( + &source, + SolveOutcome::optimal(target, best.clone()).unwrap() + ) + .unwrap(), + SolveOutcome::Infeasible + ); + assert!(matches!( + reduction.recover_result( + &source, + SolveOutcome::feasible(target, best.clone()).unwrap() + ), + Err(crate::rules::ExtractionError::InsufficientSolutionQuality) + )); assert!(!source.evaluate(&extracted).unwrap()); } @@ -93,8 +110,17 @@ fn test_exactcoverby3sets_to_minimumfaultdetectiontestset_extract_solution_ident assert_eq!( reduction - .extract_solution(&vec![vec![true], vec![true], vec![false]]) - .unwrap(), + .recover_result( + &source, + SolveOutcome::optimal( + reduction.target_problem(), + vec![vec![true], vec![true], vec![false]].clone() + ) + .unwrap() + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"), vec![true, true, false] ); assert!(source.evaluate(&vec![true, true, false]).unwrap().0); diff --git a/src/unit_tests/rules/exactcoverby3sets_staffscheduling.rs b/src/unit_tests/rules/exactcoverby3sets_staffscheduling.rs index e9e26ee06..770735706 100644 --- a/src/unit_tests/rules/exactcoverby3sets_staffscheduling.rs +++ b/src/unit_tests/rules/exactcoverby3sets_staffscheduling.rs @@ -1,9 +1,10 @@ -use super::*; use crate::models::misc::StaffScheduling; use crate::models::set::ExactCoverBy3Sets; use crate::rules::test_helpers::assert_satisfaction_round_trip_from_satisfaction_target; use crate::rules::ReduceTo; +use crate::rules::ReductionResult; use crate::solvers::BruteForce; +use crate::solvers::SolveOutcome; use crate::traits::Problem; #[test] @@ -56,7 +57,14 @@ fn test_exactcoverby3sets_to_staffscheduling_unique_cover() { let solutions = solver.find_all_witnesses(target).unwrap(); // Each satisfying target config should extract to selecting all 3 subsets for sol in &solutions { - let extracted = result.extract_solution(sol).unwrap(); + let extracted = result + .recover_result( + &source, + SolveOutcome::optimal(result.target_problem(), (sol).clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!( source.evaluate(&extracted).unwrap().0, "Extracted solution must be valid" @@ -65,7 +73,16 @@ fn test_exactcoverby3sets_to_staffscheduling_unique_cover() { // There should be exactly one satisfying assignment (up to extraction) let extracted_solutions: Vec> = solutions .iter() - .map(|s| result.extract_solution(s).unwrap()) + .map(|s| { + result + .recover_result( + &source, + SolveOutcome::optimal(result.target_problem(), (s).clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution") + }) .collect(); assert!( extracted_solutions @@ -83,16 +100,24 @@ fn test_exactcoverby3sets_to_staffscheduling_extract_solution() { // StaffScheduling config: [1, 1, 0, 0] means 1 worker on schedule 0 and 1 on schedule 1 let target_config = vec![1, 1, 0, 0]; - let extracted = result.extract_solution(&target_config).unwrap(); + let extracted = result + .recover_result( + &source, + SolveOutcome::optimal(result.target_problem(), target_config.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted, vec![true, true, false, false]); // Verify the extracted solution is valid in the source assert!(source.evaluate(&extracted).unwrap().0); // No workers cannot cover the required shifts. - assert!( - !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&result), &vec![0, 0, 0, 0]), Ok(value) if { value.is_valid() }) - ); + assert!(!ReductionResult::target_problem(&result) + .evaluate(&vec![0, 0, 0, 0]) + .unwrap() + .is_valid()); } #[test] diff --git a/src/unit_tests/rules/exactcoverby3sets_subsetproduct.rs b/src/unit_tests/rules/exactcoverby3sets_subsetproduct.rs index 2f2361518..0a5c688b0 100644 --- a/src/unit_tests/rules/exactcoverby3sets_subsetproduct.rs +++ b/src/unit_tests/rules/exactcoverby3sets_subsetproduct.rs @@ -2,6 +2,7 @@ use crate::models::misc::SubsetProduct; use crate::models::set::ExactCoverBy3Sets; use crate::rules::test_helpers::assert_satisfaction_round_trip_from_satisfaction_target; use crate::rules::{ReduceTo, ReductionResult}; +use crate::solvers::SolveOutcome; use num_bigint::BigUint; #[test] @@ -41,8 +42,14 @@ fn test_exactcoverby3sets_to_subsetproduct_extract_solution_is_identity() { assert_eq!( reduction - .extract_solution(&vec![true, true, false]) - .unwrap(), + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), vec![true, true, false].clone()) + .unwrap() + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"), vec![true, true, false] ); } diff --git a/src/unit_tests/rules/expectedretrievalcost_ilp.rs b/src/unit_tests/rules/expectedretrievalcost_ilp.rs index 86a9b561a..52b0d9d37 100644 --- a/src/unit_tests/rules/expectedretrievalcost_ilp.rs +++ b/src/unit_tests/rules/expectedretrievalcost_ilp.rs @@ -1,4 +1,5 @@ use super::*; +use crate::solvers::SolveOutcome; use crate::solvers::{BruteForce, ILPSolver}; use crate::traits::Problem; use crate::types::Min; @@ -48,7 +49,14 @@ fn test_expectedretrievalcost_to_ilp_bf_vs_ilp() { let bf_cost = problem.expected_cost(&bf_witness).unwrap().unwrap(); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be feasible"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let ilp_cost = problem.expected_cost(&extracted).unwrap().unwrap(); // ILP cost should match BF optimal cost @@ -75,7 +83,17 @@ fn test_solution_extraction() { target[reduction.z_var(r, sector, other, other_sector)] = 1; } } - assert_eq!(reduction.extract_solution(&target).unwrap(), assignment); + assert_eq!( + reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), target.clone()).unwrap() + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"), + assignment + ); assert_eq!( reduction .target_problem() @@ -96,7 +114,14 @@ fn test_expectedretrievalcost_to_ilp_closed_loop() { let ilp_solver = ILPSolver::new(); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be feasible"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let value = problem.evaluate(&extracted).unwrap(); assert!( matches!(value, Min(Some(_))), diff --git a/src/unit_tests/rules/factoring_circuit.rs b/src/unit_tests/rules/factoring_circuit.rs index 4dacf41cc..34e7c1458 100644 --- a/src/unit_tests/rules/factoring_circuit.rs +++ b/src/unit_tests/rules/factoring_circuit.rs @@ -1,4 +1,7 @@ use super::*; +use crate::rules::ReductionResult; +use crate::solvers::SolveOutcome; +use crate::traits::EvaluationError::InvalidConfiguration; include!("../jl_helpers.rs"); use crate::rules::test_helpers::assert_optimization_round_trip_from_satisfaction_target; use crate::solvers::BruteForce; @@ -226,7 +229,14 @@ fn test_extract_solution() { } } - let factoring_sol = reduction.extract_solution(&sol).unwrap(); + let factoring_sol = reduction + .recover_result( + &factoring, + SolveOutcome::optimal(reduction.target_problem(), sol.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let (p, q) = factoring_sol.clone(); assert_eq!(p, BigUint::from(2u32), "p should be 2"); assert_eq!(q, BigUint::from(3u32), "q should be 3"); @@ -239,7 +249,14 @@ fn test_extract_solution() { } } assert_eq!( - reduction.extract_solution(&sol).unwrap(), + reduction + .recover_result( + &factoring, + SolveOutcome::optimal(reduction.target_problem(), sol.clone()).unwrap() + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"), (BigUint::from(2u32), BigUint::from(3u32)) ); } @@ -384,7 +401,14 @@ fn test_factoring_to_circuit_zero_width_closed_loop() { let witness = BruteForce::new().solve(reduction.target_problem()).unwrap(); assert_eq!(witness.is_some(), value == 0); if let Some(witness) = witness { - let extracted = reduction.extract_solution(&witness).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), witness.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!(source.is_valid_factorization(&extracted)); assert!(extracted.0.is_zero()); } @@ -396,12 +420,18 @@ fn test_factoring_to_circuit_zero_width_closed_loop() { fn test_factoring_to_circuit_rejects_invalid_certificates() { let source = Factoring::with_factor_bits(6, 2, 2); let reduction = ReduceTo::::reduce_to(&source).unwrap(); - assert!( - !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![]), Ok(value) if { value.is_valid() }) - ); - assert!( - !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![false; reduction.target_problem().num_variables()]), Ok(value) if { value.is_valid() }) - ); + assert!(matches!( + ReductionResult::target_problem(&reduction).evaluate(&vec![]), + Err(InvalidConfiguration(_)) + )); + assert!(!ReductionResult::target_problem(&reduction) + .evaluate(&vec![ + false; + ReductionResult::target_problem(&reduction) + .num_variables() + ]) + .unwrap() + .is_valid()); let values = evaluate_multiplier_circuit(&reduction, 1, 1); let config = reduction .target_problem() @@ -409,9 +439,10 @@ fn test_factoring_to_circuit_rejects_invalid_certificates() { .iter() .map(|name| values[name]) .collect(); - assert!( - !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &config), Ok(value) if { value.is_valid() }) - ); + assert!(!ReductionResult::target_problem(&reduction) + .evaluate(&config) + .unwrap() + .is_valid()); } #[test] diff --git a/src/unit_tests/rules/factoring_ilp.rs b/src/unit_tests/rules/factoring_ilp.rs index 8d5c4b2d0..6c0cfbfab 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::solvers::SolveOutcome; use crate::solvers::{BruteForce, ILPSolver}; use num_bigint::BigUint; @@ -54,7 +55,14 @@ fn test_factor_6() { let ilp_solver = ILPSolver::new(); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); // Verify it's a valid factorization assert!(problem.is_valid_factorization(&extracted)); @@ -80,7 +88,14 @@ fn test_factor_15() { let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); // 4. Extract factoring solution - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); // 5. Verify: solution is valid and p × q = 15 assert!(problem.is_valid_factorization(&extracted)); @@ -98,7 +113,14 @@ fn test_factor_35() { let ilp_solver = ILPSolver::new(); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!(problem.is_valid_factorization(&extracted)); @@ -116,7 +138,14 @@ fn test_factor_one() { let ilp_solver = ILPSolver::new(); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!(problem.is_valid_factorization(&extracted)); @@ -134,7 +163,14 @@ fn test_factor_prime() { let ilp_solver = ILPSolver::new(); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!(problem.is_valid_factorization(&extracted)); @@ -152,7 +188,14 @@ fn test_factor_square() { let ilp_solver = ILPSolver::new(); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!(problem.is_valid_factorization(&extracted)); @@ -188,7 +231,14 @@ fn test_factoring_to_ilp_closed_loop() { // Get ILP solution let ilp_solver = ILPSolver::new(); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); - let ilp_factors = reduction.extract_solution(&ilp_solution).unwrap(); + let ilp_factors = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); // Get brute force solutions let bf = BruteForce::new(); @@ -221,7 +271,14 @@ fn test_solution_extraction() { // Variables: [p0, p1, q0, q1, z00, z01, z10, z11, c0, c1, c2, c3] // Each product column already matches 0110, so every carry is zero. let ilp_solution = vec![0, 1, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0]; - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted, (BigUint::from(2u32), BigUint::from(3u32))); @@ -254,7 +311,14 @@ fn test_integer_ilp_pipeline_solution() { let ilp = reduction.target_problem(); let ilp_solver = ILPSolver::new(); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); - let solution = reduction.extract_solution(&ilp_solution).unwrap(); + let solution = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!(problem.is_valid_factorization(&solution)); } @@ -269,7 +333,14 @@ fn test_asymmetric_bit_widths() { let ilp_solver = ILPSolver::new(); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!(problem.is_valid_factorization(&extracted)); diff --git a/src/unit_tests/rules/feasibleregisterassignment_ilp.rs b/src/unit_tests/rules/feasibleregisterassignment_ilp.rs index 5a58bdaef..7049cccac 100644 --- a/src/unit_tests/rules/feasibleregisterassignment_ilp.rs +++ b/src/unit_tests/rules/feasibleregisterassignment_ilp.rs @@ -1,5 +1,6 @@ use super::*; use crate::solvers::ILPSolver; +use crate::solvers::SolveOutcome; use crate::traits::Problem; use crate::types::Or; @@ -27,7 +28,14 @@ fn test_feasible_register_assignment_to_ilp_closed_loop() { let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("feasible source instance should yield a feasible ILP"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(source.evaluate(&extracted).unwrap(), Or(true)); let mut sorted = extracted.clone(); diff --git a/src/unit_tests/rules/flowshopscheduling_ilp.rs b/src/unit_tests/rules/flowshopscheduling_ilp.rs index 6bf8a37e6..39147243d 100644 --- a/src/unit_tests/rules/flowshopscheduling_ilp.rs +++ b/src/unit_tests/rules/flowshopscheduling_ilp.rs @@ -1,5 +1,6 @@ use super::*; use crate::models::algebraic::ILP; +use crate::solvers::SolveOutcome; use crate::solvers::{BruteForce, ILPSolver}; use crate::traits::Problem; use crate::types::Or; @@ -20,7 +21,14 @@ fn test_flowshopscheduling_to_ilp_closed_loop() { let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be feasible"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!( problem.evaluate(&extracted).unwrap(), Or(true), @@ -48,7 +56,14 @@ fn test_flowshopscheduling_to_ilp_single_job() { let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("single-job ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(problem.evaluate(&extracted).unwrap(), Or(true)); } @@ -64,6 +79,13 @@ fn test_flowshopscheduling_to_ilp_bf_vs_ilp() { let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(problem.evaluate(&extracted).unwrap(), Or(true)); } diff --git a/src/unit_tests/rules/graph.rs b/src/unit_tests/rules/graph.rs index 4918275a6..3b6400541 100644 --- a/src/unit_tests/rules/graph.rs +++ b/src/unit_tests/rules/graph.rs @@ -11,7 +11,8 @@ use crate::models::set::MaximumSetPacking; use crate::registry::ProblemCategory; use crate::rules::graph::{ReductionMode, ReductionStep}; use crate::rules::registry::{ReductionEntry, ReductionParameterDeclarations}; -use crate::rules::traits::{AggregateReductionResult, ReductionResult}; +use crate::rules::traits::ReductionResult; +use crate::solvers::SolveOutcome; use crate::topology::SimpleGraph; use crate::traits::Problem; use crate::types::{One, ProblemParameters, Sum}; @@ -49,7 +50,7 @@ fn symbolic_size_edge(fields: &[(&'static str, &str)], turing: bool) -> Reductio }, ), reduce_fn: Some(|_| panic!("size search must not execute reductions")), - reduce_aggregate_fn: None, + turing, } } @@ -218,7 +219,7 @@ struct SourceToMiddleAggregateResult { target: AggregateChainMiddle, } -impl AggregateReductionResult for SourceToMiddleAggregateResult { +impl ReductionResult for SourceToMiddleAggregateResult { type Source = AggregateChainSource; type Target = AggregateChainMiddle; @@ -226,8 +227,22 @@ impl AggregateReductionResult for SourceToMiddleAggregateResult { &self.target } - fn extract_value(&self, target_value: Sum) -> Sum { - Sum(target_value.0 + 2) + fn recover_result( + &self, + source: &Self::Source, + target: crate::solvers::ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + Ok(match target { + SolveOutcome::Optimal { mut solution, .. } => { + solution[0] += 2; + SolveOutcome::optimal(source, solution)? + } + SolveOutcome::Feasible { mut solution, .. } => { + solution[0] += 2; + SolveOutcome::feasible(source, solution)? + } + SolveOutcome::Infeasible => SolveOutcome::Infeasible, + }) } } @@ -235,7 +250,7 @@ struct MiddleToTargetAggregateResult { target: AggregateChainTarget, } -impl AggregateReductionResult for MiddleToTargetAggregateResult { +impl ReductionResult for MiddleToTargetAggregateResult { type Source = AggregateChainMiddle; type Target = AggregateChainTarget; @@ -243,15 +258,28 @@ impl AggregateReductionResult for MiddleToTargetAggregateResult { &self.target } - fn extract_value(&self, target_value: Sum) -> Sum { - Sum(target_value.0 + 3) + fn recover_result( + &self, + source: &Self::Source, + target: crate::solvers::ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + Ok(match target { + SolveOutcome::Optimal { mut solution, .. } => { + solution[0] += 3; + SolveOutcome::optimal(source, solution)? + } + SolveOutcome::Feasible { mut solution, .. } => { + solution[0] += 3; + SolveOutcome::feasible(source, solution)? + } + SolveOutcome::Infeasible => SolveOutcome::Infeasible, + }) } } fn reduce_source_to_middle_aggregate( any: &dyn Any, -) -> Result, crate::rules::ReductionError> -{ +) -> Result { any.downcast_ref::().ok_or( crate::rules::ReductionError::SourceTypeMismatch { source_problem: AggregateChainSource::NAME, @@ -259,15 +287,16 @@ fn reduce_source_to_middle_aggregate( expected: std::any::type_name::(), }, )?; - Ok(Box::new(SourceToMiddleAggregateResult { - target: AggregateChainMiddle, - })) + Ok(crate::rules::registry::ExecutedStep { + witness: std::rc::Rc::new(SourceToMiddleAggregateResult { + target: AggregateChainMiddle, + }), + }) } fn reduce_middle_to_target_aggregate( any: &dyn Any, -) -> Result, crate::rules::ReductionError> -{ +) -> Result { any.downcast_ref::().ok_or( crate::rules::ReductionError::SourceTypeMismatch { source_problem: AggregateChainMiddle::NAME, @@ -275,9 +304,11 @@ fn reduce_middle_to_target_aggregate( expected: std::any::type_name::(), }, )?; - Ok(Box::new(MiddleToTargetAggregateResult { - target: AggregateChainTarget, - })) + Ok(crate::rules::registry::ExecutedStep { + witness: std::rc::Rc::new(MiddleToTargetAggregateResult { + target: AggregateChainTarget, + }), + }) } struct SourceToMiddleWitnessResult { @@ -292,10 +323,34 @@ impl ReductionResult for SourceToMiddleWitnessResult { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: crate::solvers::ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + crate::solvers::SolveOutcome::Infeasible => { + Ok(crate::solvers::SolveOutcome::Infeasible) + } + crate::solvers::SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(crate::solvers::SolveOutcome::optimal(source, solution)?) + } + crate::solvers::SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(crate::solvers::SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl SourceToMiddleWitnessResult { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok(target_solution.to_vec()) } } @@ -314,8 +369,6 @@ fn reduce_source_to_middle_witness( witness: std::rc::Rc::new(SourceToMiddleWitnessResult { target: AggregateChainMiddle, }), - aggregate: None, - interpret_optimum: None, }) } @@ -350,10 +403,34 @@ impl ReductionResult for MiddleToTargetWitnessResult { &self.target } - fn extract_solution( + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: crate::solvers::ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + crate::solvers::SolveOutcome::Infeasible => { + Ok(crate::solvers::SolveOutcome::Infeasible) + } + crate::solvers::SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(crate::solvers::SolveOutcome::optimal(source, solution)?) + } + crate::solvers::SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(crate::solvers::SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl MiddleToTargetWitnessResult { + fn map_solution( + &self, + target_solution: &<::Target as crate::traits::Problem>::Solution, + ) -> crate::rules::ExtractionResult< + <::Source as crate::traits::Problem>::Solution, + > { Ok(target_solution.to_vec()) } } @@ -372,8 +449,6 @@ fn reduce_middle_to_target_witness( witness: std::rc::Rc::new(MiddleToTargetWitnessResult { target: AggregateChainTarget, }), - aggregate: None, - interpret_optimum: None, }) } @@ -392,8 +467,6 @@ fn reduce_natural_variant_witness( NaturalVariantProblem, NaturalVariantProblem, >::new(source.clone())), - aggregate: None, - interpret_optimum: None, }) } @@ -443,7 +516,7 @@ fn execute_paths_executes_a_shared_prefix_once() { let witness_edge = |reduce_fn| ReductionEdgeData { parameter_contract: empty_parameter_contract(), reduce_fn: Some(reduce_fn), - reduce_aggregate_fn: None, + turing: false, }; let graph = ReductionGraph::from_test_edges( @@ -486,7 +559,15 @@ fn execute_paths_executes_a_shared_prefix_once() { assert_eq!(execution.steps.len(), path.len()); assert_eq!( execution - .extract_solution::, _>(&vec![1usize]) + .recover_result::( + &AggregateChainSource, + SolveOutcome::Optimal { + solution: vec![1usize], + evaluation: Sum(1) + } + ) + .unwrap() + .into_solution() .unwrap(), vec![1] ); @@ -537,7 +618,7 @@ fn path_parameter_contract_errors_are_typed_and_isolated() { ReductionEdgeData { parameter_contract: empty_parameter_contract(), reduce_fn: Some(|_| panic!("metadata inspection must not execute reductions")), - reduce_aggregate_fn: None, + turing: false, }, ); @@ -558,7 +639,7 @@ fn path_parameter_contract_errors_are_typed_and_isolated() { ReductionEdgeData { parameter_contract: invalid_contract, reduce_fn: Some(|_| panic!("metadata inspection must not execute reductions")), - reduce_aggregate_fn: None, + turing: false, }, ); @@ -711,8 +792,7 @@ fn test_aggregate_reduction_chain_extracts_value_backwards() { middle_idx, ReductionEdgeData { parameter_contract: empty_parameter_contract(), - reduce_fn: None, - reduce_aggregate_fn: Some(reduce_source_to_middle_aggregate), + reduce_fn: Some(reduce_source_to_middle_aggregate), turing: false, }, ); @@ -721,8 +801,7 @@ fn test_aggregate_reduction_chain_extracts_value_backwards() { target_idx, ReductionEdgeData { parameter_contract: empty_parameter_contract(), - reduce_fn: None, - reduce_aggregate_fn: Some(reduce_middle_to_target_aggregate), + reduce_fn: Some(reduce_middle_to_target_aggregate), turing: false, }, ); @@ -755,7 +834,7 @@ fn test_aggregate_reduction_chain_extracts_value_backwards() { }; let chain = reduction_graph - .reduce_aggregate_along_path(&path, &AggregateChainSource as &dyn Any) + .reduce_along_path(&path, &AggregateChainSource as &dyn Any) .expect("aggregate reduction should not fail") .expect("expected aggregate reduction chain"); @@ -764,11 +843,23 @@ fn test_aggregate_reduction_chain_extracts_value_backwards() { .unwrap(), vec![1] ); - assert_eq!(chain.extract_value_dyn(json!(7)), json!(12)); + assert_eq!( + chain + .recover_result::( + &AggregateChainSource, + SolveOutcome::optimal(chain.target_problem::(), vec![7]) + .unwrap() + ) + .unwrap(), + SolveOutcome::Optimal { + solution: vec![12], + evaluation: Sum(12) + } + ); } #[test] -fn witness_path_search_rejects_aggregate_only_edge() { +fn witness_path_search_rejects_turing_only_edge() { let source_variant = BTreeMap::new(); let target_variant = BTreeMap::new(); let graph = build_two_node_graph( @@ -779,8 +870,7 @@ fn witness_path_search_rejects_aggregate_only_edge() { ReductionEdgeData { parameter_contract: empty_parameter_contract(), reduce_fn: None, - reduce_aggregate_fn: Some(reduce_source_to_middle_aggregate), - turing: false, + turing: true, }, ); @@ -799,13 +889,13 @@ fn witness_path_search_rejects_aggregate_only_edge() { &source_variant, AggregateChainMiddle::NAME, &target_variant, - ReductionMode::Aggregate + ReductionMode::Turing ) .is_empty()); } #[test] -fn aggregate_path_search_rejects_witness_only_edge() { +fn turing_path_search_rejects_witness_only_edge() { let source_variant = BTreeMap::new(); let target_variant = BTreeMap::new(); let graph = build_two_node_graph( @@ -816,7 +906,7 @@ fn aggregate_path_search_rejects_witness_only_edge() { ReductionEdgeData { parameter_contract: empty_parameter_contract(), reduce_fn: Some(reduce_source_to_middle_witness), - reduce_aggregate_fn: None, + turing: false, }, ); @@ -827,7 +917,7 @@ fn aggregate_path_search_rejects_witness_only_edge() { &source_variant, AggregateChainMiddle::NAME, &target_variant, - ReductionMode::Aggregate + ReductionMode::Turing ) .is_empty()); assert!(!graph @@ -842,7 +932,7 @@ fn aggregate_path_search_rejects_witness_only_edge() { } #[test] -fn witness_executor_does_not_imply_aggregate_capability() { +fn witness_executor_does_not_imply_turing_capability() { let source_variant = BTreeMap::from([("graph".to_string(), "Source".to_string())]); let target_variant = BTreeMap::from([("graph".to_string(), "Target".to_string())]); let graph = build_two_node_graph( @@ -853,7 +943,7 @@ fn witness_executor_does_not_imply_aggregate_capability() { ReductionEdgeData { parameter_contract: empty_parameter_contract(), reduce_fn: Some(reduce_natural_variant_witness), - reduce_aggregate_fn: None, + turing: false, }, ); @@ -873,13 +963,13 @@ fn witness_executor_does_not_imply_aggregate_capability() { &source_variant, NaturalVariantProblem::NAME, &target_variant, - ReductionMode::Aggregate + ReductionMode::Turing ) .is_empty()); } #[test] -fn reduce_aggregate_along_path_rejects_single_step_path() { +fn reduce_result_along_path_rejects_single_step_path() { let source_variant = BTreeMap::new(); let graph = build_two_node_graph( AggregateChainSource::NAME, @@ -888,8 +978,7 @@ fn reduce_aggregate_along_path_rejects_single_step_path() { BTreeMap::new(), ReductionEdgeData { parameter_contract: empty_parameter_contract(), - reduce_fn: None, - reduce_aggregate_fn: Some(reduce_source_to_middle_aggregate), + reduce_fn: Some(reduce_source_to_middle_aggregate), turing: false, }, ); @@ -900,13 +989,13 @@ fn reduce_aggregate_along_path_rejects_single_step_path() { }], }; assert!(graph - .reduce_aggregate_along_path(&single_step_path, &AggregateChainSource as &dyn Any) + .reduce_along_path(&single_step_path, &AggregateChainSource as &dyn Any) .expect("single-step path lookup should not fail") .is_none()); } #[test] -fn reduce_aggregate_returns_none_for_witness_only_edge() { +fn reduce_result_returns_none_for_turing_only_edge() { let source_variant = BTreeMap::new(); let target_variant = BTreeMap::new(); let graph = build_two_node_graph( @@ -916,9 +1005,9 @@ fn reduce_aggregate_returns_none_for_witness_only_edge() { target_variant.clone(), ReductionEdgeData { parameter_contract: empty_parameter_contract(), - reduce_fn: Some(reduce_source_to_middle_witness), - reduce_aggregate_fn: None, - turing: false, + reduce_fn: None, + + turing: true, }, ); let path = ReductionPath { @@ -934,8 +1023,8 @@ fn reduce_aggregate_returns_none_for_witness_only_edge() { ], }; assert!(graph - .reduce_aggregate_along_path(&path, &AggregateChainSource as &dyn Any) - .expect("witness-only edge lookup should not fail") + .reduce_along_path(&path, &AggregateChainSource as &dyn Any) + .expect("Turing-only edge lookup should not fail") .is_none()); } @@ -951,7 +1040,7 @@ fn reduce_along_path_preserves_edge_failure() { ReductionEdgeData { parameter_contract: empty_parameter_contract(), reduce_fn: Some(fail_source_to_middle_witness), - reduce_aggregate_fn: None, + turing: false, }, ); @@ -1095,7 +1184,8 @@ fn test_find_direct_path_variants() { assert!(graph .find_all_paths("Factoring", &src, "SpinGlass", &dst) .iter() - .any(|path| path.type_names() == ["Factoring", "CircuitSAT", "SpinGlass"])); + .any(|path| path.type_names() + == ["Factoring", "CircuitSAT", "DecisionSpinGlass", "SpinGlass"])); } #[test] @@ -1216,13 +1306,13 @@ fn test_sat_based_reductions() { let graph = ReductionGraph::new(); // SAT -> IS - assert!(graph.has_direct_reduction::>()); + assert!(graph.has_direct_reduction::>>()); // SAT -> KColoring assert!(graph.has_direct_reduction::>()); // SAT -> MinimumDominatingSet - assert!(graph.has_direct_reduction::>()); + assert!(graph.has_direct_reduction::>>()); } #[test] @@ -1237,7 +1327,7 @@ fn test_circuit_reductions() { assert!(graph.has_direct_reduction::()); // CircuitSAT -> SpinGlass - assert!(graph.has_direct_reduction::>()); + assert!(graph.has_direct_reduction::>>()); // Find path from Factoring to SpinGlass let src = ReductionGraph::variant_to_map(&Factoring::variant()); @@ -1246,7 +1336,8 @@ fn test_circuit_reductions() { assert!(!paths.is_empty()); assert!(paths .iter() - .any(|path| path.type_names() == ["Factoring", "CircuitSAT", "SpinGlass"])); + .any(|path| path.type_names() + == ["Factoring", "CircuitSAT", "DecisionSpinGlass", "SpinGlass"])); } #[test] @@ -1282,7 +1373,7 @@ fn test_ksat_reductions() { fn test_nae_sat_to_maxcut_reduction_registered() { let graph = ReductionGraph::new(); - assert!(graph.has_direct_reduction::>()); + assert!(graph.has_direct_reduction::>>()); } #[test] @@ -1631,7 +1722,7 @@ fn test_reduction_chain_direct() { let solver = BruteForce::new(); let target_solution = solver.solve(target).unwrap().unwrap(); - let source_solution = chain.extract_solution(&target_solution).unwrap(); + let source_solution = chain.recover_result::, MinimumVertexCover>(&problem, SolveOutcome::optimal(target, target_solution.clone()).unwrap()).map(|outcome| outcome.into_solution().unwrap()).unwrap(); let metric = problem.evaluate(&source_solution).unwrap(); assert!(metric.is_valid()); } @@ -1662,7 +1753,13 @@ fn test_reduction_chain_multi_step() { let solver = BruteForce::new(); let target_solution = solver.solve(target).unwrap().unwrap(); - let source_solution = chain.extract_solution(&target_solution).unwrap(); + let source_solution = chain + .recover_result::, MaximumSetPacking>( + &problem, + SolveOutcome::optimal(target, target_solution.clone()).unwrap(), + ) + .map(|outcome| outcome.into_solution().unwrap()) + .unwrap(); let metric = problem.evaluate(&source_solution).unwrap(); assert!(metric.is_valid()); } @@ -1709,7 +1806,7 @@ fn test_reduction_chain_with_variant_reductions() { let solver = BruteForce::new(); let target_solution = solver.solve(target).unwrap().unwrap(); - let source_solution = chain.extract_solution(&target_solution).unwrap(); + let source_solution = chain.recover_result::, MinimumVertexCover>(&mis, SolveOutcome::optimal(target, target_solution.clone()).unwrap()).map(|outcome| outcome.into_solution().unwrap()).unwrap(); let metric = mis.evaluate(&source_solution).unwrap(); assert!(metric.is_valid()); @@ -1728,9 +1825,14 @@ fn test_reduction_chain_with_variant_reductions() { ) .into_iter() .find(|path| { - path.len() == 4 + path.len() == 5 && path.type_names() - == ["KSatisfiability", "Satisfiability", "MaximumIndependentSet"] + == [ + "KSatisfiability", + "Satisfiability", + "DecisionMaximumIndependentSet", + "MaximumIndependentSet", + ] }) .expect("explicit SAT route"); @@ -1752,7 +1854,7 @@ fn test_reduction_chain_with_variant_reductions() { let target: &MaximumIndependentSet = ksat_chain.target_problem(); let target_solution = solver.solve(target).unwrap().unwrap(); - let original_solution = ksat_chain.extract_solution(&target_solution).unwrap(); + let original_solution = ksat_chain.recover_result::, MaximumIndependentSet>(&ksat, SolveOutcome::optimal(target, target_solution).unwrap()).map(|outcome| outcome.into_solution().unwrap()).unwrap(); // Verify the extracted solution satisfies the original 3-SAT formula assert!(ksat.evaluate(&original_solution).unwrap()); @@ -1999,43 +2101,38 @@ fn witness_and_value_mapping_share_one_executed_construction() { let result = Rc::new(SourceToMiddleWitnessResult { target: AggregateChainMiddle, }); - Ok(ExecutedStep { - aggregate: Some(result.clone()), - interpret_optimum: None, - witness: result, - }) + Ok(ExecutedStep { witness: result }) }], ) .unwrap(); let step = &chain.steps[0]; - let aggregate = step.aggregate.as_ref().unwrap(); assert!(std::ptr::eq( - step.witness.target_problem_any(), - aggregate.target_problem_any(), + step.witness + .target_problem_any() + .downcast_ref::() + .unwrap(), + chain.target_problem::(), )); - let witness = vec![1usize]; - assert_eq!( - chain.extract_solution::, _>(&witness).unwrap(), - witness - ); + let witness = vec![7usize]; assert_eq!( - aggregate.extract_value_dyn(serde_json::json!(7)), - serde_json::json!(7) + chain + .recover_result::( + &AggregateChainSource, + SolveOutcome::optimal( + chain.target_problem::(), + witness.clone() + ) + .unwrap(), + ) + .unwrap(), + SolveOutcome::Optimal { + solution: witness, + evaluation: Sum(7) + } ); assert_eq!(CONSTRUCTIONS.load(Ordering::SeqCst), 1); } -impl AggregateReductionResult for SourceToMiddleWitnessResult { - type Source = AggregateChainSource; - type Target = AggregateChainMiddle; - fn target_problem(&self) -> &Self::Target { - &self.target - } - fn extract_value(&self, value: Sum) -> Sum { - value - } -} - #[test] fn composed_witness_agrees_across_direct_chain_path_and_json() { use crate::rules::ReduceTo; @@ -2051,12 +2148,18 @@ fn composed_witness_agrees_across_direct_chain_path_and_json() { .evaluate(&target_solution) .unwrap() .is_valid()); + let target_result = + SolveOutcome::optimal(third.target_problem(), target_solution.clone()).unwrap(); + let middle_result = third + .recover_result(second.target_problem(), target_result.clone()) + .unwrap(); + let first_result = second + .recover_result(first.target_problem(), middle_result) + .unwrap(); let expected = first - .extract_solution( - &second - .extract_solution(&third.extract_solution(&target_solution).unwrap()) - .unwrap(), - ) + .recover_result(&source, first_result) + .unwrap() + .into_solution() .unwrap(); assert_eq!(expected, vec![false, true, false]); let path = ReductionPath { @@ -2081,18 +2184,33 @@ fn composed_witness_agrees_across_direct_chain_path_and_json() { let executed = graph.execute_paths(&[path], &source).unwrap(); assert_eq!( chain - .extract_solution::, _>(&target_solution) + .recover_result::>(&source, target_result.clone()) + .unwrap() + .into_solution() .unwrap(), expected ); assert_eq!( executed[0] - .extract_solution::, _>(&target_solution) + .recover_result::>(&source, target_result.clone()) + .unwrap() + .into_solution() .unwrap(), expected ); assert_eq!( - chain.extract_solution_json(json!(target_solution)).unwrap(), - json!(expected) + chain + .recover_result_json( + &source, + SolveOutcome::Optimal { + solution: json!(target_solution), + evaluation: String::new(), + } + ) + .unwrap(), + SolveOutcome::Optimal { + solution: json!(expected), + evaluation: "Min(1)".into() + } ); } diff --git a/src/unit_tests/rules/graphpartitioning_ilp.rs b/src/unit_tests/rules/graphpartitioning_ilp.rs index dc50e06a1..9533778e0 100644 --- a/src/unit_tests/rules/graphpartitioning_ilp.rs +++ b/src/unit_tests/rules/graphpartitioning_ilp.rs @@ -1,6 +1,7 @@ use super::*; use crate::models::algebraic::{Comparison, ObjectiveSense}; use crate::models::graph::GraphPartitioning; +use crate::solvers::SolveOutcome; use crate::solvers::{BruteForce, ILPSolver}; use crate::topology::SimpleGraph; use crate::traits::Problem; @@ -90,7 +91,14 @@ fn test_graphpartitioning_to_ilp_closed_loop() { let bf_obj = problem.evaluate(&bf_solutions[0]).unwrap(); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let ilp_obj = problem.evaluate(&extracted).unwrap(); assert_eq!(bf_obj, Min(Some(3))); @@ -122,7 +130,14 @@ fn test_solution_extraction() { ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp_solution = vec![0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 0, 0]; - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted, vec![false, false, false, true, true, true]); assert_eq!(problem.evaluate(&extracted).unwrap(), Min(Some(3))); diff --git a/src/unit_tests/rules/graphpartitioning_maxcut.rs b/src/unit_tests/rules/graphpartitioning_maxcut.rs index d2e94e59a..386ee8dff 100644 --- a/src/unit_tests/rules/graphpartitioning_maxcut.rs +++ b/src/unit_tests/rules/graphpartitioning_maxcut.rs @@ -1,6 +1,7 @@ use crate::models::graph::{GraphPartitioning, MaxCut}; use crate::rules::test_helpers::assert_optimization_round_trip_from_optimization_target; use crate::rules::{ReduceTo, ReductionResult}; +use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; fn issue_example() -> GraphPartitioning { @@ -56,7 +57,14 @@ fn test_graphpartitioning_to_maxcut_extract_solution_identity() { let target_solution = super::ISSUE_EXAMPLE_WITNESS.to_vec(); assert_eq!( - reduction.extract_solution(&target_solution).unwrap(), + reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), target_solution.clone()).unwrap() + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"), target_solution ); } @@ -66,3 +74,26 @@ fn test_graphpartitioning_to_maxcut_penalty_overflow_panics() { let result = std::panic::catch_unwind(|| super::penalty_weight(i64::MAX as usize)); assert!(result.is_err()); } + +#[test] +fn odd_partition_recovers_infeasibility_from_every_maxcut_optimum() { + use crate::solvers::{BruteForce, SolveOutcome}; + let source = GraphPartitioning::new(SimpleGraph::new(1, vec![])); + assert!(BruteForce::new().solve(&source).unwrap().is_none()); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + let optima = BruteForce::new() + .find_all_witnesses(reduction.target_problem()) + .unwrap(); + assert_eq!(optima.len(), 2); + for solution in optima { + assert_eq!( + reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), solution).unwrap() + ) + .unwrap(), + SolveOutcome::Infeasible + ); + } +} diff --git a/src/unit_tests/rules/graphpartitioning_qubo.rs b/src/unit_tests/rules/graphpartitioning_qubo.rs index 4b163d528..1c6ac8fbc 100644 --- a/src/unit_tests/rules/graphpartitioning_qubo.rs +++ b/src/unit_tests/rules/graphpartitioning_qubo.rs @@ -80,3 +80,26 @@ fn test_graphpartitioning_to_qubo_canonical_example_spec() { assert_eq!(example.target.instance["matrix"]["nrows"], 6); assert!(!example.solutions.is_empty()); } + +#[test] +fn odd_partition_recovers_infeasibility_from_every_qubo_optimum() { + use crate::solvers::{BruteForce, SolveOutcome}; + let source = GraphPartitioning::new(SimpleGraph::new(1, vec![])); + assert!(BruteForce::new().solve(&source).unwrap().is_none()); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + let optima = BruteForce::new() + .find_all_witnesses(reduction.target_problem()) + .unwrap(); + assert_eq!(optima.len(), 2); + for solution in optima { + assert_eq!( + reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), solution).unwrap() + ) + .unwrap(), + SolveOutcome::Infeasible + ); + } +} diff --git a/src/unit_tests/rules/hamiltoniancircuit_biconnectivityaugmentation.rs b/src/unit_tests/rules/hamiltoniancircuit_biconnectivityaugmentation.rs index f94f0f2bd..169d04005 100644 --- a/src/unit_tests/rules/hamiltoniancircuit_biconnectivityaugmentation.rs +++ b/src/unit_tests/rules/hamiltoniancircuit_biconnectivityaugmentation.rs @@ -3,7 +3,9 @@ use crate::rules::test_helpers::assert_satisfaction_round_trip_from_satisfaction use crate::rules::ReduceTo; use crate::rules::ReductionResult; use crate::solvers::BruteForce; +use crate::solvers::SolveOutcome; use crate::topology::SimpleGraph; +use crate::traits::EvaluationError::InvalidConfiguration; use crate::Problem; fn cycle4_hc() -> HamiltonianCircuit { @@ -66,7 +68,14 @@ fn test_hamiltoniancircuit_to_biconnectivityaugmentation_extract_solution() { // Select edges (0,1), (0,3), (1,2), (2,3) => config [1, 0, 1, 1, 0, 1] let target_config = vec![true, false, true, true, false, true]; - let extracted = reduction.extract_solution(&target_config).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), target_config.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted.len(), 4); assert!( @@ -143,9 +152,7 @@ fn test_hamiltoniancircuit_to_biconnectivityaugmentation_small_graphs() { assert_eq!(target.num_potential_edges(), 0); assert_eq!(*target.budget(), 0); assert!(!target.evaluate(&vec![]).unwrap().0); - assert!( - !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![]), Ok(value) if { value.is_valid() }) - ); + assert!(BruteForce::new().solve(&source).unwrap().is_none()); assert!(BruteForce::new().solve(target).unwrap().is_none()); } @@ -176,7 +183,15 @@ fn test_hamiltoniancircuit_to_biconnectivityaugmentation_all_graphs_and_certific let config: Vec<_> = (0..pairs.len()).map(|i| mask & (1 << i) != 0).collect(); let feasible = reduction.target_problem().evaluate(&config).unwrap().0; if feasible { - let circuit = reduction.extract_solution(&config).unwrap(); + let circuit = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), config.clone()) + .unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!(source.evaluate(&circuit).unwrap().0); target_yes = true; } @@ -195,18 +210,22 @@ fn test_hamiltoniancircuit_to_biconnectivityaugmentation_rejects_infeasible_cert let reduction = ReduceTo::>::reduce_to(&source).unwrap(); // A spanning cycle made only of non-edges exceeds the budget and is not a source cycle. - assert!( - !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![true; 3]), Ok(value) if { value.is_valid() }) - ); - assert!( - !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![false; 3]), Ok(value) if { value.is_valid() }) - ); - assert!( - !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![true; 2]), Ok(value) if { value.is_valid() }) - ); - assert!( - !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![true; 4]), Ok(value) if { value.is_valid() }) - ); + assert!(!ReductionResult::target_problem(&reduction) + .evaluate(&vec![true; 3]) + .unwrap() + .is_valid()); + assert!(!ReductionResult::target_problem(&reduction) + .evaluate(&vec![false; 3]) + .unwrap() + .is_valid()); + assert!(matches!( + ReductionResult::target_problem(&reduction).evaluate(&vec![true; 2]), + Err(InvalidConfiguration(_)) + )); + assert!(matches!( + ReductionResult::target_problem(&reduction).evaluate(&vec![true; 4]), + Err(InvalidConfiguration(_)) + )); let source = HamiltonianCircuit::new(SimpleGraph::complete(6)); let reduction = ReduceTo::>::reduce_to(&source).unwrap(); @@ -217,7 +236,8 @@ fn test_hamiltoniancircuit_to_biconnectivityaugmentation_rejects_infeasible_cert .iter() .map(|&(u, v, _)| (u < 3) == (v < 3)) .collect(); - assert!( - !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &config), Ok(value) if { value.is_valid() }) - ); + assert!(!ReductionResult::target_problem(&reduction) + .evaluate(&config) + .unwrap() + .is_valid()); } diff --git a/src/unit_tests/rules/hamiltoniancircuit_bottlenecktravelingsalesman.rs b/src/unit_tests/rules/hamiltoniancircuit_bottlenecktravelingsalesman.rs index 5623656a4..8a6e966c9 100644 --- a/src/unit_tests/rules/hamiltoniancircuit_bottlenecktravelingsalesman.rs +++ b/src/unit_tests/rules/hamiltoniancircuit_bottlenecktravelingsalesman.rs @@ -3,6 +3,7 @@ use crate::rules::test_helpers::assert_satisfaction_round_trip_from_optimization use crate::rules::ReduceTo; use crate::rules::ReductionResult; use crate::solvers::BruteForce; +use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; use crate::types::Min; use crate::Problem; @@ -78,7 +79,14 @@ fn test_hamiltoniancircuit_to_bottlenecktravelingsalesman_extract_solution_cycle .map(|(u, v)| cycle_edges.contains(&(u, v)) || cycle_edges.contains(&(v, u))) .collect(); - let extracted = reduction.extract_solution(&target_solution).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), target_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); // Bottleneck should be 1 (all selected edges are original cycle edges) assert_eq!(target.evaluate(&target_solution).unwrap(), Min(Some(1))); diff --git a/src/unit_tests/rules/hamiltoniancircuit_hamiltonianpath.rs b/src/unit_tests/rules/hamiltoniancircuit_hamiltonianpath.rs index 4f0482880..c3f367344 100644 --- a/src/unit_tests/rules/hamiltoniancircuit_hamiltonianpath.rs +++ b/src/unit_tests/rules/hamiltoniancircuit_hamiltonianpath.rs @@ -3,6 +3,7 @@ use crate::rules::test_helpers::assert_satisfaction_round_trip_from_satisfaction use crate::rules::ReduceTo; use crate::rules::ReductionResult; use crate::solvers::BruteForce; +use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; use crate::Problem; @@ -60,7 +61,14 @@ fn test_hamiltoniancircuit_to_hamiltonianpath_extract_solution() { // HP solution: s=5, 0, 1, 2, 3, v'=4, t=6 let hp_config = vec![5, 0, 1, 2, 3, 4, 6]; - let extracted = reduction.extract_solution(&hp_config).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), hp_config.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted.len(), 4); assert!( @@ -77,7 +85,14 @@ fn test_hamiltoniancircuit_to_hamiltonianpath_extract_reversed() { // HP solution reversed: t=6, v'=4, 3, 2, 1, 0, s=5 let hp_config = vec![6, 4, 3, 2, 1, 0, 5]; - let extracted = reduction.extract_solution(&hp_config).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), hp_config.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted.len(), 4); assert!( diff --git a/src/unit_tests/rules/hamiltoniancircuit_longestcircuit.rs b/src/unit_tests/rules/hamiltoniancircuit_longestcircuit.rs index 571623624..30d5eb7a0 100644 --- a/src/unit_tests/rules/hamiltoniancircuit_longestcircuit.rs +++ b/src/unit_tests/rules/hamiltoniancircuit_longestcircuit.rs @@ -1,10 +1,13 @@ +use crate::models::decision::Decision; use crate::models::graph::{HamiltonianCircuit, LongestCircuit}; -use crate::rules::test_helpers::assert_satisfaction_round_trip_from_optimization_target; +use crate::rules::test_helpers::assert_satisfaction_round_trip_from_satisfaction_target; use crate::rules::ReduceTo; use crate::rules::ReductionResult; use crate::solvers::BruteForce; +use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; -use crate::types::Max; +use crate::traits::EvaluationError::InvalidConfiguration; +use crate::types::OptimizationValue; use crate::Problem; fn cycle4_hc() -> HamiltonianCircuit { @@ -13,31 +16,37 @@ fn cycle4_hc() -> HamiltonianCircuit { #[test] fn test_hamiltoniancircuit_aggregate_requires_a_spanning_cycle() { - let reduction = ReduceTo::>::reduce_to(&cycle4_hc()).unwrap(); + let reduction = + ReduceTo::>>::reduce_to(&cycle4_hc()).unwrap(); for (value, expected) in [ - (Max(None), false), - (Max(Some(3)), false), - (Max(Some(4)), true), + (crate::types::Max(None), false), + (crate::types::Max(Some(3)), false), + (crate::types::Max(Some(4)), true), ] { assert_eq!( - crate::rules::AggregateReductionResult::extract_value(&reduction, value), + crate::types::Or(OptimizationValue::meets_bound( + &(value), + crate::rules::ReductionResult::target_problem(&reduction).bound() + )), crate::types::Or(expected), ); } let short_cycle = HamiltonianCircuit::new(SimpleGraph::new(4, vec![(0, 1), (1, 2), (0, 2)])); - let reduction = ReduceTo::>::reduce_to(&short_cycle).unwrap(); - assert!( - !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![true; 3]), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&reduction, value); value.is_valid() }) - ); + let reduction = + ReduceTo::>>::reduce_to(&short_cycle).unwrap(); + assert!(!ReductionResult::target_problem(&reduction) + .evaluate(&vec![true; 3]) + .unwrap() + .is_valid()); } #[test] fn test_hamiltoniancircuit_to_longestcircuit_closed_loop() { let source = cycle4_hc(); - let reduction = ReduceTo::>::reduce_to(&source) + let reduction = ReduceTo::>>::reduce_to(&source) .expect("reduction should succeed"); - assert_satisfaction_round_trip_from_optimization_target( + assert_satisfaction_round_trip_from_satisfaction_target( &source, &reduction, "HamiltonianCircuit -> LongestCircuit", @@ -47,56 +56,54 @@ fn test_hamiltoniancircuit_to_longestcircuit_closed_loop() { #[test] fn test_hamiltoniancircuit_to_longestcircuit_structure() { let source = cycle4_hc(); - let reduction = ReduceTo::>::reduce_to(&source) + let reduction = ReduceTo::>>::reduce_to(&source) .expect("reduction should succeed"); let target = reduction.target_problem(); // Same graph structure - assert_eq!(target.graph().num_vertices(), 4); - assert_eq!(target.graph().num_edges(), 4); + assert_eq!(target.inner().graph().num_vertices(), 4); + assert_eq!(target.inner().graph().num_edges(), 4); // All unit weights - assert!(target.edge_lengths().iter().all(|&w| w == 1)); + assert!(target.inner().edge_lengths().iter().all(|&w| w == 1)); } #[test] fn test_hamiltoniancircuit_to_longestcircuit_nonhamiltonian() { // Star graph on 4 vertices: no Hamiltonian circuit let source = HamiltonianCircuit::new(SimpleGraph::star(4)); - let reduction = ReduceTo::>::reduce_to(&source) + let reduction = ReduceTo::>>::reduce_to(&source) .expect("reduction should succeed"); let target = reduction.target_problem(); let solver = BruteForce::new(); let witness = solver.solve(target).unwrap(); - match witness { - Some(sol) => { - let value = target.evaluate(&sol).unwrap(); - // Optimal circuit length must be strictly less than n=4 - assert!( - value.unwrap() < 4, - "star graph should not have a circuit of length 4" - ); - } - None => { - // No circuit at all in a star graph — also acceptable - } - } + assert!(witness.is_none()); } #[test] fn test_hamiltoniancircuit_to_longestcircuit_extract_solution() { let source = cycle4_hc(); - let reduction = ReduceTo::>::reduce_to(&source) + let reduction = ReduceTo::>>::reduce_to(&source) .expect("reduction should succeed"); let target = reduction.target_problem(); // All edges selected forms a Hamiltonian circuit on the cycle graph let target_solution = vec![true, true, true, true]; - let extracted = reduction.extract_solution(&target_solution).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), target_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); - assert_eq!(target.evaluate(&target_solution).unwrap(), Max(Some(4))); + assert_eq!( + target.inner().evaluate(&target_solution).unwrap(), + crate::types::Max(Some(4)) + ); assert_eq!(extracted.len(), 4); assert!(source.evaluate(&extracted).unwrap()); } @@ -117,22 +124,33 @@ fn test_hamiltoniancircuit_extraction_matches_all_small_target_configurations() .collect(); let source = HamiltonianCircuit::new(SimpleGraph::new(n, edges)); let reduction = - ReduceTo::>::reduce_to(&source).unwrap(); - let target = crate::rules::AggregateReductionResult::target_problem(&reduction); - for mask in 0usize..(1 << target.num_edges()) { - let config: Vec<_> = (0..target.num_edges()) + ReduceTo::>>::reduce_to(&source).unwrap(); + let target = crate::rules::ReductionResult::target_problem(&reduction); + for mask in 0usize..(1 << target.inner().num_edges()) { + let config: Vec<_> = (0..target.inner().num_edges()) .map(|i| (mask >> i) & 1 == 1) .collect(); - let value = target.evaluate(&config).unwrap(); + let value = target.inner().evaluate(&config).unwrap(); let certifies = value.0 == Some(n as i64); if certifies { - let order = reduction.extract_solution(&config).unwrap(); + let order = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), config.clone()) + .unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!(source.evaluate(&order).unwrap().0); } } - assert!( - !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![false; target.num_edges() + 1]), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&reduction, value); value.is_valid() }) - ); + assert!(matches!( + ReductionResult::target_problem(&reduction) + .inner() + .evaluate(&vec![false; target.inner().num_edges() + 1]), + Err(InvalidConfiguration(_)) + )); } } } diff --git a/src/unit_tests/rules/hamiltoniancircuit_quadraticassignment.rs b/src/unit_tests/rules/hamiltoniancircuit_quadraticassignment.rs index 4abdae217..35087ad21 100644 --- a/src/unit_tests/rules/hamiltoniancircuit_quadraticassignment.rs +++ b/src/unit_tests/rules/hamiltoniancircuit_quadraticassignment.rs @@ -1,11 +1,13 @@ use crate::models::algebraic::QuadraticAssignment; +use crate::models::decision::Decision; use crate::models::graph::HamiltonianCircuit; -use crate::rules::test_helpers::assert_satisfaction_round_trip_from_optimization_target; +use crate::rules::test_helpers::assert_satisfaction_round_trip_from_satisfaction_target; use crate::rules::ReduceTo; use crate::rules::ReductionResult; use crate::solvers::BruteForce; +use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; -use crate::types::Min; +use crate::types::OptimizationValue; use crate::Problem; fn cycle4_hc() -> HamiltonianCircuit { @@ -15,10 +17,10 @@ fn cycle4_hc() -> HamiltonianCircuit { #[test] fn test_hamiltoniancircuit_to_quadraticassignment_closed_loop() { let source = cycle4_hc(); - let reduction = - ReduceTo::::reduce_to(&source).expect("reduction should succeed"); + let reduction = ReduceTo::>::reduce_to(&source) + .expect("reduction should succeed"); - assert_satisfaction_round_trip_from_optimization_target( + assert_satisfaction_round_trip_from_satisfaction_target( &source, &reduction, "HamiltonianCircuit -> QuadraticAssignment", @@ -28,15 +30,15 @@ fn test_hamiltoniancircuit_to_quadraticassignment_closed_loop() { #[test] fn test_hamiltoniancircuit_to_quadraticassignment_structure() { let source = cycle4_hc(); - let reduction = - ReduceTo::::reduce_to(&source).expect("reduction should succeed"); + let reduction = ReduceTo::>::reduce_to(&source) + .expect("reduction should succeed"); let target = reduction.target_problem(); - assert_eq!(target.num_facilities(), 4); - assert_eq!(target.num_locations(), 4); + assert_eq!(target.inner().num_facilities(), 4); + assert_eq!(target.inner().num_locations(), 4); // Cost matrix: cycle adjacency on positions - let cost = target.cost_matrix(); + let cost = target.inner().cost_matrix(); for (i, cost_row) in cost.iter().enumerate() { for (j, &cost_val) in cost_row.iter().enumerate() { let expected = if j == (i + 1) % 4 { 1 } else { 0 }; @@ -45,7 +47,7 @@ fn test_hamiltoniancircuit_to_quadraticassignment_structure() { } // Distance matrix: edges and diagonal cost zero, non-edges cost one. - let dist = target.distance_matrix(); + let dist = target.inner().distance_matrix(); for (k, dist_row) in dist.iter().enumerate() { for (l, &dist_val) in dist_row.iter().enumerate() { let expected = i64::from(k != l && !source.graph().has_edge(k, l)); @@ -57,8 +59,8 @@ fn test_hamiltoniancircuit_to_quadraticassignment_structure() { #[test] fn test_hamiltoniancircuit_to_quadraticassignment_optimal_cost_is_zero() { let source = cycle4_hc(); - let reduction = - ReduceTo::::reduce_to(&source).expect("reduction should succeed"); + let reduction = ReduceTo::>::reduce_to(&source) + .expect("reduction should succeed"); let target = reduction.target_problem(); // The identity permutation [0,1,2,3] is a valid HC on a 4-cycle, @@ -67,23 +69,28 @@ fn test_hamiltoniancircuit_to_quadraticassignment_optimal_cost_is_zero() { .solve(target) .unwrap() .expect("QAP should have an optimal solution"); - let value = target.evaluate(&best).unwrap(); - assert_eq!(value, Min(Some(0)), "optimal QAP cost should be zero"); + let value = target.inner().evaluate(&best).unwrap(); + assert_eq!( + value, + crate::types::Min(Some(0)), + "optimal QAP cost should be zero" + ); } #[test] fn test_hamiltoniancircuit_to_quadraticassignment_nonhamiltonian_cost_gap() { // Star graph on 4 vertices has no Hamiltonian circuit let source = HamiltonianCircuit::new(SimpleGraph::star(4)); - let reduction = - ReduceTo::::reduce_to(&source).expect("reduction should succeed"); + let reduction = ReduceTo::>::reduce_to(&source) + .expect("reduction should succeed"); let target = reduction.target_problem(); + assert!(BruteForce::new().solve(target).unwrap().is_none()); let best = BruteForce::new() - .solve(target) + .solve(target.inner()) .unwrap() .expect("QAP always has a solution"); - let value = target.evaluate(&best).unwrap(); + let value = target.inner().evaluate(&best).unwrap(); assert!( value.is_valid(), "QAP solution should have a valid objective" @@ -98,12 +105,19 @@ fn test_hamiltoniancircuit_to_quadraticassignment_nonhamiltonian_cost_gap() { #[test] fn test_hamiltoniancircuit_to_quadraticassignment_extract_solution() { let source = cycle4_hc(); - let reduction = - ReduceTo::::reduce_to(&source).expect("reduction should succeed"); + let reduction = ReduceTo::>::reduce_to(&source) + .expect("reduction should succeed"); // Permutation [0,1,2,3] visits 0->1->2->3->0 on cycle4 let target_config = vec![0, 1, 2, 3]; - let extracted = reduction.extract_solution(&target_config).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), target_config.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted, vec![0, 1, 2, 3]); assert!( source.evaluate(&extracted).unwrap().0, @@ -131,14 +145,28 @@ fn test_prism_graph_hc_via_qap_ilp_roundtrip() { let hc = HamiltonianCircuit::new(SimpleGraph::new(6, edges)); // HC → QAP → ILP → solve → extract back - let r1 = ReduceTo::::reduce_to(&hc).expect("reduction should succeed"); - let r2 = - ReduceTo::>::reduce_to(r1.target_problem()).expect("reduction should succeed"); + let r1 = ReduceTo::>::reduce_to(&hc) + .expect("reduction should succeed"); + let r2 = ReduceTo::>::reduce_to(r1.target_problem().inner()) + .expect("reduction should succeed"); let ilp_sol = ILPSolver::new() .solve(r2.target_problem()) .expect("ILP should be feasible"); - let qap_sol = r2.extract_solution(&ilp_sol).unwrap(); - let hc_sol = r1.extract_solution(&qap_sol).unwrap(); + let qap_sol = r2 + .recover_result( + r1.target_problem().inner(), + SolveOutcome::optimal(r2.target_problem(), ilp_sol).unwrap(), + ) + .map(|outcome| outcome.into_solution().unwrap()) + .unwrap(); + let hc_sol = r1 + .recover_result( + &hc, + SolveOutcome::optimal(r1.target_problem(), qap_sol.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!( hc.evaluate(&hc_sol).unwrap().0, @@ -159,23 +187,31 @@ fn test_hamiltoniancircuit_to_quadraticassignment_small_graphs_are_no() { ] { let source = HamiltonianCircuit::new(SimpleGraph::new(n, edges)); assert!(!source.evaluate(&(0..n).collect()).unwrap().0); - let reduction = ReduceTo::::reduce_to(&source).unwrap(); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); let target = reduction.target_problem(); - assert_eq!(target.num_facilities(), 3); - assert_eq!(target.num_locations(), 3); - let best = BruteForce::new().solve(target).unwrap().unwrap(); - let value = target.evaluate(&best).unwrap(); - assert_eq!(value, Min(Some(3))); - assert!(!crate::rules::AggregateReductionResult::extract_value(&reduction, value).0); + assert!(BruteForce::new().solve(target).unwrap().is_none()); + assert_eq!(target.inner().num_facilities(), 3); + assert_eq!(target.inner().num_locations(), 3); + let best = BruteForce::new().solve(target.inner()).unwrap().unwrap(); + let value = target.inner().evaluate(&best).unwrap(); + assert_eq!(value, crate::types::Min(Some(3))); assert!( - !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &best), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&reduction, value); value.is_valid() }) + !crate::types::Or(OptimizationValue::meets_bound( + &(value), + crate::rules::ReductionResult::target_problem(&reduction).bound() + )) + .0 ); + assert!(!ReductionResult::target_problem(&reduction) + .evaluate(&best) + .unwrap() + .is_valid()); } } #[test] fn test_hamiltoniancircuit_to_quadraticassignment_rejects_invalid_certificates() { - let reduction = ReduceTo::::reduce_to(&cycle4_hc()).unwrap(); + let reduction = ReduceTo::>::reduce_to(&cycle4_hc()).unwrap(); for config in [ vec![], vec![0, 1, 2], @@ -183,13 +219,27 @@ fn test_hamiltoniancircuit_to_quadraticassignment_rejects_invalid_certificates() vec![0, 0, 1, 2], vec![0, 2, 1, 3], ] { - assert!(!matches!(reduction.target_problem().evaluate(&config), - Ok(value) if crate::rules::AggregateReductionResult::extract_value(&reduction, value).0)); + assert!( + !matches!(reduction.target_problem().inner().evaluate(&config), + Ok(value) if crate::types::Or(OptimizationValue::meets_bound(&(value), crate::rules::ReductionResult::target_problem(&reduction).bound())).0) + ); } - for value in [Min(None), Min(Some(-1)), Min(Some(1))] { - assert!(!crate::rules::AggregateReductionResult::extract_value(&reduction, value).0); + for value in [crate::types::Min(None), crate::types::Min(Some(1))] { + assert!( + !crate::types::Or(OptimizationValue::meets_bound( + &(value), + crate::rules::ReductionResult::target_problem(&reduction).bound() + )) + .0 + ); } - assert!(crate::rules::AggregateReductionResult::extract_value(&reduction, Min(Some(0))).0); + assert!( + crate::types::Or(OptimizationValue::meets_bound( + &(crate::types::Min(Some(0))), + crate::rules::ReductionResult::target_problem(&reduction).bound() + )) + .0 + ); } #[test] @@ -209,7 +259,7 @@ fn test_hamiltoniancircuit_to_quadraticassignment_all_small_graphs_and_orders() edges.extend((0..n).map(|v| (v, v))); edges.extend(edges.clone()); let source = HamiltonianCircuit::new(SimpleGraph::new(n, edges)); - let reduction = ReduceTo::::reduce_to(&source).unwrap(); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); for mut encoded in 0..n.pow(u32::try_from(n).unwrap()) { let order: Vec<_> = (0..n) .map(|_| { @@ -224,27 +274,46 @@ fn test_hamiltoniancircuit_to_quadraticassignment_all_small_graphs_and_orders() .any(|(i, v)| order[..i].contains(v)) { assert_eq!( - reduction.target_problem().evaluate(&order).unwrap(), - Min(None) + reduction.target_problem().inner().evaluate(&order).unwrap(), + crate::types::Min(None) ); continue; } let missing = (0..n) .filter(|&i| !source.graph().has_edge(order[i], order[(i + 1) % n])) .count(); - let value = reduction.target_problem().evaluate(&order).unwrap(); - assert_eq!(value, Min(Some(i64::try_from(missing).unwrap()))); + let value = reduction.target_problem().inner().evaluate(&order).unwrap(); + assert_eq!( + value, + crate::types::Min(Some(i64::try_from(missing).unwrap())) + ); let expected = source.evaluate(&order).unwrap().0; assert_eq!( - crate::rules::AggregateReductionResult::extract_value(&reduction, value).0, + crate::types::Or(OptimizationValue::meets_bound( + &(value), + crate::rules::ReductionResult::target_problem(&reduction).bound() + )) + .0, expected ); if expected { - assert_eq!(reduction.extract_solution(&order).unwrap(), order); - } else { - assert!( - !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &order), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&reduction, value); value.is_valid() }) + assert_eq!( + reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), order.clone()) + .unwrap() + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"), + order ); + } else { + assert!(!ReductionResult::target_problem(&reduction) + .evaluate(&order) + .unwrap() + .is_valid()); } } } @@ -254,7 +323,6 @@ fn test_hamiltoniancircuit_to_quadraticassignment_all_small_graphs_and_orders() #[test] fn test_hamiltoniancircuit_to_quadraticassignment_registered_aggregate_path() { use crate::rules::{ReductionGraph, ReductionPath, ReductionStep}; - use crate::types::Or; let graph = ReductionGraph::new(); let path = ReductionPath { @@ -267,7 +335,7 @@ fn test_hamiltoniancircuit_to_quadraticassignment_registered_aggregate_path() { .collect(), }, ReductionStep { - name: QuadraticAssignment::NAME.to_string(), + name: Decision::::NAME.to_string(), variant: Default::default(), }, ], @@ -280,17 +348,24 @@ fn test_hamiltoniancircuit_to_quadraticassignment_registered_aggregate_path() { false, ), ] { - let chain = graph - .reduce_aggregate_along_path(&path, &source) - .unwrap() - .unwrap(); - let target = chain.target_problem::(); - let best = BruteForce::new().solve(target).unwrap().unwrap(); + let chain = graph.reduce_along_path(&path, &source).unwrap().unwrap(); + let target = chain.target_problem::>(); + let best = BruteForce::new().solve(target.inner()).unwrap().unwrap(); let optimum = target.evaluate(&best).unwrap(); - assert_eq!( - chain.extract_value_dyn(serde_json::to_value(optimum).unwrap()), - serde_json::to_value(Or(expected)).unwrap(), - ); + let outcome = if optimum.0 { + SolveOutcome::optimal(target, best).unwrap() + } else { + SolveOutcome::Infeasible + }; + let recovered = chain + .recover_result::, Decision>( + &source, outcome, + ) + .unwrap(); + assert_eq!(recovered.solution().is_some(), expected); + if let Some(witness) = recovered.solution() { + assert_eq!(source.evaluate(witness).unwrap(), crate::types::Or(true)); + } assert_eq!( BruteForce::new().solve(&source).unwrap().is_some(), expected diff --git a/src/unit_tests/rules/hamiltoniancircuit_ruralpostman.rs b/src/unit_tests/rules/hamiltoniancircuit_ruralpostman.rs index 1e7227fe9..00c77d90a 100644 --- a/src/unit_tests/rules/hamiltoniancircuit_ruralpostman.rs +++ b/src/unit_tests/rules/hamiltoniancircuit_ruralpostman.rs @@ -1,10 +1,11 @@ +use crate::models::decision::Decision; use crate::models::graph::{HamiltonianCircuit, RuralPostman}; -use crate::rules::test_helpers::assert_satisfaction_round_trip_from_optimization_target; +use crate::rules::test_helpers::assert_satisfaction_round_trip_from_satisfaction_target; use crate::rules::ReduceTo; use crate::rules::ReductionResult; use crate::solvers::BruteForce; +use crate::solvers::SolveOutcome; use crate::topology::SimpleGraph; -use crate::types::Min; use crate::Problem; fn triangle_hc() -> HamiltonianCircuit { @@ -18,10 +19,10 @@ fn cycle4_hc() -> HamiltonianCircuit { #[test] fn test_hamiltoniancircuit_to_ruralpostman_closed_loop() { let source = triangle_hc(); - let reduction = ReduceTo::>::reduce_to(&source) + let reduction = ReduceTo::>>::reduce_to(&source) .expect("reduction should succeed"); - assert_satisfaction_round_trip_from_optimization_target( + assert_satisfaction_round_trip_from_satisfaction_target( &source, &reduction, "HamiltonianCircuit -> RuralPostman (triangle)", @@ -31,10 +32,10 @@ fn test_hamiltoniancircuit_to_ruralpostman_closed_loop() { #[test] fn test_hamiltoniancircuit_to_ruralpostman_closed_loop_cycle4() { let source = cycle4_hc(); - let reduction = ReduceTo::>::reduce_to(&source) + let reduction = ReduceTo::>>::reduce_to(&source) .expect("reduction should succeed"); - assert_satisfaction_round_trip_from_optimization_target( + assert_satisfaction_round_trip_from_satisfaction_target( &source, &reduction, "HamiltonianCircuit -> RuralPostman (cycle4)", @@ -44,19 +45,19 @@ fn test_hamiltoniancircuit_to_ruralpostman_closed_loop_cycle4() { #[test] fn test_hamiltoniancircuit_to_ruralpostman_structure() { let source = triangle_hc(); - let reduction = ReduceTo::>::reduce_to(&source) + let reduction = ReduceTo::>>::reduce_to(&source) .expect("reduction should succeed"); let target = reduction.target_problem(); // 3 vertices -> 6 vertices - assert_eq!(target.num_vertices(), 6); + assert_eq!(target.inner().num_vertices(), 6); // 3 required edges + 2*3 connectivity edges = 9 - assert_eq!(target.num_edges(), 9); + assert_eq!(target.inner().num_edges(), 9); // 3 required edges (one per vertex) - assert_eq!(target.num_required_edges(), 3); + assert_eq!(target.inner().num_required_edges(), 3); // All edges have weight 1 - let weights = target.edge_lengths(); + let weights = target.inner().edge_lengths(); for (i, &w) in weights.iter().enumerate() { assert_eq!(w, 1, "edge {i} should have weight 1"); } @@ -65,23 +66,23 @@ fn test_hamiltoniancircuit_to_ruralpostman_structure() { #[test] fn test_hamiltoniancircuit_to_ruralpostman_structure_cycle4() { let source = cycle4_hc(); - let reduction = ReduceTo::>::reduce_to(&source) + let reduction = ReduceTo::>>::reduce_to(&source) .expect("reduction should succeed"); let target = reduction.target_problem(); // 4 vertices -> 8 vertices - assert_eq!(target.num_vertices(), 8); + assert_eq!(target.inner().num_vertices(), 8); // 4 required edges + 2*4 connectivity edges = 12 - assert_eq!(target.num_edges(), 12); + assert_eq!(target.inner().num_edges(), 12); // 4 required edges - assert_eq!(target.num_required_edges(), 4); + assert_eq!(target.inner().num_required_edges(), 4); } #[test] fn test_hamiltoniancircuit_to_ruralpostman_optimal_cost() { // Triangle has a Hamiltonian circuit, so optimal RPP cost should be 2n = 6 let source = triangle_hc(); - let reduction = ReduceTo::>::reduce_to(&source) + let reduction = ReduceTo::>>::reduce_to(&source) .expect("reduction should succeed"); let target = reduction.target_problem(); let best = BruteForce::new() @@ -89,8 +90,12 @@ fn test_hamiltoniancircuit_to_ruralpostman_optimal_cost() { .unwrap() .expect("should find a solution"); - let metric = target.evaluate(&best).unwrap(); - assert_eq!(metric, Min(Some(6)), "optimal cost should be 2n=6"); + let metric = target.inner().evaluate(&best).unwrap(); + assert_eq!( + metric, + crate::types::Min(Some(6)), + "optimal cost should be 2n=6" + ); } #[test] @@ -99,7 +104,7 @@ fn test_hamiltoniancircuit_to_ruralpostman_nonhamiltonian_cost_gap() { let source = HamiltonianCircuit::new(SimpleGraph::star(4)); let n = source.num_vertices(); assert_eq!(n, 4); - let reduction = ReduceTo::>::reduce_to(&source) + let reduction = ReduceTo::>>::reduce_to(&source) .expect("reduction should succeed"); let target = reduction.target_problem(); @@ -110,7 +115,7 @@ fn test_hamiltoniancircuit_to_ruralpostman_nonhamiltonian_cost_gap() { // The RPP optimal cost should exceed 2n = 8 let best = BruteForce::new().solve(target).unwrap(); if let Some(config) = best { - let metric = target.evaluate(&config).unwrap(); + let metric = target.inner().evaluate(&config).unwrap(); assert!( metric.is_valid(), "best RPP solution should be a valid circuit" @@ -127,7 +132,7 @@ fn test_hamiltoniancircuit_to_ruralpostman_nonhamiltonian_cost_gap() { #[test] fn test_hamiltoniancircuit_to_ruralpostman_extract_solution() { let source = triangle_hc(); - let reduction = ReduceTo::>::reduce_to(&source) + let reduction = ReduceTo::>>::reduce_to(&source) .expect("reduction should succeed"); let target = reduction.target_problem(); @@ -136,7 +141,14 @@ fn test_hamiltoniancircuit_to_ruralpostman_extract_solution() { .unwrap() .expect("should find a solution"); - let extracted = reduction.extract_solution(&best).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), best.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!( extracted.len(), 3, @@ -155,20 +167,30 @@ fn aggregate_distinguishes_hamiltonian_tour_cost() { (vec![(0, 1), (1, 2)], false), ] { let source = HamiltonianCircuit::new(SimpleGraph::new(3, edges)); - let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + let reduction = + ReduceTo::>>::reduce_to(&source).unwrap(); let target = reduction.target_problem(); - let solution = crate::solvers::ILPSolver::new().solve(target).unwrap(); + let solution = crate::solvers::ILPSolver::new() + .solve(target.inner()) + .unwrap(); assert_eq!( - crate::rules::AggregateReductionResult::extract_value( - &reduction, - target.evaluate(&solution).unwrap() - ), + target.evaluate(&solution).unwrap(), crate::types::Or(expected) ); if expected { assert!( source - .evaluate(&reduction.extract_solution(&solution).unwrap()) + .evaluate( + &reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), solution.clone()) + .unwrap() + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution") + ) .unwrap() .0 ); diff --git a/src/unit_tests/rules/hamiltoniancircuit_stackercrane.rs b/src/unit_tests/rules/hamiltoniancircuit_stackercrane.rs index d11e33732..56400be9d 100644 --- a/src/unit_tests/rules/hamiltoniancircuit_stackercrane.rs +++ b/src/unit_tests/rules/hamiltoniancircuit_stackercrane.rs @@ -1,11 +1,14 @@ +use crate::models::decision::Decision; use crate::models::graph::HamiltonianCircuit; use crate::models::misc::StackerCrane; -use crate::rules::test_helpers::assert_satisfaction_round_trip_from_optimization_target; +use crate::rules::test_helpers::assert_satisfaction_round_trip_from_satisfaction_target; use crate::rules::ReduceTo; use crate::rules::ReductionResult; use crate::solvers::BruteForce; +use crate::solvers::SolveOutcome; use crate::topology::SimpleGraph; -use crate::types::Min; +use crate::traits::EvaluationError::InvalidConfiguration; +use crate::types::OptimizationValue; use crate::Problem; fn cycle4_hc() -> HamiltonianCircuit { @@ -15,9 +18,10 @@ fn cycle4_hc() -> HamiltonianCircuit { #[test] fn test_hamiltoniancircuit_to_stackercrane_closed_loop() { let source = cycle4_hc(); - let reduction = ReduceTo::::reduce_to(&source).expect("reduction should succeed"); + let reduction = + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); - assert_satisfaction_round_trip_from_optimization_target( + assert_satisfaction_round_trip_from_satisfaction_target( &source, &reduction, "HamiltonianCircuit -> StackerCrane", @@ -27,22 +31,23 @@ fn test_hamiltoniancircuit_to_stackercrane_closed_loop() { #[test] fn test_hamiltoniancircuit_to_stackercrane_structure() { let source = cycle4_hc(); - let reduction = ReduceTo::::reduce_to(&source).expect("reduction should succeed"); + let reduction = + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let target = reduction.target_problem(); // 4 vertices -> 8 target vertices (2 per original vertex) - assert_eq!(target.num_vertices(), 8); + assert_eq!(target.inner().num_vertices(), 8); // 4 arcs (one per original vertex) - assert_eq!(target.num_arcs(), 4); + assert_eq!(target.inner().num_arcs(), 4); // 4 original edges -> 8 undirected connector edges - assert_eq!(target.num_edges(), 8); + assert_eq!(target.inner().num_edges(), 8); // All arcs have length 1 - for &len in target.arc_lengths() { + for &len in target.inner().arc_lengths() { assert_eq!(len, 1); } // All connector edges have length 1 - for &len in target.edge_lengths() { + for &len in target.inner().edge_lengths() { assert_eq!(len, 1); } } @@ -51,15 +56,16 @@ fn test_hamiltoniancircuit_to_stackercrane_structure() { fn test_hamiltoniancircuit_to_stackercrane_optimal_cost() { // A 4-cycle has a Hamiltonian circuit; optimal StackerCrane cost = 2n = 8. let source = cycle4_hc(); - let reduction = ReduceTo::::reduce_to(&source).expect("reduction should succeed"); + let reduction = + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let target = reduction.target_problem(); let witness = BruteForce::new() .solve(target) .unwrap() .expect("target should have a solution"); - let cost = target.evaluate(&witness).unwrap(); - assert_eq!(cost, Min(Some(8))); + let cost = target.inner().evaluate(&witness).unwrap(); + assert_eq!(cost, crate::types::Min(Some(8))); } #[test] @@ -67,13 +73,14 @@ fn test_hamiltoniancircuit_to_stackercrane_non_hamiltonian() { // Star graph on 4 vertices: no Hamiltonian circuit. // The optimal StackerCrane cost should exceed 2n = 8. let source = HamiltonianCircuit::new(SimpleGraph::star(4)); - let reduction = ReduceTo::::reduce_to(&source).expect("reduction should succeed"); + let reduction = + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let target = reduction.target_problem(); let witness = BruteForce::new().solve(target).unwrap(); match witness { Some(w) => { - let cost = target.evaluate(&w).unwrap(); + let cost = target.inner().evaluate(&w).unwrap(); assert!( cost.0.unwrap() > 8, "non-Hamiltonian graph should have cost > 2n" @@ -88,12 +95,20 @@ fn test_hamiltoniancircuit_to_stackercrane_non_hamiltonian() { #[test] fn test_hamiltoniancircuit_to_stackercrane_extract_solution() { let source = cycle4_hc(); - let reduction = ReduceTo::::reduce_to(&source).expect("reduction should succeed"); + let reduction = + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); // The identity permutation [0, 1, 2, 3] traverses arcs in order, // corresponding to vertex order 0, 1, 2, 3 in the original graph. let target_config = vec![0, 1, 2, 3]; - let extracted = reduction.extract_solution(&target_config).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), target_config.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted, vec![0, 1, 2, 3]); assert!( source.evaluate(&extracted).unwrap().0, @@ -118,9 +133,10 @@ fn test_hamiltoniancircuit_to_stackercrane_prism_graph() { (2, 5), ]; let source = HamiltonianCircuit::new(SimpleGraph::new(6, edges)); - let reduction = ReduceTo::::reduce_to(&source).expect("reduction should succeed"); + let reduction = + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); - assert_satisfaction_round_trip_from_optimization_target( + assert_satisfaction_round_trip_from_satisfaction_target( &source, &reduction, "HamiltonianCircuit -> StackerCrane (prism graph)", @@ -140,8 +156,8 @@ fn test_stackercrane_certificate_for_all_small_configurations() { .filter_map(|(i, &e)| ((mask >> i) & 1 == 1).then_some(e)) .collect(); let source = HamiltonianCircuit::new(SimpleGraph::new(n, edges)); - let reduction = ReduceTo::::reduce_to(&source).unwrap(); - let target = crate::rules::AggregateReductionResult::target_problem(&reduction); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + let target = crate::rules::ReductionResult::target_problem(&reduction); // All coordinate configurations, including repeated arc indices. for mut code in 0..n.pow(n as u32) { let config: Vec<_> = (0..n) @@ -152,23 +168,41 @@ fn test_stackercrane_certificate_for_all_small_configurations() { }) .collect(); let expected = source.evaluate(&config).unwrap().0; - let value = target.evaluate(&config).unwrap(); + let value = target.inner().evaluate(&config).unwrap(); assert_eq!( - crate::rules::AggregateReductionResult::extract_value(&reduction, value).0, + crate::types::Or(OptimizationValue::meets_bound( + &(value), + crate::rules::ReductionResult::target_problem(&reduction).bound() + )) + .0, expected ); if expected { - let order = reduction.extract_solution(&config).unwrap(); + let order = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), config.clone()) + .unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!(source.evaluate(&order).unwrap().0); } } - assert!( - !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![0; n + 1]), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&reduction, value); value.is_valid() }) - ); + assert!(matches!( + ReductionResult::target_problem(&reduction) + .inner() + .evaluate(&vec![0; n + 1]), + Err(InvalidConfiguration(_)) + )); if n > 0 { - assert!( - !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![n; n]), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&reduction, value); value.is_valid() }) - ); + assert!(matches!( + ReductionResult::target_problem(&reduction) + .inner() + .evaluate(&vec![n; n]), + Err(InvalidConfiguration(_)) + )); } } } diff --git a/src/unit_tests/rules/hamiltoniancircuit_strongconnectivityaugmentation.rs b/src/unit_tests/rules/hamiltoniancircuit_strongconnectivityaugmentation.rs index f81fd7003..8958c681d 100644 --- a/src/unit_tests/rules/hamiltoniancircuit_strongconnectivityaugmentation.rs +++ b/src/unit_tests/rules/hamiltoniancircuit_strongconnectivityaugmentation.rs @@ -3,6 +3,7 @@ use crate::rules::test_helpers::assert_satisfaction_round_trip_from_satisfaction use crate::rules::ReduceTo; use crate::rules::ReductionResult; use crate::solvers::BruteForce; +use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; use crate::Problem; @@ -90,7 +91,14 @@ fn test_hamiltoniancircuit_to_strongconnectivityaugmentation_extract_solution() assert!(target.is_valid_solution(&target_config).unwrap()); - let extracted = reduction.extract_solution(&target_config).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), target_config.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted.len(), 4); assert!( source.evaluate(&extracted).unwrap().is_valid(), diff --git a/src/unit_tests/rules/hamiltoniancircuit_travelingsalesman.rs b/src/unit_tests/rules/hamiltoniancircuit_travelingsalesman.rs index 4eecf4f08..606ccbc87 100644 --- a/src/unit_tests/rules/hamiltoniancircuit_travelingsalesman.rs +++ b/src/unit_tests/rules/hamiltoniancircuit_travelingsalesman.rs @@ -3,6 +3,7 @@ use crate::rules::test_helpers::assert_satisfaction_round_trip_from_optimization use crate::rules::ReduceTo; use crate::rules::ReductionResult; use crate::solvers::BruteForce; +use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; use crate::types::Min; use crate::Problem; @@ -70,7 +71,14 @@ fn test_hamiltoniancircuit_to_travelingsalesman_extract_solution_cycle() { .map(|(u, v)| cycle_edges.contains(&(u, v)) || cycle_edges.contains(&(v, u))) .collect(); - let extracted = reduction.extract_solution(&target_solution).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), target_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(target.evaluate(&target_solution).unwrap(), Min(Some(4))); assert_eq!(extracted.len(), 4); diff --git a/src/unit_tests/rules/hamiltonianpath_degreeconstrainedspanningtree.rs b/src/unit_tests/rules/hamiltonianpath_degreeconstrainedspanningtree.rs index 06fc3a4a9..c2f530637 100644 --- a/src/unit_tests/rules/hamiltonianpath_degreeconstrainedspanningtree.rs +++ b/src/unit_tests/rules/hamiltonianpath_degreeconstrainedspanningtree.rs @@ -1,6 +1,7 @@ use crate::models::graph::{DegreeConstrainedSpanningTree, HamiltonianPath}; use crate::rules::test_helpers::assert_satisfaction_round_trip_from_satisfaction_target; use crate::rules::{ReduceTo, ReductionResult}; +use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; use crate::traits::Problem; @@ -52,7 +53,14 @@ fn test_hamiltonianpath_to_degreeconstrainedspanningtree_extract_solution_recons &[(0, 1), (1, 2), (2, 3)], ); - let extracted = reduction.extract_solution(&target_solution).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), target_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted, vec![0, 1, 2, 3]); assert!(source.evaluate(&extracted).unwrap()); diff --git a/src/unit_tests/rules/hamiltonianpath_ilp.rs b/src/unit_tests/rules/hamiltonianpath_ilp.rs index e9bd5b743..0c84c65f5 100644 --- a/src/unit_tests/rules/hamiltonianpath_ilp.rs +++ b/src/unit_tests/rules/hamiltonianpath_ilp.rs @@ -1,4 +1,5 @@ use super::*; +use crate::solvers::SolveOutcome; use crate::solvers::{BruteForce, ILPSolver}; use crate::topology::SimpleGraph; use crate::traits::Problem; @@ -36,7 +37,14 @@ fn test_hamiltonianpath_to_ilp_closed_loop() { let ilp_solution = ilp_solver .solve(reduction.target_problem()) .expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!( problem.evaluate(&extracted).unwrap(), Or(true), @@ -63,7 +71,14 @@ fn test_hamiltonianpath_to_ilp_cycle_graph() { let ilp_solution = ilp_solver .solve(reduction.target_problem()) .expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(problem.evaluate(&extracted).unwrap(), Or(true)); } @@ -99,6 +114,13 @@ fn test_solution_extraction() { let ilp_solution = ilp_solver .solve(reduction.target_problem()) .expect("solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(problem.evaluate(&extracted).unwrap(), Or(true)); } diff --git a/src/unit_tests/rules/hamiltonianpath_isomorphicspanningtree.rs b/src/unit_tests/rules/hamiltonianpath_isomorphicspanningtree.rs index f0503e307..583199a44 100644 --- a/src/unit_tests/rules/hamiltonianpath_isomorphicspanningtree.rs +++ b/src/unit_tests/rules/hamiltonianpath_isomorphicspanningtree.rs @@ -3,6 +3,7 @@ use crate::models::graph::{HamiltonianPath, IsomorphicSpanningTree}; use crate::rules::test_helpers::assert_satisfaction_round_trip_from_satisfaction_target; use crate::rules::ReduceTo; use crate::solvers::BruteForce; +use crate::solvers::SolveOutcome; use crate::topology::SimpleGraph; use crate::traits::Problem; @@ -87,7 +88,14 @@ fn test_hamiltonianpath_to_isomorphicspanningtree_complete_graph() { .solve(result.target_problem()) .unwrap() .expect("K4 should have an IST solution"); - let extracted = result.extract_solution(&target_solution).unwrap(); + let extracted = result + .recover_result( + &source, + SolveOutcome::optimal(result.target_problem(), target_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); // Extracted solution should be a valid Hamiltonian path assert!( source.evaluate(&extracted).unwrap().0, diff --git a/src/unit_tests/rules/hamiltonianpathbetweentwovertices_longestpath.rs b/src/unit_tests/rules/hamiltonianpathbetweentwovertices_longestpath.rs index c73cb3f95..102f804be 100644 --- a/src/unit_tests/rules/hamiltonianpathbetweentwovertices_longestpath.rs +++ b/src/unit_tests/rules/hamiltonianpathbetweentwovertices_longestpath.rs @@ -1,10 +1,14 @@ -use super::*; +use crate::models::decision::Decision; use crate::models::graph::{HamiltonianPathBetweenTwoVertices, LongestPath}; -use crate::rules::test_helpers::assert_satisfaction_round_trip_from_optimization_target; +use crate::rules::test_helpers::assert_satisfaction_round_trip_from_satisfaction_target; use crate::rules::ReduceTo; +use crate::rules::ReductionResult; use crate::solvers::BruteForce; +use crate::solvers::SolveOutcome; use crate::topology::SimpleGraph; +use crate::traits::EvaluationError::InvalidConfiguration; use crate::types::One; +use crate::types::OptimizationValue; #[test] fn test_hamiltonianpathbetweentwovertices_to_longestpath_closed_loop() { @@ -14,16 +18,16 @@ fn test_hamiltonianpathbetweentwovertices_to_longestpath_closed_loop() { 0, 4, ); - let result = ReduceTo::>::reduce_to(&source) + let result = ReduceTo::>>::reduce_to(&source) .expect("reduction should succeed"); let target = result.target_problem(); - assert_eq!(target.num_vertices(), 5); - assert_eq!(target.num_edges(), 6); - assert_eq!(target.source_vertex(), 0); - assert_eq!(target.target_vertex(), 4); + assert_eq!(target.inner().num_vertices(), 5); + assert_eq!(target.inner().num_edges(), 6); + assert_eq!(target.inner().source_vertex(), 0); + assert_eq!(target.inner().target_vertex(), 4); - assert_satisfaction_round_trip_from_optimization_target( + assert_satisfaction_round_trip_from_satisfaction_target( &source, &result, "HamiltonianPathBetweenTwoVertices->LongestPath closed loop", @@ -38,10 +42,10 @@ fn test_hamiltonianpathbetweentwovertices_to_longestpath_path_graph() { 0, 3, ); - let result = ReduceTo::>::reduce_to(&source) + let result = ReduceTo::>>::reduce_to(&source) .expect("reduction should succeed"); - assert_satisfaction_round_trip_from_optimization_target( + assert_satisfaction_round_trip_from_satisfaction_target( &source, &result, "HamiltonianPathBetweenTwoVertices->LongestPath path graph", @@ -58,11 +62,12 @@ fn test_hamiltonianpathbetweentwovertices_to_longestpath_no_hamiltonian_path() { 1, 2, ); - let result = ReduceTo::>::reduce_to(&source) + let result = ReduceTo::>>::reduce_to(&source) .expect("reduction should succeed"); let solver = BruteForce::new(); + assert!(solver.solve(result.target_problem()).unwrap().is_none()); let target_best = solver - .solve(result.target_problem()) + .solve(result.target_problem().inner()) .unwrap() .expect("LongestPath should have some valid path"); @@ -82,10 +87,10 @@ fn test_hamiltonianpathbetweentwovertices_to_longestpath_complete_graph() { 0, 3, ); - let result = ReduceTo::>::reduce_to(&source) + let result = ReduceTo::>>::reduce_to(&source) .expect("reduction should succeed"); - assert_satisfaction_round_trip_from_optimization_target( + assert_satisfaction_round_trip_from_satisfaction_target( &source, &result, "HamiltonianPathBetweenTwoVertices->LongestPath complete K4", @@ -100,14 +105,14 @@ fn test_hamiltonianpathbetweentwovertices_to_longestpath_triangle() { 0, 2, ); - let result = ReduceTo::>::reduce_to(&source) + let result = ReduceTo::>>::reduce_to(&source) .expect("reduction should succeed"); let target = result.target_problem(); - assert_eq!(target.num_vertices(), 3); - assert_eq!(target.num_edges(), 3); + assert_eq!(target.inner().num_vertices(), 3); + assert_eq!(target.inner().num_edges(), 3); - assert_satisfaction_round_trip_from_optimization_target( + assert_satisfaction_round_trip_from_satisfaction_target( &source, &result, "HamiltonianPathBetweenTwoVertices->LongestPath triangle", @@ -138,28 +143,47 @@ fn test_hamiltonian_path_extraction_for_all_small_graphs_and_endpoints() { end, ); let reduction = - ReduceTo::>::reduce_to(&source).unwrap(); - let target = crate::rules::AggregateReductionResult::target_problem(&reduction); + ReduceTo::>>::reduce_to(&source) + .unwrap(); + let target = crate::rules::ReductionResult::target_problem(&reduction); for mask in 0usize..(1 << edges.len()) { let config: Vec<_> = (0..edges.len()).map(|i| (mask >> i) & 1 == 1).collect(); - let value = target.evaluate(&config).unwrap(); + let value = target.inner().evaluate(&config).unwrap(); let expected = value.0 == Some(n as i64 - 1); assert_eq!( - crate::rules::AggregateReductionResult::extract_value( - &reduction, value - ) + crate::types::Or(OptimizationValue::meets_bound( + &(value), + crate::rules::ReductionResult::target_problem(&reduction).bound() + )) .0, expected ); if expected { - let order = reduction.extract_solution(&config).unwrap(); + let order = reduction + .recover_result( + &source, + SolveOutcome::optimal( + reduction.target_problem(), + config.clone(), + ) + .unwrap(), + ) + .map(|result| { + result.into_solution().expect( + "qualifying target result must recover a source solution", + ) + }) + .unwrap(); assert!(source.evaluate(&order).unwrap().0); } } - assert!( - !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![false; edges.len() + 1]), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&reduction, value); value.is_valid() }) - ); + assert!(matches!( + ReductionResult::target_problem(&reduction) + .inner() + .evaluate(&vec![false; edges.len() + 1]), + Err(InvalidConfiguration(_)) + )); } } } diff --git a/src/unit_tests/rules/highlyconnecteddeletion_ilp.rs b/src/unit_tests/rules/highlyconnecteddeletion_ilp.rs index e56985e6c..5735ce1a6 100644 --- a/src/unit_tests/rules/highlyconnecteddeletion_ilp.rs +++ b/src/unit_tests/rules/highlyconnecteddeletion_ilp.rs @@ -2,6 +2,7 @@ use super::*; use crate::models::algebraic::{ObjectiveSense, ILP}; use crate::models::graph::HighlyConnectedDeletion; use crate::rules::test_helpers::assert_bf_vs_ilp; +use crate::solvers::SolveOutcome; use crate::topology::SimpleGraph; use crate::traits::Problem; use crate::types::Min; @@ -70,7 +71,14 @@ fn test_highlyconnecteddeletion_to_ilp_extract_solution_decode() { target_solution[3] = 1; // singleton {3} target_solution[4] = 1; // triangle {0,1,2} - let extracted = reduction.extract_solution(&target_solution).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), target_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); // Edges in input order: (0,1), (0,2), (1,2) all inside the triangle (kept); // (2,3) crosses clusters and is deleted. diff --git a/src/unit_tests/rules/ilp_bool_ilp_i64.rs b/src/unit_tests/rules/ilp_bool_ilp_i64.rs index 2c31e6393..de1bc87e7 100644 --- a/src/unit_tests/rules/ilp_bool_ilp_i64.rs +++ b/src/unit_tests/rules/ilp_bool_ilp_i64.rs @@ -1,6 +1,7 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::rules::traits::{ReduceTo, ReductionResult}; use crate::solvers::ILPSolver; +use crate::solvers::SolveOutcome; use crate::traits::Problem; #[test] @@ -30,7 +31,14 @@ fn test_ilp_bool_to_ilp_i64_closed_loop() { // Extract solution back to source and verify optimality let target_solution = ILPSolver::new().solve(target).unwrap(); - let source_solution = result.extract_solution(&target_solution).unwrap(); + let source_solution = result + .recover_result( + &source, + SolveOutcome::optimal(result.target_problem(), target_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(source.evaluate(&source_solution).unwrap(), source_obj); } diff --git a/src/unit_tests/rules/ilp_i64_ilp_bool.rs b/src/unit_tests/rules/ilp_i64_ilp_bool.rs index 027b69157..384e5d14c 100644 --- a/src/unit_tests/rules/ilp_i64_ilp_bool.rs +++ b/src/unit_tests/rules/ilp_i64_ilp_bool.rs @@ -1,6 +1,7 @@ use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::rules::traits::{ReduceTo, ReductionResult}; use crate::solvers::ILPSolver; +use crate::solvers::SolveOutcome; fn integer_ilp( bounds: &[(i64, i64)], @@ -27,7 +28,14 @@ fn solve_via_bool(source: &ILP) -> Option<(Vec, i64)> { Err(crate::solvers::ILPSolveError::Infeasible) => return None, Err(error) => panic!("ILP execution failed: {error}"), }; - let source_solution = reduction.extract_solution(&witness).unwrap(); + let source_solution = reduction + .recover_result( + source, + SolveOutcome::optimal(reduction.target_problem(), witness.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!( source.is_feasible(&source_solution).unwrap(), "decoded integer ILP solution must be feasible" diff --git a/src/unit_tests/rules/ilp_i64_ilp_f64.rs b/src/unit_tests/rules/ilp_i64_ilp_f64.rs index 5ecd98af0..af2ece291 100644 --- a/src/unit_tests/rules/ilp_i64_ilp_f64.rs +++ b/src/unit_tests/rules/ilp_i64_ilp_f64.rs @@ -2,6 +2,9 @@ use super::*; use crate::models::algebraic::{IntegerVariable, ObjectiveSense}; use crate::rules::{ReductionGraph, ReductionResult}; use crate::solvers::ILPSolver; +use crate::solvers::SolveOutcome; +use crate::traits::EvaluationError::InvalidConfiguration; +use crate::traits::Problem; use crate::types::MAX_EXACT_F64_INTEGER; #[test] @@ -25,7 +28,14 @@ fn test_ilp_i64_coefficients_to_f64_closed_loop() { ); let target_solution = ILPSolver::new().solve(reduction.target_problem()).unwrap(); assert_eq!( - reduction.extract_solution(&target_solution).unwrap(), + reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), target_solution.clone()).unwrap() + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"), vec![1, 0] ); } @@ -73,12 +83,20 @@ fn test_ilp_integer_coefficients_preserve_large_exact_constraint() { .is_feasible(&target_solution) .unwrap()); assert_eq!( - reduction.extract_solution(&target_solution).unwrap(), + reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), target_solution.clone()).unwrap() + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"), target_solution ); - assert!( - !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![]), Ok(value) if { value.is_valid() }) - ); + assert!(matches!( + ReductionResult::target_problem(&reduction).evaluate(&vec![]), + Err(InvalidConfiguration(_)) + )); } #[test] diff --git a/src/unit_tests/rules/ilp_qubo.rs b/src/unit_tests/rules/ilp_qubo.rs index 59b817492..8fe3f5c6d 100644 --- a/src/unit_tests/rules/ilp_qubo.rs +++ b/src/unit_tests/rules/ilp_qubo.rs @@ -1,7 +1,10 @@ use super::*; use crate::models::algebraic::{LinearConstraint, ObjectiveSense}; +use crate::rules::ReductionResult; use crate::solvers::BruteForce; use crate::solvers::BruteForceProblem as _; +use crate::solvers::SolveOutcome; +use crate::traits::EvaluationError::InvalidConfiguration; #[test] fn test_ilp_to_qubo_closed_loop() { @@ -25,12 +28,26 @@ fn test_ilp_to_qubo_closed_loop() { let qubo_solutions = solver.find_all_witnesses(qubo).unwrap(); for sol in &qubo_solutions { - let extracted = reduction.extract_solution(sol).unwrap(); + let extracted = reduction + .recover_result( + &ilp, + SolveOutcome::optimal(reduction.target_problem(), (sol).clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!(ilp.is_feasible(&extracted).unwrap()); } // Optimal should be [1, 0, 1] - let best = reduction.extract_solution(&qubo_solutions[0]).unwrap(); + let best = reduction + .recover_result( + &ilp, + SolveOutcome::optimal(reduction.target_problem(), qubo_solutions[0].clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(best, vec![1, 0, 1]); } @@ -53,11 +70,25 @@ fn test_ilp_to_qubo_minimize() { let qubo_solutions = solver.find_all_witnesses(qubo).unwrap(); for sol in &qubo_solutions { - let extracted = reduction.extract_solution(sol).unwrap(); + let extracted = reduction + .recover_result( + &ilp, + SolveOutcome::optimal(reduction.target_problem(), (sol).clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!(ilp.is_feasible(&extracted).unwrap()); } - let best = reduction.extract_solution(&qubo_solutions[0]).unwrap(); + let best = reduction + .recover_result( + &ilp, + SolveOutcome::optimal(reduction.target_problem(), qubo_solutions[0].clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(best, vec![1, 0, 0]); } @@ -83,7 +114,14 @@ fn test_ilp_to_qubo_equality() { assert_eq!(qubo_solutions.len(), 3); for sol in &qubo_solutions { - let extracted = reduction.extract_solution(sol).unwrap(); + let extracted = reduction + .recover_result( + &ilp, + SolveOutcome::optimal(reduction.target_problem(), (sol).clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!(ilp.is_feasible(&extracted).unwrap()); assert_eq!(extracted.iter().filter(|&&x| x == 1).count(), 2); } @@ -111,12 +149,26 @@ fn test_ilp_to_qubo_ge_with_slack() { let qubo_solutions = solver.find_all_witnesses(qubo).unwrap(); for sol in &qubo_solutions { - let extracted = reduction.extract_solution(sol).unwrap(); + let extracted = reduction + .recover_result( + &ilp, + SolveOutcome::optimal(reduction.target_problem(), (sol).clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!(ilp.is_feasible(&extracted).unwrap()); } // Optimal: exactly one variable = 1 - let best = reduction.extract_solution(&qubo_solutions[0]).unwrap(); + let best = reduction + .recover_result( + &ilp, + SolveOutcome::optimal(reduction.target_problem(), qubo_solutions[0].clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(best.iter().sum::(), 1); } @@ -142,12 +194,26 @@ fn test_ilp_to_qubo_le_with_slack() { let qubo_solutions = solver.find_all_witnesses(qubo).unwrap(); for sol in &qubo_solutions { - let extracted = reduction.extract_solution(sol).unwrap(); + let extracted = reduction + .recover_result( + &ilp, + SolveOutcome::optimal(reduction.target_problem(), (sol).clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!(ilp.is_feasible(&extracted).unwrap()); } // Optimal: exactly 2 of 3 variables = 1 (3 solutions) - let best = reduction.extract_solution(&qubo_solutions[0]).unwrap(); + let best = reduction + .recover_result( + &ilp, + SolveOutcome::optimal(reduction.target_problem(), qubo_solutions[0].clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(best.iter().sum::(), 2); } @@ -169,7 +235,6 @@ fn test_ilp_to_qubo_structure() { #[test] fn test_ilp_qubo_all_small_rows_and_target_assignments() { - use crate::rules::AggregateReductionResult; use crate::Problem; for n in 0usize..=3 { for mut code in 0..3usize.pow(n as u32) { @@ -195,7 +260,7 @@ fn test_ilp_qubo_all_small_rows_and_target_assignments() { ILP::::new(n, vec![row.clone()], objective.clone(), sense) .unwrap(); let reduction = ReduceTo::>::reduce_to(&source).unwrap(); - let target = AggregateReductionResult::target_problem(&reduction); + let target = crate::rules::ReductionResult::target_problem(&reduction); let penalty = objective.iter().map(|(_, c)| c.abs()).sum::() + rhs.abs() + 1; let constant = penalty * rhs * rhs; @@ -215,20 +280,29 @@ fn test_ilp_qubo_all_small_rows_and_target_assignments() { // Independently detect zero squared-residual penalty. let certifies = source_value.is_valid() && energy.0.unwrap() + constant == normalized_objective; - let extracted_value = - AggregateReductionResult::extract_value(&reduction, energy); + let extracted_value = reduction.map_value(energy); assert_eq!(extracted_value.is_valid(), certifies); if certifies { - let solution = reduction.extract_solution(&config).unwrap(); + let solution = reduction + .recover_result( + &source, + SolveOutcome::optimal( + reduction.target_problem(), + config.clone(), + ) + .unwrap(), + ) + .unwrap() + .into_solution() + .expect( + "qualifying target result must recover a source solution", + ); assert_eq!(source.evaluate(&solution).unwrap(), source_value); assert_eq!(extracted_value, source_value); } } let best = BruteForce::new().solve(target).unwrap().unwrap(); - let actual = AggregateReductionResult::extract_value( - &reduction, - target.evaluate(&best).unwrap(), - ); + let actual = reduction.map_value(target.evaluate(&best).unwrap()); let mut expected = match sense { ObjectiveSense::Minimize => crate::types::Extremum::minimize(None), ObjectiveSense::Maximize => crate::types::Extremum::maximize(None), @@ -243,9 +317,16 @@ fn test_ilp_qubo_all_small_rows_and_target_assignments() { .unwrap(); } assert_eq!(actual, expected); - assert!( - !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![false; target.num_vars() + 1]), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&reduction, value); value.is_valid() }) - ); + assert!(matches!( + ReductionResult::target_problem(&reduction).evaluate(&vec![ + false; + target + .num_vars( + ) + + 1 + ]), + Err(InvalidConfiguration(_)) + )); } } } @@ -255,7 +336,6 @@ fn test_ilp_qubo_all_small_rows_and_target_assignments() { #[test] fn test_ilp_qubo_inconsistent_rows_and_absent_aggregate() { - use crate::rules::AggregateReductionResult; use crate::Problem; for sense in [ObjectiveSense::Minimize, ObjectiveSense::Maximize] { let source = ILP::::new( @@ -273,25 +353,15 @@ fn test_ilp_qubo_inconsistent_rows_and_absent_aggregate() { let value = ReductionResult::target_problem(&reduction) .evaluate(&config) .unwrap(); - assert!(!AggregateReductionResult::extract_value(&reduction, value).is_valid()); - assert!( - !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &config), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&reduction, value); value.is_valid() }) - ); + assert!(!reduction.map_value(value).is_valid()); } - assert!( - !AggregateReductionResult::extract_value(&reduction, crate::types::Min(None)) - .is_valid() - ); - assert!(!AggregateReductionResult::extract_value( - &reduction, - crate::types::Min(Some(i64::MIN)) - ) - .is_valid()); - assert!(!AggregateReductionResult::extract_value( - &reduction, - crate::types::Min(Some(i64::MAX)) - ) - .is_valid()); + assert!(!reduction.map_value(crate::types::Min(None)).is_valid()); + assert!(!reduction + .map_value(crate::types::Min(Some(i64::MIN))) + .is_valid()); + assert!(!reduction + .map_value(crate::types::Min(Some(i64::MAX))) + .is_valid()); } } diff --git a/src/unit_tests/rules/integerknapsack_ilp.rs b/src/unit_tests/rules/integerknapsack_ilp.rs index 8b421114a..b08eac243 100644 --- a/src/unit_tests/rules/integerknapsack_ilp.rs +++ b/src/unit_tests/rules/integerknapsack_ilp.rs @@ -5,6 +5,7 @@ use crate::models::set::IntegerKnapsack; use crate::rules::test_helpers::assert_bf_vs_ilp; use crate::rules::{ReduceTo, ReductionResult}; use crate::solvers::ILPSolver; +use crate::solvers::SolveOutcome; #[test] fn test_integerknapsack_to_ilp_closed_loop() { @@ -16,7 +17,14 @@ fn test_integerknapsack_to_ilp_closed_loop() { let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted, vec![0, 0, 2]); } @@ -64,7 +72,14 @@ fn test_integerknapsack_to_ilp_zero_capacity() { let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("zero-capacity ILP should still be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted, vec![0, 0]); } diff --git a/src/unit_tests/rules/integralflowbundles_ilp.rs b/src/unit_tests/rules/integralflowbundles_ilp.rs index aad2e688b..7c2885d1f 100644 --- a/src/unit_tests/rules/integralflowbundles_ilp.rs +++ b/src/unit_tests/rules/integralflowbundles_ilp.rs @@ -1,5 +1,6 @@ use super::*; use crate::models::algebraic::{Comparison, ObjectiveSense, ILP}; +use crate::solvers::SolveOutcome; use crate::solvers::{BruteForce, ILPSolver}; use crate::topology::DirectedGraph; use crate::traits::Problem; @@ -78,7 +79,14 @@ fn test_integral_flow_bundles_to_ilp_closed_loop() { let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be feasible"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!(problem.evaluate(&extracted).unwrap()); } @@ -89,7 +97,15 @@ fn test_integral_flow_bundles_to_ilp_extract_solution_is_identity() { let reduction: ReductionIFBToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); assert_eq!( - reduction.extract_solution(&satisfying_config()).unwrap(), + reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), satisfying_config().clone()) + .unwrap() + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"), vec![1, 0, 1, 0, 0, 0] ); } diff --git a/src/unit_tests/rules/integralflowhomologousarcs_ilp.rs b/src/unit_tests/rules/integralflowhomologousarcs_ilp.rs index 07888d3b8..c6ff245ac 100644 --- a/src/unit_tests/rules/integralflowhomologousarcs_ilp.rs +++ b/src/unit_tests/rules/integralflowhomologousarcs_ilp.rs @@ -1,6 +1,7 @@ use super::*; use crate::models::algebraic::ILP; use crate::rules::ReduceTo; +use crate::solvers::SolveOutcome; use crate::solvers::{BruteForce, ILPSolver}; use crate::topology::DirectedGraph; use crate::traits::Problem; @@ -27,7 +28,14 @@ fn test_integralflowhomologousarcs_to_ilp_closed_loop() { let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be feasible"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!(source.evaluate(&extracted).unwrap()); } diff --git a/src/unit_tests/rules/integralflowwithmultipliers_ilp.rs b/src/unit_tests/rules/integralflowwithmultipliers_ilp.rs index bf0662855..e226aeb76 100644 --- a/src/unit_tests/rules/integralflowwithmultipliers_ilp.rs +++ b/src/unit_tests/rules/integralflowwithmultipliers_ilp.rs @@ -1,6 +1,7 @@ use super::*; use crate::models::algebraic::ILP; use crate::rules::ReduceTo; +use crate::solvers::SolveOutcome; use crate::solvers::{BruteForce, ILPSolver}; use crate::topology::DirectedGraph; use crate::traits::Problem; @@ -26,7 +27,14 @@ fn test_integralflowwithmultipliers_to_ilp_closed_loop() { let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be feasible"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!(source.evaluate(&extracted).unwrap()); } diff --git a/src/unit_tests/rules/isomorphicspanningtree_ilp.rs b/src/unit_tests/rules/isomorphicspanningtree_ilp.rs index 4f90f2ee2..d71c009a6 100644 --- a/src/unit_tests/rules/isomorphicspanningtree_ilp.rs +++ b/src/unit_tests/rules/isomorphicspanningtree_ilp.rs @@ -1,6 +1,7 @@ use super::*; use crate::models::algebraic::{ObjectiveSense, ILP}; use crate::rules::test_helpers::assert_bf_vs_ilp; +use crate::solvers::SolveOutcome; use crate::solvers::{BruteForce, ILPSolver}; use crate::topology::SimpleGraph; use crate::traits::Problem; @@ -51,7 +52,14 @@ fn test_isomorphicspanningtree_to_ilp_bf_vs_ilp() { let ilp_solution = ilp_solver .solve(reduction.target_problem()) .expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(problem.evaluate(&extracted).unwrap(), Or(true)); } @@ -67,7 +75,14 @@ fn test_solution_extraction() { let ilp_solution = ilp_solver .solve(reduction.target_problem()) .expect("solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted.len(), 3); assert_eq!(problem.evaluate(&extracted).unwrap(), Or(true)); } diff --git a/src/unit_tests/rules/kclique_balancedcompletebipartitesubgraph.rs b/src/unit_tests/rules/kclique_balancedcompletebipartitesubgraph.rs index fb22fc12f..8dfb72e47 100644 --- a/src/unit_tests/rules/kclique_balancedcompletebipartitesubgraph.rs +++ b/src/unit_tests/rules/kclique_balancedcompletebipartitesubgraph.rs @@ -1,6 +1,7 @@ use super::*; use crate::rules::test_helpers::assert_satisfaction_round_trip_from_satisfaction_target; use crate::solvers::BruteForce; +use crate::solvers::SolveOutcome; use crate::traits::Problem; use crate::types::Or; @@ -46,7 +47,14 @@ fn test_kclique_to_bcbs_complete_graph() { let bf = BruteForce::new(); let witness = bf.solve(target).unwrap().expect("K4 should contain K3"); - let extracted = reduction.extract_solution(&witness).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), witness.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(source.evaluate(&extracted).unwrap(), Or(true)); // Exactly 3 vertices should be selected assert_eq!(extracted.iter().filter(|&&selected| selected).count(), 3); @@ -96,7 +104,14 @@ fn test_kclique_to_bcbs_k_equals_2() { .solve(target) .unwrap() .expect("graph has edges, so 2-clique exists"); - let extracted = reduction.extract_solution(&witness).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), witness.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(source.evaluate(&extracted).unwrap(), Or(true)); assert_eq!(extracted.iter().filter(|&&selected| selected).count(), 2); } @@ -116,7 +131,14 @@ fn test_kclique_to_bcbs_k_equals_1() { let bf = BruteForce::new(); let witness = bf.solve(target).unwrap().expect("should find a 1-clique"); - let extracted = reduction.extract_solution(&witness).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), witness.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(source.evaluate(&extracted).unwrap(), Or(true)); assert_eq!(extracted.iter().filter(|&&selected| selected).count(), 1); } diff --git a/src/unit_tests/rules/kclique_conjunctivebooleanquery.rs b/src/unit_tests/rules/kclique_conjunctivebooleanquery.rs index 63591a562..9d325cd65 100644 --- a/src/unit_tests/rules/kclique_conjunctivebooleanquery.rs +++ b/src/unit_tests/rules/kclique_conjunctivebooleanquery.rs @@ -4,6 +4,7 @@ use crate::rules::kclique_conjunctivebooleanquery::ReductionKCliqueToCBQ; use crate::rules::test_helpers::assert_satisfaction_round_trip_from_satisfaction_target; use crate::rules::traits::{ReduceTo, ReductionResult}; use crate::solvers::BruteForce; +use crate::solvers::SolveOutcome; use crate::topology::SimpleGraph; use crate::traits::Problem; use crate::types::Or; @@ -73,7 +74,14 @@ fn test_solution_extraction() { .solve(reduction.target_problem()) .unwrap() .expect("CBQ should be satisfiable"); - let extracted = reduction.extract_solution(&cbq_witness).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), cbq_witness.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(problem.evaluate(&extracted).unwrap(), Or(true)); // All 3 vertices should be selected assert_eq!(extracted.iter().filter(|&&selected| selected).count(), 3); @@ -97,6 +105,13 @@ fn test_trivial_k1() { .solve(reduction.target_problem()) .unwrap() .expect("k=1 should be feasible"); - let extracted = reduction.extract_solution(&witness).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), witness.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(problem.evaluate(&extracted).unwrap(), Or(true)); } diff --git a/src/unit_tests/rules/kclique_ilp.rs b/src/unit_tests/rules/kclique_ilp.rs index 5e5b86e60..e93402409 100644 --- a/src/unit_tests/rules/kclique_ilp.rs +++ b/src/unit_tests/rules/kclique_ilp.rs @@ -1,4 +1,5 @@ use super::*; +use crate::solvers::SolveOutcome; use crate::solvers::{BruteForce, ILPSolver}; use crate::topology::SimpleGraph; use crate::traits::Problem; @@ -34,7 +35,14 @@ fn test_kclique_to_ilp_bf_vs_ilp() { assert_eq!(problem.evaluate(&bf_witness).unwrap(), Or(true)); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(problem.evaluate(&extracted).unwrap(), Or(true)); } @@ -48,7 +56,14 @@ fn test_solution_extraction() { let ilp_solution = ilp_solver .solve(reduction.target_problem()) .expect("solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(problem.evaluate(&extracted).unwrap(), Or(true)); // Should select at least k=3 vertices (ILP may return a larger valid clique) assert!(extracted.iter().filter(|&&selected| selected).count() >= 3); diff --git a/src/unit_tests/rules/kclique_subgraphisomorphism.rs b/src/unit_tests/rules/kclique_subgraphisomorphism.rs index 7ba1863be..3b40799dd 100644 --- a/src/unit_tests/rules/kclique_subgraphisomorphism.rs +++ b/src/unit_tests/rules/kclique_subgraphisomorphism.rs @@ -1,6 +1,7 @@ use super::*; use crate::rules::test_helpers::assert_satisfaction_round_trip_from_satisfaction_target; use crate::solvers::BruteForce; +use crate::solvers::SolveOutcome; use crate::traits::Problem; use crate::types::Or; @@ -49,7 +50,14 @@ fn test_kclique_to_subgraphisomorphism_complete_graph() { // Solve the target and extract back to source let bf = BruteForce::new(); let witness = bf.solve(target).unwrap().expect("K4 should contain K3"); - let extracted = reduction.extract_solution(&witness).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), witness.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(source.evaluate(&extracted).unwrap(), Or(true)); // Exactly 3 vertices should be selected assert_eq!(extracted.iter().filter(|&&selected| selected).count(), 3); @@ -95,7 +103,14 @@ fn test_kclique_to_subgraphisomorphism_k_equals_1() { .solve(target) .unwrap() .expect("should find a single vertex"); - let extracted = reduction.extract_solution(&witness).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), witness.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(source.evaluate(&extracted).unwrap(), Or(true)); assert_eq!(extracted.iter().filter(|&&selected| selected).count(), 1); } @@ -117,7 +132,14 @@ fn test_kclique_to_subgraphisomorphism_k_equals_2() { .solve(target) .unwrap() .expect("graph has edges, so K2 exists"); - let extracted = reduction.extract_solution(&witness).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), witness.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(source.evaluate(&extracted).unwrap(), Or(true)); assert_eq!(extracted.iter().filter(|&&selected| selected).count(), 2); } diff --git a/src/unit_tests/rules/kcoloring_bicliquecover.rs b/src/unit_tests/rules/kcoloring_bicliquecover.rs index 6246f6005..fc05f974d 100644 --- a/src/unit_tests/rules/kcoloring_bicliquecover.rs +++ b/src/unit_tests/rules/kcoloring_bicliquecover.rs @@ -1,6 +1,9 @@ use super::*; use crate::models::graph::BicliqueCover; +use crate::rules::ReductionResult; use crate::solvers::BruteForce; +use crate::solvers::SolveOutcome; +use crate::traits::EvaluationError::InvalidConfiguration; use crate::traits::Problem; use crate::types::Or; use crate::variant::KN; @@ -27,7 +30,14 @@ fn test_kcoloring_to_bicliquecover_closed_loop_trivial() { .solve(target) .unwrap() .expect("trivial target must be feasible"); - let coloring = reduction.extract_solution(&witness).unwrap(); + let coloring = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), witness.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(coloring.len(), 1); assert!(source.is_valid_solution(&coloring)); // The source brute force agrees. @@ -102,7 +112,14 @@ fn test_kcoloring_to_bicliquecover_forward_witness_path_q2() { // Witness covers all edges with rank <= n + q. assert!(target.is_valid_cover(&witness)); // Extraction recovers a proper coloring. - let extracted = reduction.extract_solution(&witness).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), witness.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!(source.is_valid_solution(&extracted)); } @@ -121,7 +138,14 @@ fn test_kcoloring_to_bicliquecover_forward_witness_cycle_q2() { let witness = forward_witness(&source, &coloring); assert!(target.is_valid_cover(&witness)); - let extracted = reduction.extract_solution(&witness).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), witness.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!(source.is_valid_solution(&extracted)); } @@ -188,7 +212,14 @@ fn test_kcoloring_to_bicliquecover_extract_solution_on_forward_witness() { let witness = forward_witness(&source, &coloring); assert!(target.is_valid_cover(&witness)); - let extracted = reduction.extract_solution(&witness).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), witness.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!(source.is_valid_solution(&extracted)); // K_3 forces 3 distinct colors. let mut seen = std::collections::BTreeSet::new(); @@ -247,7 +278,14 @@ fn test_kcoloring_to_bicliquecover_extract_trivial_layout() { assert!(cell(&witness, 0, 1)); assert!(cell(&witness, 2, 1)); - let extracted = reduction.extract_solution(&witness).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), witness.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted, vec![0]); } @@ -262,9 +300,6 @@ fn test_kcoloring_to_bicliquecover_native_loops_are_infeasible() { assert_eq!(target.graph().left_edges(), &[(0, 0)]); assert!(target.evaluate(&vec![]).unwrap().0.is_none()); assert!(BruteForce::new().solve(target).unwrap().is_none()); - assert!( - !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![]), Ok(value) if { value.is_valid() }) - ); if q == 0 { assert!(source.evaluate(&vec![0; n]).is_err()); } else { @@ -288,7 +323,14 @@ fn test_kcoloring_to_bicliquecover_normalizes_all_color_counts() { assert!(source.evaluate(&coloring).unwrap().0); let witness = forward_witness(&source, &coloring); assert!(target.evaluate(&witness).unwrap().0.is_some()); - let decoded = reduction.extract_solution(&witness).unwrap(); + let decoded = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), witness.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!(source.evaluate(&decoded).unwrap().0); } } @@ -300,15 +342,17 @@ fn test_kcoloring_to_bicliquecover_rejects_invalid_certificates() { let source = KColoring::::with_k(SimpleGraph::new(2, vec![(0, 1)]), 2); let reduction = ReduceTo::::reduce_to(&source).unwrap(); let target = reduction.target_problem(); - for invalid in [ - vec![], - vec![vec![true; 7]; 4], - vec![vec![true; 8]; 4], - vec![vec![false; 8]; 4], - ] { - assert!( - !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &invalid), Ok(value) if { value.is_valid() }) - ); + for invalid in [vec![], vec![vec![true; 7]; 4]] { + assert!(matches!( + ReductionResult::target_problem(&reduction).evaluate(&invalid), + Err(InvalidConfiguration(_)) + )); + } + for invalid in [vec![vec![true; 8]; 4], vec![vec![false; 8]; 4]] { + assert!(!ReductionResult::target_problem(&reduction) + .evaluate(&invalid) + .unwrap() + .is_valid()); } let valid = forward_witness(&source, &[0, 1]); assert!(target.evaluate(&valid).unwrap().0.is_some()); @@ -316,7 +360,17 @@ fn test_kcoloring_to_bicliquecover_rejects_invalid_certificates() { reordered.reverse(); assert!( source - .evaluate(&reduction.extract_solution(&reordered).unwrap()) + .evaluate( + &reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), reordered.clone()) + .unwrap() + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution") + ) .unwrap() .0 ); @@ -336,7 +390,15 @@ fn test_kcoloring_to_bicliquecover_repeated_reversed_edges() { let witness = forward_witness(&repeated, &[0, 1, 0]); assert!( repeated - .evaluate(&b.extract_solution(&witness).unwrap()) + .evaluate( + &b.recover_result( + &repeated, + SolveOutcome::optimal(b.target_problem(), witness.clone()).unwrap() + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution") + ) .unwrap() .0 ); @@ -360,7 +422,14 @@ fn test_kcoloring_to_bicliquecover_all_single_vertex_target_configs() { .collect(); let value = target.evaluate(&config).unwrap(); if value.0.is_some() { - let coloring = reduction.extract_solution(&config).unwrap(); + let coloring = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), config.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); feasible = true; assert!(source.evaluate(&coloring).unwrap().0); } diff --git a/src/unit_tests/rules/kcoloring_clustering.rs b/src/unit_tests/rules/kcoloring_clustering.rs index 36473f489..9fdb18205 100644 --- a/src/unit_tests/rules/kcoloring_clustering.rs +++ b/src/unit_tests/rules/kcoloring_clustering.rs @@ -1,6 +1,7 @@ use super::*; use crate::rules::test_helpers::assert_satisfaction_round_trip_from_satisfaction_target; use crate::solvers::BruteForce; +use crate::solvers::SolveOutcome; use crate::topology::SimpleGraph; use crate::variant::K3; @@ -42,7 +43,17 @@ fn test_kcoloring_to_clustering_extract_solution_identity() { let reduction = ReduceTo::::reduce_to(&source).expect("reduction should succeed"); let config = vec![0, 1, 0]; - assert_eq!(reduction.extract_solution(&config).unwrap(), config); + assert_eq!( + reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), config.clone()).unwrap() + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"), + config + ); } #[test] @@ -65,7 +76,14 @@ fn test_kcoloring_to_clustering_empty_graph() { assert_eq!(target.num_clusters(), 3); assert_eq!(target.diameter_bound(), 0); assert_eq!( - reduction.extract_solution(&vec![2]).unwrap(), + reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), vec![2].clone()).unwrap() + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"), Vec::::new() ); assert_satisfaction_round_trip_from_satisfaction_target(&source, &reduction, "empty graph"); diff --git a/src/unit_tests/rules/kcoloring_partitionintocliques.rs b/src/unit_tests/rules/kcoloring_partitionintocliques.rs index c05b094c7..1282e2d35 100644 --- a/src/unit_tests/rules/kcoloring_partitionintocliques.rs +++ b/src/unit_tests/rules/kcoloring_partitionintocliques.rs @@ -1,6 +1,7 @@ use super::*; use crate::rules::test_helpers::assert_satisfaction_round_trip_from_satisfaction_target; use crate::solvers::BruteForce; +use crate::solvers::SolveOutcome; use crate::topology::Graph; use crate::variant::KN; @@ -42,7 +43,17 @@ fn test_kcoloring_to_partitionintocliques_extract_solution_identity() { .expect("reduction should succeed"); let config = vec![0, 1, 0]; - assert_eq!(reduction.extract_solution(&config).unwrap(), config); + assert_eq!( + reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), config.clone()).unwrap() + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"), + config + ); } #[test] diff --git a/src/unit_tests/rules/kcoloring_twodimensionalconsecutivesets.rs b/src/unit_tests/rules/kcoloring_twodimensionalconsecutivesets.rs index 6be37cedb..3298cd6da 100644 --- a/src/unit_tests/rules/kcoloring_twodimensionalconsecutivesets.rs +++ b/src/unit_tests/rules/kcoloring_twodimensionalconsecutivesets.rs @@ -1,10 +1,12 @@ -use super::*; use crate::models::graph::KColoring; use crate::models::set::TwoDimensionalConsecutiveSets; use crate::rules::test_helpers::assert_satisfaction_round_trip_from_satisfaction_target; use crate::rules::traits::ReduceTo; +use crate::rules::ReductionResult; use crate::solvers::BruteForce; +use crate::solvers::SolveOutcome; use crate::topology::SimpleGraph; +use crate::traits::EvaluationError::InvalidConfiguration; use crate::traits::Problem; use crate::variant::K3; @@ -108,7 +110,14 @@ fn test_kcoloring_to_tdcs_extract_solution_valid() { .unwrap(); for target_sol in &target_solutions { - let source_sol = reduction.extract_solution(target_sol).unwrap(); + let source_sol = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), (target_sol).clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(source_sol.len(), 3); // Verify it is a valid coloring assert!( @@ -127,7 +136,14 @@ fn test_kcoloring_to_tdcs_empty_graph_has_a_target_witness() { assert_eq!(target.alphabet_size(), 1); assert_eq!(target.num_subsets(), 0); let witness = BruteForce::new().solve(target).unwrap().unwrap(); - let coloring = reduction.extract_solution(&witness).unwrap(); + let coloring = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), witness.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!(coloring.is_empty()); assert!(source.evaluate(&coloring).unwrap().0); } @@ -145,9 +161,10 @@ fn test_kcoloring_to_tdcs_native_loops_are_no() { for a in 0..3 { for b in 0..3 { for c in 0..3 { - assert!( - !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![a, b, c]), Ok(value) if { value.is_valid() }) - ); + assert!(!ReductionResult::target_problem(&reduction) + .evaluate(&vec![a, b, c]) + .unwrap() + .is_valid()); } } } @@ -158,11 +175,16 @@ fn test_kcoloring_to_tdcs_native_loops_are_no() { fn test_kcoloring_to_tdcs_rejects_noncertificates() { let source = KColoring::::new(SimpleGraph::new(2, vec![(0, 1)])); let reduction = ReduceTo::::reduce_to(&source).unwrap(); - for config in [vec![], vec![0, 1], vec![0, 1, 3], vec![0, 0, 0]] { - assert!( - !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &config), Ok(value) if { value.is_valid() }) - ); + for config in [vec![], vec![0, 1], vec![0, 1, 3]] { + assert!(matches!( + ReductionResult::target_problem(&reduction).evaluate(&config), + Err(InvalidConfiguration(_)) + )); } + assert!(!ReductionResult::target_problem(&reduction) + .evaluate(&vec![0, 0, 0]) + .unwrap() + .is_valid()); } #[test] @@ -185,7 +207,14 @@ fn test_kcoloring_to_tdcs_many_groups_gaps_and_repeated_edges() { let source = KColoring::::new(SimpleGraph::new(n, edges)); let reduction = ReduceTo::::reduce_to(&source).unwrap(); assert!(reduction.target_problem().evaluate(&grouping).unwrap().0); - let coloring = reduction.extract_solution(&grouping).unwrap(); + let coloring = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), grouping.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(coloring, expected); assert!(source.evaluate(&coloring).unwrap().0); } @@ -219,7 +248,15 @@ fn test_kcoloring_to_tdcs_all_tiny_graphs_and_target_assignments() { .collect(); let feasible = target.evaluate(&grouping).unwrap().0; if feasible { - let coloring = reduction.extract_solution(&grouping).unwrap(); + let coloring = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), grouping.clone()) + .unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!(source.evaluate(&coloring).unwrap().0); target_yes = true; } diff --git a/src/unit_tests/rules/knapsack_ilp.rs b/src/unit_tests/rules/knapsack_ilp.rs index bf72d4cff..f9ee3bcd1 100644 --- a/src/unit_tests/rules/knapsack_ilp.rs +++ b/src/unit_tests/rules/knapsack_ilp.rs @@ -1,6 +1,7 @@ use super::*; use crate::models::algebraic::{Comparison, ObjectiveSense, ILP}; use crate::rules::test_helpers::assert_bf_vs_ilp; +use crate::solvers::SolveOutcome; use crate::solvers::{BruteForce, ILPSolver}; use crate::traits::Problem; @@ -14,7 +15,14 @@ fn test_knapsack_to_ilp_closed_loop() { let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &knapsack, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted, vec![false, true, true, false]); } @@ -29,7 +37,14 @@ fn test_knapsack_to_ilp_bf_vs_ilp() { let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &knapsack, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let ilp_value = knapsack.evaluate(&extracted).unwrap(); assert_eq!(bf_value, ilp_value); @@ -61,7 +76,14 @@ fn test_knapsack_to_ilp_zero_capacity() { let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("zero-capacity ILP should still be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &knapsack, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted, vec![false, false]); } @@ -81,7 +103,14 @@ fn test_knapsack_to_ilp_empty_instance() { let ilp_solution = ILPSolver::new() .solve(ilp) .expect("empty Knapsack ILP should still be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &knapsack, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted, Vec::::new()); } diff --git a/src/unit_tests/rules/knapsack_qubo.rs b/src/unit_tests/rules/knapsack_qubo.rs index 4827ce90b..8d9e763b3 100644 --- a/src/unit_tests/rules/knapsack_qubo.rs +++ b/src/unit_tests/rules/knapsack_qubo.rs @@ -1,6 +1,7 @@ use super::*; use crate::rules::test_helpers::assert_optimization_round_trip_from_optimization_target; use crate::solvers::BruteForce; +use crate::solvers::SolveOutcome; use crate::traits::Problem; #[test] @@ -28,7 +29,14 @@ fn test_knapsack_to_qubo_single_item() { let solver = BruteForce::new(); let best_target = solver.find_all_witnesses(qubo).unwrap(); - let extracted = reduction.extract_solution(&best_target[0]).unwrap(); + let extracted = reduction + .recover_result( + &knapsack, + SolveOutcome::optimal(reduction.target_problem(), best_target[0].clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted, vec![true]); } @@ -42,7 +50,14 @@ fn test_knapsack_to_qubo_infeasible_rejected() { let best_target = solver.find_all_witnesses(qubo).unwrap(); for sol in &best_target { - let source_sol = reduction.extract_solution(sol).unwrap(); + let source_sol = reduction + .recover_result( + &knapsack, + SolveOutcome::optimal(reduction.target_problem(), (sol).clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let eval = knapsack.evaluate(&source_sol).unwrap(); assert!( eval.is_valid(), @@ -61,7 +76,14 @@ fn test_knapsack_to_qubo_empty() { let solver = BruteForce::new(); let best_target = solver.find_all_witnesses(qubo).unwrap(); - let extracted = reduction.extract_solution(&best_target[0]).unwrap(); + let extracted = reduction + .recover_result( + &knapsack, + SolveOutcome::optimal(reduction.target_problem(), best_target[0].clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted, vec![false, false]); } diff --git a/src/unit_tests/rules/ksatisfiability_acyclicpartition.rs b/src/unit_tests/rules/ksatisfiability_acyclicpartition.rs index 803d7cba1..34b11858d 100644 --- a/src/unit_tests/rules/ksatisfiability_acyclicpartition.rs +++ b/src/unit_tests/rules/ksatisfiability_acyclicpartition.rs @@ -3,7 +3,9 @@ use crate::models::formula::{CNFClause, KSatisfiability}; use crate::models::graph::AcyclicPartition; use crate::rules::{ReduceTo, ReductionResult}; use crate::solvers::BruteForce; +use crate::solvers::SolveOutcome; use crate::topology::Graph; +use crate::traits::EvaluationError::InvalidConfiguration; use crate::traits::Problem; use crate::variant::K3; @@ -23,41 +25,62 @@ fn test_ksatisfiability_to_acyclicpartition_closed_loop() { count += 1; assert!( source - .evaluate(&reduction.extract_solution(&labels).unwrap()) + .evaluate( + &reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), labels.clone()) + .unwrap() + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution") + ) .unwrap() .0 ); - let renamed = labels.iter().map(|&x| if x == 0 { 8 } else { 3 }).collect(); + let renamed: Vec<_> = labels.iter().map(|&x| if x == 0 { 8 } else { 3 }).collect(); assert!( source - .evaluate(&reduction.extract_solution(&renamed).unwrap()) + .evaluate( + &reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), renamed.clone()) + .unwrap() + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution") + ) .unwrap() .0 ); } else { - assert!( - !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &labels), Ok(value) if { value.is_valid() }) - ); + assert!(!ReductionResult::target_problem(&reduction) + .evaluate(&labels) + .unwrap() + .is_valid()); } } assert_eq!(count, 3); } #[test] -fn test_acyclicpartition_extraction_rejects_invalid_targets() { +fn test_acyclicpartition_target_rejects_invalid_assignments() { let source = KSatisfiability::::new(1, vec![CNFClause::new(vec![1, 1, 1])]); let reduction = ReduceTo::>::reduce_to(&source).unwrap(); - for labels in [ - vec![], - vec![0; 8], - vec![0; 10], - vec![9; 9], - vec![0; 9], - vec![2, 1, 1, 0, 0, 1, 1, 0, 1], - ] { - assert!( - !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &labels), Ok(value) if { value.is_valid() }) - ); + for labels in [vec![], vec![0; 8], vec![0; 10], vec![9; 9]] { + assert!(matches!( + ReductionResult::target_problem(&reduction).evaluate(&labels), + Err(InvalidConfiguration(_)) + )); + } + for labels in [vec![0; 9], vec![2, 1, 1, 0, 0, 1, 1, 0, 1]] { + assert!(!ReductionResult::target_problem(&reduction) + .evaluate(&labels) + .unwrap() + .is_valid()); } } @@ -73,7 +96,17 @@ fn test_acyclicpartition_native_empty_and_short_clauses() { for labels in witnesses { assert!( source - .evaluate(&reduction.extract_solution(&labels).unwrap()) + .evaluate( + &reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), labels.clone()) + .unwrap() + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution") + ) .unwrap() .0 ); @@ -138,7 +171,17 @@ fn test_ksatisfiability_to_acyclicpartition_multi_variable_closed_loop() { assert!(reduction.target_problem().evaluate(&labels).unwrap().0); assert!( source - .evaluate(&reduction.extract_solution(&labels).unwrap()) + .evaluate( + &reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), labels.clone()) + .unwrap() + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution") + ) .unwrap() .0 ); diff --git a/src/unit_tests/rules/ksatisfiability_bicliquecover.rs b/src/unit_tests/rules/ksatisfiability_bicliquecover.rs index 22e336c8a..0a77590a6 100644 --- a/src/unit_tests/rules/ksatisfiability_bicliquecover.rs +++ b/src/unit_tests/rules/ksatisfiability_bicliquecover.rs @@ -19,6 +19,9 @@ use super::*; use crate::models::formula::CNFClause; use crate::models::graph::BicliqueCover; +use crate::rules::ReductionResult; +use crate::solvers::SolveOutcome; +use crate::traits::EvaluationError::InvalidConfiguration; use crate::traits::Problem; use crate::variant::K3; @@ -120,15 +123,18 @@ fn test_ksatisfiability_to_bicliquecover_rejects_invalid_covers() { sparse[0][0] = true; assert!(target.evaluate(&sparse).unwrap().0.is_none()); for invalid in [ - sparse, vec![], - vec![vec![true; target.num_vertices()]; target.k()], vec![vec![false; target.num_vertices() - 1]; target.k()], ] { - assert!( - !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &invalid), Ok(value) if { value.is_valid() }) - ); + assert!(matches!( + ReductionResult::target_problem(&reduction).evaluate(&invalid), + Err(InvalidConfiguration(_)) + )); } + assert!(!ReductionResult::target_problem(&reduction) + .evaluate(&vec![vec![true; target.num_vertices()]; target.k()]) + .unwrap() + .is_valid()); } #[test] @@ -140,7 +146,14 @@ fn test_ksatisfiability_to_bicliquecover_extracts_every_row_rotation() { assert!(cost.0.is_some()); for _ in 0..cover.len() { assert_eq!(reduction.target_problem().evaluate(&cover).unwrap(), cost); - let assignment = reduction.extract_solution(&cover).unwrap(); + let assignment = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), cover.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(assignment, vec![true]); assert!(source.evaluate(&assignment).unwrap().0); cover.rotate_left(1); @@ -168,7 +181,14 @@ fn test_ksatisfiability_to_bicliquecover_closed_loop_smallest() { "forward witness must be a valid biclique cover" ); - let extracted = reduction.extract_solution(&witness).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), witness.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted.len(), 1); assert!( extracted[0], @@ -214,7 +234,14 @@ fn test_ksatisfiability_to_bicliquecover_sparse_variable_inverse() { assert_eq!(reduction.source_variables, vec![6]); assert_eq!(reduction.normalized_n, 2); let cover = super::forward_witness_single_variable_single_clause(&source); - let assignment = reduction.extract_solution(&cover).unwrap(); + let assignment = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), cover.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!( assignment, vec![false, false, false, false, false, false, true] @@ -244,7 +271,17 @@ fn test_ksatisfiability_to_bicliquecover_empty_conjunction_and_clause() { let yes = KSatisfiability::::new(n, vec![]); let reduction = ReduceTo::::reduce_to(&yes).unwrap(); assert_eq!(reduction.target_problem().num_vertices(), 0); - assert_eq!(reduction.extract_solution(&vec![]).unwrap(), vec![false; n]); + assert_eq!( + reduction + .recover_result( + &yes, + SolveOutcome::optimal(reduction.target_problem(), vec![].clone()).unwrap() + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"), + vec![false; n] + ); assert!(yes.evaluate(&vec![false; n]).unwrap().0); let no = KSatisfiability::::new_allow_less(n, vec![CNFClause::new(vec![])]); let reduction = ReduceTo::::reduce_to(&no).unwrap(); @@ -254,9 +291,6 @@ fn test_ksatisfiability_to_bicliquecover_empty_conjunction_and_clause() { .unwrap() .0 .is_none()); - assert!( - !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![]), Ok(value) if { value.is_valid() }) - ); assert!(!no.evaluate(&vec![false; n]).unwrap().0); } } diff --git a/src/unit_tests/rules/ksatisfiability_cyclicordering.rs b/src/unit_tests/rules/ksatisfiability_cyclicordering.rs index c8e312447..0e9e44bc4 100644 --- a/src/unit_tests/rules/ksatisfiability_cyclicordering.rs +++ b/src/unit_tests/rules/ksatisfiability_cyclicordering.rs @@ -1,6 +1,9 @@ use super::*; use crate::models::formula::CNFClause; use crate::models::misc::CyclicOrdering; +use crate::rules::ReductionResult; +use crate::solvers::SolveOutcome; +use crate::traits::EvaluationError::InvalidConfiguration; use crate::traits::Problem; use crate::variant::K3; // Construct the paper's seven local orders and merge their auxiliary @@ -95,7 +98,14 @@ fn test_ksatisfiability_to_cyclicordering_single_clause_reference_vector() { ); let target_solution = forward_witness(&source, &[true, true, true]); - let extracted = reduction.extract_solution(&target_solution).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), target_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted, vec![true, true, true]); assert!(source.evaluate(&extracted).unwrap().0); } @@ -141,7 +151,14 @@ fn test_ksatisfiability_to_cyclicordering_extract_solution_from_reference_witnes .0 ); assert_eq!( - reduction.extract_solution(&target_solution).unwrap(), + reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), target_solution.clone()).unwrap() + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"), vec![true, true, true] ); } @@ -154,7 +171,17 @@ fn test_ksatisfiability_to_cyclicordering_clause_gadget_truth_patterns() { let assignment: Vec<_> = (0..3).map(|bit| mask & (1 << bit) != 0).collect(); let config = forward_witness(&source, &assignment); assert!(reduction.target_problem().evaluate(&config).unwrap().0); - assert_eq!(reduction.extract_solution(&config).unwrap(), assignment); + assert_eq!( + reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), config.clone()).unwrap() + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"), + assignment + ); } } @@ -199,7 +226,14 @@ fn test_ksatisfiability_to_cyclicordering_closed_loop() { "target solution must evaluate as satisfying" ); - let extracted = reduction.extract_solution(&target_solution).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), target_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!( source.evaluate(&extracted).unwrap().0, "extracted source config must satisfy the source" @@ -235,7 +269,14 @@ fn repeated_short_and_unsorted_clauses_preserve_all_source_assignments() { if source.evaluate(&assignment).unwrap().0 { let config = forward_witness(&source, &assignment); assert!(reduction.target_problem().evaluate(&config).unwrap().0); - let extracted = reduction.extract_solution(&config).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), config.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!(source.evaluate(&extracted).unwrap().0); for (original, used) in (0..6).map(|i| (i, normalized.source_variables.contains(&i))) @@ -254,7 +295,14 @@ fn empty_formula_and_empty_clause_have_opposite_fixed_targets() { let reduction = ReduceTo::::reduce_to(&source).unwrap(); assert_eq!(reduction.target_problem().num_elements(), 1); assert_eq!( - reduction.extract_solution(&vec![0]).unwrap(), + reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), vec![0].clone()).unwrap() + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"), vec![false; num_vars] ); let source = @@ -271,9 +319,6 @@ fn empty_formula_and_empty_clause_have_opposite_fixed_targets() { vec![2, 1, 0], ] { assert!(!reduction.target_problem().evaluate(&config).unwrap().0); - assert!( - !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &config), Ok(value) if { value.is_valid() }) - ); } } } @@ -285,19 +330,33 @@ fn reject_invalid_orderings_and_accept_every_rotation() { let config = forward_witness(&source, &[true, false, true]); let n = config.len(); for shift in 0..n { - let rotated = config + let rotated: Vec<_> = config .iter() .map(|position| (position + shift) % n) .collect(); assert_eq!( - reduction.extract_solution(&rotated).unwrap(), + reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), rotated.clone()).unwrap() + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"), vec![true, false, true] ); } - for config in [vec![], vec![0; n], vec![n; n], (0..n).collect()] { - assert!( - !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &config), Ok(value) if { value.is_valid() }) - ); + for config in [vec![], vec![n; n]] { + assert!(matches!( + ReductionResult::target_problem(&reduction).evaluate(&config), + Err(InvalidConfiguration(_)) + )); + } + for config in [vec![0; n], (0..n).collect()] { + assert!(!ReductionResult::target_problem(&reduction) + .evaluate(&config) + .unwrap() + .is_valid()); } } diff --git a/src/unit_tests/rules/ksatisfiability_decisionminimumvertexcover.rs b/src/unit_tests/rules/ksatisfiability_decisionminimumvertexcover.rs index 387b9de80..1fab78ca7 100644 --- a/src/unit_tests/rules/ksatisfiability_decisionminimumvertexcover.rs +++ b/src/unit_tests/rules/ksatisfiability_decisionminimumvertexcover.rs @@ -4,6 +4,7 @@ use crate::models::formula::CNFClause; use crate::models::graph::MinimumVertexCover; use crate::rules::test_helpers::assert_satisfaction_round_trip_from_satisfaction_target; use crate::solvers::BruteForce; +use crate::solvers::SolveOutcome; use crate::topology::SimpleGraph; use crate::traits::Problem; use crate::variant::K3; @@ -82,7 +83,28 @@ fn test_ksatisfiability_to_decisionminimumvertexcover_extract_solution() { crate::types::Or(true) ); assert_eq!( - reduction.extract_solution(&cover).unwrap(), + reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), cover.clone()).unwrap() + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"), vec![false, false, true] ); } + +#[test] +fn test_ksatisfiability_to_decisionminimumvertexcover_all_negated() { + // (~x1 v ~x2 v ~x3) — 7 satisfying assignments + let ksat = KSatisfiability::::new(3, vec![CNFClause::new(vec![-1, -2, -3])]); + let reduction = ReduceTo::>>::reduce_to(&ksat) + .expect("reduction should succeed"); + + assert_satisfaction_round_trip_from_satisfaction_target( + &ksat, + &reduction, + "3SAT all negated -> MVC", + ); +} diff --git a/src/unit_tests/rules/ksatisfiability_directedtwocommodityintegralflow.rs b/src/unit_tests/rules/ksatisfiability_directedtwocommodityintegralflow.rs index 908cf2509..8d188e87e 100644 --- a/src/unit_tests/rules/ksatisfiability_directedtwocommodityintegralflow.rs +++ b/src/unit_tests/rules/ksatisfiability_directedtwocommodityintegralflow.rs @@ -7,6 +7,7 @@ use crate::models::formula::CNFClause; use crate::models::graph::DirectedTwoCommodityIntegralFlow; use crate::rules::{ReduceTo, ReductionGraph, ReductionResult}; use crate::solvers::ILPSolver; +use crate::solvers::SolveOutcome; use crate::traits::Problem; use crate::variant::K3; @@ -45,7 +46,14 @@ fn solve_target_via_ilp( Err(crate::solvers::ILPSolveError::Infeasible) => return None, Err(error) => panic!("ILP execution failed: {error}"), }; - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!( problem.evaluate(&extracted).unwrap().0, "decoded flow must be feasible" @@ -99,7 +107,17 @@ fn test_ksatisfiability_to_directedtwocommodityintegralflow_extract_solution_fro let assignment = vec![true, true, false]; let flow = reduction.encode_assignment(&assignment); assert!(reduction.target_problem().evaluate(&flow).unwrap().0); - assert_eq!(reduction.extract_solution(&flow).unwrap(), assignment); + assert_eq!( + reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), flow.clone()).unwrap() + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"), + assignment + ); } #[test] @@ -120,7 +138,14 @@ fn test_ksatisfiability_to_directedtwocommodityintegralflow_closed_loop() { .0 ); - let extracted = reduction.extract_solution(&target_solution).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), target_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!(source.evaluate(&extracted).unwrap().0); } diff --git a/src/unit_tests/rules/ksatisfiability_feasibleregisterassignment.rs b/src/unit_tests/rules/ksatisfiability_feasibleregisterassignment.rs index 526812d9e..61e9d51fa 100644 --- a/src/unit_tests/rules/ksatisfiability_feasibleregisterassignment.rs +++ b/src/unit_tests/rules/ksatisfiability_feasibleregisterassignment.rs @@ -1,7 +1,10 @@ use super::*; use crate::models::algebraic::ILP; use crate::models::formula::CNFClause; +use crate::rules::ReductionResult; use crate::solvers::ILPSolver; +use crate::solvers::SolveOutcome; +use crate::traits::EvaluationError::InvalidConfiguration; use crate::traits::Problem; use crate::types::Or; use std::collections::BTreeSet; @@ -130,7 +133,14 @@ fn test_ksatisfiability_to_feasible_register_assignment_extract_solution() { let realization = forward_witness(&source, &[true, false]); assert!(reduction.target_problem().evaluate(&realization).unwrap().0); - let extracted = reduction.extract_solution(&realization).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), realization.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted, vec![true, false]); } @@ -146,13 +156,27 @@ fn test_ksatisfiability_to_feasible_register_assignment_closed_loop_via_ilp() { let ilp_solution = ILPSolver::new() .solve(fra_to_ilp.target_problem()) .expect("satisfiable FRA gadget should reduce to a feasible ILP"); - let fra_solution = fra_to_ilp.extract_solution(&ilp_solution).unwrap(); + let fra_solution = fra_to_ilp + .recover_result( + reduction.target_problem(), + SolveOutcome::optimal(fra_to_ilp.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!( reduction.target_problem().evaluate(&fra_solution).unwrap(), Or(true) ); - let extracted = reduction.extract_solution(&fra_solution).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), fra_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(source.evaluate(&extracted).unwrap(), Or(true)); } @@ -194,15 +218,23 @@ fn native_empty_clause_is_infeasible_and_empty_conjunction_is_feasible() { vec![2, 1, 0], ] { assert!(!reduction.target_problem().evaluate(&config).unwrap().0); - assert!( - !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &config), Ok(value) if { value.is_valid() }) - ); + assert!(!ReductionResult::target_problem(&reduction) + .evaluate(&config) + .unwrap() + .is_valid()); } let source = KSatisfiability::::new(num_vars, vec![]); let reduction = ReduceTo::::reduce_to(&source).unwrap(); assert_eq!(reduction.target_problem().num_vertices(), 0); assert_eq!( - reduction.extract_solution(&vec![]).unwrap(), + reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), vec![].clone()).unwrap() + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"), vec![false; num_vars] ); } @@ -239,7 +271,14 @@ fn short_repeated_and_mixed_clauses_have_complete_forward_witnesses() { if source.evaluate(&values).unwrap().0 { let config = forward_witness(&source, &values); assert!(reduction.target_problem().evaluate(&config).unwrap().0); - let extracted = reduction.extract_solution(&config).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), config.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!(source.evaluate(&extracted).unwrap().0); for original in 0..6 { assert_eq!( @@ -284,10 +323,17 @@ fn invalid_realizations_are_rejected() { let source = issue_example(); let reduction = ReduceTo::::reduce_to(&source).unwrap(); let n = reduction.target_problem().num_vertices(); - for config in [vec![], vec![n; n], vec![0; n], (0..n).collect()] { - assert!( - !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &config), Ok(value) if { value.is_valid() }) - ); + for config in [vec![], vec![n; n]] { + assert!(matches!( + ReductionResult::target_problem(&reduction).evaluate(&config), + Err(InvalidConfiguration(_)) + )); + } + for config in [vec![0; n], (0..n).collect()] { + assert!(!ReductionResult::target_problem(&reduction) + .evaluate(&config) + .unwrap() + .is_valid()); } } diff --git a/src/unit_tests/rules/ksatisfiability_kclique.rs b/src/unit_tests/rules/ksatisfiability_kclique.rs index 157f25686..7c544c187 100644 --- a/src/unit_tests/rules/ksatisfiability_kclique.rs +++ b/src/unit_tests/rules/ksatisfiability_kclique.rs @@ -1,6 +1,9 @@ use super::*; use crate::models::formula::CNFClause; +use crate::rules::ReductionResult; use crate::solvers::BruteForce; +use crate::solvers::SolveOutcome; +use crate::traits::EvaluationError::InvalidConfiguration; use crate::traits::Problem; #[test] @@ -22,14 +25,31 @@ fn test_ksatisfiability_to_kclique_closed_loop() { assert!(witness[6]); assert!( source - .evaluate(&reduction.extract_solution(&witness).unwrap()) + .evaluate( + &reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), witness.clone()) + .unwrap() + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution") + ) .unwrap() .0 ); } let witness = vec![false, false, true, true, false, false, true]; assert_eq!( - reduction.extract_solution(&witness).unwrap(), + reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), witness.clone()).unwrap() + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"), vec![false, false, true] ); let no = KSatisfiability::::new( @@ -60,7 +80,14 @@ fn test_kclique_empty_formulas_and_short_clauses() { (1, 1) ); assert_eq!( - reduction.extract_solution(&vec![true]).unwrap(), + reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), vec![true].clone()).unwrap() + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"), vec![false; n] ); } @@ -84,7 +111,17 @@ fn test_kclique_empty_formulas_and_short_clauses() { for witness in solutions { assert!( source - .evaluate(&reduction.extract_solution(&witness).unwrap()) + .evaluate( + &reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), witness.clone()) + .unwrap() + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution") + ) .unwrap() .0 ); @@ -142,14 +179,29 @@ fn test_kclique_all_two_clause_formulas_and_target_selections() { target_yes = true; assert!( source - .evaluate(&reduction.extract_solution(&witness).unwrap()) + .evaluate( + &reduction + .recover_result( + &source, + SolveOutcome::optimal( + reduction.target_problem(), + witness.clone() + ) + .unwrap() + ) + .map(|result| result.into_solution().expect( + "qualifying target result must recover a source solution" + )) + .unwrap() + ) .unwrap() .0 ); } else { - assert!( - !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &witness), Ok(value) if { value.is_valid() }) - ); + assert!(!ReductionResult::target_problem(&reduction) + .evaluate(&witness) + .unwrap() + .is_valid()); } } assert_eq!(source_yes, target_yes); @@ -164,16 +216,21 @@ fn test_kclique_rejects_malformed_or_non_clique_selections() { vec![CNFClause::new(vec![1, 2]), CNFClause::new(vec![-1, 3])], ); let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + for bad in [vec![], vec![true; 6]] { + assert!(matches!( + ReductionResult::target_problem(&reduction).evaluate(&bad), + Err(InvalidConfiguration(_)) + )); + } for bad in [ - vec![], - vec![true; 6], vec![false; 5], vec![true, true, false, false, true], vec![true, false, true, false, true], ] { - assert!( - !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &bad), Ok(value) if { value.is_valid() }) - ); + assert!(!ReductionResult::target_problem(&reduction) + .evaluate(&bad) + .unwrap() + .is_valid()); } } diff --git a/src/unit_tests/rules/ksatisfiability_kernel.rs b/src/unit_tests/rules/ksatisfiability_kernel.rs index 902b13f78..3f5f75f1c 100644 --- a/src/unit_tests/rules/ksatisfiability_kernel.rs +++ b/src/unit_tests/rules/ksatisfiability_kernel.rs @@ -3,6 +3,9 @@ use crate::models::graph::Kernel; use crate::rules::test_helpers::assert_satisfaction_round_trip_from_satisfaction_target; use crate::rules::{ReduceTo, ReductionResult}; use crate::solvers::BruteForce; +use crate::solvers::SolveOutcome; +use crate::traits::EvaluationError::InvalidConfiguration; +use crate::traits::Problem; use crate::variant::K3; #[test] @@ -72,8 +75,17 @@ fn test_ksatisfiability_to_kernel_extract_solution_reads_variable_gadgets() { assert_eq!( reduction - .extract_solution(&vec![true, false, false, true, false, false, false]) - .unwrap(), + .recover_result( + &source, + SolveOutcome::optimal( + reduction.target_problem(), + vec![true, false, false, true, false, false, false].clone() + ) + .unwrap() + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"), vec![true, false] ); } @@ -119,7 +131,14 @@ fn test_ksatisfiability_to_kernel_native_clause_domain() { .map(|v| mask & (1 << v) != 0) .collect(); if target.evaluate(&config).unwrap().0 { - let decoded = reduction.extract_solution(&config).unwrap(); + let decoded = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), config.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!(source.evaluate(&decoded).unwrap().0); witnessed[usize::from(decoded[0])] = true; } @@ -152,7 +171,14 @@ fn test_ksatisfiability_to_kernel_sparse_inverse() { .collect(); if target.evaluate(&config).unwrap().0 { found = true; - let decoded = reduction.extract_solution(&config).unwrap(); + let decoded = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), config.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(decoded.len(), 6); assert!(source.evaluate(&decoded).unwrap().0); for (variable, value) in decoded.iter().enumerate() { @@ -173,10 +199,17 @@ fn test_ksatisfiability_to_kernel_sparse_inverse() { fn test_ksatisfiability_to_kernel_rejects_non_kernel() { let source = KSatisfiability::::new(1, vec![CNFClause::new(vec![1, 1, 1])]); let reduction = ReduceTo::::reduce_to(&source).unwrap(); - for config in [vec![], vec![false; 5], vec![true; 5], vec![false; 6]] { - assert!( - !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &config), Ok(value) if { value.is_valid() }) - ); + for config in [vec![], vec![false; 6]] { + assert!(matches!( + ReductionResult::target_problem(&reduction).evaluate(&config), + Err(InvalidConfiguration(_)) + )); + } + for config in [vec![false; 5], vec![true; 5]] { + assert!(!ReductionResult::target_problem(&reduction) + .evaluate(&config) + .unwrap() + .is_valid()); } } diff --git a/src/unit_tests/rules/ksatisfiability_minimumvertexcover.rs b/src/unit_tests/rules/ksatisfiability_minimumvertexcover.rs deleted file mode 100644 index 736120135..000000000 --- a/src/unit_tests/rules/ksatisfiability_minimumvertexcover.rs +++ /dev/null @@ -1,156 +0,0 @@ -use super::*; -use crate::models::formula::CNFClause; -use crate::models::graph::MinimumVertexCover; -use crate::rules::test_helpers::assert_satisfaction_round_trip_from_optimization_target; -use crate::solvers::BruteForce; -use crate::topology::SimpleGraph; -use crate::traits::Problem; -use crate::variant::K3; - -#[test] -fn test_ksatisfiability_to_minimumvertexcover_closed_loop() { - // (x1 v x2 v x3) ^ (~x1 v ~x2 v x3), n=3, m=2 - let ksat = KSatisfiability::::new( - 3, - vec![ - CNFClause::new(vec![1, 2, 3]), // x1 v x2 v x3 - CNFClause::new(vec![-1, -2, 3]), // ~x1 v ~x2 v x3 - ], - ); - let reduction = ReduceTo::>::reduce_to(&ksat) - .expect("reduction should succeed"); - let target = reduction.target_problem(); - - // Verify structure: 2*3 + 3*2 = 12 vertices - assert_eq!(target.num_vertices(), 12); - // Edges: 3 truth-setting + 6*2 = 15 - assert_eq!(target.num_edges(), 15); - - // Use the helper to verify full round-trip correctness - assert_satisfaction_round_trip_from_optimization_target( - &ksat, - &reduction, - "3SAT -> MVC closed loop", - ); -} - -#[test] -fn test_ksatisfiability_to_minimumvertexcover_unsatisfiable() { - // Unsatisfiable: (x1 v x1 v x1) ^ (~x1 v ~x1 v ~x1) ^ (x1 v x1 v x1) - let ksat = KSatisfiability::::new( - 1, - vec![ - CNFClause::new(vec![1, 1, 1]), - CNFClause::new(vec![-1, -1, -1]), - CNFClause::new(vec![1, 1, 1]), - ], - ); - let reduction = ReduceTo::>::reduce_to(&ksat) - .expect("reduction should succeed"); - let target = reduction.target_problem(); - - // n=1, m=3 -> 2 + 9 = 11 vertices, minimum VC should be > n + 2m = 7 - // if unsatisfiable. Actually MVC always has a solution (empty set is not valid - // for graphs with edges, but any superset works). The key property is: - // SAT is satisfiable iff MVC has size <= n + 2m. - let solver = BruteForce::new(); - let witness = solver.solve(target).unwrap(); - assert!(witness.is_some()); - let vc_config = witness.unwrap(); - let vc_size: usize = vc_config.iter().filter(|&&selected| selected).count(); - // Unsatisfiable -> minimum VC size > n + 2m = 1 + 6 = 7 - assert!(vc_size > 7); -} - -#[test] -fn test_ksatisfiability_to_minimumvertexcover_single_clause() { - // Single clause: (x1 v x2 v x3) — 7 out of 8 assignments satisfy it - let ksat = KSatisfiability::::new(3, vec![CNFClause::new(vec![1, 2, 3])]); - let reduction = ReduceTo::>::reduce_to(&ksat) - .expect("reduction should succeed"); - let target = reduction.target_problem(); - - // 2*3 + 3*1 = 9 vertices, 3 + 6 = 9 edges - assert_eq!(target.num_vertices(), 9); - assert_eq!(target.num_edges(), 9); - - assert_satisfaction_round_trip_from_optimization_target( - &ksat, - &reduction, - "3SAT single clause -> MVC", - ); -} - -#[test] -fn test_ksatisfiability_to_minimumvertexcover_extract_solution() { - // Verify specific extraction: x1=F, x2=F, x3=T - let ksat = KSatisfiability::::new( - 3, - vec![ - CNFClause::new(vec![1, 2, 3]), - CNFClause::new(vec![-1, -2, 3]), - ], - ); - let reduction = ReduceTo::>::reduce_to(&ksat) - .expect("reduction should succeed"); - - // Literal vertices: u1(0), ~u1(1), u2(2), ~u2(3), u3(4), ~u3(5) - // Clause 0 triangle: v6, v7, v8 - // Clause 1 triangle: v9, v10, v11 - // - // For x1=F, x2=F, x3=T: - // Truth-setting: pick ~u1(1), ~u2(3), u3(4) [the true literal] - // Clause 0 (1,2,3): communication edges (6,0), (7,2), (8,4). - // u1(0) not in cover -> must pick v6. u2(2) not in cover -> must pick v7. - // u3(4) in cover -> edge (8,4) covered. Triangle covered by v6 and v7. - // Clause 1 (-1,-2,3): communication edges (9,1), (10,3), (11,4). - // All three endpoints (~u1, ~u2, u3) in cover. Pick any 2 from triangle: v9, v10. - let vc_config = vec![ - false, true, false, true, true, false, true, true, false, true, true, false, - ]; - // Verify this is a valid vertex cover - assert!(reduction.target_problem().is_valid_solution(&vc_config)); - - let extracted = reduction.extract_solution(&vc_config).unwrap(); - assert_eq!(extracted, vec![false, false, true]); // x1=F, x2=F, x3=T - assert!(ksat.evaluate(&extracted).unwrap()); -} - -#[test] -fn test_ksatisfiability_to_minimumvertexcover_all_negated() { - // (~x1 v ~x2 v ~x3) — 7 satisfying assignments - let ksat = KSatisfiability::::new(3, vec![CNFClause::new(vec![-1, -2, -3])]); - let reduction = ReduceTo::>::reduce_to(&ksat) - .expect("reduction should succeed"); - - assert_satisfaction_round_trip_from_optimization_target( - &ksat, - &reduction, - "3SAT all negated -> MVC", - ); -} - -#[test] -fn test_ksatisfiability_to_minimumvertexcover_structure() { - // Verify edge structure for a simple case - let ksat = KSatisfiability::::new(2, vec![CNFClause::new(vec![1, -1, 2])]); - let reduction = ReduceTo::>::reduce_to(&ksat) - .expect("reduction should succeed"); - let target = reduction.target_problem(); - - // n=2, m=1 -> 4 + 3 = 7 vertices - assert_eq!(target.num_vertices(), 7); - // 2 truth-setting + 6*1 = 8 edges - assert_eq!(target.num_edges(), 8); - - // Minimum cover size for satisfiable formula = n + 2m = 2 + 2 = 4 - let solver = BruteForce::new(); - let witness = solver.solve(target).unwrap(); - assert!(witness.is_some()); - let vc_size: usize = witness - .unwrap() - .iter() - .filter(|&&selected| selected) - .count(); - assert_eq!(vc_size, 4); -} diff --git a/src/unit_tests/rules/ksatisfiability_monochromatictriangle.rs b/src/unit_tests/rules/ksatisfiability_monochromatictriangle.rs index 9256fc0bd..d6f10d97c 100644 --- a/src/unit_tests/rules/ksatisfiability_monochromatictriangle.rs +++ b/src/unit_tests/rules/ksatisfiability_monochromatictriangle.rs @@ -3,8 +3,10 @@ use crate::models::algebraic::{LinearConstraint, ILP}; use crate::models::formula::{CNFClause, KSatisfiability}; use crate::models::graph::MonochromaticTriangle; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::SolveOutcome; use crate::solvers::{ILPSolveError, ILPSolver}; use crate::topology::SimpleGraph; +use crate::traits::EvaluationError::InvalidConfiguration; use crate::traits::Problem; use crate::variant::K3; @@ -70,10 +72,40 @@ fn test_ksatisfiability_to_monochromatic_triangle_all_source_projections() { match ILPSolver::new().solve(&fixed) { Ok(solution) => { assert!(source.evaluate(&assignment).unwrap().0); - let coloring = to_ilp.extract_solution(&solution).unwrap(); - assert_eq!(reduction.extract_solution(&coloring).unwrap(), assignment); - let swapped = coloring.iter().map(|value| !value).collect(); - assert_eq!(reduction.extract_solution(&swapped).unwrap(), assignment); + let coloring = to_ilp + .recover_result( + reduction.target_problem(), + SolveOutcome::optimal(to_ilp.target_problem(), solution.clone()) + .unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); + assert_eq!( + reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), coloring.clone()) + .unwrap() + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"), + assignment + ); + let swapped: Vec<_> = coloring.iter().map(|value| !value).collect(); + assert_eq!( + reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), swapped.clone()) + .unwrap() + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"), + assignment + ); } Err(ILPSolveError::Infeasible) => assert!(!source.evaluate(&assignment).unwrap().0), Err(error) => panic!("unexpected solver error: {error}"), @@ -95,13 +127,24 @@ fn test_ksatisfiability_to_monochromatic_triangle_closed_loop() { let coloring = ILPSolver::new().solve(reduction.target_problem()).unwrap(); assert!( source - .evaluate(&reduction.extract_solution(&coloring).unwrap()) + .evaluate( + &reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), coloring.clone()) + .unwrap() + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution") + ) .unwrap() .0 ); - assert!( - !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![]), Ok(value) if { value.is_valid() }) - ); + assert!(matches!( + ReductionResult::target_problem(&reduction).evaluate(&vec![]), + Err(InvalidConfiguration(_)) + )); } #[test] @@ -156,7 +199,21 @@ fn test_ksatisfiability_to_monochromatic_triangle_short_and_repeated_literals() assert!(feasible); assert!( source - .evaluate(&reduction.extract_solution(&coloring).unwrap()) + .evaluate( + &reduction + .recover_result( + &source, + SolveOutcome::optimal( + reduction.target_problem(), + coloring.clone() + ) + .unwrap() + ) + .map(|result| result.into_solution().expect( + "qualifying target result must recover a source solution" + )) + .unwrap() + ) .unwrap() .0 ); @@ -173,7 +230,14 @@ fn test_ksatisfiability_to_monochromatic_triangle_zero_variables() { let reduction = ReduceTo::>::reduce_to(&source).unwrap(); let coloring = ILPSolver::new().solve(reduction.target_problem()).unwrap(); assert_eq!( - reduction.extract_solution(&coloring).unwrap(), + reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), coloring.clone()).unwrap() + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"), Vec::::new() ); } diff --git a/src/unit_tests/rules/ksatisfiability_oneinthreesatisfiability.rs b/src/unit_tests/rules/ksatisfiability_oneinthreesatisfiability.rs index 69b2b5e8d..558105ed6 100644 --- a/src/unit_tests/rules/ksatisfiability_oneinthreesatisfiability.rs +++ b/src/unit_tests/rules/ksatisfiability_oneinthreesatisfiability.rs @@ -1,7 +1,10 @@ use super::*; use crate::models::formula::{CNFClause, OneInThreeSatisfiability}; use crate::rules::test_helpers::assert_satisfaction_round_trip_from_satisfaction_target; +use crate::rules::ReductionResult; use crate::solvers::BruteForce; +use crate::solvers::SolveOutcome; +use crate::traits::EvaluationError::InvalidConfiguration; use crate::traits::Problem; use crate::variant::K3; @@ -104,7 +107,14 @@ fn test_ksatisfiability_to_oneinthreesatisfiability_extract_solution() { ]; assert!(target.evaluate(&target_solution).unwrap().0); - let extracted = reduction.extract_solution(&target_solution).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), target_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted, vec![false, false, true]); assert!(source.evaluate(&extracted).unwrap().0); } @@ -143,7 +153,14 @@ fn test_oneinthree_native_empty_short_and_sparse_clauses() { .collect(); if target.evaluate(&config).unwrap().0 { target_exists = true; - let extracted = reduction.extract_solution(&config).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), config.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted.len(), num_vars); assert!(source.evaluate(&extracted).unwrap().0); for (v, &value) in extracted.iter().enumerate() { @@ -169,7 +186,14 @@ fn test_oneinthree_all_literal_truth_patterns_and_auxiliary_assignments() { .collect(); if target.evaluate(&config).unwrap().0 { extensions[mask & 7] += 1; - let extracted = reduction.extract_solution(&config).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), config.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!(source.evaluate(&extracted).unwrap().0); } } @@ -180,10 +204,17 @@ fn test_oneinthree_all_literal_truth_patterns_and_auxiliary_assignments() { fn test_oneinthree_rejects_infeasible_target_assignments() { let source = KSatisfiability::::new(1, vec![CNFClause::new(vec![1; 3])]); let reduction = ReduceTo::::reduce_to(&source).unwrap(); - for config in [vec![], vec![false; 9], vec![true; 9], vec![false; 10]] { - assert!( - !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &config), Ok(value) if { value.is_valid() }) - ); + for config in [vec![], vec![false; 10]] { + assert!(matches!( + ReductionResult::target_problem(&reduction).evaluate(&config), + Err(InvalidConfiguration(_)) + )); + } + for config in [vec![false; 9], vec![true; 9]] { + assert!(!ReductionResult::target_problem(&reduction) + .evaluate(&config) + .unwrap() + .is_valid()); } } diff --git a/src/unit_tests/rules/ksatisfiability_preemptivescheduling.rs b/src/unit_tests/rules/ksatisfiability_preemptivescheduling.rs index eb94c7895..b34178728 100644 --- a/src/unit_tests/rules/ksatisfiability_preemptivescheduling.rs +++ b/src/unit_tests/rules/ksatisfiability_preemptivescheduling.rs @@ -3,6 +3,7 @@ use crate::models::algebraic::ILP; use crate::models::formula::CNFClause; use crate::models::misc::{PrecedenceConstrainedScheduling, PreemptiveScheduling}; use crate::solvers::ILPSolver; +use crate::solvers::SolveOutcome; use crate::traits::Problem; use crate::types::Min; use crate::variant::K3; @@ -37,7 +38,14 @@ fn solve_threshold_schedule_via_ilp( Err(crate::solvers::ILPSolveError::Infeasible) => return None, Err(error) => panic!("ILP execution failed: {error}"), }; - let slot_assignment = pcs_to_ilp.extract_solution(&ilp_solution).unwrap(); + let slot_assignment = pcs_to_ilp + .recover_result( + &pcs, + SolveOutcome::optimal(pcs_to_ilp.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let mut config = vec![vec![false; target.d_max()]; target.num_tasks()]; for (task, &slot) in slot_assignment.iter().enumerate() { @@ -75,7 +83,14 @@ fn test_ksatisfiability_to_preemptivescheduling_extract_solution_from_constructe Min(Some(4)) ); - let extracted = reduction.extract_solution(&schedule).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), schedule.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted, vec![true]); assert!(source.evaluate(&extracted).unwrap().0); } @@ -96,7 +111,14 @@ fn test_ksatisfiability_to_preemptivescheduling_multi_variable_round_trip() { construct_schedule_from_assignment(result.target_problem(), &[true, true, false], &source) .expect("satisfying assignment should yield a witness schedule"); - let extracted = result.extract_solution(&schedule).unwrap(); + let extracted = result + .recover_result( + &source, + SolveOutcome::optimal(result.target_problem(), schedule.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted, vec![true, true, false]); assert!(source.evaluate(&extracted).unwrap().0); } @@ -119,7 +141,14 @@ fn test_ksatisfiability_to_preemptivescheduling_closed_loop() { Min(Some(i64::try_from(reduction.threshold()).unwrap())) ); - let extracted = reduction.extract_solution(&target_solution).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), target_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted, vec![true]); assert!(source.evaluate(&extracted).unwrap().0); } diff --git a/src/unit_tests/rules/ksatisfiability_quadraticcongruences.rs b/src/unit_tests/rules/ksatisfiability_quadraticcongruences.rs index c1e7a6287..077308f55 100644 --- a/src/unit_tests/rules/ksatisfiability_quadraticcongruences.rs +++ b/src/unit_tests/rules/ksatisfiability_quadraticcongruences.rs @@ -1,6 +1,7 @@ use super::*; use crate::models::formula::CNFClause; use crate::solvers::BruteForce; +use crate::solvers::SolveOutcome; use crate::traits::Problem; use crate::types::Or; use num_traits::ToPrimitive; @@ -25,7 +26,17 @@ fn test_ksatisfiability_to_quadraticcongruences_closed_loop() { ); assert_eq!( source - .evaluate(&reduction.extract_solution(&solution).unwrap()) + .evaluate( + &reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), solution.clone()) + .unwrap() + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution") + ) .unwrap(), Or(true) ); @@ -58,7 +69,16 @@ fn test_native_clauses_and_arbitrary_crt_signs() { .collect(); let witness = witness_value_from_alphas(&signs, &construction.thetas); let valid = reduction.target_problem().evaluate(&witness).unwrap().0; - let extracted = reduction.extract_solution(&witness); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), witness.clone()).unwrap(), + ) + .map(|result| { + result + .into_solution() + .expect("qualifying target result must recover a source solution") + }); if valid { let extracted = extracted.unwrap(); assert_eq!(source.evaluate(&extracted).unwrap(), Or(true)); @@ -85,7 +105,18 @@ fn test_native_clauses_and_arbitrary_crt_signs() { reduction.target_problem().evaluate(&witness).unwrap(), Or(true) ); - assert_eq!(reduction.extract_solution(&witness).unwrap(), assignment); + assert_eq!( + reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), witness.clone()) + .unwrap() + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"), + assignment + ); expected.insert(assignment); } else { assert!(build_alphas(&construction, &assignment).is_none()); @@ -155,7 +186,13 @@ fn test_normalization_preserves_free_variables_and_formula() { let assignment = [true, true, true, true, false]; let witness = witness_config_for_assignment(&redundant, &assignment).unwrap(); assert_eq!( - second.extract_solution(&witness).unwrap(), + second + .recover_result( + &redundant, + SolveOutcome::optimal(second.target_problem(), witness).unwrap() + ) + .map(|outcome| outcome.into_solution().unwrap()) + .unwrap(), vec![false, true, false, false, false] ); assert!(witness_config_for_assignment(&redundant, &[]).is_none()); @@ -175,9 +212,6 @@ fn test_rejects_infeasible_and_out_of_bound_integers() { reduction.target.c() + 1u32, ] { assert_eq!(reduction.target.evaluate(&witness).unwrap(), Or(false)); - assert!( - !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &witness), Ok(value) if { value.is_valid() }) - ); } } diff --git a/src/unit_tests/rules/ksatisfiability_quadraticdiophantineequations.rs b/src/unit_tests/rules/ksatisfiability_quadraticdiophantineequations.rs index 4b9c73f43..b3b150797 100644 --- a/src/unit_tests/rules/ksatisfiability_quadraticdiophantineequations.rs +++ b/src/unit_tests/rules/ksatisfiability_quadraticdiophantineequations.rs @@ -2,6 +2,7 @@ use super::*; use crate::models::algebraic::QuadraticDiophantineEquations; use crate::models::formula::CNFClause; use crate::solvers::BruteForce; +use crate::solvers::SolveOutcome; use crate::traits::Problem; use crate::types::Or; use crate::variant::K3; @@ -30,7 +31,14 @@ fn test_ksatisfiability_to_quadraticdiophantineequations_closed_loop() { Or(true) ); - let extracted = reduction.extract_solution(&target_solution).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), target_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(source.evaluate(&extracted).unwrap(), Or(true)); } @@ -45,7 +53,14 @@ fn test_ksatisfiability_to_quadraticdiophantineequations_canonical_witness() { assert_eq!(target.evaluate(&target_config).unwrap(), Or(true)); - let extracted = reduction.extract_solution(&target_config).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), target_config.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted, vec![true, false, false]); assert_eq!(source.evaluate(&extracted).unwrap(), Or(true)); } diff --git a/src/unit_tests/rules/ksatisfiability_qubo.rs b/src/unit_tests/rules/ksatisfiability_qubo.rs index 1b91ea920..d95f061f5 100644 --- a/src/unit_tests/rules/ksatisfiability_qubo.rs +++ b/src/unit_tests/rules/ksatisfiability_qubo.rs @@ -1,8 +1,13 @@ use super::*; +use crate::models::decision::Decision; use crate::models::formula::CNFClause; +use crate::rules::ReductionResult; use crate::solvers::BruteForce; use crate::solvers::BruteForceProblem as _; +use crate::solvers::SolveOutcome; +use crate::traits::EvaluationError::InvalidConfiguration; use crate::traits::Problem; +use crate::types::OptimizationValue; use crate::variant::{K2, K3}; #[test] @@ -18,7 +23,8 @@ fn test_ksatisfiability_to_qubo_closed_loop() { CNFClause::new(vec![-2, -3]), // ¬x2 ∨ ¬x3 ], ); - let reduction = ReduceTo::>::reduce_to(&ksat).expect("reduction should succeed"); + let reduction = + ReduceTo::>>::reduce_to(&ksat).expect("reduction should succeed"); let qubo = reduction.target_problem(); let solver = BruteForce::new(); @@ -26,7 +32,14 @@ fn test_ksatisfiability_to_qubo_closed_loop() { // Verify all solutions satisfy all clauses for sol in &qubo_solutions { - let extracted = reduction.extract_solution(sol).unwrap(); + let extracted = reduction + .recover_result( + &ksat, + SolveOutcome::optimal(reduction.target_problem(), (sol).clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!(ksat.evaluate(&extracted).unwrap()); } } @@ -35,14 +48,22 @@ fn test_ksatisfiability_to_qubo_closed_loop() { fn test_ksatisfiability_to_qubo_simple() { // 2 vars, 1 clause: (x1 ∨ x2) → 3 satisfying assignments let ksat = KSatisfiability::::new(2, vec![CNFClause::new(vec![1, 2])]); - let reduction = ReduceTo::>::reduce_to(&ksat).expect("reduction should succeed"); + let reduction = + ReduceTo::>>::reduce_to(&ksat).expect("reduction should succeed"); let qubo = reduction.target_problem(); let solver = BruteForce::new(); let qubo_solutions = solver.find_all_witnesses(qubo).unwrap(); for sol in &qubo_solutions { - let extracted = reduction.extract_solution(sol).unwrap(); + let extracted = reduction + .recover_result( + &ksat, + SolveOutcome::optimal(reduction.target_problem(), (sol).clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!(ksat.evaluate(&extracted).unwrap()); } } @@ -59,14 +80,15 @@ fn test_ksatisfiability_to_qubo_contradiction() { CNFClause::new(vec![-1, -1]), // ¬x1 ∨ ¬x1 = ¬x1 ], ); - let reduction = ReduceTo::>::reduce_to(&ksat).expect("reduction should succeed"); + let reduction = + ReduceTo::>>::reduce_to(&ksat).expect("reduction should succeed"); let qubo = reduction.target_problem(); let solver = BruteForce::new(); let qubo_solutions = solver.find_all_witnesses(qubo).unwrap(); - // Both x=0 and x=1 satisfy exactly 1 clause - assert_eq!(qubo_solutions.len(), 2); + // Neither assignment meets the satisfaction threshold. + assert!(qubo_solutions.is_empty()); } #[test] @@ -80,14 +102,22 @@ fn test_ksatisfiability_to_qubo_reversed_vars() { CNFClause::new(vec![1, 2]), ], ); - let reduction = ReduceTo::>::reduce_to(&ksat).expect("reduction should succeed"); + let reduction = + ReduceTo::>>::reduce_to(&ksat).expect("reduction should succeed"); let qubo = reduction.target_problem(); let solver = BruteForce::new(); let qubo_solutions = solver.find_all_witnesses(qubo).unwrap(); for sol in &qubo_solutions { - let extracted = reduction.extract_solution(sol).unwrap(); + let extracted = reduction + .recover_result( + &ksat, + SolveOutcome::optimal(reduction.target_problem(), (sol).clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!(ksat.evaluate(&extracted).unwrap()); } } @@ -98,7 +128,8 @@ fn test_ksatisfiability_to_qubo_structure() { 3, vec![CNFClause::new(vec![1, 2]), CNFClause::new(vec![-1, 3])], ); - let reduction = ReduceTo::>::reduce_to(&ksat).expect("reduction should succeed"); + let reduction = + ReduceTo::>>::reduce_to(&ksat).expect("reduction should succeed"); let qubo = reduction.target_problem(); // QUBO should have at least the original variables @@ -120,7 +151,8 @@ fn test_k3satisfiability_to_qubo_closed_loop() { CNFClause::new(vec![3, -4, -5]), // x3 ∨ ¬x4 ∨ ¬x5 ], ); - let reduction = ReduceTo::>::reduce_to(&ksat).expect("reduction should succeed"); + let reduction = + ReduceTo::>>::reduce_to(&ksat).expect("reduction should succeed"); let qubo = reduction.target_problem(); // QUBO should have 5 + 7 = 12 variables @@ -131,7 +163,14 @@ fn test_k3satisfiability_to_qubo_closed_loop() { // Verify all extracted solutions maximize satisfied clauses for sol in &qubo_solutions { - let extracted = reduction.extract_solution(sol).unwrap(); + let extracted = reduction + .recover_result( + &ksat, + SolveOutcome::optimal(reduction.target_problem(), (sol).clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted.len(), 5); let satisfied = ksat.count_satisfied(&extracted).unwrap(); assert_eq!(satisfied, 7, "Expected all 7 clauses satisfied"); @@ -142,7 +181,8 @@ fn test_k3satisfiability_to_qubo_closed_loop() { fn test_k3satisfiability_to_qubo_single_clause() { // Single 3-SAT clause: (x1 ∨ x2 ∨ x3) — 7 satisfying assignments let ksat = KSatisfiability::::new(3, vec![CNFClause::new(vec![1, 2, 3])]); - let reduction = ReduceTo::>::reduce_to(&ksat).expect("reduction should succeed"); + let reduction = + ReduceTo::>>::reduce_to(&ksat).expect("reduction should succeed"); let qubo = reduction.target_problem(); // 3 vars + 1 auxiliary = 4 total @@ -153,7 +193,14 @@ fn test_k3satisfiability_to_qubo_single_clause() { // All solutions should satisfy the single clause for sol in &qubo_solutions { - let extracted = reduction.extract_solution(sol).unwrap(); + let extracted = reduction + .recover_result( + &ksat, + SolveOutcome::optimal(reduction.target_problem(), (sol).clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted.len(), 3); assert!(ksat.evaluate(&extracted).unwrap()); } @@ -165,14 +212,22 @@ fn test_k3satisfiability_to_qubo_single_clause() { fn test_k3satisfiability_to_qubo_all_negated() { // All negated: (¬x1 ∨ ¬x2 ∨ ¬x3) — 7 satisfying assignments let ksat = KSatisfiability::::new(3, vec![CNFClause::new(vec![-1, -2, -3])]); - let reduction = ReduceTo::>::reduce_to(&ksat).expect("reduction should succeed"); + let reduction = + ReduceTo::>>::reduce_to(&ksat).expect("reduction should succeed"); let qubo = reduction.target_problem(); let solver = BruteForce::new(); let qubo_solutions = solver.find_all_witnesses(qubo).unwrap(); for sol in &qubo_solutions { - let extracted = reduction.extract_solution(sol).unwrap(); + let extracted = reduction + .recover_result( + &ksat, + SolveOutcome::optimal(reduction.target_problem(), (sol).clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!(ksat.evaluate(&extracted).unwrap()); } // 7 out of 8 assignments satisfy (¬x1 ∨ ¬x2 ∨ ¬x3) @@ -181,8 +236,6 @@ fn test_k3satisfiability_to_qubo_all_negated() { #[test] fn test_sat_qubo_all_short_clauses_and_raw_targets() { - use crate::rules::AggregateReductionResult; - use crate::types::{Min, Or}; macro_rules! verify { ($k:ty, $width:expr) => {{ let mut clauses = vec![vec![]]; @@ -201,14 +254,14 @@ fn test_sat_qubo_all_short_clauses_and_raw_targets() { 1, vec![CNFClause::new(a.clone()), CNFClause::new(b.clone())], ); - let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + let reduction = ReduceTo::>>::reduce_to(&source).unwrap(); let target = ReductionResult::target_problem(&reduction); let mut minimum = i64::MAX; - for mask in 0..(1 << target.num_vars()) { - let witness: Vec<_> = (0..target.num_vars()) + for mask in 0..(1 << target.inner().num_vars()) { + let witness: Vec<_> = (0..target.inner().num_vars()) .map(|p| mask & (1 << p) != 0) .collect(); - let energy = target.evaluate(&witness).unwrap().0.unwrap(); + let energy = target.inner().evaluate(&witness).unwrap().0.unwrap(); let mut penalty = 0; for (j, clause) in [a, b].iter().enumerate() { let y: Vec = clause @@ -226,16 +279,36 @@ fn test_sat_qubo_all_short_clauses_and_raw_targets() { _ => unreachable!(), }; } - assert_eq!(energy - reduction.zero_penalty_energy, penalty); assert_eq!( - AggregateReductionResult::extract_value(&reduction, Min(Some(energy))), - Or(penalty == 0) + energy - *ReductionResult::target_problem(&reduction).bound(), + penalty + ); + assert_eq!( + crate::types::Or(OptimizationValue::meets_bound( + &(crate::types::Min(Some(energy))), + crate::rules::ReductionResult::target_problem(&reduction).bound() + )), + crate::types::Or(penalty == 0) ); if penalty == 0 { - let decoded = reduction.extract_solution(&witness).unwrap(); + let decoded = reduction + .recover_result( + &source, + SolveOutcome::optimal( + reduction.target_problem(), + witness.clone(), + ) + .unwrap(), + ) + .map(|result| { + result.into_solution().expect( + "qualifying target result must recover a source solution", + ) + }) + .unwrap(); assert!(source.evaluate(&decoded).unwrap().0); } else { - assert!(!matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &witness), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&reduction, value); value.is_valid() })); + assert!(!target.evaluate(&witness).unwrap().is_valid()); } minimum = minimum.min(energy); } @@ -243,22 +316,47 @@ fn test_sat_qubo_all_short_clauses_and_raw_targets() { .into_iter() .any(|x| source.evaluate(&vec![x]).unwrap().0); assert_eq!( - AggregateReductionResult::extract_value(&reduction, Min(Some(minimum))), - Or(sat) + crate::types::Or(OptimizationValue::meets_bound( + &(crate::types::Min(Some(minimum))), + crate::rules::ReductionResult::target_problem(&reduction).bound() + )), + crate::types::Or(sat) ); assert_eq!( - AggregateReductionResult::extract_value(&reduction, Min(None)), - Or(false) + crate::types::Or(OptimizationValue::meets_bound( + &(crate::types::Min(None)), + crate::rules::ReductionResult::target_problem(&reduction).bound() + )), + crate::types::Or(false) ); - assert!(!matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![]), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&reduction, value); value.is_valid() })); - assert!(!matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![false; target.num_vars() + 1]), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&reduction, value); value.is_valid() })); + assert!(matches!( + target.inner().evaluate(&vec![]), + Err(InvalidConfiguration(_)) + )); + assert!(matches!( + target + .inner() + .evaluate(&vec![false; target.inner().num_vars() + 1]), + Err(InvalidConfiguration(_)) + )); } } for n in [0, 3] { let source = KSatisfiability::<$k>::new(n, vec![]); - let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + let reduction = ReduceTo::>>::reduce_to(&source).unwrap(); assert_eq!( - reduction.extract_solution(&vec![false; n]).unwrap(), + reduction + .recover_result( + &source, + SolveOutcome::optimal( + reduction.target_problem(), + (&vec![false; n]).clone() + ) + .unwrap() + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"), vec![false; n] ); } @@ -286,24 +384,30 @@ fn test_sat_qubo_registered_aggregate_threshold() { 1, clauses.into_iter().map(CNFClause::new).collect(), ); - let reduction = ReduceTo::>::reduce_to(&source).unwrap(); - let mut witness = vec![false; reduction.target.num_vars()]; + let reduction = ReduceTo::>>::reduce_to(&source).unwrap(); + let mut witness = vec![false; reduction.target.inner().num_vars()]; witness[0] = expected; let entries = crate::rules::registry::reduction_entries(); let edge = entries .iter() .find(|e| { e.source_name == "KSatisfiability" - && e.target_name == "QUBO" + && e.target_name == "DecisionQUBO" && (e.source_variant_fn)() == KSatisfiability::<$k>::variant() && (e.target_variant_fn)() == QUBO::::variant() }) .unwrap(); let step = (edge.reduce_fn.unwrap())(&source).unwrap(); - assert_eq!( - step.interpret_optimum.as_ref().unwrap()(&witness).unwrap(), - expected - ); + let outcome = if expected { + SolveOutcome::optimal(reduction.target_problem(), witness).unwrap() + } else { + SolveOutcome::Infeasible + }; + let recovered = step + .witness + .recover_result_dyn(&source, crate::solvers::erase_outcome(outcome)) + .unwrap(); + assert_eq!(!matches!(recovered, SolveOutcome::Infeasible), expected); } }; } diff --git a/src/unit_tests/rules/ksatisfiability_registersufficiency.rs b/src/unit_tests/rules/ksatisfiability_registersufficiency.rs index 14c94748f..dd20600c5 100644 --- a/src/unit_tests/rules/ksatisfiability_registersufficiency.rs +++ b/src/unit_tests/rules/ksatisfiability_registersufficiency.rs @@ -1,6 +1,8 @@ use super::*; use crate::models::formula::CNFClause; use crate::models::misc::RegisterSufficiency; +use crate::rules::ReductionResult; +use crate::solvers::SolveOutcome; use crate::traits::Problem; use crate::types::Or; use crate::variant::K3; @@ -92,9 +94,6 @@ fn test_ksatisfiability_to_register_sufficiency_rejects_invalid_snapshot_order() let positions = positions_from_order(&order, target.num_vertices()); assert_eq!(target.evaluate(&positions).unwrap(), Or(false)); - assert!( - !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &positions), Ok(value) if { value.is_valid() }) - ); } #[test] @@ -116,7 +115,14 @@ fn test_ksatisfiability_to_register_sufficiency_forward_schedule() { Or(true) ); - let extracted = reduction.extract_solution(®ister_schedule).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), register_schedule.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(source.evaluate(&extracted).unwrap(), Or(true)); assert_eq!(extracted, vec![true]); } @@ -161,13 +167,22 @@ fn test_ksatisfiability_to_registersufficiency_closed_loop_boundaries() { let solution = BruteForce::new().solve(reduction.target_problem()).unwrap(); assert_eq!(solution.is_some(), feasible); if let Some(solution) = solution { - let extracted = reduction.extract_solution(&solution).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), solution.clone()) + .unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted, vec![false; declared]); assert_eq!(source.evaluate(&extracted).unwrap(), Or(true)); } else { - assert!( - !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![0]), Ok(value) if { value.is_valid() }) - ); + assert!(!ReductionResult::target_problem(&reduction) + .evaluate(&vec![0]) + .unwrap() + .is_valid()); } } } @@ -215,15 +230,30 @@ fn test_short_repeated_and_tautological_clauses() { source_value ); if source_value.0 { - let decoded = reduction.extract_solution(&positions).unwrap(); + let decoded = reduction + .recover_result( + &source, + SolveOutcome::optimal( + reduction.target_problem(), + positions.clone(), + ) + .unwrap(), + ) + .map(|result| { + result.into_solution().expect( + "qualifying target result must recover a source solution", + ) + }) + .unwrap(); assert_eq!(source.evaluate(&decoded).unwrap(), Or(true)); for &i in &reduction.source_variables { assert_eq!(decoded[i], original[i]); } } else { - assert!( - !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &positions), Ok(value) if { value.is_valid() }) - ); + assert!(!ReductionResult::target_problem(&reduction) + .evaluate(&positions) + .unwrap() + .is_valid()); } } } @@ -240,7 +270,14 @@ fn test_sparse_original_variables_and_padding_bound() { let layout = reduction.layout.as_ref().unwrap(); assert_eq!(layout.num_vars, 2); let positions = layout.schedule_for_assignment(&[true, false]); - let decoded = reduction.extract_solution(&positions).unwrap(); + let decoded = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), positions.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(decoded.len(), 17); assert!(decoded[1]); assert!(!decoded[16]); @@ -282,7 +319,14 @@ fn test_feasible_snapshot_may_leave_both_literals_uncomputed() { && positions[layout.x_neg(1)] > positions[layout.w(2)] ); assert_eq!( - reduction.extract_solution(&positions).unwrap(), + reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), positions.clone()).unwrap() + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"), vec![true, false, false] ); } diff --git a/src/unit_tests/rules/ksatisfiability_simultaneousincongruences.rs b/src/unit_tests/rules/ksatisfiability_simultaneousincongruences.rs index f36bb8157..701f8fd86 100644 --- a/src/unit_tests/rules/ksatisfiability_simultaneousincongruences.rs +++ b/src/unit_tests/rules/ksatisfiability_simultaneousincongruences.rs @@ -1,6 +1,7 @@ use super::*; use crate::models::formula::CNFClause; use crate::solvers::BruteForce; +use crate::solvers::SolveOutcome; use crate::traits::Problem; use crate::variant::K3; @@ -25,7 +26,14 @@ fn test_ksatisfiability_to_simultaneous_incongruences_closed_loop() { .solve(target) .unwrap() .expect("target should be satisfiable"); - let extracted = reduction.extract_solution(&target_solution).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), target_solution).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!(source.evaluate(&extracted).unwrap()); } @@ -82,7 +90,14 @@ fn test_ksatisfiability_to_simultaneous_incongruences_tautological_clause_is_red .solve(reduction.target_problem()) .unwrap() .expect("target should remain satisfiable"); - let extracted = reduction.extract_solution(&target_solution).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), target_solution).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!(source.evaluate(&extracted).unwrap()); } diff --git a/src/unit_tests/rules/ksatisfiability_subsetsum.rs b/src/unit_tests/rules/ksatisfiability_subsetsum.rs index 5cacbc628..c69dd2e14 100644 --- a/src/unit_tests/rules/ksatisfiability_subsetsum.rs +++ b/src/unit_tests/rules/ksatisfiability_subsetsum.rs @@ -1,6 +1,7 @@ use super::*; use crate::models::formula::CNFClause; use crate::solvers::BruteForce; +use crate::solvers::SolveOutcome; use crate::traits::Problem; use crate::variant::K3; use num_bigint::BigUint; @@ -30,7 +31,14 @@ fn test_ksatisfiability_to_subsetsum_closed_loop() { // Every SubsetSum solution must map back to a satisfying 3-SAT assignment for sol in &solutions { - let extracted = reduction.extract_solution(sol).unwrap(); + let extracted = reduction + .recover_result( + &ksat, + SolveOutcome::optimal(reduction.target_problem(), (sol).clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted.len(), 3); assert!(ksat.evaluate(&extracted).unwrap()); } @@ -73,7 +81,14 @@ fn test_ksatisfiability_to_subsetsum_single_clause() { // Each SubsetSum solution maps to a satisfying assignment let mut sat_assignments = std::collections::HashSet::new(); for sol in &solutions { - let extracted = reduction.extract_solution(sol).unwrap(); + let extracted = reduction + .recover_result( + &ksat, + SolveOutcome::optimal(reduction.target_problem(), (sol).clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!(ksat.evaluate(&extracted).unwrap()); sat_assignments.insert(extracted); } @@ -122,7 +137,14 @@ fn test_ksatisfiability_to_subsetsum_all_negated() { let mut sat_assignments = std::collections::HashSet::new(); for sol in &solutions { - let extracted = reduction.extract_solution(sol).unwrap(); + let extracted = reduction + .recover_result( + &ksat, + SolveOutcome::optimal(reduction.target_problem(), (sol).clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!(ksat.evaluate(&extracted).unwrap()); sat_assignments.insert(extracted); } @@ -156,7 +178,14 @@ fn test_ksatisfiability_to_subsetsum_extract_solution_example() { ]; assert!(target.evaluate(&specific_config).unwrap()); - let extracted = reduction.extract_solution(&specific_config).unwrap(); + let extracted = reduction + .recover_result( + &ksat, + SolveOutcome::optimal(reduction.target_problem(), specific_config.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted, vec![true, true, true]); // x1=T, x2=T, x3=T assert!(ksat.evaluate(&extracted).unwrap()); } diff --git a/src/unit_tests/rules/ksatisfiability_timetabledesign.rs b/src/unit_tests/rules/ksatisfiability_timetabledesign.rs index 49d711323..136d18c44 100644 --- a/src/unit_tests/rules/ksatisfiability_timetabledesign.rs +++ b/src/unit_tests/rules/ksatisfiability_timetabledesign.rs @@ -3,6 +3,7 @@ use crate::models::algebraic::ILP; use crate::models::formula::CNFClause; use crate::models::misc::TimetableDesign; use crate::solvers::ILPSolver; +use crate::solvers::SolveOutcome; use crate::traits::Problem; use crate::variant::K3; @@ -64,7 +65,14 @@ fn test_ksatisfiability_to_timetabledesign_extract_solution_from_constructed_tim .0 ); - let extracted = reduction.extract_solution(&target_solution).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), target_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!(source.evaluate(&extracted).unwrap().0); } @@ -81,7 +89,14 @@ fn test_ksatisfiability_to_timetabledesign_multi_variable_round_trip() { ) .expect("a satisfying 3SAT assignment should lift to a timetable witness"); - let extracted = reduction.extract_solution(&target_solution).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), target_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted, vec![true, true, false]); assert!(source.evaluate(&extracted).unwrap().0); } @@ -96,7 +111,14 @@ fn test_ksatisfiability_to_timetabledesign_closed_loop() { let ilp_solution = ILPSolver::new() .solve(target_reduction.target_problem()) .expect("satisfiable source instance should produce a feasible timetable"); - let target_solution = target_reduction.extract_solution(&ilp_solution).unwrap(); + let target_solution = target_reduction + .recover_result( + reduction.target_problem(), + SolveOutcome::optimal(target_reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!( reduction @@ -106,7 +128,14 @@ fn test_ksatisfiability_to_timetabledesign_closed_loop() { .0 ); - let extracted = reduction.extract_solution(&target_solution).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), target_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!(source.evaluate(&extracted).unwrap().0); } diff --git a/src/unit_tests/rules/lengthboundeddisjointpaths_ilp.rs b/src/unit_tests/rules/lengthboundeddisjointpaths_ilp.rs index 8455aaa37..ef05ab332 100644 --- a/src/unit_tests/rules/lengthboundeddisjointpaths_ilp.rs +++ b/src/unit_tests/rules/lengthboundeddisjointpaths_ilp.rs @@ -2,8 +2,11 @@ use super::*; use crate::models::algebraic::ILP; use crate::rules::test_helpers::assert_bf_vs_ilp; use crate::rules::ReduceTo; +use crate::rules::ReductionResult; +use crate::solvers::SolveOutcome; use crate::solvers::{BruteForce, ILPSolver}; use crate::topology::SimpleGraph; +use crate::traits::EvaluationError::InvalidConfiguration; use crate::traits::Problem; use crate::types::Max; @@ -51,7 +54,15 @@ fn test_lengthboundeddisjointpaths_to_ilp_triangle_subgraphs() { assert_eq!(source.evaluate(&reference).unwrap(), Max(Some(expected))); 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(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), target_solution.clone()) + .unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(source.evaluate(&extracted).unwrap(), Max(Some(expected))); } } @@ -75,7 +86,14 @@ fn test_lengthboundeddisjointpaths_to_ilp_preserves_edge_order() { .target_problem() .is_feasible(&target_solution) .unwrap()); - let extracted = reduction.extract_solution(&target_solution).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), target_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!( extracted, vec![ @@ -102,7 +120,14 @@ fn test_lengthboundeddisjointpaths_to_ilp_extracts_path_from_circulation() { .target_problem() .is_feasible(&target_solution) .unwrap()); - let extracted = reduction.extract_solution(&target_solution).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), target_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted, vec![vec![true, false, false, false]]); assert_eq!(source.evaluate(&extracted).unwrap(), Max(Some(1))); } @@ -112,9 +137,14 @@ fn test_lengthboundeddisjointpaths_to_ilp_extracts_path_from_circulation() { fn test_lengthboundeddisjointpaths_to_ilp_rejects_invalid_target_solutions() { let source = LengthBoundedDisjointPaths::new(SimpleGraph::new(2, vec![(0, 1)]), 0, 1, 1); let reduction = ReduceTo::>::reduce_to(&source).unwrap(); - for solution in [vec![], vec![2, 0, 1], vec![0, 0, 1], vec![1, 0, 0]] { - assert!( - !matches!(crate::traits::Problem::evaluate(reduction.target_problem(), &solution), Ok(value) if value.is_valid()) - ); + assert!(matches!( + ReductionResult::target_problem(&reduction).evaluate(&vec![]), + Err(InvalidConfiguration(_)) + )); + for solution in [vec![2, 0, 1], vec![0, 0, 1], vec![1, 0, 0]] { + assert!(!ReductionResult::target_problem(&reduction) + .evaluate(&solution) + .unwrap() + .is_valid()); } } diff --git a/src/unit_tests/rules/longestcircuit_ilp.rs b/src/unit_tests/rules/longestcircuit_ilp.rs index 46f03d612..ca7811768 100644 --- a/src/unit_tests/rules/longestcircuit_ilp.rs +++ b/src/unit_tests/rules/longestcircuit_ilp.rs @@ -1,5 +1,6 @@ use super::*; use crate::rules::test_helpers::assert_bf_vs_ilp; +use crate::solvers::SolveOutcome; use crate::solvers::{BruteForce, ILPSolver}; use crate::topology::SimpleGraph; use crate::traits::Problem; @@ -56,7 +57,14 @@ fn test_longestcircuit_to_ilp_closed_loop() { let ilp_solution = ilp_solver .solve(reduction.target_problem()) .expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!( problem.evaluate(&extracted).unwrap().0.is_some(), "ILP solution should be a valid circuit" @@ -92,7 +100,14 @@ fn test_solution_extraction() { let ilp_solution = ilp_solver .solve(reduction.target_problem()) .expect("solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!(problem.evaluate(&extracted).unwrap().0.is_some()); } @@ -118,7 +133,14 @@ fn test_longestcircuit_to_ilp_cycle_excludes_any_vertex() { ); let reduction = ReduceTo::>::reduce_to(&problem).unwrap(); let target_solution = ILPSolver::new().solve(reduction.target_problem()).unwrap(); - let extracted = reduction.extract_solution(&target_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), target_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted, vec![false, true, true, true]); } } @@ -136,7 +158,14 @@ fn test_longestcircuit_to_ilp_selects_one_best_cycle() { let problem = LongestCircuit::new(SimpleGraph::new(6, edges), lengths); let reduction = ReduceTo::>::reduce_to(&problem).unwrap(); let target_solution = ILPSolver::new().solve(reduction.target_problem()).unwrap(); - let extracted = reduction.extract_solution(&target_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), target_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!( problem.evaluate(&extracted).unwrap(), crate::types::Max(Some(15)) diff --git a/src/unit_tests/rules/longestcommonsubsequence_ilp.rs b/src/unit_tests/rules/longestcommonsubsequence_ilp.rs index f920fa554..c7e0361f3 100644 --- a/src/unit_tests/rules/longestcommonsubsequence_ilp.rs +++ b/src/unit_tests/rules/longestcommonsubsequence_ilp.rs @@ -1,5 +1,6 @@ use super::*; use crate::models::algebraic::ILP; +use crate::solvers::SolveOutcome; use crate::solvers::{BruteForce, ILPSolver}; use crate::traits::Problem; use crate::types::Max; @@ -17,7 +18,14 @@ fn test_lcs_to_ilp_yes_instance() { let ilp_solver = ILPSolver::new(); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be feasible"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted.len(), problem.max_length()); let value = problem.evaluate(&extracted).unwrap(); @@ -35,7 +43,14 @@ fn test_lcs_to_ilp_closed_loop_three_strings() { let ilp_solver = ILPSolver::new(); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be feasible"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let ilp_value = problem.evaluate(&extracted).unwrap(); assert!(matches!(ilp_value, Max(Some(_)))); @@ -57,7 +72,14 @@ fn test_lcs_to_ilp_extracts_valid_witness() { let ilp_solver = ILPSolver::new(); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be feasible"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted.len(), problem.max_length()); let value = problem.evaluate(&extracted).unwrap(); @@ -74,7 +96,14 @@ fn test_lcs_to_ilp_matches_brute_force() { let ilp_solver = ILPSolver::new(); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be feasible"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let ilp_value = problem.evaluate(&extracted).unwrap(); let brute_force = BruteForce::new(); @@ -95,7 +124,14 @@ fn test_lcs_to_ilp_single_position_all_padding() { let ilp_solver = ILPSolver::new(); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be feasible"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let value = problem.evaluate(&extracted).unwrap(); assert_eq!(value, Max(Some(0))); diff --git a/src/unit_tests/rules/longestcommonsubsequence_maximumindependentset.rs b/src/unit_tests/rules/longestcommonsubsequence_maximumindependentset.rs index 7daf6a1e0..7eda8d642 100644 --- a/src/unit_tests/rules/longestcommonsubsequence_maximumindependentset.rs +++ b/src/unit_tests/rules/longestcommonsubsequence_maximumindependentset.rs @@ -1,6 +1,7 @@ use super::*; use crate::rules::test_helpers::assert_optimization_round_trip_from_optimization_target; use crate::solvers::BruteForce; +use crate::solvers::SolveOutcome; use crate::topology::Graph; use crate::traits::Problem; @@ -127,7 +128,14 @@ fn test_lcs_to_mis_extract_solution() { .solve(reduction.target_problem()) .unwrap() .expect("should have a solution"); - let source_sol = reduction.extract_solution(&witness).unwrap(); + let source_sol = reduction + .recover_result( + &lcs, + SolveOutcome::optimal(reduction.target_problem(), witness.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); // The extracted solution should be valid for the source let value = lcs.evaluate(&source_sol).unwrap(); diff --git a/src/unit_tests/rules/longestpath_ilp.rs b/src/unit_tests/rules/longestpath_ilp.rs index 8e23b37be..1dedb4942 100644 --- a/src/unit_tests/rules/longestpath_ilp.rs +++ b/src/unit_tests/rules/longestpath_ilp.rs @@ -1,5 +1,6 @@ use super::*; use crate::models::algebraic::{ObjectiveSense, ILP}; +use crate::solvers::SolveOutcome; use crate::solvers::{BruteForce, ILPSolver}; use crate::topology::SimpleGraph; use crate::traits::Problem; @@ -72,7 +73,14 @@ fn test_longestpath_to_ilp_closed_loop_on_issue_example() { let ilp_solution = ilp_solver .solve(reduction.target_problem()) .expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!(problem.is_valid_solution(&extracted)); assert_eq!(problem.evaluate(&extracted).unwrap(), best_value); @@ -86,7 +94,14 @@ fn test_solution_extraction_from_handcrafted_ilp_assignment() { // 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]; - let extracted = reduction.extract_solution(&target_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), target_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted, vec![true, true]); assert_eq!(problem.evaluate(&extracted).unwrap(), Max(Some(5))); @@ -106,7 +121,14 @@ fn test_source_equals_target_uses_empty_path() { let ilp_solution = ilp_solver .solve(reduction.target_problem()) .expect("ILP should solve the trivial empty-path case"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted, vec![false, false, false]); assert_eq!(problem.evaluate(&extracted).unwrap(), Max(Some(0))); diff --git a/src/unit_tests/rules/maxcut_minimumcutintoboundedsets.rs b/src/unit_tests/rules/maxcut_minimumcutintoboundedsets.rs index 51c788b17..52488597b 100644 --- a/src/unit_tests/rules/maxcut_minimumcutintoboundedsets.rs +++ b/src/unit_tests/rules/maxcut_minimumcutintoboundedsets.rs @@ -2,6 +2,7 @@ use super::*; use crate::models::graph::{MaxCut, MinimumCutIntoBoundedSets}; use crate::rules::test_helpers::assert_optimization_round_trip_from_optimization_target; use crate::rules::traits::ReduceTo; +use crate::solvers::SolveOutcome; use crate::topology::SimpleGraph; #[test] @@ -124,7 +125,14 @@ fn test_maxcut_to_minimumcutintoboundedsets_extract_solution_size() { // Target has 8 vertices, extract should return 3 let dummy_target_sol = vec![false, true, false, true, false, true, false, true]; - let extracted = reduction.extract_solution(&dummy_target_sol).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), dummy_target_sol.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted.len(), 3); } diff --git a/src/unit_tests/rules/maxcut_minimummatrixcover.rs b/src/unit_tests/rules/maxcut_minimummatrixcover.rs index 7e6ef4a1b..5ea3fce00 100644 --- a/src/unit_tests/rules/maxcut_minimummatrixcover.rs +++ b/src/unit_tests/rules/maxcut_minimummatrixcover.rs @@ -4,6 +4,7 @@ use crate::models::graph::MaxCut; use crate::rules::test_helpers::assert_optimization_round_trip_from_optimization_target; use crate::rules::traits::ReduceTo; use crate::solvers::BruteForce; +use crate::solvers::SolveOutcome; use crate::topology::SimpleGraph; use crate::traits::Problem; use crate::types::{Max, Min}; @@ -203,7 +204,17 @@ fn test_extract_solution_is_identity() { let reduction = ReduceTo::::reduce_to(&source).expect("reduction should succeed"); let target_sol = vec![true, false, true]; - assert_eq!(reduction.extract_solution(&target_sol).unwrap(), target_sol); + assert_eq!( + reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), target_sol.clone()).unwrap() + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"), + target_sol + ); } #[test] diff --git a/src/unit_tests/rules/maximalis_ilp.rs b/src/unit_tests/rules/maximalis_ilp.rs index 928156425..11c6e8da3 100644 --- a/src/unit_tests/rules/maximalis_ilp.rs +++ b/src/unit_tests/rules/maximalis_ilp.rs @@ -1,4 +1,5 @@ use super::*; +use crate::solvers::SolveOutcome; use crate::solvers::{BruteForce, ILPSolver}; use crate::topology::SimpleGraph; use crate::traits::Problem; @@ -32,7 +33,14 @@ fn test_maximalis_to_ilp_bf_vs_ilp() { let bf_value = problem.evaluate(&bf_solutions[0]).unwrap(); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let ilp_value = problem.evaluate(&extracted).unwrap(); assert_eq!(bf_value, ilp_value); @@ -48,7 +56,14 @@ fn test_solution_extraction() { let ilp_solution = ilp_solver .solve(reduction.target_problem()) .expect("solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!(problem.evaluate(&extracted).unwrap().is_valid()); } diff --git a/src/unit_tests/rules/maximum2satisfiability_ilp.rs b/src/unit_tests/rules/maximum2satisfiability_ilp.rs index 758a80d6a..0687875dd 100644 --- a/src/unit_tests/rules/maximum2satisfiability_ilp.rs +++ b/src/unit_tests/rules/maximum2satisfiability_ilp.rs @@ -2,6 +2,7 @@ use super::*; use crate::models::algebraic::{Comparison, ObjectiveSense, ILP}; use crate::models::formula::CNFClause; use crate::rules::test_helpers::assert_bf_vs_ilp; +use crate::solvers::SolveOutcome; use crate::solvers::{BruteForce, ILPSolver}; use crate::traits::Problem; @@ -30,7 +31,14 @@ fn test_maximum2satisfiability_to_ilp_closed_loop() { let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); // Optimal: 6 satisfied clauses let value = problem.evaluate(&extracted).unwrap(); assert_eq!(value, crate::types::Max(Some(6))); @@ -47,7 +55,14 @@ fn test_maximum2satisfiability_to_ilp_bf_vs_ilp() { let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let ilp_value = problem.evaluate(&extracted).unwrap(); assert_eq!(bf_value, ilp_value); @@ -102,7 +117,14 @@ fn test_maximum2satisfiability_to_ilp_all_satisfiable() { let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let value = problem.evaluate(&extracted).unwrap(); // Both clauses should be satisfiable assert_eq!(value, crate::types::Max(Some(2))); diff --git a/src/unit_tests/rules/maximum2satisfiability_maxcut.rs b/src/unit_tests/rules/maximum2satisfiability_maxcut.rs index 17bf8c50f..60a61f147 100644 --- a/src/unit_tests/rules/maximum2satisfiability_maxcut.rs +++ b/src/unit_tests/rules/maximum2satisfiability_maxcut.rs @@ -2,6 +2,7 @@ use super::*; use crate::models::formula::{CNFClause, Maximum2Satisfiability}; use crate::models::graph::MaxCut; use crate::rules::test_helpers::assert_optimization_round_trip_from_optimization_target; +use crate::solvers::SolveOutcome; use crate::topology::SimpleGraph; use crate::traits::Problem; use crate::types::Max; @@ -67,7 +68,14 @@ fn test_maximum2satisfiability_to_maxcut_issue_affine_relation_on_all_partitions let target_solution: Vec = (0..target.num_vertices()) .map(|bit| ((mask >> bit) & 1) == 1) .collect(); - let source_solution = reduction.extract_solution(&target_solution).unwrap(); + let source_solution = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), target_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let satisfied = source.evaluate(&source_solution).unwrap().unwrap(); let cut_weight = target.evaluate(&target_solution).unwrap().unwrap(); @@ -87,22 +95,49 @@ fn test_maximum2satisfiability_to_maxcut_extract_solution_uses_reference_vertex( assert_eq!( reduction - .extract_solution(&vec![false, true, false, false]) - .unwrap(), + .recover_result( + &source, + SolveOutcome::optimal( + reduction.target_problem(), + vec![false, true, false, false].clone() + ) + .unwrap() + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"), vec![false, true, true] ); assert_eq!( reduction - .extract_solution(&vec![true, false, true, true]) - .unwrap(), + .recover_result( + &source, + SolveOutcome::optimal( + reduction.target_problem(), + vec![true, false, true, true].clone() + ) + .unwrap() + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"), vec![false, true, true] ); assert_eq!( source .evaluate( &reduction - .extract_solution(&vec![true, false, true, true]) + .recover_result( + &source, + SolveOutcome::optimal( + reduction.target_problem(), + vec![true, false, true, true].clone() + ) + .unwrap() + ) .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution") ) .unwrap(), Max(Some(5)) diff --git a/src/unit_tests/rules/maximumclique_ilp.rs b/src/unit_tests/rules/maximumclique_ilp.rs index 4cc94c341..c640eb98e 100644 --- a/src/unit_tests/rules/maximumclique_ilp.rs +++ b/src/unit_tests/rules/maximumclique_ilp.rs @@ -1,5 +1,6 @@ use super::*; use crate::solvers::ILPSolver; +use crate::solvers::SolveOutcome; /// Check if a configuration represents a valid clique in the graph. /// A clique is valid if all selected vertices are pairwise adjacent. @@ -122,7 +123,14 @@ fn test_maximumclique_to_ilp_closed_loop() { // Solve via ILP reduction let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); // Both should find optimal size = 3 (all vertices form a clique) let ilp_size = clique_size(&problem, &extracted); @@ -154,7 +162,14 @@ fn test_ilp_solution_equals_brute_force_path() { // Solve via ILP let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let ilp_size = clique_size(&problem, &extracted); assert_eq!(bf_size, 2); @@ -181,7 +196,14 @@ fn test_ilp_solution_equals_brute_force_weighted() { let bf_obj = brute_force_max_clique(&problem); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let ilp_obj = clique_size(&problem, &extracted); assert_eq!(bf_obj, 101); @@ -200,7 +222,14 @@ fn test_solution_extraction() { // Test that extraction works correctly (1:1 mapping) let ilp_solution = vec![1, 1, 0, 0]; - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted, vec![true, true, false, false]); // Verify this is a valid clique (0 and 1 are adjacent) @@ -236,7 +265,14 @@ fn test_empty_graph() { let ilp_solver = ILPSolver::new(); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); // Only one vertex should be selected assert_eq!(extracted.iter().filter(|&&selected| selected).count(), 1); @@ -261,7 +297,14 @@ fn test_complete_graph() { let ilp_solver = ILPSolver::new(); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); // All vertices should be selected assert_eq!(extracted, vec![true, true, true, true]); @@ -284,7 +327,14 @@ fn test_bipartite_graph() { let ilp_solver = ILPSolver::new(); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!(is_valid_clique(&problem, &extracted)); assert_eq!(clique_size(&problem, &extracted), 2); @@ -311,7 +361,14 @@ fn test_star_graph() { let ilp_solver = ILPSolver::new(); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!(is_valid_clique(&problem, &extracted)); assert_eq!(clique_size(&problem, &extracted), 2); diff --git a/src/unit_tests/rules/maximumclique_maximumindependentset.rs b/src/unit_tests/rules/maximumclique_maximumindependentset.rs index 2097108cf..45907db80 100644 --- a/src/unit_tests/rules/maximumclique_maximumindependentset.rs +++ b/src/unit_tests/rules/maximumclique_maximumindependentset.rs @@ -1,6 +1,7 @@ use super::*; use crate::rules::test_helpers::assert_optimization_round_trip_from_optimization_target; use crate::solvers::BruteForce; +use crate::solvers::SolveOutcome; use crate::topology::Graph; use crate::traits::Problem; use crate::types::One; @@ -54,7 +55,14 @@ fn test_maximumclique_to_maximumindependentset_triangle() { .any(|s| s.iter().filter(|&&selected| selected).count() == 3)); // Extract solution: should be the full clique {0,1,2} - let source_sol = reduction.extract_solution(&target_solutions[0]).unwrap(); + let source_sol = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), target_solutions[0].clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(source.evaluate(&source_sol).unwrap().unwrap(), 3); } diff --git a/src/unit_tests/rules/maximumcokplex_ilp.rs b/src/unit_tests/rules/maximumcokplex_ilp.rs index aa92d32f2..f24c0cc9f 100644 --- a/src/unit_tests/rules/maximumcokplex_ilp.rs +++ b/src/unit_tests/rules/maximumcokplex_ilp.rs @@ -3,6 +3,7 @@ use crate::models::algebraic::{ObjectiveSense, ILP}; use crate::models::graph::MaximumCoKPlex; use crate::rules::test_helpers::assert_bf_vs_ilp; use crate::solvers::ILPSolver; +use crate::solvers::SolveOutcome; use crate::topology::SimpleGraph; use crate::traits::Problem; use crate::types::{Max, One}; @@ -70,7 +71,14 @@ fn test_maximumcokplex_to_ilp_k_equals_1_regression() { let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("k=1 instance should be ILP-solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(source.evaluate(&extracted).unwrap(), Max(Some(2))); assert_eq!(extracted.iter().filter(|&&selected| selected).count(), 2); @@ -83,7 +91,14 @@ fn test_maximumcokplex_to_ilp_extract_solution_identity() { let reduction: ReductionCoKPlexToILP = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let target_solution = vec![1, 0, 1, 0, 1]; - let extracted = reduction.extract_solution(&target_solution).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), target_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted, vec![true, false, true, false, true]); assert_eq!(source.evaluate(&extracted).unwrap(), Max(Some(12))); diff --git a/src/unit_tests/rules/maximumcommonedgesubgraph_ilp.rs b/src/unit_tests/rules/maximumcommonedgesubgraph_ilp.rs index 7d63945d2..595781ced 100644 --- a/src/unit_tests/rules/maximumcommonedgesubgraph_ilp.rs +++ b/src/unit_tests/rules/maximumcommonedgesubgraph_ilp.rs @@ -3,6 +3,7 @@ use crate::models::algebraic::{ObjectiveSense, ILP}; use crate::models::graph::{LabelledArc, LabelledDigraph, MaximumCommonEdgeSubgraph}; use crate::rules::test_helpers::assert_bf_vs_ilp; use crate::solvers::ILPSolver; +use crate::solvers::SolveOutcome; use crate::traits::Problem; use crate::types::Max; @@ -64,7 +65,14 @@ fn test_maximumcommonedgesubgraph_to_ilp_closed_loop() { let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("matched paths ILP must be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!(source.is_valid_solution(&extracted)); assert_eq!(source.evaluate(&extracted).unwrap(), Max(Some(2))); @@ -95,7 +103,14 @@ fn test_maximumcommonedgesubgraph_to_ilp_truncated_target() { let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("truncated ILP must be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!(source.is_valid_solution(&extracted)); assert_eq!(source.evaluate(&extracted).unwrap(), Max(Some(1))); @@ -120,7 +135,14 @@ fn test_maximumcommonedgesubgraph_to_ilp_empty_graphs() { let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("empty-arc ILP must be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!(source.is_valid_solution(&extracted)); assert_eq!(source.evaluate(&extracted).unwrap(), Max(Some(0))); } @@ -138,7 +160,14 @@ fn test_maximumcommonedgesubgraph_to_ilp_self_loop() { let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("self-loop ILP must be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!(source.is_valid_solution(&extracted)); assert_eq!(source.evaluate(&extracted).unwrap(), Max(Some(1))); diff --git a/src/unit_tests/rules/maximumcontactmapoverlap_ilp.rs b/src/unit_tests/rules/maximumcontactmapoverlap_ilp.rs index 75b2378ce..d4a4290d6 100644 --- a/src/unit_tests/rules/maximumcontactmapoverlap_ilp.rs +++ b/src/unit_tests/rules/maximumcontactmapoverlap_ilp.rs @@ -3,6 +3,7 @@ use crate::models::algebraic::{ObjectiveSense, ILP}; use crate::models::graph::MaximumContactMapOverlap; use crate::rules::test_helpers::assert_bf_vs_ilp; use crate::solvers::ILPSolver; +use crate::solvers::SolveOutcome; use crate::traits::Problem; use crate::types::Max; @@ -49,7 +50,14 @@ fn test_maximumcontactmapoverlap_to_ilp_closed_loop() { let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("canonical CMO ILP must be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); // The optimal alignment preserves both contacts of G_1. assert!(source.is_valid_solution(&extracted)); @@ -74,7 +82,14 @@ fn test_maximumcontactmapoverlap_to_ilp_trivial_no_contacts() { let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("empty-contact ILP must be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!(source.is_valid_solution(&extracted)); assert_eq!(source.evaluate(&extracted).unwrap(), Max(Some(0))); } @@ -102,7 +117,14 @@ fn test_maximumcontactmapoverlap_to_ilp_order_preserving_forbidden() { let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP must be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!(source.is_valid_solution(&extracted)); assert_eq!(source.evaluate(&extracted).unwrap(), Max(Some(1))); @@ -129,7 +151,14 @@ fn test_maximumcontactmapoverlap_to_ilp_extract_solution_partial() { let mut target_sol = vec![0_i64; reduction.target_problem().num_vars()]; target_sol[1] = 1; target_sol[n2 + 2] = 1; - let extracted = reduction.extract_solution(&target_sol).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), target_sol.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); // Encoding: vertex j of G_2 is represented as j+1. assert_eq!(extracted, vec![2, 3]); assert!(source.is_valid_solution(&extracted)); diff --git a/src/unit_tests/rules/maximumdomaticnumber_ilp.rs b/src/unit_tests/rules/maximumdomaticnumber_ilp.rs index 54a6e2e1a..5a8edfff3 100644 --- a/src/unit_tests/rules/maximumdomaticnumber_ilp.rs +++ b/src/unit_tests/rules/maximumdomaticnumber_ilp.rs @@ -1,5 +1,6 @@ use super::*; use crate::models::algebraic::ObjectiveSense; +use crate::solvers::SolveOutcome; use crate::solvers::{BruteForce, ILPSolver}; use crate::traits::Problem; use crate::types::Max; @@ -21,7 +22,14 @@ fn test_maximumdomaticnumber_to_ilp_closed_loop() { // Solve via ILP reduction let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let ilp_value = problem.evaluate(&extracted).unwrap(); // Both should find domatic number = 2 @@ -76,7 +84,14 @@ fn test_maximumdomaticnumber_to_ilp_complete_graph() { let ilp_solver = ILPSolver::new(); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let value = problem.evaluate(&extracted).unwrap(); assert_eq!(value, Max(Some(3))); @@ -92,7 +107,14 @@ fn test_maximumdomaticnumber_to_ilp_single_vertex() { let ilp_solver = ILPSolver::new(); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let value = problem.evaluate(&extracted).unwrap(); assert_eq!(value, Max(Some(1))); @@ -111,7 +133,14 @@ fn test_maximumdomaticnumber_to_ilp_solution_extraction() { // x_{2,0}=1, x_{2,1}=0, x_{2,2}=0, // y_0=1, y_1=1, y_2=0 let ilp_solution = vec![1, 0, 0, 0, 1, 0, 1, 0, 0, 1, 1, 0]; - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted, vec![0, 1, 0]); // Verify this is a valid partition with 2 dominating sets diff --git a/src/unit_tests/rules/maximumedgeweightedkclique_ilp.rs b/src/unit_tests/rules/maximumedgeweightedkclique_ilp.rs index aaf58b7ee..0df071d64 100644 --- a/src/unit_tests/rules/maximumedgeweightedkclique_ilp.rs +++ b/src/unit_tests/rules/maximumedgeweightedkclique_ilp.rs @@ -2,6 +2,7 @@ use super::*; use crate::models::algebraic::{ObjectiveSense, ILP}; use crate::models::graph::MaximumEdgeWeightedKClique; use crate::rules::test_helpers::assert_bf_vs_ilp; +use crate::solvers::SolveOutcome; use crate::topology::SimpleGraph; use crate::traits::Problem; use crate::types::Max; @@ -42,7 +43,14 @@ fn test_maximumedgeweightedkclique_to_ilp_extract_solution_identity() { let source = issue_instance(); let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let target_solution = vec![1, 1, 1, 0, 1, 1, 1, 0, 0]; - let extracted = reduction.extract_solution(&target_solution).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), target_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted, vec![true, true, true, false]); assert_eq!(source.evaluate(&extracted).unwrap(), Max(Some(8))); } diff --git a/src/unit_tests/rules/maximumindependentset_gridgraph.rs b/src/unit_tests/rules/maximumindependentset_gridgraph.rs index 7053dde31..c62eab53b 100644 --- a/src/unit_tests/rules/maximumindependentset_gridgraph.rs +++ b/src/unit_tests/rules/maximumindependentset_gridgraph.rs @@ -2,6 +2,7 @@ use super::*; use crate::models::graph::MaximumIndependentSet; use crate::rules::unitdiskmapping::ksg; use crate::solvers::BruteForce; +use crate::solvers::SolveOutcome; use crate::topology::{Graph, KingsSubgraph, SimpleGraph}; use crate::types::One; @@ -90,7 +91,14 @@ fn test_mis_simple_one_to_kings_one_closed_loop() { let grid_solutions = solver.find_all_witnesses(target).unwrap(); assert!(!grid_solutions.is_empty()); - let original_solution = result.extract_solution(&grid_solutions[0]).unwrap(); + let original_solution = result + .recover_result( + &problem, + SolveOutcome::optimal(result.target_problem(), grid_solutions[0].clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(original_solution.len(), 5); let size: usize = original_solution .iter() @@ -120,7 +128,14 @@ fn test_mis_simple_one_to_kings_one_all_four_vertex_graphs() { let target_solution = solve_mis_config(target.graph().num_vertices(), &target.graph().edges()); let target_solution = crate::config::config_to_bits(&target_solution); - let source_solution = reduction.extract_solution(&target_solution).unwrap(); + let source_solution = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), target_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!( is_independent_set(&edges, &crate::config::bits_to_config(&source_solution),), diff --git a/src/unit_tests/rules/maximumindependentset_ilp.rs b/src/unit_tests/rules/maximumindependentset_ilp.rs index b20f2b265..45b981af6 100644 --- a/src/unit_tests/rules/maximumindependentset_ilp.rs +++ b/src/unit_tests/rules/maximumindependentset_ilp.rs @@ -1,6 +1,7 @@ use crate::models::algebraic::{ObjectiveSense, ILP}; use crate::models::graph::MaximumIndependentSet; use crate::rules::{ReductionChain, ReductionGraph, ReductionPath}; +use crate::solvers::SolveOutcome; use crate::solvers::{BruteForce, ILPSolver}; use crate::topology::SimpleGraph; use crate::traits::Problem; @@ -60,7 +61,13 @@ fn test_maximumindependentset_to_ilp_via_path_closed_loop() { let ilp_solver = ILPSolver::new(); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); - let extracted: Vec = chain.extract_solution(&ilp_solution).unwrap(); + let extracted: Vec = chain + .recover_result::, ILP>( + &problem, + SolveOutcome::optimal(ilp, ilp_solution).unwrap(), + ) + .map(|outcome| outcome.into_solution().unwrap()) + .unwrap(); let ilp_size = extracted.iter().filter(|&&selected| selected).count(); assert_eq!(ilp_size, 2); @@ -76,7 +83,13 @@ fn test_maximumindependentset_to_ilp_via_path_weighted() { let ilp_solver = ILPSolver::new(); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); - let extracted = chain.extract_solution(&ilp_solution).unwrap(); + let extracted = chain + .recover_result::, ILP>( + &problem, + SolveOutcome::optimal(ilp, ilp_solution).unwrap(), + ) + .map(|outcome| outcome.into_solution().unwrap()) + .unwrap(); assert_eq!(problem.evaluate(&extracted).unwrap(), Max(Some(100))); assert_eq!(extracted, vec![false, true, false]); @@ -93,6 +106,12 @@ fn test_maximumindependentset_to_ilp_bf_vs_ilp() { let bf_value_solution = BruteForce::new().solve(&problem).unwrap().unwrap(); let bf_value = problem.evaluate(&bf_value_solution).unwrap(); let ilp_solution = ILPSolver::new().solve(ilp).expect("ILP should be solvable"); - let extracted = chain.extract_solution(&ilp_solution).unwrap(); + let extracted = chain + .recover_result::, ILP>( + &problem, + SolveOutcome::optimal(ilp, ilp_solution).unwrap(), + ) + .map(|outcome| outcome.into_solution().unwrap()) + .unwrap(); assert_eq!(problem.evaluate(&extracted).unwrap(), bf_value); } diff --git a/src/unit_tests/rules/maximumindependentset_maximumclique.rs b/src/unit_tests/rules/maximumindependentset_maximumclique.rs index f6e2813b1..defa68e44 100644 --- a/src/unit_tests/rules/maximumindependentset_maximumclique.rs +++ b/src/unit_tests/rules/maximumindependentset_maximumclique.rs @@ -1,6 +1,7 @@ use super::*; use crate::rules::test_helpers::assert_optimization_round_trip_from_optimization_target; use crate::solvers::BruteForce; +use crate::solvers::SolveOutcome; use crate::traits::Problem; use crate::types::One; @@ -46,7 +47,14 @@ fn test_maximumindependentset_to_maximumclique_weighted() { let solver = BruteForce::new(); let best = solver.find_all_witnesses(target).unwrap(); for sol in &best { - let extracted = reduction.extract_solution(sol).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), (sol).clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let metric = source.evaluate(&extracted).unwrap(); assert!(metric.is_valid()); } diff --git a/src/unit_tests/rules/maximumindependentset_maximumsetpacking.rs b/src/unit_tests/rules/maximumindependentset_maximumsetpacking.rs index 9388cc29a..070b3f68d 100644 --- a/src/unit_tests/rules/maximumindependentset_maximumsetpacking.rs +++ b/src/unit_tests/rules/maximumindependentset_maximumsetpacking.rs @@ -1,4 +1,5 @@ use super::*; +use crate::solvers::SolveOutcome; include!("../jl_helpers.rs"); use crate::rules::test_helpers::assert_optimization_round_trip_from_optimization_target; use crate::solvers::BruteForce; @@ -206,7 +207,14 @@ fn test_maximumindependentset_one_to_maximumsetpacking_closed_loop() { let sp_solutions = solver.find_all_witnesses(sp_problem).unwrap(); assert!(!sp_solutions.is_empty()); - let original_solution = reduction.extract_solution(&sp_solutions[0]).unwrap(); + let original_solution = reduction + .recover_result( + &is_problem, + SolveOutcome::optimal(reduction.target_problem(), sp_solutions[0].clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(original_solution.len(), 3); let size: usize = original_solution .iter() @@ -230,7 +238,14 @@ fn test_maximumsetpacking_one_to_maximumindependentset_closed_loop() { let is_solutions = solver.find_all_witnesses(is_problem).unwrap(); assert!(!is_solutions.is_empty()); - let original_solution = reduction.extract_solution(&is_solutions[0]).unwrap(); + let original_solution = reduction + .recover_result( + &sp_problem, + SolveOutcome::optimal(reduction.target_problem(), is_solutions[0].clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(original_solution.len(), 3); let size: usize = original_solution .iter() diff --git a/src/unit_tests/rules/maximumindependentset_qubo.rs b/src/unit_tests/rules/maximumindependentset_qubo.rs index ee99c7302..1692d796b 100644 --- a/src/unit_tests/rules/maximumindependentset_qubo.rs +++ b/src/unit_tests/rules/maximumindependentset_qubo.rs @@ -3,6 +3,7 @@ use crate::models::graph::MaximumIndependentSet; use crate::rules::{ReductionChain, ReductionGraph, ReductionPath}; use crate::solvers::BruteForce; use crate::solvers::BruteForceProblem as _; +use crate::solvers::SolveOutcome; use crate::topology::SimpleGraph; use crate::traits::Problem; use crate::types::Max; @@ -47,7 +48,13 @@ fn test_maximumindependentset_to_qubo_via_path_closed_loop() { let solver = BruteForce::new(); let qubo_solutions = solver.find_all_witnesses(qubo).unwrap(); for sol in &qubo_solutions { - let extracted = chain.extract_solution(sol).unwrap(); + let extracted = chain + .recover_result::, QUBO>( + &problem, + SolveOutcome::optimal(qubo, (sol).clone()).unwrap(), + ) + .map(|outcome| outcome.into_solution().unwrap()) + .unwrap(); assert!(problem.evaluate(&extracted).unwrap().is_valid()); assert_eq!(extracted.iter().filter(|&&x| x).count(), 2); } @@ -65,7 +72,13 @@ fn test_maximumindependentset_to_qubo_via_path_weighted() { .solve(qubo) .unwrap() .expect("QUBO should be solvable via path"); - let extracted = chain.extract_solution(&qubo_solution).unwrap(); + let extracted = chain + .recover_result::, QUBO>( + &problem, + SolveOutcome::optimal(qubo, qubo_solution.clone()).unwrap(), + ) + .map(|outcome| outcome.into_solution().unwrap()) + .unwrap(); assert_eq!(problem.evaluate(&extracted).unwrap(), Max(Some(100))); assert_eq!(extracted, vec![false, true, false]); @@ -84,7 +97,13 @@ fn test_maximumindependentset_to_qubo_via_path_empty_graph() { .solve(qubo) .unwrap() .expect("QUBO should be solvable"); - let extracted = chain.extract_solution(&qubo_solution).unwrap(); + let extracted = chain + .recover_result::, QUBO>( + &problem, + SolveOutcome::optimal(qubo, qubo_solution.clone()).unwrap(), + ) + .map(|outcome| outcome.into_solution().unwrap()) + .unwrap(); assert_eq!(extracted, vec![true, true, true]); assert_eq!(problem.evaluate(&extracted).unwrap(), Max(Some(3))); diff --git a/src/unit_tests/rules/maximumindependentset_triangular.rs b/src/unit_tests/rules/maximumindependentset_triangular.rs index 7205ac3f5..167ab6124 100644 --- a/src/unit_tests/rules/maximumindependentset_triangular.rs +++ b/src/unit_tests/rules/maximumindependentset_triangular.rs @@ -1,5 +1,6 @@ use super::*; use crate::models::graph::MaximumIndependentSet; +use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph, TriangularSubgraph}; use crate::types::One; @@ -57,7 +58,14 @@ fn test_mis_simple_one_to_triangular_closed_loop() { // Map a trivial zero solution back to verify dimensions let zero_config = vec![false; target.graph().num_vertices()]; - let original_solution = result.extract_solution(&zero_config).unwrap(); + let original_solution = result + .recover_result( + &problem, + SolveOutcome::optimal(result.target_problem(), zero_config.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(original_solution.len(), 3); } @@ -78,7 +86,14 @@ fn test_mis_simple_one_to_triangular_preserves_optimum_and_witness() { target.weights(), ); let target_solution = crate::config::config_to_bits(&target_solution); - let source_solution = reduction.extract_solution(&target_solution).unwrap(); + let source_solution = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), target_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!(is_independent_set( &edges, @@ -114,7 +129,14 @@ fn test_mis_simple_one_to_triangular_all_four_vertex_graphs() { target.weights(), ); let target_solution = crate::config::config_to_bits(&target_solution); - let source_solution = reduction.extract_solution(&target_solution).unwrap(); + let source_solution = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), target_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!( is_independent_set(&edges, &crate::config::bits_to_config(&source_solution),), diff --git a/src/unit_tests/rules/maximumleafspanningtree_ilp.rs b/src/unit_tests/rules/maximumleafspanningtree_ilp.rs index 91065c778..057c2f1e8 100644 --- a/src/unit_tests/rules/maximumleafspanningtree_ilp.rs +++ b/src/unit_tests/rules/maximumleafspanningtree_ilp.rs @@ -2,6 +2,7 @@ use super::*; use crate::models::algebraic::{ObjectiveSense, ILP}; use crate::models::graph::MaximumLeafSpanningTree; use crate::rules::ReduceTo; +use crate::solvers::SolveOutcome; use crate::solvers::{BruteForce, ILPSolver}; use crate::topology::SimpleGraph; use crate::traits::Problem; @@ -56,7 +57,14 @@ fn test_maximumleafspanningtree_to_ilp_closed_loop() { let ilp_solver = ILPSolver::new(); let best_source = bf.find_all_witnesses(&problem).unwrap(); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); // All brute-force optimal solutions have the same value let bf_value = problem.evaluate(&best_source[0]).unwrap(); @@ -76,7 +84,14 @@ fn test_maximumleafspanningtree_to_ilp_canonical_closed_loop() { let ilp_solver = ILPSolver::new(); let best_source = bf.find_all_witnesses(&problem).unwrap(); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(problem.evaluate(&best_source[0]).unwrap(), Max(Some(4))); assert_eq!(problem.evaluate(&extracted).unwrap(), Max(Some(4))); @@ -103,7 +118,14 @@ fn test_solution_extraction_reads_edge_selector_prefix() { target_solution[12] = 1; // 2 -> 3 assert_eq!( - reduction.extract_solution(&target_solution).unwrap(), + reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), target_solution.clone()).unwrap() + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"), vec![true, true, true, false] ); } @@ -116,7 +138,14 @@ fn test_reduce_and_solve_via_ilp() { let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(problem.evaluate(&extracted).unwrap(), Max(Some(4))); assert!(problem.is_valid_solution(&extracted)); } @@ -138,7 +167,14 @@ fn test_maximumleafspanningtree_to_ilp_path_graph() { let ilp = reduction.target_problem(); let ilp_solver = ILPSolver::new(); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(problem.evaluate(&extracted).unwrap(), Max(Some(2))); } @@ -151,7 +187,14 @@ fn test_maximumleafspanningtree_to_ilp_star_graph() { let ilp = reduction.target_problem(); let ilp_solver = ILPSolver::new(); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(problem.evaluate(&extracted).unwrap(), Max(Some(3))); assert!(problem.is_valid_solution(&extracted)); } @@ -171,7 +214,14 @@ fn test_maximumleafspanningtree_to_ilp_complete_graph() { 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(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(problem.evaluate(&extracted).unwrap(), bf_value); assert_eq!(bf_value, Max(Some(3))); diff --git a/src/unit_tests/rules/maximumlikelihoodranking_ilp.rs b/src/unit_tests/rules/maximumlikelihoodranking_ilp.rs index e706c0c43..ca22844b4 100644 --- a/src/unit_tests/rules/maximumlikelihoodranking_ilp.rs +++ b/src/unit_tests/rules/maximumlikelihoodranking_ilp.rs @@ -1,6 +1,7 @@ use super::*; use crate::models::algebraic::{ObjectiveSense, ILP}; use crate::rules::test_helpers::assert_bf_vs_ilp; +use crate::solvers::SolveOutcome; use crate::solvers::{BruteForce, ILPSolver}; use crate::traits::Problem; use crate::types::Min; @@ -45,7 +46,14 @@ fn test_maximumlikelihoodranking_to_ilp_bf_vs_ilp() { let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let ilp_value = problem.evaluate(&extracted).unwrap(); assert_eq!(bf_value, ilp_value); @@ -62,7 +70,14 @@ fn test_maximumlikelihoodranking_to_ilp_extraction() { let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); // Verify the extracted config is a valid permutation let n = problem.num_items(); @@ -88,7 +103,14 @@ fn test_maximumlikelihoodranking_to_ilp_two_items() { assert_eq!(ilp.num_constraints(), 0); let ilp_solution = ILPSolver::new().solve(ilp).expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let value = problem.evaluate(&extracted).unwrap(); assert!(value.is_valid()); @@ -110,7 +132,14 @@ fn test_maximumlikelihoodranking_to_ilp_single_item() { let ilp_solution = ILPSolver::new() .solve(ilp) .expect("single-item ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted, vec![0]); } diff --git a/src/unit_tests/rules/maximummatching_ilp.rs b/src/unit_tests/rules/maximummatching_ilp.rs index 3dda34bfe..7eb431df8 100644 --- a/src/unit_tests/rules/maximummatching_ilp.rs +++ b/src/unit_tests/rules/maximummatching_ilp.rs @@ -1,4 +1,5 @@ use super::*; +use crate::solvers::SolveOutcome; use crate::solvers::{BruteForce, ILPSolver}; use crate::topology::SimpleGraph; use crate::traits::Problem; @@ -61,7 +62,14 @@ fn test_maximummatching_to_ilp_closed_loop() { // Solve via ILP reduction let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); // Both should find optimal size = 1 (one edge) let bf_size = problem.evaluate(&bf_solutions[0]).unwrap(); @@ -94,7 +102,14 @@ fn test_ilp_solution_equals_brute_force_path() { // Solve via ILP let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let ilp_size = problem.evaluate(&extracted).unwrap(); assert_eq!(bf_size, Max(Some(2))); @@ -122,7 +137,14 @@ fn test_ilp_solution_equals_brute_force_weighted() { let bf_obj = problem.evaluate(&bf_solutions[0]).unwrap(); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let ilp_obj = problem.evaluate(&extracted).unwrap(); assert_eq!(bf_obj, Max(Some(100))); @@ -141,7 +163,14 @@ fn test_solution_extraction() { // Test that extraction works correctly (1:1 mapping) let ilp_solution = vec![1, 1]; - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted, vec![true, true]); // Verify this is a valid matching (edges 0-1 and 2-3 are disjoint) @@ -196,7 +225,14 @@ fn test_k4_perfect_matching() { let ilp_solver = ILPSolver::new(); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!(problem.evaluate(&extracted).unwrap().is_valid()); assert_eq!(problem.evaluate(&extracted).unwrap(), Max(Some(2))); // Perfect matching has 2 edges @@ -218,7 +254,14 @@ fn test_star_graph() { let ilp_solver = ILPSolver::new(); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!(problem.evaluate(&extracted).unwrap().is_valid()); assert_eq!(problem.evaluate(&extracted).unwrap(), Max(Some(1))); @@ -238,7 +281,14 @@ fn test_bipartite_graph() { let ilp_solver = ILPSolver::new(); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!(problem.evaluate(&extracted).unwrap().is_valid()); assert_eq!(problem.evaluate(&extracted).unwrap(), Max(Some(2))); diff --git a/src/unit_tests/rules/maximummatching_maximumsetpacking.rs b/src/unit_tests/rules/maximummatching_maximumsetpacking.rs index c3fb58b8f..4081599cf 100644 --- a/src/unit_tests/rules/maximummatching_maximumsetpacking.rs +++ b/src/unit_tests/rules/maximummatching_maximumsetpacking.rs @@ -1,4 +1,5 @@ use super::*; +use crate::solvers::SolveOutcome; include!("../jl_helpers.rs"); use crate::rules::test_helpers::assert_optimization_round_trip_from_optimization_target; use crate::solvers::BruteForce; @@ -62,7 +63,14 @@ fn test_matching_to_setpacking_solution_extraction() { // Test solution extraction is 1:1 let sp_solution = vec![true, false, true]; - let matching_solution = reduction.extract_solution(&sp_solution).unwrap(); + let matching_solution = reduction + .recover_result( + &matching, + SolveOutcome::optimal(reduction.target_problem(), sp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(matching_solution, vec![true, false, true]); // Verify the extracted solution is valid for original MaximumMatching diff --git a/src/unit_tests/rules/maximumsetpacking_casts.rs b/src/unit_tests/rules/maximumsetpacking_casts.rs index 5c37d5230..7bb3e324b 100644 --- a/src/unit_tests/rules/maximumsetpacking_casts.rs +++ b/src/unit_tests/rules/maximumsetpacking_casts.rs @@ -2,6 +2,7 @@ use super::*; use crate::rules::traits::ReductionResult; use crate::rules::ReduceTo; use crate::solvers::BruteForce; +use crate::solvers::SolveOutcome; use crate::traits::Problem; #[test] @@ -17,7 +18,14 @@ fn test_maximumsetpacking_one_to_i64_cast_closed_loop() { let solver = BruteForce::new(); let target_solution = solver.solve(sp_i64).unwrap().unwrap(); - let source_solution = reduction.extract_solution(&target_solution).unwrap(); + let source_solution = reduction + .recover_result( + &sp_one, + SolveOutcome::optimal(reduction.target_problem(), target_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let metric = sp_one.evaluate(&source_solution).unwrap(); assert!(metric.is_valid()); @@ -36,7 +44,14 @@ fn test_maximumsetpacking_i64_to_f64_cast_closed_loop() { let solver = BruteForce::new(); let target_solution = solver.solve(sp_f64).unwrap().unwrap(); - let source_solution = reduction.extract_solution(&target_solution).unwrap(); + let source_solution = reduction + .recover_result( + &sp_i64, + SolveOutcome::optimal(reduction.target_problem(), target_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let metric = sp_i64.evaluate(&source_solution).unwrap(); assert!(metric.is_valid()); diff --git a/src/unit_tests/rules/maximumsetpacking_ilp.rs b/src/unit_tests/rules/maximumsetpacking_ilp.rs index d0e6b68e7..d1b6103ad 100644 --- a/src/unit_tests/rules/maximumsetpacking_ilp.rs +++ b/src/unit_tests/rules/maximumsetpacking_ilp.rs @@ -1,4 +1,5 @@ use super::*; +use crate::solvers::SolveOutcome; use crate::solvers::{BruteForce, ILPSolver}; use crate::traits::Problem; use crate::types::Max; @@ -52,7 +53,14 @@ fn test_maximumsetpacking_to_ilp_closed_loop() { let bf_solutions = bf.find_all_witnesses(&problem).unwrap(); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let bf_size: usize = bf_solutions[0].iter().filter(|&&selected| selected).count(); let ilp_size: usize = extracted.iter().filter(|&&selected| selected).count(); @@ -83,7 +91,14 @@ fn test_ilp_solution_equals_brute_force_weighted() { let bf_obj = problem.evaluate(&bf_solutions[0]).unwrap(); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let ilp_obj = problem.evaluate(&extracted).unwrap(); assert_eq!(bf_obj, Max(Some(6))); @@ -98,7 +113,14 @@ fn test_solution_extraction() { ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp_solution = vec![1, 0, 1, 0]; - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted, vec![true, false, true, false]); assert!(problem.evaluate(&extracted).unwrap().is_valid()); } @@ -114,7 +136,14 @@ fn test_disjoint_sets() { let ilp_solver = ILPSolver::new(); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted, vec![true, true, true, true]); assert!(problem.evaluate(&extracted).unwrap().is_valid()); @@ -162,14 +191,58 @@ fn extraction_maps_feasible_witnesses_through_typed_and_dynamic_paths() { .find(|path| path.len() == 1) .unwrap(); let chain = graph.reduce_along_path(&path, &source).unwrap().unwrap(); - assert_eq!(reduction.extract_solution(&vec![1]).unwrap(), vec![true]); - let extracted = reduction.extract_solution_dyn(&vec![1i64]).unwrap(); - assert_eq!(*extracted.downcast::>().unwrap(), vec![true]); + assert_eq!( + reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), vec![1].clone()).unwrap() + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"), + vec![true] + ); + let extracted = reduction + .recover_result_dyn( + &source, + crate::solvers::erase_outcome( + SolveOutcome::optimal(reduction.target_problem(), vec![1i64]).unwrap(), + ), + ) + .unwrap(); + assert_eq!( + crate::solvers::downcast_outcome::, crate::types::Max>(extracted) + .unwrap() + .into_solution() + .unwrap(), + vec![true] + ); // An unselected set is feasible even though it is not optimal. - assert_eq!(reduction.extract_solution(&vec![0]).unwrap(), vec![false]); assert_eq!( - chain.extract_solution_json(json!([0])).unwrap(), - json!([false]) + reduction + .recover_result( + &source, + SolveOutcome::feasible(reduction.target_problem(), vec![0].clone()).unwrap() + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"), + vec![false] + ); + assert_eq!( + chain + .recover_result_json( + &source, + SolveOutcome::Feasible { + solution: json!([0]), + evaluation: String::new(), + } + ) + .unwrap(), + SolveOutcome::Feasible { + solution: json!([false]), + evaluation: "Max(0)".into() + } ); } diff --git a/src/unit_tests/rules/maximumsetpacking_qubo.rs b/src/unit_tests/rules/maximumsetpacking_qubo.rs index 503e7c213..c74206f44 100644 --- a/src/unit_tests/rules/maximumsetpacking_qubo.rs +++ b/src/unit_tests/rules/maximumsetpacking_qubo.rs @@ -1,6 +1,7 @@ use super::*; use crate::solvers::BruteForce; use crate::solvers::BruteForceProblem as _; +use crate::solvers::SolveOutcome; use crate::traits::Problem; #[test] @@ -16,7 +17,14 @@ fn test_setpacking_to_qubo_closed_loop() { let qubo_solutions = solver.find_all_witnesses(qubo).unwrap(); for sol in &qubo_solutions { - let extracted = reduction.extract_solution(sol).unwrap(); + let extracted = reduction + .recover_result( + &sp, + SolveOutcome::optimal(reduction.target_problem(), (sol).clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!(sp.evaluate(&extracted).unwrap().is_valid()); assert_eq!(extracted.iter().filter(|&&x| x).count(), 2); } @@ -33,7 +41,14 @@ fn test_setpacking_to_qubo_disjoint() { let qubo_solutions = solver.find_all_witnesses(qubo).unwrap(); for sol in &qubo_solutions { - let extracted = reduction.extract_solution(sol).unwrap(); + let extracted = reduction + .recover_result( + &sp, + SolveOutcome::optimal(reduction.target_problem(), (sol).clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!(sp.evaluate(&extracted).unwrap().is_valid()); // All 3 sets should be selected assert_eq!(extracted.iter().filter(|&&x| x).count(), 3); @@ -51,7 +66,14 @@ fn test_setpacking_to_qubo_all_overlap() { let qubo_solutions = solver.find_all_witnesses(qubo).unwrap(); for sol in &qubo_solutions { - let extracted = reduction.extract_solution(sol).unwrap(); + let extracted = reduction + .recover_result( + &sp, + SolveOutcome::optimal(reduction.target_problem(), (sol).clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!(sp.evaluate(&extracted).unwrap().is_valid()); assert_eq!(extracted.iter().filter(|&&x| x).count(), 1); } diff --git a/src/unit_tests/rules/minimumcapacitatedspanningtree_ilp.rs b/src/unit_tests/rules/minimumcapacitatedspanningtree_ilp.rs index 07121cda3..4ce6fcb27 100644 --- a/src/unit_tests/rules/minimumcapacitatedspanningtree_ilp.rs +++ b/src/unit_tests/rules/minimumcapacitatedspanningtree_ilp.rs @@ -2,6 +2,7 @@ use super::*; use crate::models::algebraic::{ObjectiveSense, ILP}; use crate::models::graph::MinimumCapacitatedSpanningTree; use crate::rules::ReduceTo; +use crate::solvers::SolveOutcome; use crate::solvers::{BruteForce, ILPSolver}; use crate::topology::SimpleGraph; use crate::traits::Problem; @@ -64,7 +65,14 @@ fn test_minimumcapacitatedspanningtree_to_ilp_closed_loop() { let ilp_solver = ILPSolver::new(); let best_source = bf.find_all_witnesses(&problem).unwrap(); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let bf_value = problem.evaluate(&best_source[0]).unwrap(); let ilp_value = problem.evaluate(&extracted).unwrap(); @@ -83,7 +91,14 @@ fn test_minimumcapacitatedspanningtree_to_ilp_canonical_closed_loop() { let ilp_solver = ILPSolver::new(); let best_source = bf.find_all_witnesses(&problem).unwrap(); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(problem.evaluate(&best_source[0]).unwrap(), Min(Some(5))); assert_eq!(problem.evaluate(&extracted).unwrap(), Min(Some(5))); @@ -110,7 +125,14 @@ fn test_solution_extraction_reads_edge_selector_prefix() { } assert_eq!( - reduction.extract_solution(&target_solution).unwrap(), + reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), target_solution.clone()).unwrap() + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"), vec![true, true, false, true, false] ); } @@ -138,7 +160,14 @@ fn test_minimumcapacitatedspanningtree_to_ilp_star_tree() { 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(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(problem.evaluate(&extracted).unwrap(), Min(Some(3))); assert!(problem.is_valid_solution(&extracted).unwrap()); } @@ -158,7 +187,14 @@ fn test_minimumcapacitatedspanningtree_to_ilp_path_graph() { 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(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(problem.evaluate(&extracted).unwrap(), Min(Some(6))); assert!(problem.is_valid_solution(&extracted).unwrap()); } diff --git a/src/unit_tests/rules/minimumcostmaximumflow_minimumcostcirculation.rs b/src/unit_tests/rules/minimumcostmaximumflow_minimumcostcirculation.rs index 96f287f3a..ae1cdad3c 100644 --- a/src/unit_tests/rules/minimumcostmaximumflow_minimumcostcirculation.rs +++ b/src/unit_tests/rules/minimumcostmaximumflow_minimumcostcirculation.rs @@ -1,6 +1,7 @@ use super::*; use crate::rules::test_helpers::assert_optimization_round_trip_from_optimization_target; use crate::solvers::BruteForce; +use crate::solvers::SolveOutcome; use crate::topology::DirectedGraph; use crate::traits::Problem; use crate::types::Min; @@ -82,7 +83,14 @@ fn test_minimumcostmaximumflow_to_minimumcostcirculation_bottleneck() { // value 1 and cost 1 (the cheaper 1->3 path). let solver = BruteForce::new(); let target_witness = solver.solve(reduction.target_problem()).unwrap().unwrap(); - let extracted = reduction.extract_solution(&target_witness).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), target_witness.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(source.flow_value(&extracted).unwrap(), 1); assert_eq!(source.total_cost(&extracted).unwrap(), 1); } @@ -117,7 +125,14 @@ fn test_minimumcostmaximumflow_to_minimumcostcirculation_parallel_arcs() { // parallel arc has cost 1, so optimal source cost = 1. let solver = BruteForce::new(); let target_witness = solver.solve(target).unwrap().unwrap(); - let extracted = reduction.extract_solution(&target_witness).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), target_witness.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(source.flow_value(&extracted).unwrap(), 1); assert_eq!(source.total_cost(&extracted).unwrap(), 1); } @@ -167,7 +182,14 @@ fn test_minimumcostmaximumflow_to_minimumcostcirculation_zero_capacity_arc() { let solver = BruteForce::new(); let target_witness = solver.solve(target).unwrap().unwrap(); - let extracted = reduction.extract_solution(&target_witness).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), target_witness.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(source.flow_value(&extracted).unwrap(), 1); // Zero-capacity arc must be 0 in the extracted flow. assert_eq!(extracted[2], 0); @@ -221,7 +243,14 @@ fn test_minimumcostmaximumflow_to_minimumcostcirculation_value_priority_over_cos let solver = BruteForce::new(); let target_witness = solver.solve(reduction.target_problem()).unwrap().unwrap(); - let extracted = reduction.extract_solution(&target_witness).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), target_witness.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(source.flow_value(&extracted).unwrap(), 2); assert_eq!(source.total_cost(&extracted).unwrap(), 20); @@ -242,7 +271,14 @@ fn test_minimumcostmaximumflow_to_minimumcostcirculation_extract_solution_length // A value-3 flow closes through the added sink-to-source return arc. let m = source.num_arcs(); let padded = vec![2_usize, 1, 1, 1, 2, 3]; - let extracted = reduction.extract_solution(&padded).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), padded.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted.len(), m); assert_eq!(extracted, padded[..m].to_vec()); } diff --git a/src/unit_tests/rules/minimumcoveringbycliques_ilp.rs b/src/unit_tests/rules/minimumcoveringbycliques_ilp.rs index 4e322beef..c594952b3 100644 --- a/src/unit_tests/rules/minimumcoveringbycliques_ilp.rs +++ b/src/unit_tests/rules/minimumcoveringbycliques_ilp.rs @@ -1,6 +1,7 @@ use super::*; use crate::models::algebraic::{ObjectiveSense, ILP}; use crate::models::graph::MinimumCoveringByCliques; +use crate::solvers::SolveOutcome; use crate::solvers::{BruteForce, ILPSolver}; use crate::topology::SimpleGraph; use crate::traits::Problem; @@ -33,7 +34,14 @@ fn test_minimumcoveringbycliques_to_ilp_closed_loop() { let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(source.evaluate(&extracted).unwrap(), Min(Some(2))); assert_eq!(source.evaluate(&extracted).unwrap(), bf_value); @@ -49,7 +57,14 @@ fn test_minimumcoveringbycliques_to_ilp_empty_graph() { assert_eq!(ilp.num_vars(), 0); assert_eq!(ilp.constraints().len(), 0); assert_eq!( - reduction.extract_solution(&vec![]).unwrap(), + reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), vec![].clone()).unwrap() + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"), Vec::::new() ); assert_eq!(source.evaluate(&vec![]).unwrap(), Min(Some(0))); diff --git a/src/unit_tests/rules/minimumcoveringbycliques_minimumintersectiongraphbasis.rs b/src/unit_tests/rules/minimumcoveringbycliques_minimumintersectiongraphbasis.rs index 357591ad0..abdfa01ea 100644 --- a/src/unit_tests/rules/minimumcoveringbycliques_minimumintersectiongraphbasis.rs +++ b/src/unit_tests/rules/minimumcoveringbycliques_minimumintersectiongraphbasis.rs @@ -1,5 +1,6 @@ use super::*; use crate::rules::test_helpers::assert_optimization_round_trip_from_optimization_target; +use crate::solvers::SolveOutcome; use crate::topology::Graph; use crate::traits::Problem; use crate::types::Min; @@ -42,7 +43,14 @@ fn test_minimumcoveringbycliques_to_minimumintersectiongraphbasis_issue_example_ assert_eq!(target.evaluate(&target_solution).unwrap(), Min(Some(2))); - let extracted = reduction.extract_solution(&target_solution).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), target_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted, vec![0, 0, 0, 1]); assert_eq!(source.evaluate(&extracted).unwrap(), Min(Some(2))); @@ -72,7 +80,14 @@ fn test_minimumcoveringbycliques_to_minimumintersectiongraphbasis_empty_graph() let target_solution = vec![vec![], vec![], vec![]]; assert_eq!(target.evaluate(&target_solution).unwrap(), Min(Some(0))); assert_eq!( - reduction.extract_solution(&target_solution).unwrap(), + reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), target_solution.clone()).unwrap() + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"), Vec::::new() ); assert_eq!(source.evaluate(&vec![]).unwrap(), Min(Some(0))); diff --git a/src/unit_tests/rules/minimumcutintoboundedsets_ilp.rs b/src/unit_tests/rules/minimumcutintoboundedsets_ilp.rs index 1f31f58a5..c64ae0e96 100644 --- a/src/unit_tests/rules/minimumcutintoboundedsets_ilp.rs +++ b/src/unit_tests/rules/minimumcutintoboundedsets_ilp.rs @@ -3,6 +3,7 @@ use crate::models::algebraic::ILP; use crate::models::graph::MinimumCutIntoBoundedSets; use crate::rules::test_helpers::assert_bf_vs_ilp; use crate::rules::ReduceTo; +use crate::solvers::SolveOutcome; use crate::topology::SimpleGraph; use crate::traits::Problem; @@ -41,7 +42,14 @@ fn test_extract_solution() { let reduction: ReductionMinCutBSToILP = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let target_sol = vec![0, 0, 1, 1, 0, 1, 0]; - let extracted = reduction.extract_solution(&target_sol).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), target_sol.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted, vec![false, false, true, true]); assert!(source.evaluate(&extracted).unwrap().0.is_some()); } diff --git a/src/unit_tests/rules/minimumdiscreteplanarinversekinematics_qubo.rs b/src/unit_tests/rules/minimumdiscreteplanarinversekinematics_qubo.rs index 943a4e257..816e44782 100644 --- a/src/unit_tests/rules/minimumdiscreteplanarinversekinematics_qubo.rs +++ b/src/unit_tests/rules/minimumdiscreteplanarinversekinematics_qubo.rs @@ -1,6 +1,7 @@ use super::*; use crate::rules::test_helpers::assert_optimization_round_trip_from_optimization_target; use crate::solvers::BruteForce; +use crate::solvers::SolveOutcome; use crate::traits::Problem; use crate::types::Min; use std::f64::consts::{FRAC_PI_2, PI}; @@ -48,7 +49,15 @@ fn test_minimumdiscreteplanarinversekinematics_to_qubo_single_link() { assert_eq!(reduction.target_problem().num_vars(), 3); assert_eq!(qubo_solutions.len(), 1); assert_eq!( - reduction.extract_solution(&qubo_solutions[0]).unwrap(), + reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), qubo_solutions[0].clone()) + .unwrap() + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"), vec![1] ); assert!(matches!(source.evaluate(&vec![1]).unwrap(), Min(Some(v)) if v.abs() < EPS)); @@ -72,7 +81,15 @@ fn test_minimumdiscreteplanarinversekinematics_to_qubo_single_sample_per_link() assert_eq!(reduction.target_problem().num_vars(), 3); assert_eq!(qubo_solutions, vec![vec![true, true, true]]); assert_eq!( - reduction.extract_solution(&qubo_solutions[0]).unwrap(), + reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), qubo_solutions[0].clone()) + .unwrap() + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"), vec![0, 0, 0] ); assert!(matches!(source.evaluate(&vec![0, 0, 0]).unwrap(), Min(Some(v)) if v.abs() < EPS)); @@ -100,10 +117,7 @@ fn test_minimumdiscreteplanarinversekinematics_to_qubo_empty_allowed_pairs() { .target_problem() .evaluate(&target_solution) .unwrap(); - assert_eq!( - crate::rules::AggregateReductionResult::extract_value(&reduction, value), - Min(None) - ); + assert_eq!(reduction.map_value(value), Min(None)); } } @@ -164,30 +178,42 @@ fn optimum_energy_recovers_distance_and_infeasibility() { .find_all_witnesses(reduction.target_problem()) .unwrap() { - let completed = crate::solvers::complete_reduction( - &source, - &chain, - &crate::solvers::SolveOutcome::Optimal { - solution: serde_json::to_value(&solution).unwrap(), - evaluation: String::new(), - }, - ) - .unwrap(); + let completed = chain + .recover_result_json( + &source, + SolveOutcome::Optimal { + solution: serde_json::to_value(&solution).unwrap(), + evaluation: String::new(), + }, + ) + .unwrap(); assert_eq!( - matches!(completed, crate::solvers::SolveOutcome::Optimal { .. }), + matches!(completed, SolveOutcome::Optimal { .. }), expected.is_some() ); - let recovered = crate::rules::AggregateReductionResult::extract_value( - &reduction, - reduction.target_problem().evaluate(&solution).unwrap(), - ) - .0; + let recovered = reduction + .map_value(reduction.target_problem().evaluate(&solution).unwrap()) + .0; match (expected, recovered) { (Some(expected), Some(actual)) => { assert!((actual - expected).abs() < EPS); assert_eq!( source - .evaluate(&reduction.extract_solution(&solution).unwrap()) + .evaluate( + &reduction + .recover_result( + &source, + SolveOutcome::optimal( + reduction.target_problem(), + solution.clone() + ) + .unwrap() + ) + .map(|result| result.into_solution().expect( + "qualifying target result must recover a source solution" + )) + .unwrap() + ) .unwrap(), Min(Some(expected)) ); @@ -196,10 +222,7 @@ fn optimum_energy_recovers_distance_and_infeasibility() { other => panic!("source and recovered outcomes disagree: {other:?}"), } } - assert_eq!( - crate::rules::AggregateReductionResult::extract_value(&reduction, Min(None)), - Min(None) - ); + assert_eq!(reduction.map_value(Min(None)), Min(None)); } } diff --git a/src/unit_tests/rules/minimumdominatingset_ilp.rs b/src/unit_tests/rules/minimumdominatingset_ilp.rs index deddfa504..fc73ede17 100644 --- a/src/unit_tests/rules/minimumdominatingset_ilp.rs +++ b/src/unit_tests/rules/minimumdominatingset_ilp.rs @@ -1,4 +1,5 @@ use super::*; +use crate::solvers::SolveOutcome; use crate::solvers::{BruteForce, ILPSolver}; use crate::traits::Problem; use crate::types::Min; @@ -66,7 +67,14 @@ fn test_minimumdominatingset_to_ilp_closed_loop() { // Solve via ILP reduction let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let ilp_size = problem.evaluate(&extracted).unwrap(); // Both should find optimal size = 1 (just the center) @@ -100,7 +108,14 @@ fn test_ilp_solution_equals_brute_force_path() { // Solve via ILP let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let ilp_size = problem.evaluate(&extracted).unwrap(); assert_eq!(bf_size, Min(Some(2))); @@ -129,7 +144,14 @@ fn test_ilp_solution_equals_brute_force_weighted() { let bf_obj = problem.evaluate(&bf_solutions[0]).unwrap(); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let ilp_obj = problem.evaluate(&extracted).unwrap(); assert_eq!(bf_obj, Min(Some(3))); @@ -148,7 +170,14 @@ fn test_solution_extraction() { // Test that extraction works correctly (1:1 mapping) let ilp_solution = vec![1, 0, 1, 0]; - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted, vec![true, false, true, false]); // Verify this is a valid DS (0 dominates 0,1 and 2 dominates 2,3) @@ -179,7 +208,14 @@ fn test_isolated_vertices() { let ilp_solver = ILPSolver::new(); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); // Vertex 2 must be selected (isolated) assert!(extracted[2]); @@ -200,7 +236,14 @@ fn test_complete_graph() { let ilp_solver = ILPSolver::new(); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!(problem.evaluate(&extracted).unwrap().is_valid()); assert_eq!(problem.evaluate(&extracted).unwrap(), Min(Some(1))); @@ -216,7 +259,14 @@ fn test_single_vertex() { let ilp_solver = ILPSolver::new(); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted, vec![true]); @@ -243,7 +293,14 @@ fn test_cycle_graph() { let bf_size = problem.evaluate(&bf_solutions[0]).unwrap(); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let ilp_size = problem.evaluate(&extracted).unwrap(); assert_eq!(bf_size, ilp_size); diff --git a/src/unit_tests/rules/minimumedgecostflow_ilp.rs b/src/unit_tests/rules/minimumedgecostflow_ilp.rs index fa9a3c65f..0738a4fa1 100644 --- a/src/unit_tests/rules/minimumedgecostflow_ilp.rs +++ b/src/unit_tests/rules/minimumedgecostflow_ilp.rs @@ -1,5 +1,6 @@ use super::*; use crate::models::algebraic::{ObjectiveSense, ILP}; +use crate::solvers::SolveOutcome; use crate::solvers::{BruteForce, ILPSolver}; use crate::topology::DirectedGraph; use crate::traits::Problem; @@ -78,7 +79,14 @@ fn test_minimumedgecostflow_to_ilp_closed_loop() { let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be feasible"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let ilp_value = problem.evaluate(&extracted).unwrap(); assert_eq!(ilp_value, bf_value); @@ -100,7 +108,14 @@ fn test_minimumedgecostflow_to_ilp_small_closed_loop() { let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be feasible"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(problem.evaluate(&extracted).unwrap(), bf_value); } @@ -142,7 +157,14 @@ fn test_minimumedgecostflow_to_ilp_extract_solution() { target_solution[10] = 1; // y on arc (2,4) target_solution[11] = 1; // y on arc (3,4) - let extracted = reduction.extract_solution(&target_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), target_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted.len(), 6); assert_eq!(extracted, vec![0, 1, 2, 0, 1, 2]); assert_eq!(problem.evaluate(&extracted).unwrap(), Min(Some(3))); diff --git a/src/unit_tests/rules/minimumexternalmacrodatacompression_ilp.rs b/src/unit_tests/rules/minimumexternalmacrodatacompression_ilp.rs index 2892f3b53..3dd3982da 100644 --- a/src/unit_tests/rules/minimumexternalmacrodatacompression_ilp.rs +++ b/src/unit_tests/rules/minimumexternalmacrodatacompression_ilp.rs @@ -1,6 +1,7 @@ use super::*; use crate::models::algebraic::{ObjectiveSense, ILP}; use crate::solvers::ILPSolver; +use crate::solvers::SolveOutcome; use crate::traits::Problem; use crate::types::Min; @@ -14,7 +15,14 @@ fn test_emdc_to_ilp_closed_loop() { let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let value = problem.evaluate(&extracted).unwrap(); assert!(value.is_valid(), "Extracted solution should be valid"); assert_eq!(value, Min(Some(2))); @@ -33,7 +41,14 @@ fn test_emdc_to_ilp_compression_wins() { let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let value = problem.evaluate(&extracted).unwrap(); assert!(value.is_valid(), "Extracted solution should be valid"); assert_eq!(value, Min(Some(12))); @@ -78,7 +93,14 @@ fn test_emdc_to_ilp_empty() { assert!(ilp.constraints().is_empty()); // For empty ILP, the solution is empty - let extracted = reduction.extract_solution(&vec![]).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), vec![].clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let value = problem.evaluate(&extracted).unwrap(); assert_eq!(value, Min(Some(0))); } @@ -102,7 +124,14 @@ fn test_emdc_to_ilp_single_char() { let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let value = problem.evaluate(&extracted).unwrap(); assert!(value.is_valid()); assert_eq!(value, Min(Some(1))); @@ -121,7 +150,14 @@ fn test_emdc_to_ilp_repeated_string() { let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let value = problem.evaluate(&extracted).unwrap(); assert!(value.is_valid()); assert_eq!(value, Min(Some(3))); diff --git a/src/unit_tests/rules/minimumfaultdetectiontestset_ilp.rs b/src/unit_tests/rules/minimumfaultdetectiontestset_ilp.rs index 7726cafac..ff3744cac 100644 --- a/src/unit_tests/rules/minimumfaultdetectiontestset_ilp.rs +++ b/src/unit_tests/rules/minimumfaultdetectiontestset_ilp.rs @@ -3,6 +3,7 @@ use crate::models::algebraic::{Comparison, ObjectiveSense}; use crate::models::misc::MinimumFaultDetectionTestSet; use crate::rules::test_helpers::assert_bf_vs_ilp; use crate::solvers::ILPSolver; +use crate::solvers::SolveOutcome; use crate::traits::Problem; use crate::types::Min; @@ -61,7 +62,14 @@ fn test_minimumfaultdetectiontestset_to_ilp_closed_loop() { let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted, vec![vec![true, false], vec![false, true]]); assert_eq!(problem.evaluate(&extracted).unwrap(), Min(Some(2))); @@ -102,7 +110,14 @@ fn test_reduction_handles_instances_without_internal_vertices() { let ilp_solution = ILPSolver::new() .solve(ilp) .expect("ILP should be feasible when there are no internal vertices"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted, vec![vec![false]]); assert_eq!(problem.evaluate(&extracted).unwrap(), Min(Some(0))); diff --git a/src/unit_tests/rules/minimumfeedbackarcset_ilp.rs b/src/unit_tests/rules/minimumfeedbackarcset_ilp.rs index 09413a71b..0296ef6f9 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::solvers::SolveOutcome; use crate::solvers::{BruteForce, ILPSolver}; use crate::topology::DirectedGraph; use crate::traits::Problem; @@ -44,7 +45,14 @@ fn test_minimumfeedbackarcset_to_ilp_bf_vs_ilp() { // Solve via ILP reduction let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let ilp_value = problem.evaluate(&extracted).unwrap(); // Both should find optimal value = 1 @@ -62,7 +70,14 @@ fn test_solution_extraction() { // 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]; - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted, vec![false, false, true]); // Verify this is a valid FAS (removing arc 2->0 breaks the 3-cycle) @@ -87,7 +102,14 @@ fn test_minimumfeedbackarcset_to_ilp_trivial() { let ilp_solver = ILPSolver::new(); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let value = problem.evaluate(&extracted).unwrap(); assert_eq!(value, Min(Some(0)), "DAG needs no arc removal"); diff --git a/src/unit_tests/rules/minimumfeedbackvertexset_ilp.rs b/src/unit_tests/rules/minimumfeedbackvertexset_ilp.rs index 8b47862ae..75eb03c42 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::solvers::SolveOutcome; use crate::solvers::{BruteForce, ILPSolver}; use crate::topology::DirectedGraph; use crate::traits::Problem; @@ -39,7 +40,14 @@ fn test_minimumfeedbackvertexset_to_ilp_closed_loop() { // Solve via ILP reduction let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let ilp_size = problem.evaluate(&extracted).unwrap(); // Both should find optimal size = 1 @@ -89,7 +97,14 @@ fn test_cycle_of_triangles() { let ilp_solver = ILPSolver::new(); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let size = problem.evaluate(&extracted).unwrap(); assert_eq!(size, Min(Some(3)), "FVS should be 3"); @@ -106,7 +121,14 @@ fn test_dag_no_removal() { let ilp_solver = ILPSolver::new(); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let size = problem.evaluate(&extracted).unwrap(); assert_eq!(size, Min(Some(0)), "DAG needs no removal"); @@ -128,7 +150,14 @@ fn test_single_vertex() { let ilp_solver = ILPSolver::new(); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted, vec![false]); assert_eq!(problem.evaluate(&extracted).unwrap(), Min(Some(0))); @@ -153,7 +182,14 @@ fn test_weighted() { let ilp_solver = ILPSolver::new(); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); // Should remove vertex 1 (cheapest) assert!(extracted[1], "Should remove vertex true (cheapest)"); @@ -176,7 +212,14 @@ fn test_two_disjoint_cycles() { let ilp = reduction.target_problem(); let ilp_solver = ILPSolver::new(); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let ilp_size = problem.evaluate(&extracted).unwrap(); assert_eq!(bf_size, Min(Some(2))); @@ -193,7 +236,14 @@ fn test_solution_extraction() { // 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]; - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted, vec![true, false, false]); // Verify this is a valid FVS (removing vertex 0 breaks the 3-cycle) diff --git a/src/unit_tests/rules/minimumfeedbackvertexset_minimumcodegenerationunlimitedregisters.rs b/src/unit_tests/rules/minimumfeedbackvertexset_minimumcodegenerationunlimitedregisters.rs index 8780a8d97..091816d56 100644 --- a/src/unit_tests/rules/minimumfeedbackvertexset_minimumcodegenerationunlimitedregisters.rs +++ b/src/unit_tests/rules/minimumfeedbackvertexset_minimumcodegenerationunlimitedregisters.rs @@ -2,6 +2,9 @@ use super::{issue_example_source, ReductionFVSToCodeGen}; use crate::models::misc::MinimumCodeGenerationUnlimitedRegisters; use crate::rules::test_helpers::assert_optimization_round_trip_from_optimization_target; use crate::rules::ReduceTo; +use crate::solvers::SolveOutcome; +use crate::traits::EvaluationError::InvalidConfiguration; +use crate::traits::Problem; #[test] fn test_minimumfeedbackvertexset_to_minimumcodegenerationunlimitedregisters_closed_loop() { @@ -48,7 +51,14 @@ fn test_codegen_start_nodes_cover_self_loops_and_parallel_arcs() { assert_eq!(target.num_internal(), 7); let config = (0..7).collect(); assert_eq!(target.evaluate(&config).unwrap(), Min(Some(9))); - let removed = reduction.extract_solution(&config).unwrap(); + let removed = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), config.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(removed, vec![true, false, true]); assert_eq!(source.evaluate(&removed).unwrap(), Min(Some(2))); } @@ -63,16 +73,30 @@ fn test_codegen_empty_graph_and_invalid_orders() { let reduction = ReduceTo::::reduce_to(&empty).unwrap(); assert_eq!(reduction.target_problem().num_vertices(), 1); assert_eq!( - reduction.extract_solution(&vec![]).unwrap(), + reduction + .recover_result( + &empty, + SolveOutcome::optimal(reduction.target_problem(), vec![].clone()).unwrap() + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"), Vec::::new() ); let source = issue_example_source(); let reduction = ReduceTo::::reduce_to(&source).unwrap(); - for config in [vec![], vec![9; 6], vec![0; 6], vec![1, 0, 2, 3, 4, 5]] { - assert!( - !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &config), Ok(value) if { value.is_valid() }) - ); + for config in [vec![], vec![9; 6]] { + assert!(matches!( + ReductionResult::target_problem(&reduction).evaluate(&config), + Err(InvalidConfiguration(_)) + )); + } + for config in [vec![0; 6], vec![1, 0, 2, 3, 4, 5]] { + assert!(!ReductionResult::target_problem(&reduction) + .evaluate(&config) + .unwrap() + .is_valid()); } } @@ -124,7 +148,18 @@ fn test_codegen_every_small_evaluation_permutation() { &mut |p| { let config = p.to_vec(); if let Min(Some(cost)) = target.evaluate(&config).unwrap() { - let removed = reduction.extract_solution(&config).unwrap(); + let removed = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), config.clone()) + .unwrap(), + ) + .map(|result| { + result.into_solution().expect( + "qualifying target result must recover a source solution", + ) + }) + .unwrap(); let Min(Some(size)) = source.evaluate(&removed).unwrap() else { panic!("every valid target order must extract an FVS"); }; diff --git a/src/unit_tests/rules/minimumgraphbandwidth_ilp.rs b/src/unit_tests/rules/minimumgraphbandwidth_ilp.rs index 9eec5a8d3..6cd90ea31 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::solvers::SolveOutcome; use crate::solvers::{BruteForce, ILPSolver}; use crate::topology::SimpleGraph; use crate::traits::Problem; @@ -35,7 +36,14 @@ fn test_minimumgraphbandwidth_to_ilp_closed_loop() { let ilp_solution = ilp_solver .solve(reduction.target_problem()) .expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let ilp_value = problem.evaluate(&extracted).unwrap(); assert!( ilp_value.0.is_some(), @@ -61,7 +69,14 @@ fn test_minimumgraphbandwidth_to_ilp_path() { let ilp_solution = ilp_solver .solve(reduction.target_problem()) .expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let value = problem.evaluate(&extracted).unwrap(); assert_eq!( value, diff --git a/src/unit_tests/rules/minimumhittingset_ilp.rs b/src/unit_tests/rules/minimumhittingset_ilp.rs index aa15bc9d6..ae1cc416f 100644 --- a/src/unit_tests/rules/minimumhittingset_ilp.rs +++ b/src/unit_tests/rules/minimumhittingset_ilp.rs @@ -1,4 +1,5 @@ use super::*; +use crate::solvers::SolveOutcome; use crate::solvers::{BruteForce, ILPSolver}; use crate::traits::Problem; @@ -24,7 +25,14 @@ fn test_minimumhittingset_to_ilp_bf_vs_ilp() { let bf_solutions = bf.find_all_witnesses(&problem).unwrap(); let bf_value = problem.evaluate(&bf_solutions[0]).unwrap(); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let ilp_value = problem.evaluate(&extracted).unwrap(); assert_eq!(bf_value, ilp_value); assert!(ilp_value.is_valid()); @@ -36,7 +44,14 @@ fn test_solution_extraction() { let reduction: ReductionHSToILP = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp_solution = vec![0, 1, 0]; - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted, vec![false, true, false]); assert!(problem.evaluate(&extracted).unwrap().is_valid()); } diff --git a/src/unit_tests/rules/minimuminternalmacrodatacompression_ilp.rs b/src/unit_tests/rules/minimuminternalmacrodatacompression_ilp.rs index 09908a2b9..473bbf45e 100644 --- a/src/unit_tests/rules/minimuminternalmacrodatacompression_ilp.rs +++ b/src/unit_tests/rules/minimuminternalmacrodatacompression_ilp.rs @@ -1,6 +1,7 @@ use crate::models::algebraic::ILP; use crate::models::misc::MinimumInternalMacroDataCompression; use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::solvers::SolveOutcome; use crate::solvers::{BruteForce, ILPSolver}; use crate::traits::Problem; use crate::types::Min; @@ -15,7 +16,14 @@ fn test_imdc_to_ilp_closed_loop_simple() { let solver = ILPSolver::new(); let target_witness = solver.solve(target).expect("ILP should be feasible"); - let source_config = reduction.extract_solution(&target_witness).unwrap(); + let source_config = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), target_witness.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let val = source.evaluate(&source_config).unwrap(); assert!(val.0.is_some()); assert_eq!(val.0.unwrap(), 2); @@ -31,7 +39,14 @@ fn test_imdc_to_ilp_closed_loop_repeated() { let solver = ILPSolver::new(); let target_witness = solver.solve(target).expect("ILP should be feasible"); - let source_config = reduction.extract_solution(&target_witness).unwrap(); + let source_config = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), target_witness.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let val = source.evaluate(&source_config).unwrap(); assert!(val.0.is_some()); assert_eq!(val.0.unwrap(), 4); @@ -48,7 +63,14 @@ fn test_imdc_to_ilp_closed_loop_low_pointer_cost() { let solver = ILPSolver::new(); let target_witness = solver.solve(target).expect("ILP should be feasible"); - let source_config = reduction.extract_solution(&target_witness).unwrap(); + let source_config = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), target_witness.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let val = source.evaluate(&source_config).unwrap(); assert!(val.0.is_some()); // Verify against brute force @@ -63,7 +85,14 @@ fn test_imdc_to_ilp_empty_string() { let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); let target = reduction.target_problem(); assert_eq!(target.num_variables(), 0); - let source_config = reduction.extract_solution(&vec![]).unwrap(); + let source_config = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), vec![].clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(source.evaluate(&source_config).unwrap(), Min(Some(0))); } @@ -77,7 +106,14 @@ fn test_imdc_to_ilp_single_char() { let solver = ILPSolver::new(); let target_witness = solver.solve(target).expect("ILP should be feasible"); - let source_config = reduction.extract_solution(&target_witness).unwrap(); + let source_config = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), target_witness.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(source.evaluate(&source_config).unwrap(), Min(Some(1))); } @@ -113,7 +149,14 @@ fn test_imdc_to_ilp_vs_brute_force() { let target_witness = ILPSolver::new() .solve(target) .expect("ILP should be feasible"); - let source_config = reduction.extract_solution(&target_witness).unwrap(); + let source_config = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), target_witness.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let ilp_val = source.evaluate(&source_config).unwrap(); assert_eq!( diff --git a/src/unit_tests/rules/minimummatrixcover_ilp.rs b/src/unit_tests/rules/minimummatrixcover_ilp.rs index 950ec637b..345b86a2e 100644 --- a/src/unit_tests/rules/minimummatrixcover_ilp.rs +++ b/src/unit_tests/rules/minimummatrixcover_ilp.rs @@ -2,6 +2,7 @@ use super::*; use crate::models::algebraic::MinimumMatrixCover; use crate::models::algebraic::{ObjectiveSense, ILP}; use crate::rules::test_helpers::assert_bf_vs_ilp; +use crate::solvers::SolveOutcome; use crate::solvers::{BruteForce, ILPSolver}; use crate::traits::Problem; use crate::types::Min; @@ -22,7 +23,14 @@ fn test_minimum_matrix_cover_to_ilp_closed_loop() { let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let value = problem.evaluate(&extracted).unwrap(); assert_eq!(value, Min(Some(-20))); } @@ -66,7 +74,14 @@ fn test_minimum_matrix_cover_to_ilp_bf_vs_ilp() { let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let ilp_value = problem.evaluate(&extracted).unwrap(); assert_eq!(bf_value, ilp_value); @@ -80,7 +95,14 @@ fn test_minimum_matrix_cover_to_ilp_2x2() { let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let value = problem.evaluate(&extracted).unwrap(); // Optimal: different signs → value = -(3+2) = -5 assert_eq!(value, Min(Some(-5))); @@ -102,7 +124,14 @@ fn test_minimum_matrix_cover_to_ilp_1x1() { let ilp_solution = ILPSolver::new() .solve(ilp) .expect("1x1 ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(problem.evaluate(&extracted).unwrap(), Min(Some(5))); } @@ -117,7 +146,14 @@ fn test_minimum_matrix_cover_to_ilp_diagonal_matrix() { let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("diagonal ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); // All configs give value 2+3+1 = 6 assert_eq!(problem.evaluate(&extracted).unwrap(), Min(Some(6))); } @@ -135,7 +171,14 @@ fn test_minimum_matrix_cover_to_ilp_asymmetric() { let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let ilp_value = problem.evaluate(&extracted).unwrap(); assert_eq!(bf_value, ilp_value); diff --git a/src/unit_tests/rules/minimummaximalmatching_ilp.rs b/src/unit_tests/rules/minimummaximalmatching_ilp.rs index 342a422fd..8beed4e5d 100644 --- a/src/unit_tests/rules/minimummaximalmatching_ilp.rs +++ b/src/unit_tests/rules/minimummaximalmatching_ilp.rs @@ -1,4 +1,5 @@ use super::*; +use crate::solvers::SolveOutcome; use crate::solvers::{BruteForce, ILPSolver}; use crate::topology::SimpleGraph; use crate::traits::Problem; @@ -36,7 +37,14 @@ fn test_minimummaximalmatching_to_ilp_closed_loop() { let ilp_solution = ilp_solver .solve(reduction.target_problem()) .expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let ilp_value = problem.evaluate(&extracted).unwrap(); assert_eq!(bf_value, Min(Some(1))); @@ -58,7 +66,14 @@ fn test_minimummaximalmatching_to_ilp_path_p6() { let ilp_solution = ilp_solver .solve(reduction.target_problem()) .expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(problem.evaluate(&extracted).unwrap(), Min(Some(2))); } @@ -74,7 +89,14 @@ fn test_minimummaximalmatching_to_ilp_triangle() { let ilp_solution = ilp_solver .solve(reduction.target_problem()) .expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(problem.evaluate(&extracted).unwrap(), Min(Some(1))); assert!(problem.evaluate(&extracted).unwrap().is_valid()); diff --git a/src/unit_tests/rules/minimummaximalmatching_maximumachromaticnumber.rs b/src/unit_tests/rules/minimummaximalmatching_maximumachromaticnumber.rs index 3a7f521c6..b40c7e47c 100644 --- a/src/unit_tests/rules/minimummaximalmatching_maximumachromaticnumber.rs +++ b/src/unit_tests/rules/minimummaximalmatching_maximumachromaticnumber.rs @@ -1,6 +1,7 @@ use crate::models::graph::{MaximumAchromaticNumber, MinimumMaximalMatching}; use crate::rules::traits::{ReduceTo, ReductionResult}; use crate::solvers::BruteForce; +use crate::solvers::SolveOutcome; use crate::topology::{BipartiteGraph, Graph, SimpleGraph}; use crate::traits::Problem; use crate::types::{Max, Min}; @@ -57,7 +58,14 @@ fn test_minimummaximalmatching_to_maximumachromaticnumber_closed_loop() { "complement(T-tree) must admit an achromatic 4-coloring" ); for witness in &target_witnesses { - let extracted = reduction.extract_solution(witness).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), (witness).clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!( source.evaluate(&extracted).unwrap(), Min(Some(1)), @@ -94,7 +102,14 @@ fn test_extract_solution_known_coloring() { // The single size-2 class {v2, v1} is the G-edge (v1, v2) = // unified edge (1, 3), source-edge index 1 in the edges list. let coloring = vec![1, 0, 3, 0, 2]; - let extracted = reduction.extract_solution(&coloring).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), coloring.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted, vec![false, true, false, false]); assert_eq!(source.evaluate(&extracted).unwrap(), Min(Some(1))); } @@ -115,14 +130,28 @@ fn test_extract_solution_recovers_suboptimal_matchings() { // Source edges in unified order: (0,3), (1,3), (1,4), (2,3). // Edge 0 = (v0, v1) selected; edge 2 = (v2, v3) selected. let coloring_a = vec![0, 1, 2, 0, 1]; - let extracted_a = reduction.extract_solution(&coloring_a).unwrap(); + let extracted_a = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), coloring_a.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted_a, vec![true, false, true, false]); assert_eq!(source.evaluate(&extracted_a).unwrap(), Min(Some(2))); // Suboptimal matching {(v1, v4), (v2, v3)} -> pair v1 with v4 and v2 // with v3; v0 takes a singleton color. Edge 2 = (v2, v3); edge 3 = (v1, v4). let coloring_b = vec![2, 0, 1, 1, 0]; - let extracted_b = reduction.extract_solution(&coloring_b).unwrap(); + let extracted_b = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), coloring_b.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted_b, vec![false, false, true, true]); assert_eq!(source.evaluate(&extracted_b).unwrap(), Min(Some(2))); } diff --git a/src/unit_tests/rules/minimummaximalmatching_minimummatrixdomination.rs b/src/unit_tests/rules/minimummaximalmatching_minimummatrixdomination.rs index 9d0a6ab52..f4fe1ee03 100644 --- a/src/unit_tests/rules/minimummaximalmatching_minimummatrixdomination.rs +++ b/src/unit_tests/rules/minimummaximalmatching_minimummatrixdomination.rs @@ -2,6 +2,7 @@ use crate::models::algebraic::MinimumMatrixDomination; use crate::models::graph::MinimumMaximalMatching; use crate::rules::traits::{ReduceTo, ReductionResult}; use crate::solvers::BruteForce; +use crate::solvers::SolveOutcome; use crate::topology::{BipartiteGraph, Graph}; use crate::traits::Problem; use crate::types::Min; @@ -61,7 +62,14 @@ fn test_minimummaximalmatching_to_minimummatrixdomination_closed_loop() { "matrix domination has at least one optimum" ); for witness in &target_witnesses { - let extracted = reduction.extract_solution(witness).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), (witness).clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!( source.evaluate(&extracted).unwrap(), Min(Some(2)), @@ -114,7 +122,14 @@ fn test_extract_solution_returns_maximal_matching() { .solve(target) .unwrap() .expect("matrix domination has an optimum"); - let extracted = reduction.extract_solution(&target_witness).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), target_witness.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); // The result must be a valid maximal matching of the source graph and // realize mm(B) = 2. @@ -190,7 +205,14 @@ fn test_extract_solution_yg_transform_on_non_matching_eds() { let target = reduction.target_problem(); assert_eq!(target.evaluate(&target_witness).unwrap(), Min(Some(2))); - let extracted = reduction.extract_solution(&target_witness).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), target_witness.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); // The extracted configuration must be a valid maximal matching of B of // size 2 (= mm(B)). Crucially it cannot be {(l0, r1), (l0, r2)} because diff --git a/src/unit_tests/rules/minimummetricdimension_ilp.rs b/src/unit_tests/rules/minimummetricdimension_ilp.rs index 431e64235..3338541bf 100644 --- a/src/unit_tests/rules/minimummetricdimension_ilp.rs +++ b/src/unit_tests/rules/minimummetricdimension_ilp.rs @@ -1,4 +1,5 @@ use super::*; +use crate::solvers::SolveOutcome; use crate::solvers::{BruteForce, ILPSolver}; use crate::traits::Problem; use crate::types::Min; @@ -23,7 +24,14 @@ fn test_minimummetricdimension_to_ilp_closed_loop() { // Solve via ILP reduction let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let ilp_size = problem.evaluate(&extracted).unwrap(); // Both should find optimal size = 2 @@ -84,7 +92,14 @@ fn test_minimummetricdimension_to_ilp_path_graph() { let ilp_solver = ILPSolver::new(); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!(problem.evaluate(&extracted).unwrap().is_valid()); assert_eq!(problem.evaluate(&extracted).unwrap(), Min(Some(1))); @@ -108,7 +123,14 @@ fn test_minimummetricdimension_to_ilp_complete_graph() { let bf_size = problem.evaluate(&bf_solutions[0]).unwrap(); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let ilp_size = problem.evaluate(&extracted).unwrap(); assert_eq!(bf_size, Min(Some(3))); @@ -123,7 +145,14 @@ fn test_minimummetricdimension_to_ilp_solution_extraction() { // Test that extraction works correctly (1:1 mapping) let ilp_solution = vec![1, 0, 0]; - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted, vec![true, false, false]); // Verify this is a valid resolving set @@ -143,7 +172,14 @@ fn test_minimummetricdimension_to_ilp_cycle() { let ilp_solver = ILPSolver::new(); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!(problem.evaluate(&extracted).unwrap().is_valid()); assert_eq!(problem.evaluate(&extracted).unwrap(), Min(Some(2))); diff --git a/src/unit_tests/rules/minimummultiwaycut_ilp.rs b/src/unit_tests/rules/minimummultiwaycut_ilp.rs index 95d63b69d..03e44cd25 100644 --- a/src/unit_tests/rules/minimummultiwaycut_ilp.rs +++ b/src/unit_tests/rules/minimummultiwaycut_ilp.rs @@ -1,5 +1,6 @@ use super::*; use crate::models::algebraic::ObjectiveSense; +use crate::solvers::SolveOutcome; use crate::solvers::{BruteForce, ILPSolver}; use crate::topology::SimpleGraph; use crate::traits::Problem; @@ -44,7 +45,14 @@ fn test_minimummultiwaycut_to_ilp_closed_loop() { // Solve via ILP let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let ilp_obj = problem.evaluate(&extracted).unwrap(); // Optimal cut cost is 8 @@ -66,7 +74,14 @@ fn test_triangle_with_3_terminals() { let ilp_solver = ILPSolver::new(); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let obj = problem.evaluate(&extracted).unwrap(); assert_eq!(obj, Min(Some(6))); @@ -85,7 +100,14 @@ fn test_two_terminals() { let ilp_solver = ILPSolver::new(); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let obj = problem.evaluate(&extracted).unwrap(); assert_eq!(obj, Min(Some(1))); @@ -123,7 +145,14 @@ fn test_solution_extraction() { ilp_solution[15 + 3] = 1; // edge (3,4) cut ilp_solution[15 + 4] = 1; // edge (0,4) cut - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted, vec![true, false, false, true, true, false]); let obj = problem.evaluate(&extracted).unwrap(); diff --git a/src/unit_tests/rules/minimummultiwaycut_qubo.rs b/src/unit_tests/rules/minimummultiwaycut_qubo.rs index d7f76eb19..cb615fe65 100644 --- a/src/unit_tests/rules/minimummultiwaycut_qubo.rs +++ b/src/unit_tests/rules/minimummultiwaycut_qubo.rs @@ -1,6 +1,7 @@ use super::*; use crate::solvers::BruteForce; use crate::solvers::BruteForceProblem as _; +use crate::solvers::SolveOutcome; use crate::traits::Problem; use crate::types::Min; @@ -35,7 +36,17 @@ fn signed_cut_weights_preserve_every_target_optimum() { for solution in solutions { assert_eq!( source - .evaluate(&reduction.extract_solution(&solution).unwrap()) + .evaluate( + &reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), solution.clone()) + .unwrap() + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution") + ) .unwrap(), Min(Some(optimum)) ); @@ -59,7 +70,14 @@ fn test_minimummultiwaycut_to_qubo_closed_loop() { // All QUBO optimal solutions should extract to valid source solutions with cost 8 for sol in &qubo_solutions { - let extracted = reduction.extract_solution(sol).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), (sol).clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let metric = source.evaluate(&extracted).unwrap(); assert_eq!(metric, Min(Some(8))); } @@ -81,7 +99,14 @@ fn test_minimummultiwaycut_to_qubo_small() { // All solutions should extract to valid cuts for sol in &qubo_solutions { - let extracted = reduction.extract_solution(sol).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), (sol).clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let metric = source.evaluate(&extracted).unwrap(); // With 2 terminals and path 0-1-2, minimum cut is 1 (cut either edge) assert_eq!(metric, Min(Some(1))); diff --git a/src/unit_tests/rules/minimumsetcovering_ilp.rs b/src/unit_tests/rules/minimumsetcovering_ilp.rs index ab5a48864..915c2c57f 100644 --- a/src/unit_tests/rules/minimumsetcovering_ilp.rs +++ b/src/unit_tests/rules/minimumsetcovering_ilp.rs @@ -1,4 +1,5 @@ use super::*; +use crate::solvers::SolveOutcome; use crate::solvers::{BruteForce, ILPSolver}; use crate::traits::Problem; use crate::types::Min; @@ -58,7 +59,14 @@ fn test_minimumsetcovering_to_ilp_closed_loop() { // Solve via ILP reduction let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); // Both should find optimal size = 2 let bf_size: usize = bf_solutions[0].iter().filter(|&&selected| selected).count(); @@ -95,7 +103,14 @@ fn test_ilp_solution_equals_brute_force_weighted() { let bf_obj = problem.evaluate(&bf_solutions[0]).unwrap(); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let ilp_obj = problem.evaluate(&extracted).unwrap(); assert_eq!(bf_obj, Min(Some(6))); @@ -113,7 +128,14 @@ fn test_solution_extraction() { // Test that extraction works correctly (1:1 mapping) let ilp_solution = vec![1, 1]; - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted, vec![true, true]); // Verify this is a valid set cover @@ -142,7 +164,14 @@ fn test_single_set_covers_all() { let ilp = reduction.target_problem(); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); // First set alone covers everything with weight 1 assert_eq!(extracted, vec![true, false, false, false]); @@ -162,7 +191,14 @@ fn test_overlapping_sets() { let ilp = reduction.target_problem(); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); // Need both sets to cover all elements assert_eq!(extracted, vec![true, true]); diff --git a/src/unit_tests/rules/minimumsummulticenter_ilp.rs b/src/unit_tests/rules/minimumsummulticenter_ilp.rs index a6d254c93..829290368 100644 --- a/src/unit_tests/rules/minimumsummulticenter_ilp.rs +++ b/src/unit_tests/rules/minimumsummulticenter_ilp.rs @@ -1,6 +1,7 @@ use super::*; use crate::models::algebraic::{ObjectiveSense, ILP}; use crate::models::graph::MinimumSumMulticenter; +use crate::solvers::SolveOutcome; use crate::solvers::{BruteForce, ILPSolver}; use crate::topology::SimpleGraph; use crate::traits::Problem; @@ -50,7 +51,14 @@ fn test_minimumsummulticenter_to_ilp_bf_vs_ilp() { let bf_cost = problem.evaluate(&bf_witness).unwrap().unwrap(); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!( extracted.len(), 3, @@ -91,7 +99,14 @@ fn test_minimumsummulticenter_to_ilp_respects_weighted_shortest_paths() { let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!( extracted, bf_witness, @@ -120,7 +135,14 @@ fn test_solution_extraction() { 0, 1, 0, // y_{1,0}, y_{1,1}, y_{1,2} 0, 1, 0, // y_{2,0}, y_{2,1}, y_{2,2} ]; - let extracted = reduction.extract_solution(&target_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), target_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted, vec![false, true, false]); assert_eq!(problem.evaluate(&extracted).unwrap().unwrap(), 2); } @@ -139,7 +161,14 @@ fn test_minimumsummulticenter_to_ilp_trivial() { let ilp_solver = ILPSolver::new(); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted.len(), 1); assert_eq!(extracted, vec![true]); assert_eq!(problem.evaluate(&extracted).unwrap().unwrap(), 0); diff --git a/src/unit_tests/rules/minimumtardinesssequencing_ilp.rs b/src/unit_tests/rules/minimumtardinesssequencing_ilp.rs index 7f8da1782..5bd5f3670 100644 --- a/src/unit_tests/rules/minimumtardinesssequencing_ilp.rs +++ b/src/unit_tests/rules/minimumtardinesssequencing_ilp.rs @@ -1,6 +1,7 @@ use super::*; use crate::models::algebraic::ILP; use crate::rules::test_helpers::assert_bf_vs_ilp; +use crate::solvers::SolveOutcome; use crate::solvers::{BruteForce, ILPSolver}; use crate::traits::Problem; use crate::types::One; @@ -27,7 +28,14 @@ fn test_minimumtardinesssequencing_to_ilp_bf_vs_ilp() { let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let ilp_value = problem.evaluate(&extracted).unwrap(); assert_eq!(bf_value, ilp_value); @@ -42,7 +50,14 @@ fn test_minimumtardinesssequencing_to_ilp_no_precedences() { let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!(problem.evaluate(&extracted).unwrap().is_valid()); } @@ -54,7 +69,14 @@ fn test_minimumtardinesssequencing_to_ilp_all_tight() { let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let value = problem.evaluate(&extracted).unwrap(); assert!(value.is_valid()); assert_eq!(value.0, Some(2)); @@ -87,7 +109,14 @@ fn test_minimumtardinesssequencing_weighted_to_ilp_vs_brute_force() { let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let ilp_value = problem.evaluate(&extracted).unwrap(); assert_eq!(bf_value, ilp_value); diff --git a/src/unit_tests/rules/minimumvertexcover_comparativecontainment.rs b/src/unit_tests/rules/minimumvertexcover_comparativecontainment.rs index 14c27cbc6..25501c28c 100644 --- a/src/unit_tests/rules/minimumvertexcover_comparativecontainment.rs +++ b/src/unit_tests/rules/minimumvertexcover_comparativecontainment.rs @@ -1,6 +1,9 @@ use super::*; use crate::rules::test_helpers::assert_satisfaction_round_trip_from_satisfaction_target; +use crate::rules::ReductionResult; use crate::solvers::BruteForce; +use crate::solvers::SolveOutcome; +use crate::traits::EvaluationError::InvalidConfiguration; use crate::traits::Problem; #[test] @@ -22,7 +25,17 @@ fn test_minimumvertexcover_to_comparativecontainment_closed_loop() { .unwrap(); assert!( source - .evaluate(&reduction.extract_solution(&witness).unwrap()) + .evaluate( + &reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), witness.clone()) + .unwrap() + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution") + ) .unwrap() .0 ); @@ -85,11 +98,27 @@ fn test_signed_containment_all_small_graphs_and_witnesses() { let valid = source.evaluate(&witness).unwrap().0; assert_eq!(target.evaluate(&witness).unwrap().0, valid); if valid { - assert_eq!(reduction.extract_solution(&witness).unwrap(), witness); - } else { - assert!( - !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &witness), Ok(value) if { value.is_valid() }) + assert_eq!( + reduction + .recover_result( + &source, + SolveOutcome::optimal( + reduction.target_problem(), + witness.clone() + ) + .unwrap() + ) + .map(|result| result.into_solution().expect( + "qualifying target result must recover a source solution" + )) + .unwrap(), + witness ); + } else { + assert!(!ReductionResult::target_problem(&reduction) + .evaluate(&witness) + .unwrap() + .is_valid()); } } } @@ -109,11 +138,22 @@ fn test_signed_containment_duplicate_edges_and_invalid_length() { ); let reduction = ReduceTo::>::reduce_to(&source).unwrap(); let witness = vec![true, false, false]; - assert_eq!(reduction.extract_solution(&witness).unwrap(), witness); + assert_eq!( + reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), witness.clone()).unwrap() + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"), + witness + ); for bad in [vec![], vec![true; 4]] { - assert!( - !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &bad), Ok(value) if { value.is_valid() }) - ); + assert!(matches!( + ReductionResult::target_problem(&reduction).evaluate(&bad), + Err(InvalidConfiguration(_)) + )); } } diff --git a/src/unit_tests/rules/minimumvertexcover_ensemblecomputation.rs b/src/unit_tests/rules/minimumvertexcover_ensemblecomputation.rs index 0249d4a50..8a5d8f63d 100644 --- a/src/unit_tests/rules/minimumvertexcover_ensemblecomputation.rs +++ b/src/unit_tests/rules/minimumvertexcover_ensemblecomputation.rs @@ -3,7 +3,9 @@ use crate::models::misc::EnsembleComputation; use crate::rules::traits::ReduceTo; use crate::rules::ReductionResult; use crate::solvers::BruteForce; +use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; +use crate::traits::EvaluationError::InvalidConfiguration; use crate::traits::Problem; use crate::types::{Min, One}; @@ -41,7 +43,14 @@ fn test_minimumvertexcover_to_ensemblecomputation_closed_loop() { // Every extracted solution must be a valid vertex cover let witnesses = solver.find_all_witnesses(target).unwrap(); for witness in &witnesses { - let source_config = reduction.extract_solution(witness).unwrap(); + let source_config = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), (witness).clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(source_config.len(), 2); assert_eq!(source.evaluate(&source_config).unwrap(), Min(Some(1))); assert!( @@ -105,7 +114,14 @@ fn test_extract_solution_correctness() { let target = reduction.target_problem(); assert_eq!(target.evaluate(&config).unwrap(), Min(Some(2))); - let cover = reduction.extract_solution(&config).unwrap(); + let cover = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), config.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(cover, vec![true, false]); assert!(is_valid_cover(&graph, &cover)); } @@ -123,7 +139,14 @@ fn test_extract_from_non_normalized_witness() { let target = reduction.target_problem(); assert_eq!(target.evaluate(&config).unwrap(), Min(Some(2))); - let cover = reduction.extract_solution(&config).unwrap(); + let cover = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), config.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(cover, vec![true, false]); assert!(is_valid_cover(&graph, &cover)); } @@ -155,7 +178,15 @@ fn test_minimumvertexcover_to_ensemblecomputation_zero_vertices() { assert_eq!(reduction.target_problem().budget(), 1); // No targets: even these out-of-range suffix operands have no semantics. assert_eq!( - reduction.extract_solution(&vec![usize::MAX; 2]).unwrap(), + reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), vec![usize::MAX; 2].clone()) + .unwrap() + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"), Vec::::new() ); } @@ -164,10 +195,15 @@ fn test_minimumvertexcover_to_ensemblecomputation_zero_vertices() { fn test_minimumvertexcover_to_ensemblecomputation_rejects_invalid_programs() { let source = MinimumVertexCover::new(SimpleGraph::new(2, vec![(0, 1)]), vec![One; 2]); let reduction = ReduceTo::::reduce_to(&source).unwrap(); - for program in [vec![], vec![0; 6], vec![3, 0, 1, 2, 0, 1]] { - assert!( - !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &program), Ok(value) if { value.is_valid() }) - ); + assert!(matches!( + ReductionResult::target_problem(&reduction).evaluate(&vec![]), + Err(InvalidConfiguration(_)) + )); + for program in [vec![0; 6], vec![3, 0, 1, 2, 0, 1]] { + assert!(!ReductionResult::target_problem(&reduction) + .evaluate(&program) + .unwrap() + .is_valid()); } } @@ -182,7 +218,14 @@ fn test_minimumvertexcover_to_ensemblecomputation_unused_and_repeated_operations reduction.target_problem().evaluate(&program).unwrap(), Min(Some(6)) ); - let cover = reduction.extract_solution(&program).unwrap(); + let cover = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), program.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(cover, vec![true, true, false, false, false]); assert!(is_valid_cover(source.graph(), &cover)); assert!(cover.iter().filter(|&&v| v).count() <= 6 - 2); @@ -245,7 +288,14 @@ fn test_minimumvertexcover_to_ensemblecomputation_all_small_pair_families() { let Min(Some(length)) = reduction.target_problem().evaluate(&program).unwrap() else { panic!("pair-family program must compute every triple"); }; - let cover = reduction.extract_solution(&program).unwrap(); + let cover = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), program.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let size = cover.iter().filter(|&&v| v).count(); assert!(is_valid_cover(source.graph(), &cover)); assert!(size <= length as usize - edges.len()); @@ -271,7 +321,14 @@ fn test_minimumvertexcover_to_ensemblecomputation_loops_and_parallel_edges() { reduction.target_problem().evaluate(&program).unwrap(), Min(Some(4)) ); - let cover = reduction.extract_solution(&program).unwrap(); + let cover = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), program.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(cover, vec![true, true, false]); assert_eq!(source.evaluate(&cover).unwrap(), Min(Some(2))); @@ -279,8 +336,17 @@ fn test_minimumvertexcover_to_ensemblecomputation_loops_and_parallel_edges() { let reduction = ReduceTo::::reduce_to(&source).unwrap(); assert_eq!( reduction - .extract_solution(&vec![1, 0, usize::MAX, usize::MAX]) - .unwrap(), + .recover_result( + &source, + SolveOutcome::optimal( + reduction.target_problem(), + vec![1, 0, usize::MAX, usize::MAX].clone() + ) + .unwrap() + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"), vec![true] ); } diff --git a/src/unit_tests/rules/minimumvertexcover_ilp.rs b/src/unit_tests/rules/minimumvertexcover_ilp.rs index 371655368..5e549d62f 100644 --- a/src/unit_tests/rules/minimumvertexcover_ilp.rs +++ b/src/unit_tests/rules/minimumvertexcover_ilp.rs @@ -1,6 +1,7 @@ use crate::models::algebraic::{ObjectiveSense, ILP}; use crate::models::graph::MinimumVertexCover; use crate::rules::{ReductionChain, ReductionGraph, ReductionPath}; +use crate::solvers::SolveOutcome; use crate::solvers::{BruteForce, ILPSolver}; use crate::topology::SimpleGraph; use crate::traits::Problem; @@ -57,7 +58,13 @@ fn test_minimumvertexcover_to_ilp_via_path_closed_loop() { let ilp_solver = ILPSolver::new(); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); - let extracted: Vec = chain.extract_solution(&ilp_solution).unwrap(); + let extracted: Vec = chain + .recover_result::, ILP>( + &problem, + SolveOutcome::optimal(ilp, ilp_solution.clone()).unwrap(), + ) + .map(|outcome| outcome.into_solution().unwrap()) + .unwrap(); let ilp_size = extracted.iter().filter(|&&selected| selected).count(); assert_eq!(ilp_size, 2); @@ -73,7 +80,13 @@ fn test_minimumvertexcover_to_ilp_via_path_weighted() { let ilp_solver = ILPSolver::new(); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); - let extracted = chain.extract_solution(&ilp_solution).unwrap(); + let extracted = chain + .recover_result::, ILP>( + &problem, + SolveOutcome::optimal(ilp, ilp_solution.clone()).unwrap(), + ) + .map(|outcome| outcome.into_solution().unwrap()) + .unwrap(); assert_eq!(problem.evaluate(&extracted).unwrap(), Min(Some(1))); assert_eq!(extracted, vec![false, true, false]); @@ -90,6 +103,12 @@ fn test_minimumvertexcover_to_ilp_bf_vs_ilp() { let bf_solutions = BruteForce::new().find_all_witnesses(&problem).unwrap(); let bf_value = problem.evaluate(&bf_solutions[0]).unwrap(); let ilp_solution = ILPSolver::new().solve(ilp).expect("ILP should be solvable"); - let extracted = chain.extract_solution(&ilp_solution).unwrap(); + let extracted = chain + .recover_result::, ILP>( + &problem, + SolveOutcome::optimal(ilp, ilp_solution.clone()).unwrap(), + ) + .map(|outcome| outcome.into_solution().unwrap()) + .unwrap(); assert_eq!(problem.evaluate(&extracted).unwrap(), bf_value); } diff --git a/src/unit_tests/rules/minimumvertexcover_minimumfeedbackarcset.rs b/src/unit_tests/rules/minimumvertexcover_minimumfeedbackarcset.rs index ef8a2d1fb..7bf0f902f 100644 --- a/src/unit_tests/rules/minimumvertexcover_minimumfeedbackarcset.rs +++ b/src/unit_tests/rules/minimumvertexcover_minimumfeedbackarcset.rs @@ -7,6 +7,7 @@ use crate::rules::traits::ReductionResult; use crate::rules::ReduceTo; #[cfg(feature = "example-db")] use crate::solvers::BruteForce; +use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; #[cfg(feature = "example-db")] use crate::traits::Problem; @@ -114,7 +115,14 @@ fn test_solution_extraction() { // Target has 9 arcs; first 3 are internal. Extract should take first 3. let target_config = vec![true, true, false, false, false, false, false, false, false]; - let source_config = reduction.extract_solution(&target_config).unwrap(); + let source_config = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), target_config.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(source_config, vec![true, true, false]); } diff --git a/src/unit_tests/rules/minimumvertexcover_minimumfeedbackvertexset.rs b/src/unit_tests/rules/minimumvertexcover_minimumfeedbackvertexset.rs index 9230ebe1c..8757fa584 100644 --- a/src/unit_tests/rules/minimumvertexcover_minimumfeedbackvertexset.rs +++ b/src/unit_tests/rules/minimumvertexcover_minimumfeedbackvertexset.rs @@ -7,6 +7,7 @@ use crate::rules::traits::ReductionResult; use crate::rules::ReduceTo; #[cfg(feature = "example-db")] use crate::solvers::BruteForce; +use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; #[cfg(feature = "example-db")] use crate::traits::Problem; @@ -82,8 +83,17 @@ fn test_identity_solution_extraction() { assert_eq!( reduction - .extract_solution(&vec![true, false, true, false, true]) - .unwrap(), + .recover_result( + &source, + SolveOutcome::optimal( + reduction.target_problem(), + vec![true, false, true, false, true].clone() + ) + .unwrap() + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"), vec![true, false, true, false, true] ); } diff --git a/src/unit_tests/rules/minimumvertexcover_minimumhittingset.rs b/src/unit_tests/rules/minimumvertexcover_minimumhittingset.rs index e80a1fcd3..5cc662a78 100644 --- a/src/unit_tests/rules/minimumvertexcover_minimumhittingset.rs +++ b/src/unit_tests/rules/minimumvertexcover_minimumhittingset.rs @@ -1,6 +1,7 @@ use super::*; use crate::rules::test_helpers::assert_optimization_round_trip_from_optimization_target; use crate::solvers::BruteForce; +use crate::solvers::SolveOutcome; #[test] fn test_minimumvertexcover_to_minimumhittingset_closed_loop() { @@ -129,6 +130,13 @@ fn test_vc_to_hs_solution_extraction() { ReduceTo::::reduce_to(&vc_problem).expect("reduction should succeed"); let target_solution = vec![false, true, false]; - let extracted = reduction.extract_solution(&target_solution).unwrap(); + let extracted = reduction + .recover_result( + &vc_problem, + SolveOutcome::optimal(reduction.target_problem(), target_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted, vec![false, true, false]); } diff --git a/src/unit_tests/rules/minimumvertexcover_minimummaximalmatching.rs b/src/unit_tests/rules/minimumvertexcover_minimummaximalmatching.rs index 8677416d5..262c05657 100644 --- a/src/unit_tests/rules/minimumvertexcover_minimummaximalmatching.rs +++ b/src/unit_tests/rules/minimumvertexcover_minimummaximalmatching.rs @@ -78,7 +78,7 @@ fn test_minimumvertexcover_to_minimummaximalmatching_has_no_runtime_modes() { assert!(!graph.has_direct_reduction_by_name_mode( "MinimumVertexCover", "MinimumMaximalMatching", - ReductionMode::Aggregate, + ReductionMode::Witness, )); assert!(!graph.has_direct_reduction_by_name_mode( "MinimumVertexCover", diff --git a/src/unit_tests/rules/minimumvertexcover_minimumweightandorgraph.rs b/src/unit_tests/rules/minimumvertexcover_minimumweightandorgraph.rs index 6b93320a5..a06e1c9a7 100644 --- a/src/unit_tests/rules/minimumvertexcover_minimumweightandorgraph.rs +++ b/src/unit_tests/rules/minimumvertexcover_minimumweightandorgraph.rs @@ -8,6 +8,7 @@ use crate::rules::traits::ReductionResult; use crate::rules::ReduceTo; #[cfg(feature = "example-db")] use crate::solvers::BruteForce; +use crate::solvers::SolveOutcome; use crate::topology::SimpleGraph; use crate::traits::Problem; use crate::types::Min; @@ -85,7 +86,14 @@ fn test_weighted_vertices_are_charged_on_sink_arcs() { assert_eq!(target.evaluate(&target_solution).unwrap(), Min(Some(5))); assert_eq!(target.arc_weights(), &[1, 1, 1, 1, 1, 1, 4, 1, 3]); assert_eq!( - reduction.extract_solution(&target_solution).unwrap(), + reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), target_solution.clone()).unwrap() + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"), vec![false, true, false] ); } diff --git a/src/unit_tests/rules/minimumvertexcover_qubo.rs b/src/unit_tests/rules/minimumvertexcover_qubo.rs index 6fc0509bb..3a8ff90b0 100644 --- a/src/unit_tests/rules/minimumvertexcover_qubo.rs +++ b/src/unit_tests/rules/minimumvertexcover_qubo.rs @@ -3,6 +3,7 @@ use crate::models::graph::MinimumVertexCover; use crate::rules::{ReductionChain, ReductionGraph, ReductionPath}; use crate::solvers::BruteForce; use crate::solvers::BruteForceProblem as _; +use crate::solvers::SolveOutcome; use crate::topology::SimpleGraph; use crate::traits::Problem; use crate::types::Min; @@ -60,7 +61,13 @@ fn test_minimumvertexcover_to_qubo_via_path_closed_loop() { let solver = BruteForce::new(); let qubo_solutions = solver.find_all_witnesses(qubo).unwrap(); for sol in &qubo_solutions { - let extracted = chain.extract_solution(sol).unwrap(); + let extracted = chain + .recover_result::, QUBO>( + &problem, + SolveOutcome::optimal(qubo, (sol).clone()).unwrap(), + ) + .map(|outcome| outcome.into_solution().unwrap()) + .unwrap(); assert!(problem.evaluate(&extracted).unwrap().is_valid()); assert_eq!(extracted.iter().filter(|&&x| x).count(), 2); } @@ -78,7 +85,13 @@ fn test_minimumvertexcover_to_qubo_via_path_weighted() { .solve(qubo) .unwrap() .expect("QUBO should be solvable via path"); - let extracted = chain.extract_solution(&qubo_solution).unwrap(); + let extracted = chain + .recover_result::, QUBO>( + &problem, + SolveOutcome::optimal(qubo, qubo_solution.clone()).unwrap(), + ) + .map(|outcome| outcome.into_solution().unwrap()) + .unwrap(); assert_eq!(problem.evaluate(&extracted).unwrap(), Min(Some(1))); assert_eq!(extracted, vec![false, true, false]); @@ -100,7 +113,13 @@ fn test_minimumvertexcover_to_qubo_via_path_star_graph() { .solve(qubo) .unwrap() .expect("QUBO should be solvable"); - let extracted = chain.extract_solution(&qubo_solution).unwrap(); + let extracted = chain + .recover_result::, QUBO>( + &problem, + SolveOutcome::optimal(qubo, qubo_solution.clone()).unwrap(), + ) + .map(|outcome| outcome.into_solution().unwrap()) + .unwrap(); assert_eq!(problem.evaluate(&extracted).unwrap(), Min(Some(1))); assert_eq!(extracted.iter().filter(|&&x| x).count(), 1); diff --git a/src/unit_tests/rules/minimumweightdecoding_ilp.rs b/src/unit_tests/rules/minimumweightdecoding_ilp.rs index fa462ed7d..69cffe26e 100644 --- a/src/unit_tests/rules/minimumweightdecoding_ilp.rs +++ b/src/unit_tests/rules/minimumweightdecoding_ilp.rs @@ -1,5 +1,6 @@ use super::*; use crate::models::algebraic::{ObjectiveSense, ILP}; +use crate::solvers::SolveOutcome; use crate::solvers::{BruteForce, ILPSolver}; use crate::traits::Problem; use crate::types::Min; @@ -65,7 +66,14 @@ fn test_minimumweightdecoding_to_ilp_closed_loop() { let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be feasible"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let ilp_value = problem.evaluate(&extracted).unwrap(); assert_eq!(ilp_value, bf_value); @@ -87,7 +95,14 @@ fn test_minimumweightdecoding_to_ilp_small_closed_loop() { let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be feasible"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(problem.evaluate(&extracted).unwrap(), bf_value); } @@ -123,7 +138,14 @@ fn test_minimumweightdecoding_to_ilp_extract_solution() { // Row 1: H[1][2]=1 → sum=1, s=1 → 1-1=0 → k_1=0 ✓ // Row 2: H[2][2]=0 → sum=0, s=0 → 0-0=0 → k_2=0 ✓ let target_solution = vec![0, 0, 1, 0, 0, 0, 0]; - let extracted = reduction.extract_solution(&target_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), target_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted.len(), 4); assert_eq!(extracted, vec![false, false, true, false]); assert_eq!(problem.evaluate(&extracted).unwrap(), Min(Some(1))); diff --git a/src/unit_tests/rules/minmaxmulticenter_ilp.rs b/src/unit_tests/rules/minmaxmulticenter_ilp.rs index 743658bfb..1c2751867 100644 --- a/src/unit_tests/rules/minmaxmulticenter_ilp.rs +++ b/src/unit_tests/rules/minmaxmulticenter_ilp.rs @@ -1,6 +1,7 @@ use super::*; use crate::models::algebraic::{ObjectiveSense, ILP}; use crate::models::graph::MinMaxMulticenter; +use crate::solvers::SolveOutcome; use crate::solvers::{BruteForce, ILPSolver}; use crate::topology::SimpleGraph; use crate::traits::Problem; @@ -53,7 +54,14 @@ fn test_minmaxmulticenter_to_ilp_bf_vs_ilp() { assert_eq!(problem.evaluate(&bf_witness).unwrap(), Min(Some(1))); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!( extracted.len(), 3, @@ -83,7 +91,14 @@ fn test_solution_extraction() { 0, 1, 0, // y_{2,0}, y_{2,1}, y_{2,2} 1, // z ]; - let extracted = reduction.extract_solution(&target_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), target_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted, vec![false, true, false]); assert_eq!(problem.evaluate(&extracted).unwrap(), Min(Some(1))); } @@ -108,7 +123,14 @@ fn test_minmaxmulticenter_to_ilp_weighted() { let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(problem.evaluate(&extracted).unwrap(), Min(Some(100))); } @@ -124,7 +146,14 @@ fn test_minmaxmulticenter_to_ilp_trivial() { let ilp_solver = ILPSolver::new(); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted.len(), 1); assert_eq!(problem.evaluate(&extracted).unwrap(), Min(Some(0))); } diff --git a/src/unit_tests/rules/mixedchinesepostman_ilp.rs b/src/unit_tests/rules/mixedchinesepostman_ilp.rs index 46e5817de..19a726299 100644 --- a/src/unit_tests/rules/mixedchinesepostman_ilp.rs +++ b/src/unit_tests/rules/mixedchinesepostman_ilp.rs @@ -1,6 +1,7 @@ use super::*; use crate::models::algebraic::ILP; use crate::rules::ReduceTo; +use crate::solvers::SolveOutcome; use crate::solvers::{BruteForce, ILPSolver}; use crate::topology::MixedGraph; use crate::traits::Problem; @@ -23,7 +24,14 @@ fn test_mixedchinesepostman_to_ilp_closed_loop() { let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be feasible"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!(source.evaluate(&extracted).unwrap().0.is_some()); } @@ -45,7 +53,14 @@ fn test_mixedchinesepostman_to_ilp_bf_vs_ilp() { let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be feasible"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let ilp_value = source.evaluate(&extracted).unwrap(); assert_eq!( @@ -71,7 +86,14 @@ fn test_mixedchinesepostman_to_ilp_weighted() { let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be feasible"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let ilp_value = source.evaluate(&extracted).unwrap(); assert_eq!( @@ -93,7 +115,14 @@ fn test_mixedchinesepostman_to_ilp_with_isolated_vertices() { ); 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(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(source.evaluate(&extracted).unwrap().0, Some(69)); } diff --git a/src/unit_tests/rules/monochromatictriangle_ilp.rs b/src/unit_tests/rules/monochromatictriangle_ilp.rs index b2c0ba372..95fbf1317 100644 --- a/src/unit_tests/rules/monochromatictriangle_ilp.rs +++ b/src/unit_tests/rules/monochromatictriangle_ilp.rs @@ -2,6 +2,7 @@ use crate::models::algebraic::{ObjectiveSense, ILP}; use crate::models::graph::MonochromaticTriangle; use crate::rules::traits::{ReduceTo, ReductionResult}; use crate::solvers::ILPSolver; +use crate::solvers::SolveOutcome; use crate::topology::SimpleGraph; use crate::traits::Problem; @@ -46,7 +47,14 @@ fn test_monochromatic_triangle_to_ilp_closed_loop() { let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("K4 should admit a monochromatic-triangle-free 2-edge-coloring"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!( extracted, @@ -82,7 +90,14 @@ fn test_monochromatic_triangle_to_ilp_extract_solution_identity() { let reduction = ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let coloring = vec![0, 0, 1, 1, 0, 1]; - let extracted = reduction.extract_solution(&coloring).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), coloring.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted, vec![false, false, true, true, false, true]); assert!(problem.evaluate(&extracted).unwrap()); diff --git a/src/unit_tests/rules/multiplechoicebranching_ilp.rs b/src/unit_tests/rules/multiplechoicebranching_ilp.rs index 6c995f893..d11313829 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::solvers::SolveOutcome; use crate::solvers::{BruteForce, ILPSolver}; use crate::topology::DirectedGraph; use crate::traits::Problem; @@ -18,7 +19,14 @@ fn test_multiplechoicebranching_to_ilp_closed_loop() { match expected { Some(_) => { let target = ILPSolver::new().solve(reduction.target_problem()).unwrap(); - let actual = reduction.extract_solution(&target).unwrap(); + let actual = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), target.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!(problem.evaluate(&actual).unwrap().0); } None => assert_eq!( @@ -63,7 +71,14 @@ fn test_multiplechoicebranching_to_ilp_empty_graph() { let reduction = ReduceTo::>::reduce_to(&problem).unwrap(); let target = ILPSolver::new().solve(reduction.target_problem()).unwrap(); assert_eq!( - reduction.extract_solution(&target).unwrap(), + reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), target.clone()).unwrap() + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"), Vec::::new() ); } diff --git a/src/unit_tests/rules/multiplecopyfileallocation_ilp.rs b/src/unit_tests/rules/multiplecopyfileallocation_ilp.rs index 27d7b9ce5..9f05960b2 100644 --- a/src/unit_tests/rules/multiplecopyfileallocation_ilp.rs +++ b/src/unit_tests/rules/multiplecopyfileallocation_ilp.rs @@ -1,5 +1,6 @@ use super::*; use crate::models::graph::MultipleCopyFileAllocation; +use crate::solvers::SolveOutcome; use crate::solvers::{BruteForce, ILPSolver}; use crate::topology::SimpleGraph; use crate::traits::Problem; @@ -48,7 +49,14 @@ fn test_multiplecopyfileallocation_to_ilp_bf_vs_ilp() { assert!(problem.evaluate(&bf_witness).unwrap().0.is_some()); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!( extracted.len(), 3, @@ -76,7 +84,14 @@ fn test_solution_extraction() { 0, 1, 0, // y_{1,0}, y_{1,1}, y_{1,2} 0, 1, 0, // y_{2,0}, y_{2,1}, y_{2,2} ]; - let extracted = reduction.extract_solution(&target_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), target_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted, vec![false, true, false]); assert_eq!(problem.evaluate(&extracted).unwrap(), Min(Some(7))); } @@ -95,7 +110,14 @@ fn test_multiplecopyfileallocation_to_ilp_trivial() { let ilp_solver = ILPSolver::new(); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted.len(), 1); assert_eq!(problem.evaluate(&extracted).unwrap(), Min(Some(3))); } @@ -110,7 +132,14 @@ fn test_multiplecopyfileallocation_unreachable_assignments_are_forbidden() { let reduction = ReduceTo::>::reduce_to(&problem).unwrap(); let direct = BruteForce::new().solve(&problem).unwrap().unwrap(); let target = ILPSolver::new().solve(reduction.target_problem()).unwrap(); - let extracted = reduction.extract_solution(&target).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), target.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!( problem.evaluate(&extracted).unwrap(), diff --git a/src/unit_tests/rules/multiprocessorscheduling_ilp.rs b/src/unit_tests/rules/multiprocessorscheduling_ilp.rs index 25921170e..0441df218 100644 --- a/src/unit_tests/rules/multiprocessorscheduling_ilp.rs +++ b/src/unit_tests/rules/multiprocessorscheduling_ilp.rs @@ -1,4 +1,5 @@ use super::*; +use crate::solvers::SolveOutcome; use crate::solvers::{BruteForce, ILPSolver}; use crate::traits::Problem; use crate::types::Or; @@ -49,7 +50,14 @@ fn test_multiprocessorscheduling_to_ilp_bf_vs_ilp() { assert_eq!(problem.evaluate(&bf_witness).unwrap(), Or(true)); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be feasible"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!( problem.evaluate(&extracted).unwrap(), Or(true), @@ -67,7 +75,14 @@ fn test_solution_extraction() { // Manually set: task 0 → proc 0, task 1 → proc 1, task 2 → proc 0 // Variables: x_{0,0}=1, x_{0,1}=0, x_{1,0}=0, x_{1,1}=1, x_{2,0}=1, x_{2,1}=0 let ilp_solution = vec![1, 0, 0, 1, 1, 0]; - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted, vec![0, 1, 0]); // loads: proc 0 = 1+3=4 ≤ 5, proc 1 = 2 ≤ 5 assert_eq!(problem.evaluate(&extracted).unwrap(), Or(true)); @@ -88,6 +103,13 @@ fn test_multiprocessorscheduling_to_ilp_trivial() { let ilp_solver = ILPSolver::new(); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be feasible"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(problem.evaluate(&extracted).unwrap(), Or(true)); } diff --git a/src/unit_tests/rules/naesatisfiability_ilp.rs b/src/unit_tests/rules/naesatisfiability_ilp.rs index 4e168df0c..de9eda49f 100644 --- a/src/unit_tests/rules/naesatisfiability_ilp.rs +++ b/src/unit_tests/rules/naesatisfiability_ilp.rs @@ -1,4 +1,5 @@ use super::*; +use crate::solvers::SolveOutcome; use crate::solvers::{BruteForce, ILPSolver}; use crate::traits::Problem; use crate::types::Or; @@ -50,7 +51,14 @@ fn test_naesatisfiability_to_ilp_bf_vs_ilp() { assert_eq!(problem.evaluate(&bf_witness).unwrap(), Or(true)); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be feasible"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(problem.evaluate(&extracted).unwrap(), Or(true)); } @@ -107,7 +115,14 @@ fn test_naesatisfiability_to_ilp_negative_literals() { let ilp_solution = ilp_solver .solve(ilp) .expect("NAE-SAT with (¬x1 ∨ x2) is feasible"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!( problem.evaluate(&extracted).unwrap(), Or(true), diff --git a/src/unit_tests/rules/naesatisfiability_maxcut.rs b/src/unit_tests/rules/naesatisfiability_maxcut.rs index 165fecb08..128a3682f 100644 --- a/src/unit_tests/rules/naesatisfiability_maxcut.rs +++ b/src/unit_tests/rules/naesatisfiability_maxcut.rs @@ -1,11 +1,16 @@ use super::*; +use crate::models::decision::Decision; use crate::models::formula::CNFClause; use crate::models::formula::NAESatisfiability; use crate::models::graph::MaxCut; -use crate::rules::test_helpers::assert_satisfaction_round_trip_from_optimization_target; +use crate::rules::test_helpers::assert_satisfaction_round_trip_from_satisfaction_target; +use crate::rules::ReductionResult; use crate::solvers::BruteForce; +use crate::solvers::SolveOutcome; use crate::topology::SimpleGraph; +use crate::traits::EvaluationError::InvalidConfiguration; use crate::traits::Problem; +use crate::types::OptimizationValue; #[test] fn test_naesatisfiability_to_maxcut_closed_loop() { @@ -19,16 +24,16 @@ fn test_naesatisfiability_to_maxcut_closed_loop() { CNFClause::new(vec![-1, -2, 3]), ], ); - let reduction = - ReduceTo::>::reduce_to(&naesat).expect("reduction should succeed"); + let reduction = ReduceTo::>>::reduce_to(&naesat) + .expect("reduction should succeed"); let target = reduction.target_problem(); // 2*3 = 6 vertices - assert_eq!(target.num_vertices(), 6); + assert_eq!(target.inner().num_vertices(), 6); // 3 variable edges + 3 + 3 = 9 clause edges - assert_eq!(target.num_edges(), 9); + assert_eq!(target.inner().num_edges(), 9); - assert_satisfaction_round_trip_from_optimization_target( + assert_satisfaction_round_trip_from_satisfaction_target( &naesat, &reduction, "NAESAT -> MaxCut closed loop", @@ -39,15 +44,15 @@ fn test_naesatisfiability_to_maxcut_closed_loop() { fn test_naesatisfiability_to_maxcut_single_clause() { // Single clause: (x1, x2, x3) — NAE-satisfying iff not all same let naesat = NAESatisfiability::new(3, vec![CNFClause::new(vec![1, 2, 3])]); - let reduction = - ReduceTo::>::reduce_to(&naesat).expect("reduction should succeed"); + let reduction = ReduceTo::>>::reduce_to(&naesat) + .expect("reduction should succeed"); let target = reduction.target_problem(); // 6 vertices, 3 variable + 3 clause = 6 edges - assert_eq!(target.num_vertices(), 6); - assert_eq!(target.num_edges(), 6); + assert_eq!(target.inner().num_vertices(), 6); + assert_eq!(target.inner().num_edges(), 6); - assert_satisfaction_round_trip_from_optimization_target( + assert_satisfaction_round_trip_from_satisfaction_target( &naesat, &reduction, "NAESAT single clause -> MaxCut", @@ -59,15 +64,15 @@ fn test_naesatisfiability_to_maxcut_two_literal_clause() { // Clause with 2 literals: (x1, ~x2) — always NAE-satisfying unless x1=T, x2=F or x1=F, x2=T... actually (x1, ~x2) is NAE-unsatisfied when both literals are same: x1=T,~x2=T (x2=F) or x1=F,~x2=F (x2=T). // NAE-satisfied when x1 != ~x2, i.e., x1 == x2. let naesat = NAESatisfiability::new(2, vec![CNFClause::new(vec![1, -2])]); - let reduction = - ReduceTo::>::reduce_to(&naesat).expect("reduction should succeed"); + let reduction = ReduceTo::>>::reduce_to(&naesat) + .expect("reduction should succeed"); let target = reduction.target_problem(); // 4 vertices, 2 variable + 1 clause = 3 edges - assert_eq!(target.num_vertices(), 4); - assert_eq!(target.num_edges(), 3); + assert_eq!(target.inner().num_vertices(), 4); + assert_eq!(target.inner().num_edges(), 3); - assert_satisfaction_round_trip_from_optimization_target( + assert_satisfaction_round_trip_from_satisfaction_target( &naesat, &reduction, "NAESAT 2-literal clause -> MaxCut", @@ -78,15 +83,15 @@ fn test_naesatisfiability_to_maxcut_two_literal_clause() { fn test_naesatisfiability_to_maxcut_four_literal_clause() { // Clause with 4 literals: (x1, x2, ~x3, x4) let naesat = NAESatisfiability::new(4, vec![CNFClause::new(vec![1, 2, -3, 4])]); - let reduction = - ReduceTo::>::reduce_to(&naesat).expect("reduction should succeed"); + let reduction = ReduceTo::>>::reduce_to(&naesat) + .expect("reduction should succeed"); let target = reduction.target_problem(); // One auxiliary variable and two triangles: 10 vertices, 5 + 6 edges. - assert_eq!(target.num_vertices(), 10); - assert_eq!(target.num_edges(), 11); + assert_eq!(target.inner().num_vertices(), 10); + assert_eq!(target.inner().num_edges(), 11); - assert_satisfaction_round_trip_from_optimization_target( + assert_satisfaction_round_trip_from_satisfaction_target( &naesat, &reduction, "NAESAT 4-literal clause -> MaxCut", @@ -103,15 +108,22 @@ fn test_naesatisfiability_to_maxcut_extract_solution() { CNFClause::new(vec![-1, 3, 2]), ], ); - let reduction = - ReduceTo::>::reduce_to(&naesat).expect("reduction should succeed"); + let reduction = ReduceTo::>>::reduce_to(&naesat) + .expect("reduction should succeed"); // Vertices: x1(0), ~x1(1), x2(2), ~x2(3), x3(4), ~x3(5) // x1=T -> vertex 0 in set 1, vertex 1 in set 0 // x2=F -> vertex 2 in set 0, vertex 3 in set 1 // x3=T -> vertex 4 in set 1, vertex 5 in set 0 let target_config = vec![true, false, false, true, true, false]; - let extracted = reduction.extract_solution(&target_config).unwrap(); + let extracted = reduction + .recover_result( + &naesat, + SolveOutcome::optimal(reduction.target_problem(), target_config.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted, vec![true, false, true]); // x1=T, x2=F, x3=T // Verify this is a valid NAE-SAT solution @@ -129,15 +141,15 @@ fn test_naesatisfiability_to_maxcut_mixed_clause_sizes() { CNFClause::new(vec![-1, -3]), // 2 literals -> 1 pair ], ); - let reduction = - ReduceTo::>::reduce_to(&naesat).expect("reduction should succeed"); + let reduction = ReduceTo::>>::reduce_to(&naesat) + .expect("reduction should succeed"); let target = reduction.target_problem(); // 6 vertices, 3 variable + (1 + 3 + 1) = 8 edges - assert_eq!(target.num_vertices(), 6); - assert_eq!(target.num_edges(), 8); + assert_eq!(target.inner().num_vertices(), 6); + assert_eq!(target.inner().num_edges(), 8); - assert_satisfaction_round_trip_from_optimization_target( + assert_satisfaction_round_trip_from_satisfaction_target( &naesat, &reduction, "NAESAT mixed clause sizes -> MaxCut", @@ -155,8 +167,8 @@ fn test_naesatisfiability_to_maxcut_optimal_cut_value() { CNFClause::new(vec![-1, -2, 3]), ], ); - let reduction = - ReduceTo::>::reduce_to(&naesat).expect("reduction should succeed"); + let reduction = ReduceTo::>>::reduce_to(&naesat) + .expect("reduction should succeed"); let target = reduction.target_problem(); let solver = BruteForce::new(); @@ -164,27 +176,37 @@ fn test_naesatisfiability_to_maxcut_optimal_cut_value() { assert!(witness.is_some()); let config = witness.unwrap(); - let cut_value = target.cut_size(&config).unwrap(); + let cut_value = target.inner().cut_size(&config).unwrap(); // n=3, m=2, M=3, k1=3, k2=3 // Expected: 3*3 + (3-1) + (3-1) = 9 + 2 + 2 = 13 assert_eq!(cut_value, 13); } fn check_every_cut(source: &NAESatisfiability) { - use crate::rules::AggregateReductionResult; - let reduction = ReduceTo::>::reduce_to(source).unwrap(); - let target = AggregateReductionResult::target_problem(&reduction); + let reduction = ReduceTo::>>::reduce_to(source).unwrap(); + let target = crate::rules::ReductionResult::target_problem(&reduction); let mut decoded = vec![false; 1 << source.num_vars()]; let mut best = i64::MIN; - for mask in 0..(1usize << target.num_vertices()) { - let cut = (0..target.num_vertices()) + for mask in 0..(1usize << target.inner().num_vertices()) { + let cut = (0..target.inner().num_vertices()) .map(|i| mask & (1 << i) != 0) .collect(); - let value = target.evaluate(&cut).unwrap(); + let value = target.inner().evaluate(&cut).unwrap(); best = best.max(value.0.unwrap()); - let certificate = AggregateReductionResult::extract_value(&reduction, value).0; + let certificate = crate::types::Or(OptimizationValue::meets_bound( + &(value), + crate::rules::ReductionResult::target_problem(&reduction).bound(), + )) + .0; if certificate { - let assignment = reduction.extract_solution(&cut).unwrap(); + let assignment = reduction + .recover_result( + source, + SolveOutcome::optimal(reduction.target_problem(), cut.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!(source.evaluate(&assignment).unwrap().0); assert_eq!( assignment, @@ -206,13 +228,26 @@ fn check_every_cut(source: &NAESatisfiability) { assert_eq!(has_extension, source.evaluate(&assignment).unwrap().0); } assert_eq!( - AggregateReductionResult::extract_value(&reduction, crate::types::Max(Some(best))).0, + crate::types::Or(OptimizationValue::meets_bound( + &(crate::types::Max(Some(best))), + crate::rules::ReductionResult::target_problem(&reduction).bound() + )) + .0, decoded.iter().any(|&valid| valid) ); - assert!(!AggregateReductionResult::extract_value(&reduction, crate::types::Max(None)).0); assert!( - !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![false; target.num_vertices() + 1]), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&reduction, value); value.is_valid() }) + !crate::types::Or(OptimizationValue::meets_bound( + &(crate::types::Max(None)), + crate::rules::ReductionResult::target_problem(&reduction).bound() + )) + .0 ); + assert!(matches!( + ReductionResult::target_problem(&reduction) + .inner() + .evaluate(&vec![false; target.inner().num_vertices() + 1]), + Err(InvalidConfiguration(_)) + )); } #[test] @@ -248,8 +283,8 @@ fn test_naesatisfiability_to_maxcut_long_clause_interactions() { ], ); check_every_cut(&source); - let reduction = ReduceTo::>::reduce_to(&source).unwrap(); - assert_eq!(reduction.feasible_cut, 26); + let reduction = ReduceTo::>>::reduce_to(&source).unwrap(); + assert_eq!(*ReductionResult::target_problem(&reduction).bound(), 26); for clauses in [ vec![vec![1, 1, 1, 1, 1]], vec![vec![1, 2, 3, 1, 2], vec![1, -2], vec![2, -3]], diff --git a/src/unit_tests/rules/naesatisfiability_partitionintoperfectmatchings.rs b/src/unit_tests/rules/naesatisfiability_partitionintoperfectmatchings.rs index faf837e52..5b1c965e7 100644 --- a/src/unit_tests/rules/naesatisfiability_partitionintoperfectmatchings.rs +++ b/src/unit_tests/rules/naesatisfiability_partitionintoperfectmatchings.rs @@ -3,6 +3,7 @@ use crate::models::formula::{CNFClause, NAESatisfiability}; use crate::models::graph::PartitionIntoPerfectMatchings; use crate::rules::test_helpers::assert_satisfaction_round_trip_from_satisfaction_target; use crate::solvers::BruteForce; +use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; use crate::traits::Problem; @@ -255,7 +256,14 @@ fn test_naesatisfiability_to_partitionintoperfectmatchings_constructed_witness_r .evaluate(&target_solution) .unwrap()); assert_eq!( - reduction.extract_solution(&target_solution).unwrap(), + reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), target_solution.clone()).unwrap() + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"), source_solution ); } @@ -274,7 +282,14 @@ fn test_naesatisfiability_to_partitionintoperfectmatchings_two_literal_clause_no assert_eq!(target.num_matchings(), 2); assert!(target.evaluate(&target_solution).unwrap()); assert_eq!( - reduction.extract_solution(&target_solution).unwrap(), + reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), target_solution.clone()).unwrap() + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"), source_solution ); } diff --git a/src/unit_tests/rules/naesatisfiability_setsplitting.rs b/src/unit_tests/rules/naesatisfiability_setsplitting.rs index e35cfbfd7..c35bb0e97 100644 --- a/src/unit_tests/rules/naesatisfiability_setsplitting.rs +++ b/src/unit_tests/rules/naesatisfiability_setsplitting.rs @@ -3,6 +3,7 @@ use crate::models::set::SetSplitting; use crate::rules::test_helpers::assert_satisfaction_round_trip_from_satisfaction_target; use crate::rules::{ReduceTo, ReductionResult}; use crate::solvers::BruteForce; +use crate::solvers::SolveOutcome; use crate::traits::Problem; fn rule_example_problem() -> NAESatisfiability { @@ -54,8 +55,17 @@ fn test_naesatisfiability_to_setsplitting_extract_solution_uses_positive_literal assert_eq!( reduction - .extract_solution(&vec![true, true, false, false, false, true]) - .unwrap(), + .recover_result( + &source, + SolveOutcome::optimal( + reduction.target_problem(), + vec![true, true, false, false, false, true].clone() + ) + .unwrap() + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"), vec![true, true, false] ); } @@ -67,7 +77,14 @@ fn test_naesatisfiability_to_setsplitting_target_witness_extracts_to_satisfying_ let solver = BruteForce::new(); let target_solution = solver.solve(reduction.target_problem()).unwrap().unwrap(); - let source_solution = reduction.extract_solution(&target_solution).unwrap(); + let source_solution = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), target_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!(source.evaluate(&source_solution).unwrap()); } diff --git a/src/unit_tests/rules/numerical3dimensionalmatching_numericalmatchingwithtargetsums.rs b/src/unit_tests/rules/numerical3dimensionalmatching_numericalmatchingwithtargetsums.rs index cf2136f51..7eada88e2 100644 --- a/src/unit_tests/rules/numerical3dimensionalmatching_numericalmatchingwithtargetsums.rs +++ b/src/unit_tests/rules/numerical3dimensionalmatching_numericalmatchingwithtargetsums.rs @@ -2,6 +2,7 @@ use super::*; use crate::models::misc::{Numerical3DimensionalMatching, NumericalMatchingWithTargetSums}; use crate::rules::test_helpers::assert_satisfaction_round_trip_from_satisfaction_target; use crate::solvers::BruteForce; +use crate::solvers::SolveOutcome; use crate::traits::Problem; fn yes_problem() -> Numerical3DimensionalMatching { @@ -54,7 +55,14 @@ fn test_n3dm_to_nmts_extracts_target_witness_into_source_witness() { .0 ); - let extracted = reduction.extract_solution(&target_solution).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), target_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted, vec![2, 0, 1, 0, 2, 1]); assert!(source.evaluate(&extracted).unwrap().0); } @@ -74,7 +82,14 @@ fn test_n3dm_to_nmts_handles_repeated_targets() { .0 ); - let extracted = reduction.extract_solution(&target_solution).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), target_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted.len(), 4); assert!(source.evaluate(&extracted).unwrap().0); } diff --git a/src/unit_tests/rules/numericalmatchingwithtargetsums_ilp.rs b/src/unit_tests/rules/numericalmatchingwithtargetsums_ilp.rs index 50dc188a1..9b635d849 100644 --- a/src/unit_tests/rules/numericalmatchingwithtargetsums_ilp.rs +++ b/src/unit_tests/rules/numericalmatchingwithtargetsums_ilp.rs @@ -2,6 +2,7 @@ use super::*; use crate::models::algebraic::{Comparison, ObjectiveSense, ILP}; use crate::rules::test_helpers::assert_bf_vs_ilp; use crate::solvers::ILPSolver; +use crate::solvers::SolveOutcome; use crate::traits::Problem; use crate::types::Or; @@ -16,7 +17,14 @@ fn test_numericalmatchingwithtargetsums_to_ilp_closed_loop() { let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(problem.evaluate(&extracted).unwrap(), Or(true)); } @@ -75,7 +83,14 @@ fn test_numericalmatchingwithtargetsums_to_ilp_single_pair() { let ilp_solution = ILPSolver::new() .solve(ilp) .expect("single-pair ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted, vec![0]); assert_eq!(problem.evaluate(&extracted).unwrap(), Or(true)); } @@ -95,7 +110,14 @@ fn test_numericalmatchingwithtargetsums_to_ilp_compatible_triples_only() { assert_eq!(ilp.num_vars(), 2); let ilp_solution = ILPSolver::new().solve(ilp).expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted, vec![0, 1]); assert_eq!(problem.evaluate(&extracted).unwrap(), Or(true)); } diff --git a/src/unit_tests/rules/openshopscheduling_ilp.rs b/src/unit_tests/rules/openshopscheduling_ilp.rs index 904f7fbf2..eedf2bf05 100644 --- a/src/unit_tests/rules/openshopscheduling_ilp.rs +++ b/src/unit_tests/rules/openshopscheduling_ilp.rs @@ -2,6 +2,7 @@ use super::*; use crate::models::algebraic::ILP; use crate::models::misc::OpenShopScheduling; use crate::solvers::ILPSolver; +use crate::solvers::SolveOutcome; use crate::traits::Problem; use crate::types::Min; @@ -63,7 +64,14 @@ fn test_openshopscheduling_to_ilp_closed_loop_small() { .solve(reduction.target_problem()) .expect("ILP should be feasible"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &p, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let value = p.evaluate(&extracted).unwrap(); assert!( value.0.is_some(), @@ -82,7 +90,14 @@ fn test_openshopscheduling_to_ilp_closed_loop_medium() { .solve(reduction.target_problem()) .expect("ILP should be feasible"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &p, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let value = p.evaluate(&extracted).unwrap(); assert!( value.0.is_some(), @@ -102,7 +117,14 @@ fn test_openshopscheduling_to_ilp_extract_solution_respects_start_times() { let reduction: ReductionOSSToILP = 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(); + let extracted = reduction + .recover_result( + &p, + SolveOutcome::optimal(reduction.target_problem(), target_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted, vec![0, 1, 1, 0]); assert_eq!(p.evaluate(&extracted).unwrap(), Min(Some(3))); } @@ -118,7 +140,14 @@ fn test_openshopscheduling_to_ilp_single_job() { let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be feasible"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &p, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let value = p.evaluate(&extracted).unwrap(); assert!(value.0.is_some()); assert_eq!(value, Min(Some(7))); @@ -133,7 +162,14 @@ fn test_openshopscheduling_to_ilp_single_machine() { let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be feasible"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &p, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let value = p.evaluate(&extracted).unwrap(); assert!(value.0.is_some()); assert_eq!(value, Min(Some(6))); diff --git a/src/unit_tests/rules/optimallineararrangement_consecutiveonesmatrixaugmentation.rs b/src/unit_tests/rules/optimallineararrangement_consecutiveonesmatrixaugmentation.rs index fff241b12..00e074817 100644 --- a/src/unit_tests/rules/optimallineararrangement_consecutiveonesmatrixaugmentation.rs +++ b/src/unit_tests/rules/optimallineararrangement_consecutiveonesmatrixaugmentation.rs @@ -2,8 +2,11 @@ use super::*; use crate::models::decision::Decision; use crate::models::graph::OptimalLinearArrangement; use crate::rules::ReduceTo; +use crate::rules::ReductionResult; use crate::solvers::BruteForce; +use crate::solvers::SolveOutcome; use crate::topology::SimpleGraph; +use crate::traits::EvaluationError::InvalidConfiguration; use crate::traits::Problem; use crate::types::Or; @@ -59,7 +62,14 @@ fn test_optimallineararrangement_to_consecutiveonesmatrixaugmentation_closed_loo assert_eq!(target.evaluate(&target_witness).unwrap(), Or(true)); // Reconstructed source arrangement must be a valid arrangement of length <= k. - let arrangement = reduction.extract_solution(&target_witness).unwrap(); + let arrangement = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), target_witness.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(source.evaluate(&arrangement).unwrap(), Or(true)); } @@ -99,12 +109,20 @@ fn test_optimallineararrangement_to_consecutiveonesmatrixaugmentation_edgeless_s assert_eq!(target.evaluate(&witness).unwrap(), Or(true)); // Reconstructed source arrangement covers all 3 vertices and is YES. - let arrangement = reduction.extract_solution(&witness).unwrap(); + let arrangement = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), witness.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(arrangement.len(), 3); assert_eq!(source.evaluate(&arrangement).unwrap(), Or(true)); - assert!( - !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![]), Ok(value) if { value.is_valid() }) - ); + assert!(matches!( + ReductionResult::target_problem(&reduction).evaluate(&vec![]), + Err(InvalidConfiguration(_)) + )); } #[test] @@ -136,9 +154,10 @@ fn test_optimallineararrangement_to_consecutiveonesmatrixaugmentation_negative_b BruteForce::new().solve(&source).unwrap().is_none(), "P_6 has no arrangement of length <= 4" ); - assert!( - !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![]), Ok(value) if { value.is_valid() }) - ); + assert!(matches!( + ReductionResult::target_problem(&reduction).evaluate(&vec![]), + Err(InvalidConfiguration(_)) + )); } #[test] @@ -184,14 +203,25 @@ fn test_optimallineararrangement_to_consecutiveonesmatrixaugmentation_native_dom if let Some(witness) = witness { assert_eq!( source - .evaluate(&reduction.extract_solution(&witness).unwrap()) + .evaluate( + &reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), witness.clone()) + .unwrap() + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution") + ) .unwrap(), Or(true) ); } else { - assert!( - !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &(0..target.num_cols()).collect()), Ok(value) if { value.is_valid() }) - ); + assert!(!ReductionResult::target_problem(&reduction) + .evaluate(&(0..target.num_cols()).collect()) + .unwrap() + .is_valid()); } } } @@ -200,13 +230,22 @@ fn test_optimallineararrangement_to_consecutiveonesmatrixaugmentation_native_dom fn test_optimallineararrangement_to_consecutiveonesmatrixaugmentation_certificate() { let source = decision_ola(SimpleGraph::new(3, vec![(0, 2)]), 1); let reduction = ReduceTo::::reduce_to(&source).unwrap(); - assert!( - !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![0, 1, 2]), Ok(value) if { value.is_valid() }) - ); - assert!( - !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![0, 1, 3]), Ok(value) if { value.is_valid() }) - ); - let arrangement = reduction.extract_solution(&vec![2, 0, 1]).unwrap(); + assert!(!ReductionResult::target_problem(&reduction) + .evaluate(&vec![0, 1, 2]) + .unwrap() + .is_valid()); + assert!(matches!( + ReductionResult::target_problem(&reduction).evaluate(&vec![0, 1, 3]), + Err(InvalidConfiguration(_)) + )); + let arrangement = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), vec![2, 0, 1].clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(arrangement, vec![1, 2, 0]); assert_eq!(source.evaluate(&arrangement).unwrap(), Or(true)); } diff --git a/src/unit_tests/rules/optimallineararrangement_ilp.rs b/src/unit_tests/rules/optimallineararrangement_ilp.rs index c45f0bf41..7a6fef307 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::solvers::SolveOutcome; use crate::solvers::{BruteForce, ILPSolver}; use crate::topology::SimpleGraph; use crate::traits::Problem; @@ -34,7 +35,14 @@ fn test_optimallineararrangement_to_ilp_closed_loop() { let ilp_solution = ilp_solver .solve(reduction.target_problem()) .expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!( problem.evaluate(&extracted).unwrap().0.is_some(), "ILP solution should produce a valid arrangement" @@ -64,7 +72,14 @@ fn test_optimallineararrangement_to_ilp_with_chords() { let ilp_solution = ilp_solver .solve(reduction.target_problem()) .expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!(problem.evaluate(&extracted).unwrap().0.is_some()); } @@ -77,7 +92,14 @@ fn test_solution_extraction() { let ilp_solution = ilp_solver .solve(reduction.target_problem()) .expect("solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!(problem.evaluate(&extracted).unwrap().0.is_some()); } diff --git a/src/unit_tests/rules/optimallineararrangement_sequencingtominimizeweightedcompletiontime.rs b/src/unit_tests/rules/optimallineararrangement_sequencingtominimizeweightedcompletiontime.rs index 0347eda91..295031185 100644 --- a/src/unit_tests/rules/optimallineararrangement_sequencingtominimizeweightedcompletiontime.rs +++ b/src/unit_tests/rules/optimallineararrangement_sequencingtominimizeweightedcompletiontime.rs @@ -1,6 +1,7 @@ use super::*; use crate::rules::test_helpers::assert_optimization_round_trip_from_optimization_target; use crate::solvers::BruteForce; +use crate::solvers::SolveOutcome; use crate::topology::SimpleGraph; use crate::traits::Problem; use crate::types::Min; @@ -98,7 +99,14 @@ fn test_optimallineararrangement_to_sequencingtominimizeweightedcompletiontime_e ) { let (source, reduction) = reduce_path(4); let schedule = vec![3, 2, 6, 1, 5, 0, 4]; - let extracted = reduction.extract_solution(&schedule).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), schedule.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted, vec![3, 2, 1, 0]); assert_eq!(source.evaluate(&extracted).unwrap(), Min(Some(3))); diff --git a/src/unit_tests/rules/optimumcommunicationspanningtree_ilp.rs b/src/unit_tests/rules/optimumcommunicationspanningtree_ilp.rs index 4e0f1cf8c..1220733fc 100644 --- a/src/unit_tests/rules/optimumcommunicationspanningtree_ilp.rs +++ b/src/unit_tests/rules/optimumcommunicationspanningtree_ilp.rs @@ -1,6 +1,7 @@ use super::*; use crate::models::algebraic::{ObjectiveSense, ILP}; use crate::rules::test_helpers::assert_bf_vs_ilp; +use crate::solvers::SolveOutcome; use crate::solvers::{BruteForce, ILPSolver}; use crate::traits::Problem; use crate::types::Min; @@ -76,7 +77,14 @@ fn test_ocst_to_ilp_bf_vs_ilp_k3() { let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let ilp_value = problem.evaluate(&extracted).unwrap(); assert_eq!(bf_value, ilp_value); @@ -95,7 +103,14 @@ fn test_ocst_to_ilp_bf_vs_ilp_k4() { let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let ilp_value = problem.evaluate(&extracted).unwrap(); assert_eq!(bf_value, ilp_value); @@ -111,7 +126,14 @@ fn test_ocst_to_ilp_extraction() { let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); // Should be a valid config with m=3 entries assert_eq!(extracted.len(), 3); @@ -153,7 +175,14 @@ fn test_ocst_zero_requirement_pairs_still_enforce_spanning_tree() { ); let reduction = ReduceTo::>::reduce_to(&problem).unwrap(); let solution = ILPSolver::new().solve(reduction.target_problem()).unwrap(); - let extracted = reduction.extract_solution(&solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(problem.evaluate(&extracted).unwrap(), Min(Some(55))); } diff --git a/src/unit_tests/rules/paintshop_ilp.rs b/src/unit_tests/rules/paintshop_ilp.rs index aa721eda1..24ef1b36a 100644 --- a/src/unit_tests/rules/paintshop_ilp.rs +++ b/src/unit_tests/rules/paintshop_ilp.rs @@ -1,6 +1,7 @@ use super::*; use crate::models::algebraic::{ObjectiveSense, ILP}; use crate::rules::test_helpers::assert_bf_vs_ilp; +use crate::solvers::SolveOutcome; use crate::solvers::{BruteForce, ILPSolver}; use crate::traits::Problem; @@ -40,7 +41,14 @@ fn test_paintshop_to_ilp_bf_vs_ilp() { let ilp_solution = ilp_solver .solve(reduction.target_problem()) .expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let ilp_value = problem.evaluate(&extracted).unwrap(); assert_eq!(bf_value, ilp_value); @@ -56,7 +64,14 @@ fn test_solution_extraction() { let ilp_solution = ilp_solver .solve(reduction.target_problem()) .expect("solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted.len(), 1); // Either 0 or 1 is valid; coloring is [x, 1-x], switches = 1 assert!(problem.evaluate(&extracted).unwrap().is_valid()); diff --git a/src/unit_tests/rules/paintshop_qubo.rs b/src/unit_tests/rules/paintshop_qubo.rs index 69ec5351d..cd97aaed0 100644 --- a/src/unit_tests/rules/paintshop_qubo.rs +++ b/src/unit_tests/rules/paintshop_qubo.rs @@ -1,6 +1,7 @@ use super::*; use crate::rules::test_helpers::assert_optimization_round_trip_from_optimization_target; use crate::solvers::BruteForce; +use crate::solvers::SolveOutcome; #[test] fn test_paintshop_to_qubo_closed_loop() { @@ -47,7 +48,14 @@ fn test_paintshop_to_qubo_optimal_value() { // Extract solutions and verify they are optimal for the source for sol in &best_target { - let source_sol = reduction.extract_solution(sol).unwrap(); + let source_sol = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), (sol).clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let switches = source.count_switches(&source_sol).unwrap(); // Optimal is 2 switches assert_eq!(switches, 2, "Expected 2 switches for optimal solution"); diff --git a/src/unit_tests/rules/partiallyorderedknapsack_ilp.rs b/src/unit_tests/rules/partiallyorderedknapsack_ilp.rs index f907bd954..778b7909a 100644 --- a/src/unit_tests/rules/partiallyorderedknapsack_ilp.rs +++ b/src/unit_tests/rules/partiallyorderedknapsack_ilp.rs @@ -1,4 +1,5 @@ use super::*; +use crate::solvers::SolveOutcome; use crate::solvers::{BruteForce, ILPSolver}; use crate::traits::Problem; @@ -28,7 +29,14 @@ fn test_partiallyorderedknapsack_to_ilp_bf_vs_ilp() { let bf_value = problem.evaluate(&bf_solutions[0]).unwrap(); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let ilp_value = problem.evaluate(&extracted).unwrap(); assert_eq!(bf_value, ilp_value); @@ -44,7 +52,14 @@ fn test_solution_extraction() { let ilp_solution = ilp_solver .solve(reduction.target_problem()) .expect("solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!(problem.evaluate(&extracted).unwrap().is_valid()); } diff --git a/src/unit_tests/rules/partition_binpacking.rs b/src/unit_tests/rules/partition_binpacking.rs index 4d0185802..1c4e07e88 100644 --- a/src/unit_tests/rules/partition_binpacking.rs +++ b/src/unit_tests/rules/partition_binpacking.rs @@ -2,6 +2,7 @@ use super::*; use crate::models::misc::{BinPacking, Partition}; use crate::rules::test_helpers::assert_satisfaction_round_trip_from_optimization_target; use crate::solvers::BruteForce; +use crate::solvers::SolveOutcome; use crate::traits::Problem; use crate::types::Min; @@ -49,6 +50,22 @@ fn test_partition_to_binpacking_odd_total_is_not_satisfying() { let value = target.evaluate(&best).unwrap(); assert_eq!(value, Min(Some(3))); - let extracted = reduction.extract_solution(&best).unwrap(); + let extracted = reduction.map_solution(&best).unwrap(); + assert_eq!( + reduction + .recover_result( + &source, + SolveOutcome::optimal(target, best.clone()).unwrap() + ) + .unwrap(), + SolveOutcome::Infeasible + ); + assert!(matches!( + reduction.recover_result( + &source, + SolveOutcome::feasible(target, best.clone()).unwrap() + ), + Err(crate::rules::ExtractionError::InsufficientSolutionQuality) + )); assert!(!source.evaluate(&extracted).unwrap()); } diff --git a/src/unit_tests/rules/partition_cosineproductintegration.rs b/src/unit_tests/rules/partition_cosineproductintegration.rs index 3d84ffddc..02ae55da3 100644 --- a/src/unit_tests/rules/partition_cosineproductintegration.rs +++ b/src/unit_tests/rules/partition_cosineproductintegration.rs @@ -2,6 +2,7 @@ use super::*; use crate::models::misc::{CosineProductIntegration, Partition}; use crate::rules::test_helpers::assert_satisfaction_round_trip_from_satisfaction_target; use crate::solvers::BruteForce; +use crate::solvers::SolveOutcome; use crate::traits::Problem; fn reduce_partition(sizes: &[i64]) -> (Partition, ReductionPartitionToCPI) { @@ -70,7 +71,14 @@ fn test_partition_to_cosineproductintegration_solution_extraction() { let target_solutions = solver.find_all_witnesses(target).unwrap(); for sol in &target_solutions { - let extracted = reduction.extract_solution(sol).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), (sol).clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted.len(), source.num_elements()); let target_valid = target.evaluate(sol).unwrap(); let source_valid = source.evaluate(&extracted).unwrap(); diff --git a/src/unit_tests/rules/partition_integralflowwithmultipliers.rs b/src/unit_tests/rules/partition_integralflowwithmultipliers.rs index b0289975d..3d7da8957 100644 --- a/src/unit_tests/rules/partition_integralflowwithmultipliers.rs +++ b/src/unit_tests/rules/partition_integralflowwithmultipliers.rs @@ -3,6 +3,7 @@ use crate::models::graph::IntegralFlowWithMultipliers; use crate::models::misc::Partition; use crate::rules::test_helpers::assert_satisfaction_round_trip_from_satisfaction_target; use crate::solvers::BruteForce; +use crate::solvers::SolveOutcome; #[test] fn test_partition_to_integralflowwithmultipliers_closed_loop() { @@ -85,8 +86,17 @@ fn test_partition_to_integralflowwithmultipliers_extract_solution() { assert_eq!( reduction - .extract_solution(&vec![1, 0, 1, 0, 1, 0, 2, 0, 4, 0, 6, 0, 12]) - .unwrap(), + .recover_result( + &source, + SolveOutcome::optimal( + reduction.target_problem(), + vec![1, 0, 1, 0, 1, 0, 2, 0, 4, 0, 6, 0, 12].clone() + ) + .unwrap() + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"), vec![true, false, true, false, true, false] ); } diff --git a/src/unit_tests/rules/partition_knapsack.rs b/src/unit_tests/rules/partition_knapsack.rs index c4d2274df..07b436620 100644 --- a/src/unit_tests/rules/partition_knapsack.rs +++ b/src/unit_tests/rules/partition_knapsack.rs @@ -2,6 +2,7 @@ use super::*; use crate::models::misc::Partition; use crate::rules::test_helpers::assert_satisfaction_round_trip_from_optimization_target; use crate::solvers::BruteForce; +use crate::solvers::SolveOutcome; use crate::traits::Problem; use crate::types::Max; @@ -41,6 +42,22 @@ fn test_partition_to_knapsack_odd_total_is_not_satisfying() { assert_eq!(target.evaluate(&best).unwrap(), Max(Some(5))); - let extracted = reduction.extract_solution(&best).unwrap(); + let extracted = reduction.map_solution(&best).unwrap(); + assert_eq!( + reduction + .recover_result( + &source, + SolveOutcome::optimal(target, best.clone()).unwrap() + ) + .unwrap(), + SolveOutcome::Infeasible + ); + assert!(matches!( + reduction.recover_result( + &source, + SolveOutcome::feasible(target, best.clone()).unwrap() + ), + Err(crate::rules::ExtractionError::InsufficientSolutionQuality) + )); assert!(!source.evaluate(&extracted).unwrap()); } diff --git a/src/unit_tests/rules/partition_multiprocessorscheduling.rs b/src/unit_tests/rules/partition_multiprocessorscheduling.rs index cbec3fcc6..1b72a9212 100644 --- a/src/unit_tests/rules/partition_multiprocessorscheduling.rs +++ b/src/unit_tests/rules/partition_multiprocessorscheduling.rs @@ -2,6 +2,7 @@ use super::*; use crate::models::misc::Partition; use crate::rules::test_helpers::assert_satisfaction_round_trip_from_satisfaction_target; use crate::solvers::BruteForce; +use crate::solvers::SolveOutcome; use crate::traits::Problem; fn reduce_partition(sizes: &[i64]) -> (Partition, ReductionPartitionToMPS) { @@ -84,7 +85,14 @@ fn test_partition_to_multiprocessorscheduling_solution_extraction() { let target_solutions = solver.find_all_witnesses(target).unwrap(); for sol in &target_solutions { - let extracted = reduction.extract_solution(sol).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), (sol).clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); // Solution length should match number of elements assert_eq!(extracted.len(), source.num_elements()); // Extracted solution should satisfy source if target is satisfied diff --git a/src/unit_tests/rules/partition_openshopscheduling.rs b/src/unit_tests/rules/partition_openshopscheduling.rs index 3145ec02b..16a25f44e 100644 --- a/src/unit_tests/rules/partition_openshopscheduling.rs +++ b/src/unit_tests/rules/partition_openshopscheduling.rs @@ -1,37 +1,56 @@ use super::*; use crate::models::algebraic::ILP; +use crate::models::decision::Decision; use crate::models::misc::{OpenShopScheduling, Partition}; +use crate::rules::ReductionResult; use crate::solvers::ILPSolver; +use crate::solvers::SolveOutcome; +use crate::traits::EvaluationError::InvalidConfiguration; use crate::traits::Problem; +use crate::types::OptimizationValue; fn solve_target(target: &OpenShopScheduling) -> Vec { 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"); - reduction.extract_solution(&ilp_solution).unwrap() + reduction + .recover_result( + target, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution") } #[test] fn test_partition_to_open_shop_scheduling_closed_loop() { let source = Partition::new(vec![1, 2, 3]).unwrap(); - let reduction = ReduceTo::::reduce_to(&source).unwrap(); - let target_solution = solve_target(reduction.target_problem()); - let extracted = reduction.extract_solution(&target_solution).unwrap(); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + let target_solution = solve_target(reduction.target_problem().inner()); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), target_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!(source.evaluate(&extracted).unwrap()); } #[test] fn test_partition_to_open_shop_scheduling_structure() { let source = Partition::new(vec![1, 2, 3]).unwrap(); - let reduction = - ReduceTo::::reduce_to(&source).expect("reduction should succeed"); + let reduction = ReduceTo::>::reduce_to(&source) + .expect("reduction should succeed"); let target = reduction.target_problem(); - assert_eq!(target.num_jobs(), 4); - assert_eq!(target.num_machines(), 3); + assert_eq!(target.inner().num_jobs(), 4); + assert_eq!(target.inner().num_machines(), 3); assert_eq!( - target.processing_times(), + target.inner().processing_times(), &[vec![1, 1, 1], vec![2, 2, 2], vec![3, 3, 3], vec![3, 3, 3]] ); } @@ -39,9 +58,16 @@ fn test_partition_to_open_shop_scheduling_structure() { #[test] fn test_partition_to_open_shop_scheduling_extract_solution() { let source = Partition::new(vec![1, 2, 3]).unwrap(); - let reduction = ReduceTo::::reduce_to(&source).unwrap(); - let target_solution = solve_target(reduction.target_problem()); - let extracted = reduction.extract_solution(&target_solution).unwrap(); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + let target_solution = solve_target(reduction.target_problem().inner()); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), target_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted.len(), 3); assert!(source.evaluate(&extracted).unwrap()); } @@ -49,22 +75,23 @@ fn test_partition_to_open_shop_scheduling_extract_solution() { #[test] fn test_partition_to_open_shop_scheduling_odd_total_is_not_satisfying() { let source = Partition::new(vec![2, 4, 5]).unwrap(); - let reduction = ReduceTo::::reduce_to(&source).unwrap(); - let best = solve_target(reduction.target_problem()); - assert!( - !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &best), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&reduction, value); value.is_valid() }) - ); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + let best = solve_target(reduction.target_problem().inner()); + assert!(!ReductionResult::target_problem(&reduction) + .evaluate(&best) + .unwrap() + .is_valid()); } #[test] fn test_partition_to_open_shop_scheduling_preserves_construction_overflow() { let source = Partition::new(vec![1_i64 << 61, 1_i64 << 61]).unwrap(); - let error = ReduceTo::::reduce_to(&source).unwrap_err(); + let error = ReduceTo::>::reduce_to(&source).unwrap_err(); assert!(matches!( error, crate::rules::ReductionError::Construction { source_problem: "Partition", - target_problem: "OpenShopScheduling", + target_problem: "DecisionOpenShopScheduling", cause: crate::registry::ConstructionError::IntegerOverflow(_), } )); @@ -72,7 +99,6 @@ fn test_partition_to_open_shop_scheduling_preserves_construction_overflow() { #[test] fn test_partition_to_open_shop_all_small_partitions_and_machine_orders() { - use crate::rules::AggregateReductionResult; let permutations = [ [0, 1, 2], [0, 2, 1], @@ -91,10 +117,14 @@ fn test_partition_to_open_shop_all_small_partitions_and_machine_orders() { }) .collect(); let source = Partition::new(sizes.clone()).unwrap(); - let reduction = ReduceTo::::reduce_to(&source).unwrap(); - let target = AggregateReductionResult::target_problem(&reduction); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + let target = crate::rules::ReductionResult::target_problem(&reduction); assert!( - !AggregateReductionResult::extract_value(&reduction, crate::types::Min(None)).0 + !crate::types::Or(OptimizationValue::meets_bound( + &(crate::types::Min(None)), + crate::rules::ReductionResult::target_problem(&reduction).bound() + )) + .0 ); for mask in 0..(1usize << n) { let assignment: Vec<_> = (0..n).map(|i| mask & (1 << i) != 0).collect(); @@ -118,41 +148,70 @@ fn test_partition_to_open_shop_all_small_partitions_and_machine_orders() { assert_eq!(time, (phase + 1) * half); } } - let value = target.evaluate(&schedule).unwrap(); + let value = target.inner().evaluate(&schedule).unwrap(); assert_eq!(value, crate::types::Min(Some(3 * half as i64))); - assert!(AggregateReductionResult::extract_value(&reduction, value).0); - assert_eq!(reduction.extract_solution(&schedule).unwrap(), assignment); - let delayed: Vec<_> = schedule.iter().map(|&time| time + 1).collect(); - assert!(target.evaluate(&delayed).unwrap().0.is_some()); assert!( - !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &delayed), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&reduction, value); value.is_valid() }) + crate::types::Or(OptimizationValue::meets_bound( + &(value), + crate::rules::ReductionResult::target_problem(&reduction).bound() + )) + .0 ); + assert_eq!( + reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), schedule.clone()) + .unwrap() + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"), + assignment + ); + let delayed: Vec<_> = schedule.iter().map(|&time| time + 1).collect(); + assert!(target.inner().evaluate(&delayed).unwrap().0.is_some()); + assert!(!ReductionResult::target_problem(&reduction) + .evaluate(&delayed) + .unwrap() + .is_valid()); } } - assert!( - !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![0; (n + 1) * 3]), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&reduction, value); value.is_valid() }) - ); - assert!( - !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![0; (n + 1) * 3 + 1]), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&reduction, value); value.is_valid() }) - ); + assert!(!ReductionResult::target_problem(&reduction) + .evaluate(&vec![0; (n + 1) * 3]) + .unwrap() + .is_valid()); + assert!(matches!( + ReductionResult::target_problem(&reduction) + .inner() + .evaluate(&vec![0; (n + 1) * 3 + 1]), + Err(InvalidConfiguration(_)) + )); } } } #[test] fn test_partition_to_open_shop_odd_singleton_certificate() { - use crate::rules::AggregateReductionResult; let source = Partition::new(vec![1]).unwrap(); - let reduction = ReduceTo::::reduce_to(&source).unwrap(); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); let schedule = vec![0, 1, 2, 0, 0, 0]; let value = ReductionResult::target_problem(&reduction) + .inner() .evaluate(&schedule) .unwrap(); assert_eq!(value, crate::types::Min(Some(3))); - assert!(!AggregateReductionResult::extract_value(&reduction, value).0); assert!( - !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &schedule), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&reduction, value); value.is_valid() }) + !crate::types::Or(OptimizationValue::meets_bound( + &(value), + crate::rules::ReductionResult::target_problem(&reduction).bound() + )) + .0 ); + assert!(!ReductionResult::target_problem(&reduction) + .evaluate(&schedule) + .unwrap() + .is_valid()); } #[test] @@ -160,16 +219,30 @@ fn test_partition_to_open_shop_odd_singleton_certificate() { fn test_partition_to_open_shop_certificate_near_horizon_limit() { let size = i64::MAX / 9; let source = Partition::new(vec![size, size]).unwrap(); - let reduction = ReduceTo::::reduce_to(&source).unwrap(); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); let a = usize::try_from(size).unwrap(); let schedule = vec![0, a, 2 * a, 2 * a, 0, a, a, 2 * a, 0]; assert_eq!( - reduction.target_problem().evaluate(&schedule).unwrap(), + reduction + .target_problem() + .inner() + .evaluate(&schedule) + .unwrap(), crate::types::Min(Some(3 * size)) ); assert!( source - .evaluate(&reduction.extract_solution(&schedule).unwrap()) + .evaluate( + &reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), schedule.clone()) + .unwrap() + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution") + ) .unwrap() .0 ); diff --git a/src/unit_tests/rules/partition_productionplanning.rs b/src/unit_tests/rules/partition_productionplanning.rs index 83790b2b5..443adbb13 100644 --- a/src/unit_tests/rules/partition_productionplanning.rs +++ b/src/unit_tests/rules/partition_productionplanning.rs @@ -2,6 +2,7 @@ use super::*; use crate::models::misc::{Partition, ProductionPlanning}; use crate::rules::test_helpers::assert_satisfaction_round_trip_from_satisfaction_target; use crate::solvers::BruteForce; +use crate::solvers::SolveOutcome; #[test] fn test_partition_to_productionplanning_closed_loop() { @@ -52,7 +53,15 @@ fn test_partition_to_productionplanning_extract_solution() { ReduceTo::::reduce_to(&source).expect("reduction should succeed"); assert_eq!( - reduction.extract_solution(&vec![0, 0, 0, 4, 6, 0]).unwrap(), + reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), vec![0, 0, 0, 4, 6, 0].clone()) + .unwrap() + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"), vec![false, false, false, true, true] ); } diff --git a/src/unit_tests/rules/partition_sequencingtominimizetardytaskweight.rs b/src/unit_tests/rules/partition_sequencingtominimizetardytaskweight.rs index 79e249fa3..d999e5faa 100644 --- a/src/unit_tests/rules/partition_sequencingtominimizetardytaskweight.rs +++ b/src/unit_tests/rules/partition_sequencingtominimizetardytaskweight.rs @@ -1,20 +1,23 @@ #[cfg(feature = "example-db")] use super::canonical_rule_example_specs; +use crate::models::decision::Decision; use crate::models::misc::{Partition, SequencingToMinimizeTardyTaskWeight}; -use crate::rules::test_helpers::assert_satisfaction_round_trip_from_optimization_target; +use crate::rules::test_helpers::assert_satisfaction_round_trip_from_satisfaction_target; use crate::rules::traits::ReductionResult; use crate::rules::ReduceTo; use crate::solvers::BruteForce; +use crate::solvers::SolveOutcome; +use crate::traits::EvaluationError::InvalidConfiguration; use crate::traits::Problem; -use crate::types::Min; +use crate::types::OptimizationValue; #[test] fn test_partition_to_sequencing_to_minimize_tardy_task_weight_closed_loop() { let source = Partition::new(vec![3, 1, 1, 2, 2, 1]).unwrap(); - let reduction = ReduceTo::::reduce_to(&source) + let reduction = ReduceTo::>::reduce_to(&source) .expect("reduction should succeed"); - assert_satisfaction_round_trip_from_optimization_target( + assert_satisfaction_round_trip_from_satisfaction_target( &source, &reduction, "Partition -> SequencingToMinimizeTardyTaskWeight closed loop", @@ -24,24 +27,32 @@ fn test_partition_to_sequencing_to_minimize_tardy_task_weight_closed_loop() { #[test] fn test_partition_to_sequencing_to_minimize_tardy_task_weight_structure() { let source = Partition::new(vec![3, 1, 1, 2, 2, 1]).unwrap(); - let reduction = ReduceTo::::reduce_to(&source) + let reduction = ReduceTo::>::reduce_to(&source) .expect("reduction should succeed"); let target = reduction.target_problem(); - assert_eq!(target.lengths(), &[3, 1, 1, 2, 2, 1]); - assert_eq!(target.weights(), &[3, 1, 1, 2, 2, 1]); - assert_eq!(target.deadlines(), &[5, 5, 5, 5, 5, 5]); - assert_eq!(target.num_tasks(), source.num_elements()); + assert_eq!(target.inner().lengths(), &[3, 1, 1, 2, 2, 1]); + assert_eq!(target.inner().weights(), &[3, 1, 1, 2, 2, 1]); + assert_eq!(target.inner().deadlines(), &[5, 5, 5, 5, 5, 5]); + assert_eq!(target.inner().num_tasks(), source.num_elements()); } #[test] fn test_partition_to_sequencing_to_minimize_tardy_task_weight_extract_solution() { let source = Partition::new(vec![3, 1, 1, 2, 2, 1]).unwrap(); - let reduction = ReduceTo::::reduce_to(&source) + let reduction = ReduceTo::>::reduce_to(&source) .expect("reduction should succeed"); assert_eq!( - reduction.extract_solution(&vec![1, 2, 4, 5, 0, 3]).unwrap(), + reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), vec![1, 2, 4, 5, 0, 3].clone()) + .unwrap() + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"), vec![true, false, false, true, false, false] ); } @@ -49,25 +60,24 @@ fn test_partition_to_sequencing_to_minimize_tardy_task_weight_extract_solution() #[test] fn test_partition_to_sequencing_to_minimize_tardy_task_weight_odd_total_is_unsatisfying() { let source = Partition::new(vec![2, 4, 5]).unwrap(); - let reduction = ReduceTo::::reduce_to(&source) + let reduction = ReduceTo::>::reduce_to(&source) .expect("reduction should succeed"); let target = reduction.target_problem(); + assert!(BruteForce::new().solve(target).unwrap().is_none()); let best = BruteForce::new() - .solve(target) + .solve(target.inner()) .unwrap() .expect("target should always have an optimal schedule"); - assert_eq!(target.evaluate(&best).unwrap(), Min(Some(6))); - assert!( - !crate::rules::AggregateReductionResult::extract_value( - &reduction, - target.evaluate(&best).unwrap() - ) - .0 - ); - assert!( - !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &best), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&reduction, value); value.is_valid() }) + assert_eq!( + target.inner().evaluate(&best).unwrap(), + crate::types::Min(Some(6)) ); + assert!(!target.evaluate(&best).unwrap().0); + assert!(!ReductionResult::target_problem(&reduction) + .evaluate(&best) + .unwrap() + .is_valid()); } #[cfg(feature = "example-db")] @@ -82,18 +92,18 @@ fn test_partition_to_sequencing_to_minimize_tardy_task_weight_canonical_example_ assert_eq!(example.source.problem, "Partition"); assert_eq!( example.target.problem, - "SequencingToMinimizeTardyTaskWeight" + "DecisionSequencingToMinimizeTardyTaskWeight" ); assert_eq!( - example.target.instance["lengths"], + example.target.instance["inner"]["lengths"], serde_json::json!([3, 1, 1, 2, 2, 1]) ); assert_eq!( - example.target.instance["weights"], + example.target.instance["inner"]["weights"], serde_json::json!([3, 1, 1, 2, 2, 1]) ); assert_eq!( - example.target.instance["deadlines"], + example.target.instance["inner"]["deadlines"], serde_json::json!([5, 5, 5, 5, 5, 5]) ); assert_eq!(example.solutions.len(), 1); @@ -108,7 +118,7 @@ fn test_partition_to_sequencing_to_minimize_tardy_task_weight_canonical_example_ let source: Partition = serde_json::from_value(example.source.instance.clone()) .expect("source example deserializes"); - let target: SequencingToMinimizeTardyTaskWeight = + let target: Decision = serde_json::from_value(example.target.instance.clone()) .expect("target example deserializes"); @@ -133,8 +143,9 @@ fn test_partition_to_tardy_weight_all_small_configurations() { .collect(); let source = Partition::new(sizes).unwrap(); let reduction = - ReduceTo::::reduce_to(&source).unwrap(); - let target = crate::rules::AggregateReductionResult::target_problem(&reduction); + ReduceTo::>::reduce_to(&source) + .unwrap(); + let target = crate::rules::ReductionResult::target_problem(&reduction); let source_feasible = (0..1usize << n).any(|mask| { let bits = (0..n).map(|i| mask & (1 << i) != 0).collect(); source.evaluate(&bits).unwrap().0 @@ -148,33 +159,54 @@ fn test_partition_to_tardy_weight_all_small_configurations() { task }) .collect(); - let value = target.evaluate(&schedule).unwrap(); + let value = target.inner().evaluate(&schedule).unwrap(); if let Some(weight) = value.0 { optimum = optimum.min(weight); } - let certified = - crate::rules::AggregateReductionResult::extract_value(&reduction, value).0; + let certified = crate::types::Or(OptimizationValue::meets_bound( + &(value), + crate::rules::ReductionResult::target_problem(&reduction).bound(), + )) + .0; if certified { - let bits = reduction.extract_solution(&schedule).unwrap(); + let bits = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), schedule.clone()) + .unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!(source.evaluate(&bits).unwrap().0); } } assert_eq!( - crate::rules::AggregateReductionResult::extract_value( - &reduction, - Min(Some(optimum)), - ) + crate::types::Or(OptimizationValue::meets_bound( + &(crate::types::Min(Some(optimum))), + crate::rules::ReductionResult::target_problem(&reduction).bound() + )) .0, source_feasible ); + assert!(matches!( + ReductionResult::target_problem(&reduction) + .inner() + .evaluate(&vec![]), + Err(InvalidConfiguration(_)) + )); + assert!(matches!( + ReductionResult::target_problem(&reduction) + .inner() + .evaluate(&vec![n as usize; n as usize]), + Err(InvalidConfiguration(_)) + )); assert!( - !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![]), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&reduction, value); value.is_valid() }) - ); - assert!( - !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![n as usize; n as usize]), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&reduction, value); value.is_valid() }) - ); - assert!( - !crate::rules::AggregateReductionResult::extract_value(&reduction, Min(None),).0 + !crate::types::Or(OptimizationValue::meets_bound( + &(crate::types::Min(None)), + crate::rules::ReductionResult::target_problem(&reduction).bound() + )) + .0 ); } } @@ -191,15 +223,30 @@ fn test_partition_to_tardy_weight_full_i64_domain() { ] { let source = Partition::new(sizes).unwrap(); let reduction = - ReduceTo::::reduce_to(&source).unwrap(); - let value = reduction.target_problem().evaluate(&schedule).unwrap(); - assert_eq!(value, Min(Some(expected))); + ReduceTo::>::reduce_to(&source).unwrap(); + let value = reduction + .target_problem() + .inner() + .evaluate(&schedule) + .unwrap(); + assert_eq!(value, crate::types::Min(Some(expected))); assert_eq!( - crate::rules::AggregateReductionResult::extract_value(&reduction, value).0, + crate::types::Or(OptimizationValue::meets_bound( + &(value), + crate::rules::ReductionResult::target_problem(&reduction).bound() + )) + .0, balanced ); if balanced { - let bits = reduction.extract_solution(&schedule).unwrap(); + let bits = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), schedule.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!(source.evaluate(&bits).unwrap().0); } } diff --git a/src/unit_tests/rules/partition_subsetsum.rs b/src/unit_tests/rules/partition_subsetsum.rs index f6104bb9a..60f253066 100644 --- a/src/unit_tests/rules/partition_subsetsum.rs +++ b/src/unit_tests/rules/partition_subsetsum.rs @@ -1,6 +1,9 @@ use super::*; use crate::rules::test_helpers::assert_satisfaction_round_trip_from_satisfaction_target; +use crate::rules::ReductionResult; use crate::solvers::BruteForce; +use crate::traits::EvaluationError::InvalidConfiguration; +use crate::traits::Problem; #[test] fn test_partition_to_subsetsum_closed_loop() { @@ -66,7 +69,8 @@ fn test_partition_to_subsetsum_rejects_wrong_solution_length() { let source = Partition::new(vec![1, 1, 2, 2]).unwrap(); let reduction = ReduceTo::::reduce_to(&source).expect("reduction should succeed"); - assert!( - !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![false, true, false]), Ok(value) if { value.is_valid() }) - ); + assert!(matches!( + ReductionResult::target_problem(&reduction).evaluate(&vec![false, true, false]), + Err(InvalidConfiguration(_)) + )); } diff --git a/src/unit_tests/rules/partition_sumofsquarespartition.rs b/src/unit_tests/rules/partition_sumofsquarespartition.rs index 89bd1f915..41feb96ac 100644 --- a/src/unit_tests/rules/partition_sumofsquarespartition.rs +++ b/src/unit_tests/rules/partition_sumofsquarespartition.rs @@ -1,7 +1,10 @@ use super::*; use crate::models::misc::{Partition, SumOfSquaresPartition}; use crate::rules::test_helpers::assert_satisfaction_round_trip_from_optimization_target; +use crate::rules::ReductionResult; use crate::solvers::BruteForce; +use crate::solvers::SolveOutcome; +use crate::traits::EvaluationError::InvalidConfiguration; use crate::traits::Problem; use crate::types::Min; @@ -31,12 +34,13 @@ fn test_partition_to_sumofsquarespartition_closed_loop() { let target_witnesses = solver.find_all_witnesses(target_no_even).unwrap(); assert!(!target_witnesses.is_empty()); for witness in &target_witnesses { - let extracted = reduction_no_even.extract_solution(witness).unwrap(); - assert_eq!(extracted.len(), source_no_even.num_elements()); - assert!( - !source_no_even.evaluate(&extracted).unwrap().0, - "even-sum but unbalanced NO Partition: extracted witness {extracted:?} should not satisfy source" - ); + let recovered = reduction_no_even + .recover_result( + &source_no_even, + SolveOutcome::optimal(target_no_even, witness.clone()).unwrap(), + ) + .unwrap(); + assert_eq!(recovered, SolveOutcome::Infeasible); } // Confirm the source is genuinely NO via direct solve. let direct_witness = solver.solve(&source_no_even).unwrap(); @@ -48,11 +52,13 @@ fn test_partition_to_sumofsquarespartition_closed_loop() { let target_witnesses_odd = solver.find_all_witnesses(target_no_odd).unwrap(); assert!(!target_witnesses_odd.is_empty()); for witness in &target_witnesses_odd { - let extracted = reduction_no_odd.extract_solution(witness).unwrap(); - assert!( - !source_no_odd.evaluate(&extracted).unwrap().0, - "odd-sum NO Partition: extracted witness {extracted:?} should not satisfy source" - ); + let recovered = reduction_no_odd + .recover_result( + &source_no_odd, + SolveOutcome::optimal(target_no_odd, witness.clone()).unwrap(), + ) + .unwrap(); + assert_eq!(recovered, SolveOutcome::Infeasible); } assert!(solver.solve(&source_no_odd).unwrap().is_none()); } @@ -107,18 +113,24 @@ fn test_partition_to_sumofsquarespartition_singleton_sentinel() { assert!(!target_witnesses.is_empty()); for witness in &target_witnesses { - let extracted = reduction.extract_solution(witness).unwrap(); - assert_eq!(extracted.len(), source.num_elements()); + let mapped = reduction.map_solution(witness).unwrap(); + assert_eq!(mapped.len(), source.num_elements()); assert_eq!( - extracted, + mapped, witness[..source.num_elements()] .iter() .map(|&value| value != 0) .collect::>() ); - assert!( - !source.evaluate(&extracted).unwrap().0, - "singleton Partition: extracted witness must yield Or(false)" + assert!(!source.evaluate(&mapped).unwrap().0); + assert_eq!( + reduction + .recover_result( + &source, + SolveOutcome::optimal(target, witness.clone()).unwrap() + ) + .unwrap(), + SolveOutcome::Infeasible ); } @@ -142,7 +154,14 @@ fn test_partition_to_sumofsquarespartition_solution_extraction_identity() { .collect(); for witness in &target_witnesses { - let extracted = reduction.extract_solution(witness).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), (witness).clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!( extracted, witness.iter().map(|&value| value != 0).collect::>() @@ -153,7 +172,8 @@ fn test_partition_to_sumofsquarespartition_solution_extraction_identity() { ); } - assert!( - !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![0]), Ok(value) if { value.is_valid() }) - ); + assert!(matches!( + ReductionResult::target_problem(&reduction).evaluate(&vec![0]), + Err(InvalidConfiguration(_)) + )); } diff --git a/src/unit_tests/rules/partitionintocliques_ilp.rs b/src/unit_tests/rules/partitionintocliques_ilp.rs index 39a13c9c8..c69615749 100644 --- a/src/unit_tests/rules/partitionintocliques_ilp.rs +++ b/src/unit_tests/rules/partitionintocliques_ilp.rs @@ -1,5 +1,6 @@ use super::*; use crate::solvers::ILPSolver; +use crate::solvers::SolveOutcome; use crate::traits::Problem; use crate::types::Or; @@ -19,7 +20,14 @@ fn test_partitionintocliques_to_ilp_closed_loop() { let target_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("two disjoint edges form two cliques"); - let source_solution = reduction.extract_solution(&target_solution).unwrap(); + let source_solution = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), target_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(problem.evaluate(&source_solution).unwrap(), Or(true)); } diff --git a/src/unit_tests/rules/partitionintocliques_minimumcoveringbycliques.rs b/src/unit_tests/rules/partitionintocliques_minimumcoveringbycliques.rs index fc2d1c934..bed7d9089 100644 --- a/src/unit_tests/rules/partitionintocliques_minimumcoveringbycliques.rs +++ b/src/unit_tests/rules/partitionintocliques_minimumcoveringbycliques.rs @@ -1,8 +1,12 @@ use super::*; -use crate::rules::test_helpers::assert_satisfaction_round_trip_from_optimization_target; +use crate::models::decision::Decision; +use crate::rules::test_helpers::assert_satisfaction_round_trip_from_satisfaction_target; +use crate::rules::ReductionResult; +use crate::solvers::SolveOutcome; use crate::topology::Graph; +use crate::traits::EvaluationError::InvalidConfiguration; use crate::traits::Problem; -use crate::types::Min; +use crate::types::OptimizationValue; #[test] fn test_partitionintocliques_target_bound_rejects_overflow() { @@ -18,16 +22,20 @@ fn test_partitionintocliques_target_bound_rejects_overflow() { #[test] fn test_partitionintocliques_aggregate_applies_gadget_offset() { let source = PartitionIntoCliques::new(SimpleGraph::new(3, vec![(0, 1)]), 2); - let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + let reduction = + ReduceTo::>>::reduce_to(&source).unwrap(); // K + 2m + 2 = 6, including both directed-edge gadgets and the side cliques. for (value, expected) in [ - (Min(None), false), - (Min(Some(5)), true), - (Min(Some(6)), true), - (Min(Some(7)), false), + (crate::types::Min(None), false), + (crate::types::Min(Some(5)), true), + (crate::types::Min(Some(6)), true), + (crate::types::Min(Some(7)), false), ] { assert_eq!( - crate::rules::AggregateReductionResult::extract_value(&reduction, value), + crate::types::Or(OptimizationValue::meets_bound( + &(value), + crate::rules::ReductionResult::target_problem(&reduction).bound() + )), crate::types::Or(expected), ); } @@ -36,10 +44,10 @@ fn test_partitionintocliques_aggregate_applies_gadget_offset() { #[test] fn test_partitionintocliques_to_minimumcoveringbycliques_closed_loop() { let source = PartitionIntoCliques::new(SimpleGraph::empty(1), 1); - let reduction = ReduceTo::>::reduce_to(&source) + let reduction = ReduceTo::>>::reduce_to(&source) .expect("reduction should succeed"); - assert_satisfaction_round_trip_from_optimization_target( + assert_satisfaction_round_trip_from_satisfaction_target( &source, &reduction, "PartitionIntoCliques -> MinimumCoveringByCliques closed loop", @@ -49,38 +57,38 @@ fn test_partitionintocliques_to_minimumcoveringbycliques_closed_loop() { #[test] fn test_partitionintocliques_to_minimumcoveringbycliques_orlin_example_structure() { let source = PartitionIntoCliques::new(SimpleGraph::new(3, vec![(0, 1)]), 2); - let reduction = ReduceTo::>::reduce_to(&source) + let reduction = ReduceTo::>>::reduce_to(&source) .expect("reduction should succeed"); let target = reduction.target_problem(); let layout = OrlinLayout::new(source.graph()); - assert_eq!(target.graph().num_vertices(), 14); - assert_eq!(target.graph().num_edges(), 53); + assert_eq!(target.inner().graph().num_vertices(), 14); + assert_eq!(target.inner().graph().num_edges(), 53); // Left clique on x_0, x_1, x_2, a_(0,1), a_(1,0) - assert!(target.graph().has_edge(0, 1)); - assert!(target.graph().has_edge(0, 2)); - assert!(target.graph().has_edge(1, 2)); - assert!(target.graph().has_edge(0, 6)); - assert!(target.graph().has_edge(1, 7)); + assert!(target.inner().graph().has_edge(0, 1)); + assert!(target.inner().graph().has_edge(0, 2)); + assert!(target.inner().graph().has_edge(1, 2)); + assert!(target.inner().graph().has_edge(0, 6)); + assert!(target.inner().graph().has_edge(1, 7)); // Right clique on y_0, y_1, y_2, b_(0,1), b_(1,0) - assert!(target.graph().has_edge(3, 4)); - assert!(target.graph().has_edge(3, 5)); - assert!(target.graph().has_edge(4, 5)); - assert!(target.graph().has_edge(3, 8)); - assert!(target.graph().has_edge(4, 9)); + assert!(target.inner().graph().has_edge(3, 4)); + assert!(target.inner().graph().has_edge(3, 5)); + assert!(target.inner().graph().has_edge(4, 5)); + assert!(target.inner().graph().has_edge(3, 8)); + assert!(target.inner().graph().has_edge(4, 9)); // Matching and gadget cross edges from the issue body - assert!(target.graph().has_edge(0, 3)); - assert!(target.graph().has_edge(1, 4)); - assert!(target.graph().has_edge(0, 4)); - assert!(target.graph().has_edge(0, 8)); - assert!(target.graph().has_edge(6, 4)); - assert!(target.graph().has_edge(6, 8)); + assert!(target.inner().graph().has_edge(0, 3)); + assert!(target.inner().graph().has_edge(1, 4)); + assert!(target.inner().graph().has_edge(0, 4)); + assert!(target.inner().graph().has_edge(0, 8)); + assert!(target.inner().graph().has_edge(6, 4)); + assert!(target.inner().graph().has_edge(6, 8)); let target_solution = edge_labels_from_clique_cover( - target.graph(), + target.inner().graph(), &[ vec![layout.x(0), layout.x(1), layout.y(0), layout.y(1)], vec![layout.x(2), layout.y(2)], @@ -98,9 +106,19 @@ fn test_partitionintocliques_to_minimumcoveringbycliques_orlin_example_structure }, ], ); - assert_eq!(target.evaluate(&target_solution).unwrap(), Min(Some(6))); assert_eq!( - reduction.extract_solution(&target_solution).unwrap(), + target.inner().evaluate(&target_solution).unwrap(), + crate::types::Min(Some(6)) + ); + assert_eq!( + reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), target_solution.clone()).unwrap() + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"), vec![0, 0, 1] ); } @@ -108,13 +126,13 @@ fn test_partitionintocliques_to_minimumcoveringbycliques_orlin_example_structure #[test] fn test_partitionintocliques_to_minimumcoveringbycliques_unsat_extracts_invalid_source() { let source = PartitionIntoCliques::new(SimpleGraph::new(2, vec![]), 1); - let reduction = ReduceTo::>::reduce_to(&source) + let reduction = ReduceTo::>>::reduce_to(&source) .expect("reduction should succeed"); let target = reduction.target_problem(); let layout = OrlinLayout::new(source.graph()); let target_solution = edge_labels_from_clique_cover( - target.graph(), + target.inner().graph(), &[ { let mut clique = layout.left_vertices(); @@ -130,20 +148,16 @@ fn test_partitionintocliques_to_minimumcoveringbycliques_unsat_extracts_invalid_ vec![layout.x(1), layout.y(1)], ], ); - assert_eq!(target.evaluate(&target_solution).unwrap(), Min(Some(4))); - - assert!( - !crate::rules::AggregateReductionResult::extract_value( - &reduction, - target.evaluate(&target_solution).unwrap() - ) - .0 + assert_eq!( + target.inner().evaluate(&target_solution).unwrap(), + crate::types::Min(Some(4)) ); + + assert!(!target.evaluate(&target_solution).unwrap().0); } #[test] fn test_partitionintocliques_native_bounds_and_adjacency_semantics() { - use crate::rules::AggregateReductionResult; for (n, edges) in [ (0, vec![]), (1, vec![(0, 0)]), @@ -161,7 +175,8 @@ fn test_partitionintocliques_native_bounds_and_adjacency_semantics() { } let source = source.unwrap(); let reduction = - ReduceTo::>::reduce_to(&source).unwrap(); + ReduceTo::>>::reduce_to(&source) + .unwrap(); let target = ReductionResult::target_problem(&reduction); let layout = OrlinLayout::new(source.graph()); let mut cliques: Vec> = @@ -175,36 +190,55 @@ fn test_partitionintocliques_native_bounds_and_adjacency_semantics() { let mut right = layout.right_vertices(); right.push(layout.z_right()); cliques.push(right); - let witness = edge_labels_from_clique_cover(target.graph(), &cliques); - let value = target.evaluate(&witness).unwrap(); + let witness = edge_labels_from_clique_cover(target.inner().graph(), &cliques); + let value = target.inner().evaluate(&witness).unwrap(); assert_eq!( value, - Min(Some((n + layout.num_directed_pairs() + 2) as i64)) + crate::types::Min(Some((n + layout.num_directed_pairs() + 2) as i64)) ); assert_eq!( - AggregateReductionResult::extract_value(&reduction, value).0, + crate::types::Or(OptimizationValue::meets_bound( + &(value), + crate::rules::ReductionResult::target_problem(&reduction).bound() + )) + .0, n <= bound ); if n <= bound { - let decoded = reduction.extract_solution(&witness).unwrap(); + let decoded = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), witness.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(decoded, (0..n).collect::>()); if bound <= n + 1 { assert!(source.evaluate(&decoded).unwrap().0); } } else { - assert!( - !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &witness), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&reduction, value); value.is_valid() }) - ); + assert!(!ReductionResult::target_problem(&reduction) + .evaluate(&witness) + .unwrap() + .is_valid()); } - assert!( - !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![0; witness.len()]), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&reduction, value); value.is_valid() }) - ); - assert!( - !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![0; witness.len() + 1]), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&reduction, value); value.is_valid() }) - ); + assert!(!ReductionResult::target_problem(&reduction) + .evaluate(&vec![0; witness.len()]) + .unwrap() + .is_valid()); + assert!(matches!( + ReductionResult::target_problem(&reduction) + .inner() + .evaluate(&vec![0; witness.len() + 1]), + Err(InvalidConfiguration(_)) + )); let q = layout.num_directed_pairs(); - assert_eq!(target.num_vertices(), 2 * n + 2 * q + 4); - assert_eq!(target.num_edges(), (n + q) * (n + q) + 4 * n + 7 * q + 2); + assert_eq!(target.inner().num_vertices(), 2 * n + 2 * q + 4); + assert_eq!( + target.inner().num_edges(), + (n + q) * (n + q) + 4 * n + 7 * q + 2 + ); } } } diff --git a/src/unit_tests/rules/partitionintopathsoflength2_boundedcomponentspanningforest.rs b/src/unit_tests/rules/partitionintopathsoflength2_boundedcomponentspanningforest.rs index 81b97ac33..e7e7b3011 100644 --- a/src/unit_tests/rules/partitionintopathsoflength2_boundedcomponentspanningforest.rs +++ b/src/unit_tests/rules/partitionintopathsoflength2_boundedcomponentspanningforest.rs @@ -3,6 +3,7 @@ use crate::models::graph::{BoundedComponentSpanningForest, PartitionIntoPathsOfL use crate::rules::test_helpers::assert_satisfaction_round_trip_from_satisfaction_target; use crate::rules::ReduceTo; use crate::solvers::BruteForce; +use crate::solvers::SolveOutcome; use crate::topology::SimpleGraph; use crate::traits::Problem; @@ -92,7 +93,14 @@ fn test_partitionintopathsoflength2_to_boundedcomponentspanningforest_extract_so .expect("reduction should succeed"); let target_config = vec![0, 0, 0, 1, 1, 1]; - let extracted = result.extract_solution(&target_config).unwrap(); + let extracted = result + .recover_result( + &source, + SolveOutcome::optimal(result.target_problem(), target_config.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted, vec![0, 0, 0, 1, 1, 1]); // Verify the extracted solution is valid in the source diff --git a/src/unit_tests/rules/partitionintopathsoflength2_ilp.rs b/src/unit_tests/rules/partitionintopathsoflength2_ilp.rs index 35544e23d..f4e89f5fd 100644 --- a/src/unit_tests/rules/partitionintopathsoflength2_ilp.rs +++ b/src/unit_tests/rules/partitionintopathsoflength2_ilp.rs @@ -1,4 +1,5 @@ use super::*; +use crate::solvers::SolveOutcome; use crate::solvers::{BruteForce, ILPSolver}; use crate::topology::SimpleGraph; use crate::traits::Problem; @@ -44,7 +45,14 @@ fn test_partitionintopathsoflength2_to_ilp_bf_vs_ilp() { assert_eq!(problem.evaluate(&bf_witness).unwrap(), Or(true)); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be feasible"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!( problem.evaluate(&extracted).unwrap(), Or(true), @@ -69,7 +77,14 @@ fn test_solution_extraction() { 1, 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 1, // x vars 1, 0, 1, 0, 0, 1, 0, 1, // y vars ]; - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted, vec![0, 0, 0, 1, 1, 1]); assert_eq!(problem.evaluate(&extracted).unwrap(), Or(true)); } @@ -85,7 +100,14 @@ fn test_partitionintopathsoflength2_to_ilp_trivial() { let ilp_solver = ILPSolver::new(); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be feasible"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!( problem.evaluate(&extracted).unwrap(), Or(true), diff --git a/src/unit_tests/rules/partitionintotriangles_ilp.rs b/src/unit_tests/rules/partitionintotriangles_ilp.rs index 0173f42b3..6211c80ac 100644 --- a/src/unit_tests/rules/partitionintotriangles_ilp.rs +++ b/src/unit_tests/rules/partitionintotriangles_ilp.rs @@ -1,4 +1,5 @@ use super::*; +use crate::solvers::SolveOutcome; use crate::solvers::{BruteForce, ILPSolver}; use crate::topology::SimpleGraph; use crate::traits::Problem; @@ -44,7 +45,14 @@ fn test_partitionintotriangles_to_ilp_bf_vs_ilp() { assert_eq!(problem.evaluate(&bf_witness).unwrap(), Or(true)); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be feasible"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!( problem.evaluate(&extracted).unwrap(), Or(true), @@ -63,7 +71,14 @@ fn test_solution_extraction() { // x_{v,g}: v0g0=1,v0g1=0, v1g0=1,v1g1=0, v2g0=1,v2g1=0, // v3g0=0,v3g1=1, v4g0=0,v4g1=1, v5g0=0,v5g1=1 let ilp_solution = vec![1, 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 1]; - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted, vec![0, 0, 0, 1, 1, 1]); assert_eq!(problem.evaluate(&extracted).unwrap(), Or(true)); } @@ -79,6 +94,13 @@ fn test_partitionintotriangles_to_ilp_trivial() { let ilp_solver = ILPSolver::new(); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be feasible"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(problem.evaluate(&extracted).unwrap(), Or(true)); } diff --git a/src/unit_tests/rules/pathconstrainednetworkflow_ilp.rs b/src/unit_tests/rules/pathconstrainednetworkflow_ilp.rs index ac4111f81..effad9d02 100644 --- a/src/unit_tests/rules/pathconstrainednetworkflow_ilp.rs +++ b/src/unit_tests/rules/pathconstrainednetworkflow_ilp.rs @@ -1,6 +1,7 @@ use super::*; use crate::models::algebraic::ILP; use crate::rules::ReduceTo; +use crate::solvers::SolveOutcome; use crate::solvers::{BruteForce, ILPSolver}; use crate::topology::DirectedGraph; use crate::traits::Problem; @@ -26,7 +27,14 @@ fn test_pathconstrainednetworkflow_to_ilp_closed_loop() { let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be feasible"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!(source.evaluate(&extracted).unwrap()); } diff --git a/src/unit_tests/rules/precedenceconstrainedscheduling_ilp.rs b/src/unit_tests/rules/precedenceconstrainedscheduling_ilp.rs index a454000dd..7fbff0786 100644 --- a/src/unit_tests/rules/precedenceconstrainedscheduling_ilp.rs +++ b/src/unit_tests/rules/precedenceconstrainedscheduling_ilp.rs @@ -1,5 +1,6 @@ use super::*; use crate::models::algebraic::{ObjectiveSense, ILP}; +use crate::solvers::SolveOutcome; use crate::solvers::{BruteForce, ILPSolver}; use crate::traits::Problem; @@ -47,7 +48,14 @@ fn test_precedenceconstrainedscheduling_to_ilp_closed_loop() { let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be feasible for feasible instance"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!( problem.evaluate(&extracted).unwrap().0, @@ -76,7 +84,14 @@ fn test_precedenceconstrainedscheduling_to_ilp_extract_solution() { // Manually: task 0 at slot 0, task 1 at slot 0, task 2 at slot 1 // x_{0,0}=1, x_{0,1}=0, x_{1,0}=1, x_{1,1}=0, x_{2,0}=0, x_{2,1}=1 let ilp_solution = vec![1, 0, 1, 0, 0, 1]; - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted, vec![0, 0, 1]); assert!( problem.evaluate(&extracted).unwrap().0, diff --git a/src/unit_tests/rules/preemptivescheduling_ilp.rs b/src/unit_tests/rules/preemptivescheduling_ilp.rs index cd0a78b23..66f3d25fc 100644 --- a/src/unit_tests/rules/preemptivescheduling_ilp.rs +++ b/src/unit_tests/rules/preemptivescheduling_ilp.rs @@ -1,6 +1,7 @@ use super::*; use crate::models::algebraic::ILP; use crate::solvers::ILPSolver; +use crate::solvers::SolveOutcome; use crate::traits::Problem; use crate::types::Min; @@ -49,7 +50,14 @@ fn test_preemptivescheduling_to_ilp_closed_loop() { let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be feasible"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &p, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let value = p.evaluate(&extracted).unwrap(); assert!( value.0.is_some(), @@ -75,7 +83,14 @@ fn test_preemptivescheduling_to_ilp_medium_closed_loop() { let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be feasible"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &p, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let value = p.evaluate(&extracted).unwrap(); assert!( value.0.is_some(), @@ -114,7 +129,14 @@ fn test_preemptivescheduling_to_ilp_extract_solution() { 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 extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &p, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted, vec![vec![true, false], vec![false, true]]); assert_eq!(p.evaluate(&extracted).unwrap(), Min(Some(2))); } diff --git a/src/unit_tests/rules/prizecollectingsteinerforest_steinertree.rs b/src/unit_tests/rules/prizecollectingsteinerforest_steinertree.rs index e7694a537..59e203800 100644 --- a/src/unit_tests/rules/prizecollectingsteinerforest_steinertree.rs +++ b/src/unit_tests/rules/prizecollectingsteinerforest_steinertree.rs @@ -1,6 +1,7 @@ use super::*; use crate::rules::test_helpers::assert_optimization_round_trip_from_optimization_target; use crate::solvers::BruteForce; +use crate::solvers::SolveOutcome; use crate::topology::SimpleGraph; use crate::traits::Problem; use crate::types::Min; @@ -72,7 +73,14 @@ fn test_prizecollectingsteinerforest_to_steinertree_extract_witness_canonical() .solve(target) .unwrap() .expect("target SteinerTree must be feasible"); - let source_witness = reduction.extract_solution(&target_witness).unwrap(); + let source_witness = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), target_witness.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(source_witness.0.len(), source.num_vertices()); assert_eq!(source_witness.1.len(), source.num_edges()); @@ -194,8 +202,22 @@ fn test_zero_prize_forest_through_steiner_tree_and_ilp() { assert_eq!(reduction.target_problem().num_terminals(), 1); let ilp = ReduceTo::>::reduce_to(reduction.target_problem()).unwrap(); let raw = ILPSolver::new().solve(ilp.target_problem()).unwrap(); - let tree = ilp.extract_solution(&raw).unwrap(); - let forest = reduction.extract_solution(&tree).unwrap(); + let tree = ilp + .recover_result( + reduction.target_problem(), + SolveOutcome::optimal(ilp.target_problem(), raw.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); + let forest = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), tree.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(source.evaluate(&forest).unwrap(), Min(Some(expected))); assert_optimization_round_trip_from_optimization_target( &source, @@ -232,7 +254,14 @@ fn low_prizes_do_not_bypass_component_costs() { reduction.target_problem().evaluate(&tree).unwrap(), Min(Some(expected + offset)) ); - let forest = reduction.extract_solution(&tree).unwrap(); + let forest = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), tree.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(source.evaluate(&forest).unwrap(), Min(Some(expected))); } } diff --git a/src/unit_tests/rules/quadraticassignment_ilp.rs b/src/unit_tests/rules/quadraticassignment_ilp.rs index c68fd73e1..8540933fc 100644 --- a/src/unit_tests/rules/quadraticassignment_ilp.rs +++ b/src/unit_tests/rules/quadraticassignment_ilp.rs @@ -1,4 +1,5 @@ use super::*; +use crate::solvers::SolveOutcome; use crate::solvers::{BruteForce, ILPSolver}; use crate::traits::Problem; @@ -35,7 +36,14 @@ fn test_quadraticassignment_to_ilp_closed_loop() { let ilp_solution = ilp_solver .solve(reduction.target_problem()) .expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let ilp_value = problem.evaluate(&extracted).unwrap(); assert!( @@ -64,7 +72,14 @@ fn test_quadraticassignment_to_ilp_2x2() { let ilp_solution = ilp_solver .solve(reduction.target_problem()) .expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let ilp_value = problem.evaluate(&extracted).unwrap(); assert!(ilp_value.is_valid()); @@ -80,7 +95,14 @@ fn test_solution_extraction() { let ilp_solution = ilp_solver .solve(reduction.target_problem()) .expect("solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let metric = problem.evaluate(&extracted).unwrap(); assert!(metric.is_valid()); } @@ -104,7 +126,14 @@ fn test_quadraticassignment_to_ilp_rectangular() { let ilp_solution = ilp_solver .solve(reduction.target_problem()) .expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let ilp_value = problem.evaluate(&extracted).unwrap(); assert!(ilp_value.is_valid()); diff --git a/src/unit_tests/rules/qubo_casts.rs b/src/unit_tests/rules/qubo_casts.rs index 002a36469..b27a26911 100644 --- a/src/unit_tests/rules/qubo_casts.rs +++ b/src/unit_tests/rules/qubo_casts.rs @@ -1,5 +1,6 @@ use super::*; use crate::rules::{ReduceTo, ReductionError, ReductionGraph, ReductionResult}; +use crate::solvers::SolveOutcome; use crate::types::MAX_EXACT_F64_INTEGER; #[test] @@ -14,7 +15,15 @@ fn test_qubo_i64_to_f64_closed_loop() { .matrix() ); assert_eq!( - reduction.extract_solution(&vec![true, false]).unwrap(), + reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), vec![true, false].clone()) + .unwrap() + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"), vec![true, false] ); } diff --git a/src/unit_tests/rules/qubo_ilp.rs b/src/unit_tests/rules/qubo_ilp.rs index db5628fd4..7d333ae4a 100644 --- a/src/unit_tests/rules/qubo_ilp.rs +++ b/src/unit_tests/rules/qubo_ilp.rs @@ -1,5 +1,6 @@ use super::*; use crate::rules::test_helpers::assert_bf_vs_ilp; +use crate::solvers::SolveOutcome; use crate::solvers::{BruteForce, ILPSolver}; use crate::traits::Problem; @@ -37,7 +38,14 @@ fn test_qubo_to_ilp_bf_vs_ilp() { let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &qubo, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let ilp_value = qubo.evaluate(&extracted).unwrap(); assert_eq!(bf_value, ilp_value); @@ -56,7 +64,14 @@ fn test_qubo_to_ilp_diagonal_only() { assert!(ilp.constraints().is_empty()); let best = ILPSolver::new().solve(ilp).unwrap(); - let extracted = reduction.extract_solution(&best).unwrap(); + let extracted = reduction + .recover_result( + &qubo, + SolveOutcome::optimal(reduction.target_problem(), best.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted, vec![false, true]); } @@ -79,6 +94,13 @@ fn test_qubo_to_ilp_3var() { assert_eq!(ilp.constraints().len(), 6); let best = ILPSolver::new().solve(ilp).unwrap(); - let extracted = reduction.extract_solution(&best).unwrap(); + let extracted = reduction + .recover_result( + &qubo, + SolveOutcome::optimal(reduction.target_problem(), best.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted, vec![true, false, true]); } diff --git a/src/unit_tests/rules/rectilinearpicturecompression_ilp.rs b/src/unit_tests/rules/rectilinearpicturecompression_ilp.rs index 056c224de..82b347a16 100644 --- a/src/unit_tests/rules/rectilinearpicturecompression_ilp.rs +++ b/src/unit_tests/rules/rectilinearpicturecompression_ilp.rs @@ -1,4 +1,5 @@ use super::*; +use crate::solvers::SolveOutcome; use crate::solvers::{BruteForce, ILPSolver}; use crate::traits::Problem; use crate::types::Or; @@ -28,7 +29,14 @@ fn test_rectilinearpicturecompression_to_ilp_bf_vs_ilp() { assert_eq!(problem.evaluate(&bf_witness).unwrap(), Or(true)); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(problem.evaluate(&extracted).unwrap(), Or(true)); } @@ -41,7 +49,14 @@ fn test_solution_extraction() { let ilp_solution = ilp_solver .solve(reduction.target_problem()) .expect("solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(problem.evaluate(&extracted).unwrap(), Or(true)); } diff --git a/src/unit_tests/rules/reduction_path_parity.rs b/src/unit_tests/rules/reduction_path_parity.rs index bb10d89a9..3a27b5e88 100644 --- a/src/unit_tests/rules/reduction_path_parity.rs +++ b/src/unit_tests/rules/reduction_path_parity.rs @@ -9,6 +9,7 @@ use crate::rules::test_helpers::assert_optimization_round_trip_chain; use crate::rules::ReductionGraph; use crate::solvers::BruteForce; use crate::solvers::BruteForceProblem as _; +use crate::solvers::SolveOutcome; use crate::topology::SimpleGraph; use crate::traits::Problem; @@ -55,7 +56,13 @@ fn test_jl_parity_maxcut_to_spinglass_path() { let solver = BruteForce::new(); let target_solution = solver.solve(target).unwrap().unwrap(); - let source_solution = chain.extract_solution(&target_solution).unwrap(); + let source_solution = chain + .recover_result::, SpinGlass>( + &source, + SolveOutcome::optimal(target, target_solution).unwrap(), + ) + .map(|outcome| outcome.into_solution().unwrap()) + .unwrap(); // Source solution should be valid let metric = source.evaluate(&source_solution).unwrap(); @@ -118,7 +125,9 @@ fn test_jl_parity_factoring_to_spinglass_path() { let rpath = graph .find_all_paths("Factoring", &src_var, "SpinGlass", &dst_var) .into_iter() - .find(|path| path.type_names() == ["Factoring", "CircuitSAT", "SpinGlass"]) + .find(|path| { + path.type_names() == ["Factoring", "CircuitSAT", "DecisionSpinGlass", "SpinGlass"] + }) .expect("explicit CircuitSAT route"); // Canonical factor order uses the smaller width first. @@ -144,7 +153,14 @@ fn test_jl_parity_factoring_to_spinglass_path() { let ilp_solution = ilp_solver .solve(ilp) .expect("ILP solver should find factoring solution"); - let factoring_solution = reduction.extract_solution(&ilp_solution).unwrap(); + let factoring_solution = reduction + .recover_result( + &factoring, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let metric = factoring.evaluate(&factoring_solution).unwrap(); assert!(metric.unwrap(), "Factoring->ILP solution must be valid"); } diff --git a/src/unit_tests/rules/registersufficiency_ilp.rs b/src/unit_tests/rules/registersufficiency_ilp.rs index aa1621b4b..163b1adfe 100644 --- a/src/unit_tests/rules/registersufficiency_ilp.rs +++ b/src/unit_tests/rules/registersufficiency_ilp.rs @@ -1,6 +1,7 @@ use super::*; use crate::models::misc::RegisterSufficiency; use crate::solvers::ILPSolver; +use crate::solvers::SolveOutcome; use crate::traits::Problem; use crate::types::Or; @@ -50,7 +51,14 @@ fn test_register_sufficiency_to_ilp_closed_loop() { let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("feasible register-sufficiency instance should yield a feasible ILP"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(source.evaluate(&extracted).unwrap(), Or(true)); let mut sorted = extracted.clone(); @@ -114,7 +122,14 @@ fn test_register_sufficiency_to_ilp_canonical_example_spec() { let target_config: Vec = serde_json::from_value(solution.target_config.clone()).unwrap(); assert_eq!(source.evaluate(&source_config).unwrap(), Or(true)); assert_eq!( - reduction.extract_solution(&target_config).unwrap(), + reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), target_config.clone()).unwrap() + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"), source_config ); } diff --git a/src/unit_tests/rules/registry.rs b/src/unit_tests/rules/registry.rs index b05d19fa9..ed7a6cef5 100644 --- a/src/unit_tests/rules/registry.rs +++ b/src/unit_tests/rules/registry.rs @@ -10,7 +10,6 @@ fn entry_with(declarations: fn() -> ReductionParameterDeclarations) -> Reduction parameter_declarations_fn: declarations, module_path: module_path!(), reduce_fn: None, - reduce_aggregate_fn: None, turing: false, } } diff --git a/src/unit_tests/rules/resourceconstrainedscheduling_ilp.rs b/src/unit_tests/rules/resourceconstrainedscheduling_ilp.rs index eb2438fb6..12b3b76b1 100644 --- a/src/unit_tests/rules/resourceconstrainedscheduling_ilp.rs +++ b/src/unit_tests/rules/resourceconstrainedscheduling_ilp.rs @@ -1,6 +1,7 @@ use super::*; use crate::models::algebraic::ILP; use crate::rules::test_helpers::assert_bf_vs_ilp; +use crate::solvers::SolveOutcome; use crate::solvers::{BruteForce, ILPSolver}; use crate::traits::Problem; use crate::types::Or; @@ -39,7 +40,14 @@ fn test_resourceconstrainedscheduling_to_ilp_bf_vs_ilp() { let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(problem.evaluate(&extracted).unwrap(), Or(true)); } diff --git a/src/unit_tests/rules/rootedtreearrangement_rootedtreestorageassignment.rs b/src/unit_tests/rules/rootedtreearrangement_rootedtreestorageassignment.rs index da6346190..064a95e05 100644 --- a/src/unit_tests/rules/rootedtreearrangement_rootedtreestorageassignment.rs +++ b/src/unit_tests/rules/rootedtreearrangement_rootedtreestorageassignment.rs @@ -1,6 +1,7 @@ use super::*; use crate::rules::test_helpers::assert_satisfaction_round_trip_from_satisfaction_target; use crate::solvers::BruteForce; +use crate::solvers::SolveOutcome; #[test] fn test_rootedtreearrangement_to_rootedtreestorageassignment_closed_loop() { @@ -88,7 +89,14 @@ fn test_rootedtreearrangement_to_rootedtreestorageassignment_solution_extraction // Target solution: parent array [0, 0] means tree rooted at 0 with 1->0 let target_config = vec![0, 0]; - let source_config = reduction.extract_solution(&target_config).unwrap(); + let source_config = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), target_config.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); // Source config should be [parent_array | identity_mapping] = [0, 0, 0, 1] assert_eq!(source_config, vec![0, 0, 0, 1]); diff --git a/src/unit_tests/rules/rootedtreestorageassignment_ilp.rs b/src/unit_tests/rules/rootedtreestorageassignment_ilp.rs index a19c78a93..eb16c787c 100644 --- a/src/unit_tests/rules/rootedtreestorageassignment_ilp.rs +++ b/src/unit_tests/rules/rootedtreestorageassignment_ilp.rs @@ -1,5 +1,6 @@ use super::*; use crate::models::algebraic::{ObjectiveSense, ILP}; +use crate::solvers::SolveOutcome; use crate::solvers::{BruteForce, ILPSolver}; use crate::traits::Problem; use crate::types::Or; @@ -37,7 +38,15 @@ fn test_rootedtreestorageassignment_to_ilp_bf_vs_ilp() { match ilp_result { Ok(ilp_solution) => { - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()) + .unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let ilp_value = problem.evaluate(&extracted).unwrap(); assert!(ilp_value.0, "ILP solution should be feasible"); assert!(bf_value.0, "BF should also find feasible solution"); @@ -83,7 +92,14 @@ fn test_solution_extraction() { let ilp_solution = ilp_solver .solve(reduction.target_problem()) .expect("solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted.len(), 3); assert_eq!(problem.evaluate(&extracted).unwrap(), Or(true)); } diff --git a/src/unit_tests/rules/ruralpostman_ilp.rs b/src/unit_tests/rules/ruralpostman_ilp.rs index ac981b878..d8ef4538b 100644 --- a/src/unit_tests/rules/ruralpostman_ilp.rs +++ b/src/unit_tests/rules/ruralpostman_ilp.rs @@ -1,6 +1,7 @@ use super::*; use crate::models::algebraic::ILP; use crate::rules::ReduceTo; +use crate::solvers::SolveOutcome; use crate::solvers::{BruteForce, ILPSolver}; use crate::topology::SimpleGraph; use crate::traits::Problem; @@ -23,7 +24,14 @@ fn test_ruralpostman_to_ilp_closed_loop() { let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be feasible"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!(source.evaluate(&extracted).unwrap().0.is_some()); } @@ -49,7 +57,14 @@ fn test_ruralpostman_to_ilp_optimization() { let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be feasible"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let ilp_value = source.evaluate(&extracted).unwrap(); assert!(ilp_value.0.is_some(), "ILP solution must be valid"); @@ -79,7 +94,14 @@ fn test_ruralpostman_empty_required_set_extracts_zero_multiplicities() { ); let reduction = ReduceTo::>::reduce_to(&source).unwrap(); let target = ILPSolver::new().solve(reduction.target_problem()).unwrap(); - let extracted = reduction.extract_solution(&target).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), target.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted, vec![0, 0]); assert_eq!(source.evaluate(&extracted).unwrap().0, Some(0)); diff --git a/src/unit_tests/rules/sat_circuitsat.rs b/src/unit_tests/rules/sat_circuitsat.rs index 259064674..c93b6166b 100644 --- a/src/unit_tests/rules/sat_circuitsat.rs +++ b/src/unit_tests/rules/sat_circuitsat.rs @@ -3,6 +3,7 @@ use crate::models::formula::{CNFClause, CircuitSAT, Satisfiability}; use crate::rules::test_helpers::assert_satisfaction_round_trip_from_satisfaction_target; use crate::rules::ReduceTo; use crate::solvers::BruteForce; +use crate::solvers::SolveOutcome; #[test] fn test_sat_to_circuitsat_closed_loop() { @@ -62,7 +63,14 @@ fn test_sat_to_circuitsat_single_literal_clause() { .solve(result.target_problem()) .unwrap() .expect("CircuitSAT should have a satisfying solution"); - let extracted = result.extract_solution(&target_solution).unwrap(); + let extracted = result + .recover_result( + &sat, + SolveOutcome::optimal(result.target_problem(), target_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted, vec![true, true]); } diff --git a/src/unit_tests/rules/sat_coloring.rs b/src/unit_tests/rules/sat_coloring.rs index bae7be66f..d61a824d1 100644 --- a/src/unit_tests/rules/sat_coloring.rs +++ b/src/unit_tests/rules/sat_coloring.rs @@ -1,4 +1,5 @@ use super::*; +use crate::solvers::SolveOutcome; include!("../jl_helpers.rs"); use crate::models::formula::CNFClause; use crate::solvers::BruteForce; @@ -84,7 +85,14 @@ fn test_unsatisfiable_formula() { // OR no valid coloring exists that extracts to a satisfying SAT assignment let mut found_satisfying = false; for sol in &solutions { - let sat_sol = reduction.extract_solution(sol).unwrap(); + let sat_sol = reduction + .recover_result( + &sat, + SolveOutcome::optimal(reduction.target_problem(), (sol).clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); if sat.is_satisfying(&sat_sol) { found_satisfying = true; break; @@ -201,7 +209,14 @@ fn test_single_literal_clauses() { let mut found_correct = false; for sol in &solutions { - let sat_sol = reduction.extract_solution(sol).unwrap(); + let sat_sol = reduction + .recover_result( + &sat, + SolveOutcome::optimal(reduction.target_problem(), (sol).clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); if sat_sol == vec![true, true] { found_correct = true; break; @@ -282,7 +297,14 @@ fn test_manual_coloring_extraction() { let valid_coloring = vec![0, 1, 2, 0, 1]; assert_eq!(coloring.graph().num_vertices(), 5); - let extracted = reduction.extract_solution(&valid_coloring).unwrap(); + let extracted = reduction + .recover_result( + &sat, + SolveOutcome::optimal(reduction.target_problem(), valid_coloring.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); // x1 should be true (1) because vertex 3 has color 0 which equals TRUE vertex's color assert_eq!(extracted, vec![true]); } @@ -298,14 +320,28 @@ fn test_extraction_with_different_color_assignment() { // Different valid coloring: TRUE=2, FALSE=0, AUX=1 // x1 must have color 2 (TRUE), NOT_x1 must have color 0 (FALSE) let coloring_permuted = vec![2, 0, 1, 2, 0]; - let extracted = reduction.extract_solution(&coloring_permuted).unwrap(); + let extracted = reduction + .recover_result( + &sat, + SolveOutcome::optimal(reduction.target_problem(), coloring_permuted.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); // x1 should still be true because its color equals TRUE vertex's color assert_eq!(extracted, vec![true]); // Another permutation: TRUE=1, FALSE=2, AUX=0 // x1 has color 1 (TRUE), NOT_x1 has color 2 (FALSE) let coloring_permuted2 = vec![1, 2, 0, 1, 2]; - let extracted2 = reduction.extract_solution(&coloring_permuted2).unwrap(); + let extracted2 = reduction + .recover_result( + &sat, + SolveOutcome::optimal(reduction.target_problem(), coloring_permuted2.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted2, vec![true]); } @@ -335,7 +371,14 @@ fn test_jl_parity_sat_to_coloring() { let target_sol = ilp_solver .solve(target) .expect("ILP should find a coloring"); - let extracted = result.extract_solution(&target_sol).unwrap(); + let extracted = result + .recover_result( + &source, + SolveOutcome::optimal(result.target_problem(), target_sol.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let best_source: HashSet> = BruteForce::new() .find_all_witnesses(&source) .unwrap() diff --git a/src/unit_tests/rules/sat_ksat.rs b/src/unit_tests/rules/sat_ksat.rs index e7b3bf744..ecc0f9ab4 100644 --- a/src/unit_tests/rules/sat_ksat.rs +++ b/src/unit_tests/rules/sat_ksat.rs @@ -1,4 +1,5 @@ use super::*; +use crate::solvers::SolveOutcome; include!("../jl_helpers.rs"); use crate::rules::test_helpers::assert_satisfaction_round_trip_from_satisfaction_target; use crate::solvers::BruteForce; @@ -159,7 +160,14 @@ fn test_sat_to_3sat_solution_extraction() { // Extract and verify solutions for ksat_sol in &ksat_solutions { - let sat_sol = reduction.extract_solution(ksat_sol).unwrap(); + let sat_sol = reduction + .recover_result( + &sat, + SolveOutcome::optimal(reduction.target_problem(), (ksat_sol).clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); // Should only have original 2 variables assert_eq!(sat_sol.len(), 2); // Should satisfy original problem @@ -195,7 +203,14 @@ fn test_3sat_to_sat_solution_extraction() { let reduction = ReduceTo::::reduce_to(&ksat).expect("reduction should succeed"); let sol = vec![true, false, true]; - let extracted = reduction.extract_solution(&sol).unwrap(); + let extracted = reduction + .recover_result( + &ksat, + SolveOutcome::optimal(reduction.target_problem(), sol.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted, vec![true, false, true]); } diff --git a/src/unit_tests/rules/sat_maximumindependentset.rs b/src/unit_tests/rules/sat_maximumindependentset.rs index f4fb3b2c5..ba947f90f 100644 --- a/src/unit_tests/rules/sat_maximumindependentset.rs +++ b/src/unit_tests/rules/sat_maximumindependentset.rs @@ -1,7 +1,12 @@ use super::*; +use crate::models::decision::Decision; +use crate::rules::ReductionResult; +use crate::solvers::SolveOutcome; +use crate::traits::EvaluationError::InvalidConfiguration; +use crate::types::OptimizationValue; include!("../jl_helpers.rs"); use crate::models::formula::CNFClause; -use crate::rules::test_helpers::assert_satisfaction_round_trip_from_optimization_target; +use crate::rules::test_helpers::assert_satisfaction_round_trip_from_satisfaction_target; use crate::solvers::BruteForce; use crate::topology::Graph; use crate::traits::Problem; @@ -46,14 +51,14 @@ fn test_boolvar_complement() { fn test_sat_to_maximumindependentset_closed_loop() { // Simple SAT: (x1) - one clause with one literal let sat = Satisfiability::new(1, vec![CNFClause::new(vec![1])]); - let reduction = ReduceTo::>::reduce_to(&sat) + let reduction = ReduceTo::>>::reduce_to(&sat) .expect("reduction should succeed"); let is_problem = reduction.target_problem(); // Should have 1 vertex (one literal) - assert_eq!(is_problem.graph().num_vertices(), 1); + assert_eq!(is_problem.inner().graph().num_vertices(), 1); // No edges (single vertex can't form a clique) - assert_eq!(is_problem.graph().num_edges(), 0); + assert_eq!(is_problem.inner().graph().num_edges(), 0); } #[test] @@ -61,14 +66,14 @@ fn test_two_clause_sat_to_is() { // SAT: (x1) AND (NOT x1) // This is unsatisfiable let sat = Satisfiability::new(1, vec![CNFClause::new(vec![1]), CNFClause::new(vec![-1])]); - let reduction = ReduceTo::>::reduce_to(&sat) + let reduction = ReduceTo::>>::reduce_to(&sat) .expect("reduction should succeed"); let is_problem = reduction.target_problem(); // Should have 2 vertices - assert_eq!(is_problem.graph().num_vertices(), 2); + assert_eq!(is_problem.inner().graph().num_vertices(), 2); // Should have 1 edge (between x1 and NOT x1) - assert_eq!(is_problem.graph().num_edges(), 1); + assert_eq!(is_problem.inner().graph().num_edges(), 1); // Maximum IS should have size 1 (can't select both) let solver = BruteForce::new(); @@ -82,17 +87,31 @@ fn test_two_clause_sat_to_is() { fn test_extract_solution_basic() { // Simple case: (x1 OR x2) let sat = Satisfiability::new(2, vec![CNFClause::new(vec![1, 2])]); - let reduction = ReduceTo::>::reduce_to(&sat) + let reduction = ReduceTo::>>::reduce_to(&sat) .expect("reduction should succeed"); // Select vertex 0 (literal x1) let is_sol = vec![true, false]; - let sat_sol = reduction.extract_solution(&is_sol).unwrap(); + let sat_sol = reduction + .recover_result( + &sat, + SolveOutcome::optimal(reduction.target_problem(), is_sol.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(sat_sol, vec![true, false]); // x1=true, x2=false // Select vertex 1 (literal x2) let is_sol = vec![false, true]; - let sat_sol = reduction.extract_solution(&is_sol).unwrap(); + let sat_sol = reduction + .recover_result( + &sat, + SolveOutcome::optimal(reduction.target_problem(), is_sol.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(sat_sol, vec![false, true]); // x1=false, x2=true } @@ -100,11 +119,18 @@ fn test_extract_solution_basic() { fn test_extract_solution_with_negation() { // (NOT x1) - selecting NOT x1 means x1 should be false let sat = Satisfiability::new(1, vec![CNFClause::new(vec![-1])]); - let reduction = ReduceTo::>::reduce_to(&sat) + let reduction = ReduceTo::>>::reduce_to(&sat) .expect("reduction should succeed"); let is_sol = vec![true]; - let sat_sol = reduction.extract_solution(&is_sol).unwrap(); + let sat_sol = reduction + .recover_result( + &sat, + SolveOutcome::optimal(reduction.target_problem(), is_sol.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(sat_sol, vec![false]); // x1=false (so NOT x1 is true) } @@ -112,13 +138,13 @@ fn test_extract_solution_with_negation() { fn test_clique_edges_in_clause() { // A clause with 3 literals should form a clique (3 edges) let sat = Satisfiability::new(3, vec![CNFClause::new(vec![1, 2, 3])]); - let reduction = ReduceTo::>::reduce_to(&sat) + let reduction = ReduceTo::>>::reduce_to(&sat) .expect("reduction should succeed"); let is_problem = reduction.target_problem(); // 3 vertices, 3 edges (complete graph K3) - assert_eq!(is_problem.graph().num_vertices(), 3); - assert_eq!(is_problem.graph().num_edges(), 3); + assert_eq!(is_problem.inner().graph().num_vertices(), 3); + assert_eq!(is_problem.inner().graph().num_edges(), 3); } #[test] @@ -134,12 +160,12 @@ fn test_complement_edges_across_clauses() { CNFClause::new(vec![2]), ], ); - let reduction = ReduceTo::>::reduce_to(&sat) + let reduction = ReduceTo::>>::reduce_to(&sat) .expect("reduction should succeed"); let is_problem = reduction.target_problem(); - assert_eq!(is_problem.graph().num_vertices(), 3); - assert_eq!(is_problem.graph().num_edges(), 1); // Only the complement edge + assert_eq!(is_problem.inner().graph().num_vertices(), 3); + assert_eq!(is_problem.inner().graph().num_edges(), 1); // Only the complement edge } #[test] @@ -148,31 +174,31 @@ fn test_is_structure() { 3, vec![CNFClause::new(vec![1, 2]), CNFClause::new(vec![-1, 3])], ); - let reduction = ReduceTo::>::reduce_to(&sat) + let reduction = ReduceTo::>>::reduce_to(&sat) .expect("reduction should succeed"); let is_problem = reduction.target_problem(); // IS should have vertices for literals in clauses - assert_eq!(is_problem.graph().num_vertices(), 4); // 2 + 2 literals + assert_eq!(is_problem.inner().graph().num_vertices(), 4); // 2 + 2 literals } #[test] fn test_empty_sat() { // Empty SAT (trivially satisfiable) let sat = Satisfiability::new(0, vec![]); - let reduction = ReduceTo::>::reduce_to(&sat) + let reduction = ReduceTo::>>::reduce_to(&sat) .expect("reduction should succeed"); let is_problem = reduction.target_problem(); - assert_eq!(is_problem.graph().num_vertices(), 0); - assert_eq!(is_problem.graph().num_edges(), 0); + assert_eq!(is_problem.inner().graph().num_vertices(), 0); + assert_eq!(is_problem.inner().graph().num_edges(), 0); assert_eq!(reduction.num_clauses(), 0); } #[test] fn test_literals_accessor() { let sat = Satisfiability::new(2, vec![CNFClause::new(vec![1, -2])]); - let reduction = ReduceTo::>::reduce_to(&sat) + let reduction = ReduceTo::>>::reduce_to(&sat) .expect("reduction should succeed"); let literals = reduction.literals(); @@ -216,8 +242,9 @@ fn test_jl_parity_sat_to_independentset() { let inst = &jl_find_instance_by_label(&sat_data, label)["instance"]; let (num_vars, clauses) = jl_parse_sat_clauses(inst); let source = Satisfiability::new(num_vars, clauses); - let result = ReduceTo::>::reduce_to(&source) - .expect("reduction should succeed"); + let result = + ReduceTo::>>::reduce_to(&source) + .expect("reduction should succeed"); let solver = BruteForce::new(); let sat_solutions: HashSet> = solver .find_all_witnesses(&source) @@ -226,22 +253,12 @@ fn test_jl_parity_sat_to_independentset() { .collect(); for case in data["cases"].as_array().unwrap() { if sat_solutions.is_empty() { - let target_solution = BruteForce::new() + assert!(BruteForce::new() .solve(result.target_problem()) .unwrap() - .expect("SAT->IS: target should have an optimal solution"); - assert!( - !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&result), &target_solution), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&result, value); value.is_valid() }) - ); - assert_eq!( - crate::rules::AggregateReductionResult::extract_value( - &result, - result.target_problem().evaluate(&target_solution).unwrap(), - ), - Or(false), - ); + .is_none()); } else { - assert_satisfaction_round_trip_from_optimization_target( + assert_satisfaction_round_trip_from_satisfaction_target( &source, &result, &format!("SAT->IS [{label}]"), @@ -277,26 +294,41 @@ fn test_sat_to_independentset_all_certificates() { ], ); let reduction = - ReduceTo::>::reduce_to(&source).unwrap(); + ReduceTo::>>::reduce_to(&source) + .unwrap(); let target = reduction.target_problem(); assert!(std::ptr::eq( target, - crate::rules::AggregateReductionResult::target_problem(&reduction) + crate::rules::ReductionResult::target_problem(&reduction) )); let mut accepted = false; for mask in 0..(1usize << target.num_vertices()) { let config: Vec = (0..target.num_vertices()) .map(|i| mask & (1 << i) != 0) .collect(); - let value = target.evaluate(&config).unwrap(); - let certificate = value == Max(Some(2)); + let value = target.inner().evaluate(&config).unwrap(); + let certificate = value == crate::types::Max(Some(2)); assert_eq!( - crate::rules::AggregateReductionResult::extract_value(&reduction, value), - Or(certificate) + crate::types::Or(OptimizationValue::meets_bound( + &(value), + crate::rules::ReductionResult::target_problem(&reduction).bound() + )), + crate::types::Or(certificate) ); if certificate { - let assignment = reduction.extract_solution(&config).unwrap(); - assert_eq!(source.evaluate(&assignment).unwrap(), Or(true)); + let assignment = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), config.clone()) + .unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); + assert_eq!( + source.evaluate(&assignment).unwrap(), + crate::types::Or(true) + ); accepted = true; } } @@ -304,22 +336,36 @@ fn test_sat_to_independentset_all_certificates() { accepted, BruteForce::new().solve(&source).unwrap().is_some() ); - assert!( - !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![false; target.num_vertices() + 1]), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&reduction, value); value.is_valid() }) - ); + assert!(matches!( + ReductionResult::target_problem(&reduction) + .inner() + .evaluate(&vec![false; target.num_vertices() + 1]), + Err(InvalidConfiguration(_)) + )); } } for num_vars in [0, 3] { let source = Satisfiability::new(num_vars, vec![]); let reduction = - ReduceTo::>::reduce_to(&source).unwrap(); + ReduceTo::>>::reduce_to(&source) + .unwrap(); assert_eq!( - reduction.extract_solution(&vec![]).unwrap(), + reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), vec![].clone()).unwrap() + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"), vec![false; num_vars] ); assert_eq!( - crate::rules::AggregateReductionResult::extract_value(&reduction, Max(None)), - Or(false) + crate::types::Or(OptimizationValue::meets_bound( + &(crate::types::Max(None)), + crate::rules::ReductionResult::target_problem(&reduction).bound() + )), + crate::types::Or(false) ); } } diff --git a/src/unit_tests/rules/sat_minimumdominatingset.rs b/src/unit_tests/rules/sat_minimumdominatingset.rs index 21c6331a0..b28c9ef8d 100644 --- a/src/unit_tests/rules/sat_minimumdominatingset.rs +++ b/src/unit_tests/rules/sat_minimumdominatingset.rs @@ -1,8 +1,13 @@ use super::*; +use crate::models::decision::Decision; +use crate::rules::ReductionResult; +use crate::solvers::SolveOutcome; +use crate::traits::EvaluationError::InvalidConfiguration; use crate::traits::Problem; +use crate::types::OptimizationValue; include!("../jl_helpers.rs"); use crate::models::formula::CNFClause; -use crate::rules::test_helpers::assert_satisfaction_round_trip_from_optimization_target; +use crate::rules::test_helpers::assert_satisfaction_round_trip_from_satisfaction_target; use crate::solvers::BruteForce; use crate::topology::Graph; @@ -10,48 +15,55 @@ use crate::topology::Graph; fn test_sat_to_minimumdominatingset_closed_loop() { // Simple SAT: (x1) - one variable, one clause let sat = Satisfiability::new(1, vec![CNFClause::new(vec![1])]); - let reduction = ReduceTo::>::reduce_to(&sat) + let reduction = ReduceTo::>>::reduce_to(&sat) .expect("reduction should succeed"); let ds_problem = reduction.target_problem(); // Should have 3 vertices (variable gadget) + 1 clause vertex = 4 vertices - assert_eq!(ds_problem.graph().num_vertices(), 4); + assert_eq!(ds_problem.inner().graph().num_vertices(), 4); // Edges: 3 for triangle + 1 from positive literal to clause = 4 // Triangle edges: (0,1), (0,2), (1,2) // Clause edge: (0, 3) since x1 positive connects to clause vertex - assert_eq!(ds_problem.graph().num_edges(), 4); + assert_eq!(ds_problem.inner().graph().num_edges(), 4); } #[test] fn test_two_variable_sat_to_ds() { // SAT: (x1 OR x2) let sat = Satisfiability::new(2, vec![CNFClause::new(vec![1, 2])]); - let reduction = ReduceTo::>::reduce_to(&sat) + let reduction = ReduceTo::>>::reduce_to(&sat) .expect("reduction should succeed"); let ds_problem = reduction.target_problem(); // 2 variables * 3 = 6 gadget vertices + 1 clause vertex = 7 - assert_eq!(ds_problem.graph().num_vertices(), 7); + assert_eq!(ds_problem.inner().graph().num_vertices(), 7); // Edges: // - 3 edges for first triangle: (0,1), (0,2), (1,2) // - 3 edges for second triangle: (3,4), (3,5), (4,5) // - 2 edges from literals to clause: (0,6), (3,6) - assert_eq!(ds_problem.graph().num_edges(), 8); + assert_eq!(ds_problem.inner().graph().num_edges(), 8); } #[test] fn test_extract_solution_positive_literal() { // (x1) - select positive literal let sat = Satisfiability::new(1, vec![CNFClause::new(vec![1])]); - let reduction = ReduceTo::>::reduce_to(&sat) + let reduction = ReduceTo::>>::reduce_to(&sat) .expect("reduction should succeed"); // Solution: select vertex 0 (positive literal x1) // This dominates vertices 1, 2 (gadget) and vertex 3 (clause) let ds_sol = vec![true, false, false, false]; - let sat_sol = reduction.extract_solution(&ds_sol).unwrap(); + let sat_sol = reduction + .recover_result( + &sat, + SolveOutcome::optimal(reduction.target_problem(), ds_sol.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(sat_sol, vec![true]); // x1 = true } @@ -59,13 +71,20 @@ fn test_extract_solution_positive_literal() { fn test_extract_solution_negative_literal() { // (NOT x1) - select negative literal let sat = Satisfiability::new(1, vec![CNFClause::new(vec![-1])]); - let reduction = ReduceTo::>::reduce_to(&sat) + let reduction = ReduceTo::>>::reduce_to(&sat) .expect("reduction should succeed"); // Solution: select vertex 1 (negative literal NOT x1) // This dominates vertices 0, 2 (gadget) and vertex 3 (clause) let ds_sol = vec![false, true, false, false]; - let sat_sol = reduction.extract_solution(&ds_sol).unwrap(); + let sat_sol = reduction + .recover_result( + &sat, + SolveOutcome::optimal(reduction.target_problem(), ds_sol.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(sat_sol, vec![false]); // x1 = false } @@ -73,13 +92,20 @@ fn test_extract_solution_negative_literal() { fn test_extract_solution_unused_variable() { // The unit clause x1 leaves x2 unused. let sat = Satisfiability::new(2, vec![CNFClause::new(vec![1])]); - let reduction = ReduceTo::>::reduce_to(&sat) + let reduction = ReduceTo::>>::reduce_to(&sat) .expect("reduction should succeed"); // Only x1 occurs, so its triangle is the only gadget. The unused x2 // remains false in the extracted source assignment. let ds_sol = vec![true, false, false, false]; - let sat_sol = reduction.extract_solution(&ds_sol).unwrap(); + let sat_sol = reduction + .recover_result( + &sat, + SolveOutcome::optimal(reduction.target_problem(), ds_sol.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(sat_sol, vec![true, false]); // x1 = true, x2 = false (from dummy) } @@ -89,24 +115,24 @@ fn test_ds_structure() { 3, vec![CNFClause::new(vec![1, 2]), CNFClause::new(vec![-1, 3])], ); - let reduction = ReduceTo::>::reduce_to(&sat) + let reduction = ReduceTo::>>::reduce_to(&sat) .expect("reduction should succeed"); let ds_problem = reduction.target_problem(); // 3 vars * 3 = 9 gadget vertices + 2 clause vertices = 11 - assert_eq!(ds_problem.graph().num_vertices(), 11); + assert_eq!(ds_problem.inner().graph().num_vertices(), 11); } #[test] fn test_empty_sat() { // Empty SAT (trivially satisfiable) let sat = Satisfiability::new(0, vec![]); - let reduction = ReduceTo::>::reduce_to(&sat) + let reduction = ReduceTo::>>::reduce_to(&sat) .expect("reduction should succeed"); let ds_problem = reduction.target_problem(); - assert_eq!(ds_problem.graph().num_vertices(), 0); - assert_eq!(ds_problem.graph().num_edges(), 0); + assert_eq!(ds_problem.inner().graph().num_vertices(), 0); + assert_eq!(ds_problem.inner().graph().num_edges(), 0); assert_eq!(reduction.num_clauses(), 0); assert_eq!(reduction.num_literals(), 0); } @@ -115,23 +141,23 @@ fn test_empty_sat() { fn test_multiple_literals_same_variable() { // Clause with repeated variable: (x1 OR NOT x1) - tautology let sat = Satisfiability::new(1, vec![CNFClause::new(vec![1, -1])]); - let reduction = ReduceTo::>::reduce_to(&sat) + let reduction = ReduceTo::>>::reduce_to(&sat) .expect("reduction should succeed"); let ds_problem = reduction.target_problem(); // 3 gadget vertices + 1 clause vertex = 4 - assert_eq!(ds_problem.graph().num_vertices(), 4); + assert_eq!(ds_problem.inner().graph().num_vertices(), 4); // Edges: // - 3 for triangle // - 2 from literals to clause (both positive and negative literals connect) - assert_eq!(ds_problem.graph().num_edges(), 5); + assert_eq!(ds_problem.inner().graph().num_edges(), 5); } #[test] fn test_accessors() { let sat = Satisfiability::new(2, vec![CNFClause::new(vec![1, -2])]); - let reduction = ReduceTo::>::reduce_to(&sat) + let reduction = ReduceTo::>>::reduce_to(&sat) .expect("reduction should succeed"); assert_eq!(reduction.num_literals(), 2); @@ -141,52 +167,40 @@ fn test_accessors() { #[test] fn test_extract_solution_too_many_selected() { let sat = Satisfiability::new(1, vec![CNFClause::new(vec![1])]); - let reduction = ReduceTo::>::reduce_to(&sat) + let reduction = ReduceTo::>>::reduce_to(&sat) .expect("reduction should succeed"); let ds_sol = vec![true, true, false, false]; - assert!( - !crate::rules::AggregateReductionResult::extract_value( - &reduction, - reduction.target_problem().evaluate(&ds_sol).unwrap() - ) - .0 - ); + assert!(!reduction.target_problem().evaluate(&ds_sol).unwrap().0); } #[test] -fn test_extract_solution_rejects_unselected_variable_gadget() { +fn test_value_mapping_rejects_unselected_variable_gadget() { let sat = Satisfiability::new(1, vec![CNFClause::new(vec![1])]); - let reduction = ReduceTo::>::reduce_to(&sat) + let reduction = ReduceTo::>>::reduce_to(&sat) .expect("reduction should succeed"); assert!( - !crate::rules::AggregateReductionResult::extract_value( - &reduction, - reduction - .target_problem() - .evaluate(&vec![false, false, false, false]) - .unwrap() - ) - .0 + !reduction + .target_problem() + .evaluate(&vec![false, false, false, false]) + .unwrap() + .0 ); } #[test] -fn test_extract_solution_rejects_selected_clause_vertex() { +fn test_value_mapping_rejects_selected_clause_vertex() { let sat = Satisfiability::new(1, vec![CNFClause::new(vec![1])]); - let reduction = ReduceTo::>::reduce_to(&sat) + let reduction = ReduceTo::>>::reduce_to(&sat) .expect("reduction should succeed"); assert!( - !crate::rules::AggregateReductionResult::extract_value( - &reduction, - reduction - .target_problem() - .evaluate(&vec![true, false, false, true]) - .unwrap() - ) - .0 + !reduction + .target_problem() + .evaluate(&vec![true, false, false, true]) + .unwrap() + .0 ); } @@ -194,18 +208,18 @@ fn test_extract_solution_rejects_selected_clause_vertex() { fn test_negated_variable_connection() { // (NOT x1 OR NOT x2) - both negated let sat = Satisfiability::new(2, vec![CNFClause::new(vec![-1, -2])]); - let reduction = ReduceTo::>::reduce_to(&sat) + let reduction = ReduceTo::>>::reduce_to(&sat) .expect("reduction should succeed"); let ds_problem = reduction.target_problem(); // 2 * 3 = 6 gadget vertices + 1 clause = 7 - assert_eq!(ds_problem.graph().num_vertices(), 7); + assert_eq!(ds_problem.inner().graph().num_vertices(), 7); // Edges: // - 3 for first triangle: (0,1), (0,2), (1,2) // - 3 for second triangle: (3,4), (3,5), (4,5) // - 2 from negated literals to clause: (1,6), (4,6) - assert_eq!(ds_problem.graph().num_edges(), 8); + assert_eq!(ds_problem.inner().graph().num_edges(), 8); } #[test] @@ -243,8 +257,9 @@ fn test_jl_parity_sat_to_dominatingset() { let inst = &jl_find_instance_by_label(&sat_data, label)["instance"]; let (num_vars, clauses) = jl_parse_sat_clauses(inst); let source = Satisfiability::new(num_vars, clauses); - let result = ReduceTo::>::reduce_to(&source) - .expect("reduction should succeed"); + let result = + ReduceTo::>>::reduce_to(&source) + .expect("reduction should succeed"); let solver = BruteForce::new(); let sat_solutions: HashSet> = solver .find_all_witnesses(&source) @@ -253,15 +268,12 @@ fn test_jl_parity_sat_to_dominatingset() { .collect(); for case in data["cases"].as_array().unwrap() { if sat_solutions.is_empty() { - let target_solution = BruteForce::new() + assert!(BruteForce::new() .solve(result.target_problem()) .unwrap() - .expect("SAT->DS: target should have an optimal solution"); - assert!( - !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&result), &target_solution), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&result, value); value.is_valid() }) - ); + .is_none()); } else { - assert_satisfaction_round_trip_from_optimization_target( + assert_satisfaction_round_trip_from_satisfaction_target( &source, &result, &format!("SAT->DS [{label}]"), @@ -291,26 +303,37 @@ fn test_sat_to_dominatingset_native_certificates() { ] { let source = Satisfiability::new(n, clauses.into_iter().map(CNFClause::new).collect()); let result = - ReduceTo::>::reduce_to(&source).unwrap(); + ReduceTo::>>::reduce_to(&source) + .unwrap(); let target = result.target_problem(); assert!(std::ptr::eq( target, - crate::rules::AggregateReductionResult::target_problem(&result) + crate::rules::ReductionResult::target_problem(&result) )); let mut accepted = false; for mask in 0..(1usize << target.num_vertices()) { let config: Vec<_> = (0..target.num_vertices()) .map(|i| mask & (1 << i) != 0) .collect(); - let value = target.evaluate(&config).unwrap(); - let certificate = value == Min(Some(result.target_size)); + let value = target.inner().evaluate(&config).unwrap(); + let certificate = value == crate::types::Min(Some(*result.target_problem().bound())); assert_eq!( - crate::rules::AggregateReductionResult::extract_value(&result, value), - Or(certificate) + crate::types::Or(OptimizationValue::meets_bound( + &(value), + crate::rules::ReductionResult::target_problem(&result).bound() + )), + crate::types::Or(certificate) ); if certificate { - let x = result.extract_solution(&config).unwrap(); - assert_eq!(source.evaluate(&x).unwrap(), Or(true)); + let x = result + .recover_result( + &source, + SolveOutcome::optimal(result.target_problem(), config.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); + assert_eq!(source.evaluate(&x).unwrap(), crate::types::Or(true)); accepted = true; } } @@ -318,9 +341,12 @@ fn test_sat_to_dominatingset_native_certificates() { accepted, BruteForce::new().solve(&source).unwrap().is_some() ); - assert!( - !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&result), &vec![false; target.num_vertices() + 1]), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&result, value); value.is_valid() }) - ); + assert!(matches!( + ReductionResult::target_problem(&result) + .inner() + .evaluate(&vec![false; target.num_vertices() + 1]), + Err(InvalidConfiguration(_)) + )); } } @@ -329,7 +355,8 @@ fn test_sat_to_dominatingset_sparse_declared_variables() { for clauses in [vec![], vec![CNFClause::new(vec![i64::MAX])]] { let source = Satisfiability::new(i64::MAX as usize, clauses); let result = - ReduceTo::>::reduce_to(&source).unwrap(); + ReduceTo::>>::reduce_to(&source) + .unwrap(); assert_eq!(result.num_literals(), i64::MAX as usize); assert!(result.target_problem().num_vertices() <= 4); // Construction is compact. Extracting an i64::MAX-length source vector diff --git a/src/unit_tests/rules/satisfiability_integralflowhomologousarcs.rs b/src/unit_tests/rules/satisfiability_integralflowhomologousarcs.rs index 28a7a9df2..8c9a50eb8 100644 --- a/src/unit_tests/rules/satisfiability_integralflowhomologousarcs.rs +++ b/src/unit_tests/rules/satisfiability_integralflowhomologousarcs.rs @@ -7,6 +7,7 @@ use crate::models::graph::IntegralFlowHomologousArcs; use crate::rules::test_helpers::assert_satisfaction_round_trip_from_satisfaction_target; use crate::rules::{ReduceTo, ReductionGraph, ReductionResult}; use crate::solvers::BruteForce; +use crate::solvers::SolveOutcome; use crate::traits::Problem; fn issue_example() -> Satisfiability { @@ -65,7 +66,14 @@ fn test_satisfiability_to_integralflowhomologousarcs_issue_example_assignment_en let satisfying_flow = reduction.encode_assignment(&satisfying_assignment); assert!(target.evaluate(&satisfying_flow).unwrap().0); assert_eq!( - reduction.extract_solution(&satisfying_flow).unwrap(), + reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), satisfying_flow.clone()).unwrap() + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"), satisfying_assignment ); diff --git a/src/unit_tests/rules/satisfiability_maximum2satisfiability.rs b/src/unit_tests/rules/satisfiability_maximum2satisfiability.rs index d5ed64c13..8e6552669 100644 --- a/src/unit_tests/rules/satisfiability_maximum2satisfiability.rs +++ b/src/unit_tests/rules/satisfiability_maximum2satisfiability.rs @@ -1,9 +1,14 @@ use super::*; +use crate::models::decision::Decision; use crate::models::formula::{CNFClause, Maximum2Satisfiability, Satisfiability}; -use crate::rules::test_helpers::assert_satisfaction_round_trip_from_optimization_target; +use crate::rules::test_helpers::assert_satisfaction_round_trip_from_satisfaction_target; use crate::rules::traits::ReduceTo; +use crate::rules::ReductionResult; use crate::solvers::BruteForce; +use crate::solvers::SolveOutcome; +use crate::traits::EvaluationError::InvalidConfiguration; use crate::traits::Problem; +use crate::types::OptimizationValue; #[test] fn test_satisfiability_to_maximum2satisfiability_structure() { @@ -12,19 +17,19 @@ fn test_satisfiability_to_maximum2satisfiability_structure() { vec![CNFClause::new(vec![1, -2, 3]), CNFClause::new(vec![-1, 2])], ); - let reduction = - ReduceTo::::reduce_to(&source).expect("reduction should succeed"); + let reduction = ReduceTo::>::reduce_to(&source) + .expect("reduction should succeed"); let target = reduction.target_problem(); - let aggregate_target = crate::rules::AggregateReductionResult::target_problem(&reduction); + let aggregate_target = crate::rules::ReductionResult::target_problem(&reduction); assert!(std::ptr::eq(target, aggregate_target)); - assert_eq!(aggregate_target.num_clauses(), 30); - assert_eq!(target.num_vars(), 7); - assert_eq!(target.num_clauses(), 30); - assert_eq!(target.clauses()[0].literals, vec![1, 1]); - assert_eq!(target.clauses()[4].literals, vec![-1, 2]); - assert_eq!(target.clauses()[10].literals, vec![-1, -1]); - assert_eq!(target.clauses()[20].literals, vec![-1, -1]); + assert_eq!(aggregate_target.inner().num_clauses(), 30); + assert_eq!(target.inner().num_vars(), 7); + assert_eq!(target.inner().num_clauses(), 30); + assert_eq!(target.inner().clauses()[0].literals, vec![1, 1]); + assert_eq!(target.inner().clauses()[4].literals, vec![-1, 2]); + assert_eq!(target.inner().clauses()[10].literals, vec![-1, -1]); + assert_eq!(target.inner().clauses()[20].literals, vec![-1, -1]); } #[test] @@ -34,11 +39,11 @@ fn test_satisfiability_to_maximum2satisfiability_closed_loop() { vec![CNFClause::new(vec![1, -2, 3]), CNFClause::new(vec![-1, 2])], ); - let reduction = - ReduceTo::::reduce_to(&source).expect("reduction should succeed"); + let reduction = ReduceTo::>::reduce_to(&source) + .expect("reduction should succeed"); let target = reduction.target_problem(); - assert_satisfaction_round_trip_from_optimization_target( + assert_satisfaction_round_trip_from_satisfaction_target( &source, &reduction, "SAT -> Maximum2Satisfiability closed loop", @@ -46,6 +51,7 @@ fn test_satisfiability_to_maximum2satisfiability_closed_loop() { assert_eq!( target + .inner() .evaluate(&BruteForce::new().solve(target).unwrap().unwrap()) .unwrap() .0, @@ -57,28 +63,34 @@ fn test_satisfiability_to_maximum2satisfiability_closed_loop() { fn test_satisfiability_to_maximum2satisfiability_unsatisfiable_gap() { let source = Satisfiability::new(1, vec![CNFClause::new(vec![1]), CNFClause::new(vec![-1])]); - let reduction = - ReduceTo::::reduce_to(&source).expect("reduction should succeed"); + let reduction = ReduceTo::>::reduce_to(&source) + .expect("reduction should succeed"); let target = reduction.target_problem(); + assert!(BruteForce::new().solve(target).unwrap().is_none()); assert_eq!( target - .evaluate(&BruteForce::new().solve(target).unwrap().unwrap()) + .inner() + .evaluate(&BruteForce::new().solve(target.inner()).unwrap().unwrap()) .unwrap() .0, Some(55) ); let target_solution = BruteForce::new() - .solve(target) + .solve(target.inner()) .unwrap() .expect("MAX-2-SAT target should always have a witness"); - assert!( - !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &target_solution), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&reduction, value); value.is_valid() }) - ); + assert!(!ReductionResult::target_problem(&reduction) + .evaluate(&target_solution) + .unwrap() + .is_valid()); assert_eq!( - crate::rules::AggregateReductionResult::extract_value(&reduction, Max(Some(55))), - Or(false) + crate::types::Or(OptimizationValue::meets_bound( + &(crate::types::Max(Some(55))), + crate::rules::ReductionResult::target_problem(&reduction).bound() + )), + crate::types::Or(false) ); } @@ -86,15 +98,17 @@ fn test_satisfiability_to_maximum2satisfiability_unsatisfiable_gap() { fn test_satisfiability_to_maximum2satisfiability_empty_clause() { let source = Satisfiability::new(1, vec![CNFClause::new(vec![])]); - let reduction = - ReduceTo::::reduce_to(&source).expect("reduction should succeed"); + let reduction = ReduceTo::>::reduce_to(&source) + .expect("reduction should succeed"); let target = reduction.target_problem(); + assert!(BruteForce::new().solve(target).unwrap().is_none()); - assert_eq!(target.num_vars(), 4); - assert_eq!(target.num_clauses(), 20); + assert_eq!(target.inner().num_vars(), 4); + assert_eq!(target.inner().num_clauses(), 20); assert_eq!( target - .evaluate(&BruteForce::new().solve(target).unwrap().unwrap()) + .inner() + .evaluate(&BruteForce::new().solve(target.inner()).unwrap().unwrap()) .unwrap() .0, Some(13) @@ -145,38 +159,56 @@ fn test_satisfiability_to_maximum2satisfiability_every_target_witness() { } } for source in sources { - let reduction = ReduceTo::::reduce_to(&source).unwrap(); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); let target = reduction.target_problem(); - let threshold = (target.num_clauses() / 10 * 7) as i64; + let threshold = (target.inner().num_clauses() / 10 * 7) as i64; let mut best = 0; - for bits in 0usize..(1 << target.num_vars()) { - let assignment = (0..target.num_vars()) + for bits in 0usize..(1 << target.inner().num_vars()) { + let assignment = (0..target.inner().num_vars()) .map(|i| bits & (1 << i) != 0) .collect(); - let value = target.evaluate(&assignment).unwrap(); + let value = target.inner().evaluate(&assignment).unwrap(); best = best.max(value.0.unwrap()); - let expected = value == Max(Some(threshold)); + let expected = value == crate::types::Max(Some(threshold)); assert_eq!( - crate::rules::AggregateReductionResult::extract_value(&reduction, value), - Or(expected) + crate::types::Or(OptimizationValue::meets_bound( + &(value), + crate::rules::ReductionResult::target_problem(&reduction).bound() + )), + crate::types::Or(expected) ); if expected { - let decoded = reduction.extract_solution(&assignment).unwrap(); + let decoded = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), assignment.clone()) + .unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!(source.evaluate(&decoded).unwrap().0); } else { - assert!( - !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &assignment), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&reduction, value); value.is_valid() }) - ); + assert!(!ReductionResult::target_problem(&reduction) + .evaluate(&assignment) + .unwrap() + .is_valid()); } } let source_yes = BruteForce::new().solve(&source).unwrap().is_some(); assert_eq!(best == threshold, source_yes); - assert!( - !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![false; target.num_vars() + 1]), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&reduction, value); value.is_valid() }) - ); + assert!(matches!( + ReductionResult::target_problem(&reduction) + .inner() + .evaluate(&vec![false; target.inner().num_vars() + 1]), + Err(InvalidConfiguration(_)) + )); assert_eq!( - crate::rules::AggregateReductionResult::extract_value(&reduction, Max(None)), - Or(false) + crate::types::Or(OptimizationValue::meets_bound( + &(crate::types::Max(None)), + crate::rules::ReductionResult::target_problem(&reduction).bound() + )), + crate::types::Or(false) ); } } diff --git a/src/unit_tests/rules/satisfiability_naesatisfiability.rs b/src/unit_tests/rules/satisfiability_naesatisfiability.rs index a88b6fcb4..30d384507 100644 --- a/src/unit_tests/rules/satisfiability_naesatisfiability.rs +++ b/src/unit_tests/rules/satisfiability_naesatisfiability.rs @@ -1,6 +1,9 @@ use super::*; use crate::rules::test_helpers::assert_satisfaction_round_trip_from_satisfaction_target; +use crate::rules::ReductionResult; use crate::solvers::BruteForce; +use crate::solvers::SolveOutcome; +use crate::traits::EvaluationError::InvalidConfiguration; use crate::traits::Problem; #[test] @@ -68,8 +71,17 @@ fn test_solution_extraction_sentinel_false() { // target_solution: [1, 0, 1, 0] means x1=true, x2=false, x3=true, sentinel=false let extracted = reduction - .extract_solution(&vec![true, false, true, false]) - .unwrap(); + .recover_result( + &sat, + SolveOutcome::optimal( + reduction.target_problem(), + vec![true, false, true, false].clone(), + ) + .unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted, vec![true, false, true]); } @@ -81,8 +93,17 @@ fn test_solution_extraction_distinguishes_zero_assignment_from_malformed_input() assert_eq!( reduction - .extract_solution(&vec![false, false, false]) - .unwrap(), + .recover_result( + &sat, + SolveOutcome::optimal( + reduction.target_problem(), + vec![false, false, false].clone() + ) + .unwrap() + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"), vec![false, false] ); @@ -90,12 +111,16 @@ fn test_solution_extraction_distinguishes_zero_assignment_from_malformed_input() .target_problem() .evaluate(&vec![false, false]) .is_err()); - assert!( - !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![false, false, false, false]), Ok(value) if { value.is_valid() }) - ); - assert!(crate::rules::DynReductionResult::target_solution_from_json( + assert!(matches!( + ReductionResult::target_problem(&reduction).evaluate(&vec![false, false, false, false]), + Err(InvalidConfiguration(_)) + )); + assert!(crate::rules::DynReductionResult::target_result_from_json( &reduction, - serde_json::json!([false, 2, false]) + SolveOutcome::Optimal { + solution: serde_json::json!([false, 2, false]), + evaluation: String::new() + } ) .is_err()); } @@ -111,8 +136,17 @@ fn test_solution_extraction_sentinel_true() { // target_solution: [0, 1, 0, 1] means x1=false, x2=true, x3=false, sentinel=true // Complement: x1=true, x2=false, x3=true let extracted = reduction - .extract_solution(&vec![false, true, false, true]) - .unwrap(); + .recover_result( + &sat, + SolveOutcome::optimal( + reduction.target_problem(), + vec![false, true, false, true].clone(), + ) + .unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted, vec![true, false, true]); } @@ -210,7 +244,14 @@ fn test_all_satisfying_assignments_map_back() { let nae_solutions = solver.find_all_witnesses(naesat).unwrap(); for nae_sol in &nae_solutions { - let sat_sol = reduction.extract_solution(nae_sol).unwrap(); + let sat_sol = reduction + .recover_result( + &sat, + SolveOutcome::optimal(reduction.target_problem(), (nae_sol).clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(sat_sol.len(), 2); assert!( sat.evaluate(&sat_sol).unwrap().0, diff --git a/src/unit_tests/rules/satisfiability_nontautology.rs b/src/unit_tests/rules/satisfiability_nontautology.rs index ab9608124..bb0b7abb5 100644 --- a/src/unit_tests/rules/satisfiability_nontautology.rs +++ b/src/unit_tests/rules/satisfiability_nontautology.rs @@ -2,6 +2,7 @@ use crate::models::formula::{CNFClause, NonTautology, Satisfiability}; use crate::rules::test_helpers::assert_satisfaction_round_trip_from_satisfaction_target; use crate::rules::{ReduceTo, ReductionGraph, ReductionResult}; use crate::solvers::BruteForce; +use crate::solvers::SolveOutcome; #[test] fn test_satisfiability_to_non_tautology_structure() { @@ -58,7 +59,14 @@ fn test_satisfiability_to_non_tautology_extract_solution_is_identity() { .expect("target should have a witness"); assert_eq!( - reduction.extract_solution(&target_solution).unwrap(), + reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), target_solution.clone()).unwrap() + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"), target_solution ); } diff --git a/src/unit_tests/rules/schedulingtominimizeweightedcompletiontime_ilp.rs b/src/unit_tests/rules/schedulingtominimizeweightedcompletiontime_ilp.rs index eaab98e84..526714ed2 100644 --- a/src/unit_tests/rules/schedulingtominimizeweightedcompletiontime_ilp.rs +++ b/src/unit_tests/rules/schedulingtominimizeweightedcompletiontime_ilp.rs @@ -1,6 +1,7 @@ use super::*; use crate::models::algebraic::{ObjectiveSense, ILP}; use crate::models::misc::SchedulingToMinimizeWeightedCompletionTime; +use crate::solvers::SolveOutcome; use crate::solvers::{BruteForce, ILPSolver}; use crate::traits::Problem; use crate::types::Min; @@ -55,7 +56,14 @@ fn test_solution_extraction() { // y vars: index 6 sol[6] = 1; // y_{0,1} = 1 - let extracted = reduction.extract_solution(&sol).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), sol.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted, vec![0, 1]); // Each on separate processor: C(0)=1, C(1)=2, WCT = 1*3 + 2*1 = 5 assert_eq!(problem.evaluate(&extracted).unwrap(), Min(Some(5))); @@ -77,7 +85,14 @@ fn test_ilp_matches_bruteforce_small() { 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(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let ilp_value = problem.evaluate(&extracted).unwrap(); assert_eq!(ilp_value, bf_value); @@ -96,7 +111,14 @@ fn test_issue_example_closed_loop() { 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(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(problem.evaluate(&extracted).unwrap(), Min(Some(47))); } @@ -109,7 +131,14 @@ fn test_single_task_single_processor() { 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(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(problem.evaluate(&extracted).unwrap(), Min(Some(15))); } @@ -130,7 +159,14 @@ fn test_equal_tasks_multiple_processors() { 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(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let ilp_value = problem.evaluate(&extracted).unwrap(); assert_eq!(ilp_value, bf_value); diff --git a/src/unit_tests/rules/schedulingwithindividualdeadlines_ilp.rs b/src/unit_tests/rules/schedulingwithindividualdeadlines_ilp.rs index b5f904249..bbf4a3c81 100644 --- a/src/unit_tests/rules/schedulingwithindividualdeadlines_ilp.rs +++ b/src/unit_tests/rules/schedulingwithindividualdeadlines_ilp.rs @@ -1,5 +1,6 @@ use super::*; use crate::models::algebraic::{ObjectiveSense, ILP}; +use crate::solvers::SolveOutcome; use crate::solvers::{BruteForce, ILPSolver}; use crate::traits::Problem; @@ -50,7 +51,14 @@ fn test_schedulingwithindividualdeadlines_to_ilp_fixes_unused_slots() { .collect(); assert_eq!(witnesses.len(), 2); for witness in witnesses { - let source_solution = reduction.extract_solution(&witness).unwrap(); + let source_solution = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), witness.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!(problem.evaluate(&source_solution).unwrap()); } } @@ -73,7 +81,14 @@ fn test_schedulingwithindividualdeadlines_to_ilp_closed_loop() { let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be feasible"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!( problem.evaluate(&extracted).unwrap().0, @@ -103,7 +118,14 @@ fn test_schedulingwithindividualdeadlines_to_ilp_extract_solution() { // max_deadline=3: x_{j,t} at j*3+t // x_{0,0}=1, x_{0,1}=0, x_{0,2}=0, x_{1,0}=1, x_{1,1}=0, x_{1,2}=0, x_{2,0}=0, x_{2,1}=1, x_{2,2}=0 let ilp_solution = vec![1, 0, 0, 1, 0, 0, 0, 1, 0]; - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted, vec![0, 0, 1]); assert!( problem.evaluate(&extracted).unwrap().0, diff --git a/src/unit_tests/rules/sequencingtominimizemaximumcumulativecost_ilp.rs b/src/unit_tests/rules/sequencingtominimizemaximumcumulativecost_ilp.rs index 617788d35..799972299 100644 --- a/src/unit_tests/rules/sequencingtominimizemaximumcumulativecost_ilp.rs +++ b/src/unit_tests/rules/sequencingtominimizemaximumcumulativecost_ilp.rs @@ -1,6 +1,7 @@ use super::*; use crate::models::algebraic::ILP; use crate::rules::ReduceTo; +use crate::solvers::SolveOutcome; use crate::solvers::{BruteForce, ILPSolver}; use crate::traits::Problem; @@ -18,7 +19,14 @@ fn test_sequencingtominimizemaximumcumulativecost_to_ilp_closed_loop() { let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let ilp_value = problem.evaluate(&extracted).unwrap(); assert!( @@ -45,7 +53,14 @@ fn test_sequencingtominimizemaximumcumulativecost_to_ilp_bf_vs_ilp() { let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!(problem.evaluate(&extracted).unwrap().0.is_some()); } @@ -56,6 +71,13 @@ fn test_sequencingtominimizemaximumcumulativecost_to_ilp_no_precedences() { let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!(problem.evaluate(&extracted).unwrap().0.is_some()); } diff --git a/src/unit_tests/rules/sequencingtominimizetardytaskweight_ilp.rs b/src/unit_tests/rules/sequencingtominimizetardytaskweight_ilp.rs index c90ea934a..b889d0716 100644 --- a/src/unit_tests/rules/sequencingtominimizetardytaskweight_ilp.rs +++ b/src/unit_tests/rules/sequencingtominimizetardytaskweight_ilp.rs @@ -1,7 +1,10 @@ use super::*; use crate::models::algebraic::ILP; use crate::rules::test_helpers::assert_bf_vs_ilp; +use crate::rules::ReductionResult; +use crate::solvers::SolveOutcome; use crate::solvers::{BruteForce, ILPSolver}; +use crate::traits::EvaluationError::InvalidConfiguration; use crate::traits::Problem; #[test] @@ -29,7 +32,14 @@ fn test_sequencingtominimizetardytaskweight_to_ilp_bf_vs_ilp() { let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let ilp_value = problem.evaluate(&extracted).unwrap(); assert_eq!(bf_value, ilp_value); @@ -45,7 +55,14 @@ fn test_sequencingtominimizetardytaskweight_to_ilp_all_on_time() { let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let value = problem.evaluate(&extracted).unwrap(); assert!(value.is_valid()); assert_eq!(value.0, Some(0)); @@ -69,7 +86,14 @@ fn test_sequencingtominimizetardytaskweight_to_ilp_optimal_ordering() { let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let ilp_value = problem.evaluate(&extracted).unwrap(); let bf = BruteForce::new(); @@ -135,22 +159,39 @@ fn test_tardy_ilp_signed_permutations_and_all_indicators() { let value = target.evaluate(&bits).unwrap(); assert_eq!(value.is_valid(), exact); if exact { - let extracted = reduction.extract_solution(&bits); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), bits.clone()) + .unwrap(), + ) + .map(|result| { + result.into_solution().expect( + "qualifying target result must recover a source solution", + ) + }); assert_eq!(value.value, source_value.0); assert_eq!(extracted.unwrap(), schedule); } } } - assert!( - !matches!(crate::traits::Problem::evaluate(reduction.target_problem(), &vec![0; target.num_vars() + 1]), Ok(value) if value.is_valid()) - ); + assert!(matches!( + ReductionResult::target_problem(&reduction).evaluate(&vec![ + 0; + target.num_vars() + + 1 + ]), + Err(InvalidConfiguration(_)) + )); if count > 0 { - assert!( - !matches!(crate::traits::Problem::evaluate(reduction.target_problem(), &vec![0; target.num_vars()]), Ok(value) if value.is_valid()) - ); - assert!( - !matches!(crate::traits::Problem::evaluate(reduction.target_problem(), &vec![2; target.num_vars()]), Ok(value) if value.is_valid()) - ); + assert!(!ReductionResult::target_problem(&reduction) + .evaluate(&vec![0; target.num_vars()]) + .unwrap() + .is_valid()); + assert!(!ReductionResult::target_problem(&reduction) + .evaluate(&vec![2; target.num_vars()]) + .unwrap() + .is_valid()); } } } @@ -170,7 +211,14 @@ fn test_tardy_ilp_complete_small_binary_target_space() { .collect(); let value = target.evaluate(&bits).unwrap(); if value.is_valid() { - let schedule = reduction.extract_solution(&bits).unwrap(); + let schedule = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), bits.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(source.evaluate(&schedule).unwrap().0, value.value); feasible += 1; } @@ -197,7 +245,17 @@ fn test_tardy_ilp_numeric_boundaries_and_representability_errors() { reduction.target_problem().evaluate(&bits).unwrap().value, source.evaluate(&vec![0]).unwrap().0 ); - assert_eq!(reduction.extract_solution(&bits).unwrap(), vec![0]); + assert_eq!( + reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), bits.clone()).unwrap() + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"), + vec![0] + ); } for (lengths, weights, deadlines) in [ (vec![i64::MAX, i64::MAX], vec![1, 1], vec![0, 0]), diff --git a/src/unit_tests/rules/sequencingtominimizeweightedcompletiontime_ilp.rs b/src/unit_tests/rules/sequencingtominimizeweightedcompletiontime_ilp.rs index 0d00777ef..efc543d9d 100644 --- a/src/unit_tests/rules/sequencingtominimizeweightedcompletiontime_ilp.rs +++ b/src/unit_tests/rules/sequencingtominimizeweightedcompletiontime_ilp.rs @@ -1,6 +1,7 @@ use super::*; use crate::models::algebraic::{ObjectiveSense, ILP}; use crate::models::misc::SequencingToMinimizeWeightedCompletionTime; +use crate::solvers::SolveOutcome; use crate::solvers::{BruteForce, ILPSolver}; use crate::traits::Problem; use crate::types::Min; @@ -45,7 +46,14 @@ fn test_extract_solution_encodes_schedule_as_lehmer_code() { // Completion times C0 = 3, C1 = 1 imply schedule [1, 0]. // y_{0,1} = 0 means task 1 before task 0. - let extracted = reduction.extract_solution(&vec![3, 1, 0]).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), vec![3, 1, 0].clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted, vec![1, 0]); assert_eq!(problem.evaluate(&extracted).unwrap(), Min(Some(14))); } @@ -62,7 +70,14 @@ fn test_issue_example_closed_loop() { 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(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted, vec![1, 3, 0, 4, 2]); assert_eq!(problem.evaluate(&extracted).unwrap(), Min(Some(46))); @@ -87,7 +102,14 @@ fn test_ilp_matches_bruteforce_optimum() { 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(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let ilp_metric = problem.evaluate(&extracted).unwrap(); assert_eq!(ilp_metric, brute_force_metric); @@ -155,7 +177,14 @@ fn test_ilp_pipeline_matches_source_optimum() { let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be solvable"); - let source_solution = reduction.extract_solution(&ilp_solution).unwrap(); + let source_solution = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(source_solution, vec![1, 3, 0, 4, 2]); assert_eq!(problem.evaluate(&source_solution).unwrap(), Min(Some(46))); diff --git a/src/unit_tests/rules/sequencingtominimizeweightedtardiness_ilp.rs b/src/unit_tests/rules/sequencingtominimizeweightedtardiness_ilp.rs index 6c85b75bd..a7429692c 100644 --- a/src/unit_tests/rules/sequencingtominimizeweightedtardiness_ilp.rs +++ b/src/unit_tests/rules/sequencingtominimizeweightedtardiness_ilp.rs @@ -1,5 +1,6 @@ use super::*; use crate::models::algebraic::ILP; +use crate::solvers::SolveOutcome; use crate::solvers::{BruteForce, ILPSolver}; use crate::traits::Problem; use crate::types::Or; @@ -14,7 +15,14 @@ fn test_sequencingtominimizeweightedtardiness_to_ilp_closed_loop() { let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(problem.evaluate(&extracted).unwrap(), Or(true)); } @@ -33,7 +41,14 @@ fn test_sequencingtominimizeweightedtardiness_to_ilp_bf_vs_ilp() { let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(problem.evaluate(&extracted).unwrap(), Or(true)); } @@ -63,6 +78,13 @@ fn test_sequencingtominimizeweightedtardiness_to_ilp_no_tardiness() { let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(problem.evaluate(&extracted).unwrap(), Or(true)); } diff --git a/src/unit_tests/rules/sequencingwithdeadlinesandsetuptimes_ilp.rs b/src/unit_tests/rules/sequencingwithdeadlinesandsetuptimes_ilp.rs index c839db5b9..035df68ef 100644 --- a/src/unit_tests/rules/sequencingwithdeadlinesandsetuptimes_ilp.rs +++ b/src/unit_tests/rules/sequencingwithdeadlinesandsetuptimes_ilp.rs @@ -1,6 +1,7 @@ use super::*; use crate::models::algebraic::ILP; use crate::rules::test_helpers::assert_bf_vs_ilp; +use crate::solvers::SolveOutcome; use crate::solvers::{BruteForce, ILPSolver}; use crate::traits::Problem; use crate::types::Or; @@ -39,7 +40,14 @@ fn test_sequencingwithdeadlinesandsetuptimes_to_ilp_feasible_paper_example() { let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be feasible"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(problem.evaluate(&extracted).unwrap(), Or(true)); } @@ -68,7 +76,14 @@ fn test_sequencingwithdeadlinesandsetuptimes_to_ilp_setup_time_respected() { let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be feasible"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(problem.evaluate(&extracted).unwrap(), Or(true)); } @@ -95,7 +110,14 @@ fn test_sequencingwithdeadlinesandsetuptimes_to_ilp_bf_vs_ilp_small() { "BF and ILP should agree on feasibility" ); if let Ok(ilp_solution) = ilp_result { - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(problem.evaluate(&extracted).unwrap(), Or(true)); } } @@ -114,6 +136,13 @@ fn test_sequencingwithdeadlinesandsetuptimes_to_ilp_no_setup_same_compiler() { let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("should be feasible with no switches"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(problem.evaluate(&extracted).unwrap(), Or(true)); } diff --git a/src/unit_tests/rules/sequencingwithinintervals_ilp.rs b/src/unit_tests/rules/sequencingwithinintervals_ilp.rs index aaf208a33..7b78dbef1 100644 --- a/src/unit_tests/rules/sequencingwithinintervals_ilp.rs +++ b/src/unit_tests/rules/sequencingwithinintervals_ilp.rs @@ -1,5 +1,6 @@ use super::*; use crate::models::algebraic::{ObjectiveSense, ILP}; +use crate::solvers::SolveOutcome; use crate::solvers::{BruteForce, ILPSolver}; use crate::traits::Problem; @@ -59,7 +60,14 @@ fn test_sequencingwithinintervals_to_ilp_closed_loop() { let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be feasible"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!( problem.evaluate(&extracted).unwrap().0, @@ -106,7 +114,14 @@ fn test_sequencingwithinintervals_to_ilp_extract_solution() { // task 0 at offset 0, task 1 at offset 0 // vars: x_{0,0}=1, x_{0,1}=0, x_{1,0}=1, x_{1,1}=0 let ilp_solution = vec![1, 0, 1, 0]; - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted, vec![0, 0]); assert!( problem.evaluate(&extracted).unwrap().0, diff --git a/src/unit_tests/rules/sequencingwithreleasetimesanddeadlines_ilp.rs b/src/unit_tests/rules/sequencingwithreleasetimesanddeadlines_ilp.rs index a4b81ef0d..9c983dcf3 100644 --- a/src/unit_tests/rules/sequencingwithreleasetimesanddeadlines_ilp.rs +++ b/src/unit_tests/rules/sequencingwithreleasetimesanddeadlines_ilp.rs @@ -1,6 +1,7 @@ use super::*; use crate::models::algebraic::ILP; use crate::rules::test_helpers::assert_bf_vs_ilp; +use crate::solvers::SolveOutcome; use crate::solvers::{BruteForce, ILPSolver}; use crate::traits::Problem; use crate::types::Or; @@ -29,7 +30,14 @@ fn test_sequencingwithreleasetimesanddeadlines_to_ilp_bf_vs_ilp() { let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(problem.evaluate(&extracted).unwrap(), Or(true)); } @@ -66,6 +74,13 @@ fn test_sequencingwithreleasetimesanddeadlines_to_ilp_single_task() { let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("single-task ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(problem.evaluate(&extracted).unwrap(), Or(true)); } diff --git a/src/unit_tests/rules/setsplitting_betweenness.rs b/src/unit_tests/rules/setsplitting_betweenness.rs index b43fa7fb1..74e3952da 100644 --- a/src/unit_tests/rules/setsplitting_betweenness.rs +++ b/src/unit_tests/rules/setsplitting_betweenness.rs @@ -3,6 +3,7 @@ use crate::models::set::SetSplitting; use crate::rules::test_helpers::assert_satisfaction_round_trip_from_satisfaction_target; use crate::rules::{ReduceTo, ReductionResult}; use crate::solvers::BruteForce; +use crate::solvers::SolveOutcome; fn small_yes_instance() -> SetSplitting { SetSplitting::new(3, vec![vec![0, 1, 2]]) @@ -53,8 +54,17 @@ fn test_setsplitting_to_betweenness_issue_yes_instance_structure() { ); assert_eq!( reduction - .extract_solution(&vec![8, 2, 9, 0, 1, 4, 3, 6, 7, 5]) - .unwrap(), + .recover_result( + &source, + SolveOutcome::optimal( + reduction.target_problem(), + vec![8, 2, 9, 0, 1, 4, 3, 6, 7, 5].clone() + ) + .unwrap() + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"), vec![true, false, true, false, false] ); } diff --git a/src/unit_tests/rules/setsplitting_ilp.rs b/src/unit_tests/rules/setsplitting_ilp.rs index ef7505faf..29669eec0 100644 --- a/src/unit_tests/rules/setsplitting_ilp.rs +++ b/src/unit_tests/rules/setsplitting_ilp.rs @@ -1,4 +1,5 @@ use super::*; +use crate::solvers::SolveOutcome; use crate::solvers::{BruteForce, ILPSolver}; use crate::traits::Problem; use crate::types::Or; @@ -52,7 +53,14 @@ fn test_setsplitting_to_ilp_closed_loop() { let ilp_solver = ILPSolver::new(); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be feasible"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!( problem.evaluate(&extracted).unwrap(), @@ -92,7 +100,14 @@ fn test_setsplitting_bf_vs_ilp() { let ilp_solution = ilp_solver .solve(reduction.target_problem()) .expect("ILP should be feasible"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let ilp_result = problem.evaluate(&extracted).unwrap(); assert_eq!(bf_result, ilp_result, "BruteForce and ILP must agree"); diff --git a/src/unit_tests/rules/shortestcommonsupersequence_ilp.rs b/src/unit_tests/rules/shortestcommonsupersequence_ilp.rs index 80ed3daef..d7c482a3f 100644 --- a/src/unit_tests/rules/shortestcommonsupersequence_ilp.rs +++ b/src/unit_tests/rules/shortestcommonsupersequence_ilp.rs @@ -1,5 +1,6 @@ use super::*; use crate::models::algebraic::{ObjectiveSense, ILP}; +use crate::solvers::SolveOutcome; use crate::solvers::{BruteForce, ILPSolver}; use crate::traits::Problem; @@ -29,7 +30,14 @@ fn test_shortestcommonsupersequence_to_ilp_closed_loop() { let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let ilp_value = problem.evaluate(&extracted).unwrap(); assert_eq!( bf_value, ilp_value, @@ -50,7 +58,14 @@ fn test_shortestcommonsupersequence_to_ilp_bf_vs_ilp() { let ilp_solution = ilp_solver .solve(reduction.target_problem()) .expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!(problem.evaluate(&extracted).unwrap().0.is_some()); } @@ -64,7 +79,14 @@ fn test_solution_extraction() { let ilp_solution = ilp_solver .solve(reduction.target_problem()) .expect("solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted.len(), problem.max_length()); assert!(problem.evaluate(&extracted).unwrap().0.is_some()); } diff --git a/src/unit_tests/rules/shortestweightconstrainedpath_ilp.rs b/src/unit_tests/rules/shortestweightconstrainedpath_ilp.rs index 1b2f1bb32..bd0ca8eae 100644 --- a/src/unit_tests/rules/shortestweightconstrainedpath_ilp.rs +++ b/src/unit_tests/rules/shortestweightconstrainedpath_ilp.rs @@ -1,5 +1,6 @@ use super::*; use crate::models::algebraic::{ObjectiveSense, ILP}; +use crate::solvers::SolveOutcome; use crate::solvers::{BruteForce, ILPSolver}; use crate::topology::SimpleGraph; use crate::traits::Problem; @@ -56,7 +57,15 @@ fn test_shortestweightconstrainedpath_to_ilp_bf_vs_ilp() { match ilp_result { Ok(ilp_solution) => { - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()) + .unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let ilp_value = problem.evaluate(&extracted).unwrap(); // Both should agree on the optimal length assert_eq!(ilp_value, bf_value); @@ -78,7 +87,14 @@ fn test_solution_extraction() { // 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 let target_solution = vec![1, 0, 1, 0, 0, 1, 2]; - let extracted = reduction.extract_solution(&target_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), target_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted, vec![true, true]); // length = 2 + 3 = 5 @@ -102,7 +118,14 @@ fn test_shortestweightconstrainedpath_to_ilp_trivial() { let ilp_solution = ilp_solver .solve(reduction.target_problem()) .expect("ILP should solve the trivial s==t case"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted, vec![false, false]); assert_eq!(problem.evaluate(&extracted).unwrap(), Min(Some(0))); diff --git a/src/unit_tests/rules/sparsematrixcompression_ilp.rs b/src/unit_tests/rules/sparsematrixcompression_ilp.rs index ebc2628b0..866848ca6 100644 --- a/src/unit_tests/rules/sparsematrixcompression_ilp.rs +++ b/src/unit_tests/rules/sparsematrixcompression_ilp.rs @@ -2,6 +2,7 @@ use super::*; use crate::models::algebraic::{ObjectiveSense, ILP}; use crate::rules::test_helpers::assert_bf_vs_ilp; use crate::rules::{ReduceTo, ReductionResult}; +use crate::solvers::SolveOutcome; use crate::solvers::{BruteForce, ILPSolver}; use crate::traits::Problem; use crate::types::Or; @@ -63,7 +64,14 @@ fn test_smc_to_ilp_bf_vs_ilp() { let ilp_solution = ilp_solver .solve(reduction.target_problem()) .expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(problem.evaluate(&extracted).unwrap(), Or(true)); } diff --git a/src/unit_tests/rules/spinglass_maxcut.rs b/src/unit_tests/rules/spinglass_maxcut.rs index 2acf06eba..1c51b2876 100644 --- a/src/unit_tests/rules/spinglass_maxcut.rs +++ b/src/unit_tests/rules/spinglass_maxcut.rs @@ -1,4 +1,5 @@ use super::*; +use crate::solvers::SolveOutcome; include!("../jl_helpers.rs"); use crate::rules::test_helpers::assert_optimization_round_trip_from_optimization_target; use crate::solvers::BruteForce; @@ -35,7 +36,14 @@ fn test_solution_extraction_no_ancilla() { ReduceTo::>::reduce_to(&sg).expect("reduction should succeed"); let mc_sol = vec![false, true]; - let extracted = reduction.extract_solution(&mc_sol).unwrap(); + let extracted = reduction + .recover_result( + &sg, + SolveOutcome::optimal(reduction.target_problem(), mc_sol.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted, vec![-1, 1]); } @@ -47,12 +55,26 @@ fn test_solution_extraction_with_ancilla() { // A false ancilla represents spin -1, so flip to normalize it to +1. let mc_sol = vec![false, true, false]; - let extracted = reduction.extract_solution(&mc_sol).unwrap(); + let extracted = reduction + .recover_result( + &sg, + SolveOutcome::optimal(reduction.target_problem(), mc_sol.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted, vec![1, -1]); // A true ancilla already represents spin +1. let mc_sol = vec![false, true, true]; - let extracted = reduction.extract_solution(&mc_sol).unwrap(); + let extracted = reduction + .recover_result( + &sg, + SolveOutcome::optimal(reduction.target_problem(), mc_sol.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted, vec![-1, 1]); } diff --git a/src/unit_tests/rules/spinglass_qubo.rs b/src/unit_tests/rules/spinglass_qubo.rs index 4d1443b1d..526de061b 100644 --- a/src/unit_tests/rules/spinglass_qubo.rs +++ b/src/unit_tests/rules/spinglass_qubo.rs @@ -1,4 +1,5 @@ use super::*; +use crate::solvers::SolveOutcome; include!("../jl_helpers.rs"); use crate::rules::test_helpers::assert_optimization_round_trip_from_optimization_target; use crate::solvers::BruteForce; @@ -226,7 +227,15 @@ fn test_qubo_to_spinglass_preserves_small_nonzero_coefficients() { for left in [-1, 1] { for right in [-1, 1] { let spins = vec![left, right]; - let bits = reduction.extract_solution(&spins).unwrap(); + let bits = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), spins.clone()) + .unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let source_value = source.evaluate(&bits).unwrap().0.unwrap(); let target_value = target.evaluate(&spins).unwrap().0.unwrap(); assert_eq!(source_value, target_value + offset); diff --git a/src/unit_tests/rules/stackercrane_ilp.rs b/src/unit_tests/rules/stackercrane_ilp.rs index 849e3339c..8925af865 100644 --- a/src/unit_tests/rules/stackercrane_ilp.rs +++ b/src/unit_tests/rules/stackercrane_ilp.rs @@ -2,6 +2,7 @@ use super::*; use crate::models::algebraic::ILP; use crate::rules::test_helpers::assert_bf_vs_ilp; use crate::rules::ReduceTo; +use crate::solvers::SolveOutcome; #[test] fn test_stackercrane_to_ilp_closed_loop() { @@ -60,7 +61,15 @@ fn test_stackercrane_to_ilp_all_binary_assignments() { for bits in 0..(1 << 12) { let solution = (0..12).map(|i| i64::from(bits & (1 << i) != 0)).collect(); if let Some(cost) = target.evaluate(&solution).unwrap().value { - let permutation = reduction.extract_solution(&solution).unwrap(); + let permutation = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), solution.clone()) + .unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!( source.evaluate(&permutation).unwrap(), crate::types::Min(Some(cost + 1)) diff --git a/src/unit_tests/rules/steinertree_ilp.rs b/src/unit_tests/rules/steinertree_ilp.rs index ad07a1f90..58b7df86c 100644 --- a/src/unit_tests/rules/steinertree_ilp.rs +++ b/src/unit_tests/rules/steinertree_ilp.rs @@ -1,5 +1,8 @@ use super::*; +use crate::rules::ReductionResult; use crate::solvers::ILPSolver; +use crate::solvers::SolveOutcome; +use crate::traits::EvaluationError::InvalidConfiguration; use crate::traits::Problem; use crate::types::Min; @@ -73,7 +76,14 @@ fn test_steinertree_to_ilp_closed_loop() { let source = SteinerTree::new(SimpleGraph::new(n, edges), weights, terminals); let reduction = ReduceTo::>::reduce_to(&source).unwrap(); let witness = ILPSolver::new().solve(reduction.target_problem()).unwrap(); - let decoded = reduction.extract_solution(&witness).unwrap(); + let decoded = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), witness.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(source.evaluate(&decoded).unwrap(), Min(Some(optimum))); assert_eq!( reduction.target_problem().evaluate(&witness).unwrap().value, @@ -107,7 +117,17 @@ fn test_steiner_all_source_trees_lift_and_preserve_objective() { target.evaluate(&witness).unwrap().value, source.evaluate(&selected).unwrap().0 ); - assert_eq!(reduction.extract_solution(&witness).unwrap(), selected); + assert_eq!( + reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), witness.clone()).unwrap() + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"), + selected + ); } } } @@ -122,23 +142,27 @@ fn test_steiner_every_small_raw_target_and_malformed_witness() { let witness: Vec<_> = (0..target.num_vars()).map(|v| (mask >> v) & 1).collect(); if target.evaluate(&witness).unwrap().is_valid() { feasible_count += 1; - let decoded = reduction.extract_solution(&witness).unwrap(); + let decoded = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), witness.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(source.evaluate(&decoded).unwrap(), Min(Some(-3))); - } else { - assert!( - !matches!(crate::traits::Problem::evaluate(reduction.target_problem(), &witness), Ok(value) if value.is_valid()) - ); } } - for bad in [ - vec![], - vec![1; target.num_vars() + 1], - vec![2; target.num_vars()], - ] { - assert!( - !matches!(crate::traits::Problem::evaluate(reduction.target_problem(), &bad), Ok(value) if value.is_valid()) - ); + for bad in [vec![], vec![1; target.num_vars() + 1]] { + assert!(matches!( + ReductionResult::target_problem(&reduction).evaluate(&bad), + Err(InvalidConfiguration(_)) + )); } + assert!(!ReductionResult::target_problem(&reduction) + .evaluate(&vec![2; target.num_vars()]) + .unwrap() + .is_valid()); assert_eq!(feasible_count, 1); } @@ -170,6 +194,16 @@ fn test_single_terminal_tree_lifts_include_empty_tree() { reduction.target_problem().evaluate(&witness).unwrap().value, source.evaluate(&selected).unwrap().0 ); - assert_eq!(reduction.extract_solution(&witness).unwrap(), selected); + assert_eq!( + reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), witness.clone()).unwrap() + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"), + selected + ); } } diff --git a/src/unit_tests/rules/stringtostringcorrection_ilp.rs b/src/unit_tests/rules/stringtostringcorrection_ilp.rs index 9132a7132..d32edca59 100644 --- a/src/unit_tests/rules/stringtostringcorrection_ilp.rs +++ b/src/unit_tests/rules/stringtostringcorrection_ilp.rs @@ -1,5 +1,6 @@ use super::*; use crate::models::algebraic::{ObjectiveSense, ILP}; +use crate::solvers::SolveOutcome; use crate::solvers::{BruteForce, ILPSolver}; use crate::traits::Problem; use crate::types::Or; @@ -32,7 +33,14 @@ fn test_stringtostringcorrection_to_ilp_bf_vs_ilp() { let ilp_solution = ilp_solver .solve(reduction.target_problem()) .expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(problem.evaluate(&extracted).unwrap(), Or(true)); } @@ -46,7 +54,14 @@ fn test_solution_extraction_delete() { let ilp_solution = ilp_solver .solve(reduction.target_problem()) .expect("solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted.len(), 1); assert_eq!(problem.evaluate(&extracted).unwrap(), Or(true)); } @@ -87,6 +102,13 @@ fn test_stringtostringcorrection_to_ilp_swap() { let ilp_solution = ilp_solver .solve(reduction.target_problem()) .expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(problem.evaluate(&extracted).unwrap(), Or(true)); } diff --git a/src/unit_tests/rules/strongconnectivityaugmentation_ilp.rs b/src/unit_tests/rules/strongconnectivityaugmentation_ilp.rs index 7d1b060dc..bda1ec085 100644 --- a/src/unit_tests/rules/strongconnectivityaugmentation_ilp.rs +++ b/src/unit_tests/rules/strongconnectivityaugmentation_ilp.rs @@ -2,6 +2,7 @@ use super::*; use crate::models::algebraic::ILP; use crate::models::graph::StrongConnectivityAugmentation; use crate::rules::ReduceTo; +use crate::solvers::SolveOutcome; use crate::solvers::{BruteForce, ILPSolver}; use crate::topology::DirectedGraph; use crate::traits::Problem; @@ -30,7 +31,14 @@ fn test_strongconnectivityaugmentation_to_ilp_closed_loop() { // Solve ILP let ilp_solver = ILPSolver::new(); let ilp_sol = ilp_solver.solve(ilp).expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_sol).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), ilp_sol.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!( source.evaluate(&extracted).unwrap().0, @@ -46,7 +54,14 @@ fn test_extract_solution() { let ilp = reduction.target_problem(); let solver = ILPSolver::new(); let ilp_sol = solver.solve(ilp).expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_sol).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), ilp_sol.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted.len(), 2); assert!(source.evaluate(&extracted).unwrap().0); } @@ -59,7 +74,14 @@ fn test_trivial_single_vertex() { let ilp = reduction.target_problem(); let solver = ILPSolver::new(); let ilp_sol = solver.solve(ilp).expect("trivial should be solvable"); - let extracted = reduction.extract_solution(&ilp_sol).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), ilp_sol.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!(source.evaluate(&extracted).unwrap().0); } diff --git a/src/unit_tests/rules/subgraphisomorphism_ilp.rs b/src/unit_tests/rules/subgraphisomorphism_ilp.rs index cfb73ae65..2a7f60929 100644 --- a/src/unit_tests/rules/subgraphisomorphism_ilp.rs +++ b/src/unit_tests/rules/subgraphisomorphism_ilp.rs @@ -1,4 +1,5 @@ use super::*; +use crate::solvers::SolveOutcome; use crate::solvers::{BruteForce, ILPSolver}; use crate::topology::SimpleGraph; use crate::traits::Problem; @@ -40,7 +41,14 @@ fn test_subgraphisomorphism_to_ilp_closed_loop() { let ilp_solution = ilp_solver .solve(reduction.target_problem()) .expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!( problem.evaluate(&extracted).unwrap(), Or(true), @@ -70,7 +78,14 @@ fn test_subgraphisomorphism_to_ilp_path_in_cycle() { let ilp_solution = ilp_solver .solve(reduction.target_problem()) .expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(problem.evaluate(&extracted).unwrap(), Or(true)); } @@ -102,7 +117,14 @@ fn test_solution_extraction() { let ilp_solution = ilp_solver .solve(reduction.target_problem()) .expect("solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(problem.evaluate(&extracted).unwrap(), Or(true)); } diff --git a/src/unit_tests/rules/subsetsum_closestvectorproblem.rs b/src/unit_tests/rules/subsetsum_closestvectorproblem.rs index d60b64e14..9106df612 100644 --- a/src/unit_tests/rules/subsetsum_closestvectorproblem.rs +++ b/src/unit_tests/rules/subsetsum_closestvectorproblem.rs @@ -1,20 +1,34 @@ use super::*; use crate::models::algebraic::ClosestVectorProblem; +use crate::models::decision::Decision; +use crate::rules::ReductionResult; +use crate::solvers::SolveOutcome; +use crate::traits::EvaluationError::InvalidConfiguration; use crate::traits::Problem; +use crate::types::OptimizationValue; #[test] fn test_subsetsum_to_closestvectorproblem_closed_loop() { let source = SubsetSum::new(vec![3u32, 7, 1, 8], 11u32); - let reduction = ReduceTo::>::reduce_to(&source).unwrap(); - let target_solution = - crate::solvers::customized::closest_vector_problem::solve(reduction.target_problem()) - .unwrap(); - let source_solution = reduction.extract_solution(&target_solution).unwrap(); + let reduction = ReduceTo::>>::reduce_to(&source).unwrap(); + let target_solution = crate::solvers::customized::closest_vector_problem::solve( + reduction.target_problem().inner(), + ) + .unwrap(); + let source_solution = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), target_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!(source.evaluate(&source_solution).unwrap().0); assert_eq!( reduction .target_problem() + .inner() .evaluate(&target_solution) .unwrap() .0, @@ -25,12 +39,12 @@ fn test_subsetsum_to_closestvectorproblem_closed_loop() { #[test] fn test_subsetsum_to_closestvectorproblem_structure() { let source = SubsetSum::new(vec![3u32, 7, 1, 8], 11u32); - let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + let reduction = ReduceTo::>>::reduce_to(&source).unwrap(); let target = reduction.target_problem(); - assert_eq!(target.num_basis_vectors(), 7); - assert_eq!(target.ambient_dimension(), 12); - assert_eq!(&target.target()[..8], &[0, 0, 0, 0, 1, 1, 1, 1]); + assert_eq!(target.inner().num_basis_vectors(), 7); + assert_eq!(target.inner().ambient_dimension(), 12); + assert_eq!(&target.inner().target()[..8], &[0, 0, 0, 0, 1, 1, 1, 1]); assert_eq!( ClosestVectorProblem::::variant(), vec![("target", "i64")] @@ -40,17 +54,27 @@ fn test_subsetsum_to_closestvectorproblem_structure() { #[test] fn test_subsetsum_to_closestvectorproblem_binary_minimizers() { let source = SubsetSum::new(vec![3u32, 7, 1, 8], 11u32); - let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + let reduction = ReduceTo::>>::reduce_to(&source).unwrap(); let target = reduction.target_problem(); for solution in [vec![1, 0, 0, 1, 0, 0, 0], vec![1, 1, 1, 0, 1, 1, 1]] { assert_eq!( - target.evaluate(&solution).unwrap().0, + target.inner().evaluate(&solution).unwrap().0, Some(BigRational::from_integer(4.into())) ); assert!( source - .evaluate(&reduction.extract_solution(&solution).unwrap()) + .evaluate( + &reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), solution.clone()) + .unwrap() + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution") + ) .unwrap() .0 ); @@ -60,13 +84,15 @@ fn test_subsetsum_to_closestvectorproblem_binary_minimizers() { #[test] fn test_subsetsum_to_closestvectorproblem_unsatisfiable_instance() { let source = SubsetSum::new(vec![2u32, 4, 6], 5u32); - let reduction = ReduceTo::>::reduce_to(&source).unwrap(); - let solution = - crate::solvers::customized::closest_vector_problem::solve(reduction.target_problem()) - .unwrap(); + let reduction = ReduceTo::>>::reduce_to(&source).unwrap(); + let solution = crate::solvers::customized::closest_vector_problem::solve( + reduction.target_problem().inner(), + ) + .unwrap(); assert!( reduction .target_problem() + .inner() .evaluate(&solution) .unwrap() .unwrap() @@ -79,29 +105,49 @@ fn test_subsetsum_to_closestvectorproblem_binary_carries_preserve_large_inputs() use num_bigint::BigUint; let size = BigUint::from(1u32) << 70usize; let source = SubsetSum::new(vec![size.clone()], size); - let result = ReduceTo::>::reduce_to(&source).unwrap(); - let mut witness = vec![0; result.target_problem().num_basis_vectors()]; + let result = ReduceTo::>>::reduce_to(&source).unwrap(); + let mut witness = vec![0; result.target_problem().inner().num_basis_vectors()]; witness[0] = 1; assert_eq!( - result.target_problem().evaluate(&witness).unwrap(), - Min(Some(BigRational::from_integer(1.into()))) + result.target_problem().inner().evaluate(&witness).unwrap(), + crate::types::Min(Some(BigRational::from_integer(1.into()))) + ); + assert_eq!( + result + .recover_result( + &source, + SolveOutcome::optimal(result.target_problem(), witness.clone()).unwrap() + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"), + vec![true] ); - assert_eq!(result.extract_solution(&witness).unwrap(), vec![true]); assert!(result .target_problem() + .inner() .basis() .iter() .flatten() .all(|&x| (-2..=1).contains(&x))); let source = SubsetSum::new(vec![1u32; 40], 20u32); - let result = ReduceTo::>::reduce_to(&source).unwrap(); - let mut witness = vec![0; result.target_problem().num_basis_vectors()]; + let result = ReduceTo::>>::reduce_to(&source).unwrap(); + let mut witness = vec![0; result.target_problem().inner().num_basis_vectors()]; witness[..20].fill(1); witness[40..].copy_from_slice(&[1, 2, 5, 10]); assert!( source - .evaluate(&result.extract_solution(&witness).unwrap()) + .evaluate( + &result + .recover_result( + &source, + SolveOutcome::optimal(result.target_problem(), witness.clone()).unwrap() + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution") + ) .unwrap() .0 ); @@ -118,13 +164,13 @@ fn test_subsetsum_to_closestvectorproblem_all_small_coefficients() { (vec![2, 4], 5), ] { let source = SubsetSum::new(sizes, target_sum); - let result = ReduceTo::>::reduce_to(&source).unwrap(); + let result = ReduceTo::>>::reduce_to(&source).unwrap(); let target = result.target_problem(); assert!(std::ptr::eq( target, - crate::rules::AggregateReductionResult::target_problem(&result) + crate::rules::ReductionResult::target_problem(&result) )); - let dimensions = target.num_basis_vectors(); + let dimensions = target.inner().num_basis_vectors(); let mut accepted = false; for index in 0..4usize.pow(dimensions as u32) { let mut index = index; @@ -135,17 +181,27 @@ fn test_subsetsum_to_closestvectorproblem_all_small_coefficients() { x }) .collect(); - let value = target.evaluate(&config).unwrap(); + let value = target.inner().evaluate(&config).unwrap(); let certificate = value - == Min(Some(BigRational::from_integer( + == crate::types::Min(Some(BigRational::from_integer( source.num_elements().into(), ))); assert_eq!( - crate::rules::AggregateReductionResult::extract_value(&result, value), - Or(certificate) + crate::types::Or(OptimizationValue::meets_bound( + &(value), + crate::rules::ReductionResult::target_problem(&result).bound() + )), + crate::types::Or(certificate) ); if certificate { - let x = result.extract_solution(&config).unwrap(); + let x = result + .recover_result( + &source, + SolveOutcome::optimal(result.target_problem(), config.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!(source.evaluate(&x).unwrap().0); accepted = true; } @@ -157,12 +213,18 @@ fn test_subsetsum_to_closestvectorproblem_all_small_coefficients() { .unwrap() .is_some() ); - assert!( - !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&result), &vec![0; dimensions + 1]), Ok(value) if { let value = crate::rules::AggregateReductionResult::extract_value(&result, value.clone()); value.is_valid() }) - ); + assert!(matches!( + ReductionResult::target_problem(&result) + .inner() + .evaluate(&vec![0; dimensions + 1]), + Err(InvalidConfiguration(_)) + )); assert_eq!( - crate::rules::AggregateReductionResult::extract_value(&result, Min(None)), - Or(false) + crate::types::Or(OptimizationValue::meets_bound( + &(crate::types::Min(None)), + crate::rules::ReductionResult::target_problem(&result).bound() + )), + crate::types::Or(false) ); } } diff --git a/src/unit_tests/rules/subsetsum_integerexpressionmembership.rs b/src/unit_tests/rules/subsetsum_integerexpressionmembership.rs index fbb1b4363..6078cce0b 100644 --- a/src/unit_tests/rules/subsetsum_integerexpressionmembership.rs +++ b/src/unit_tests/rules/subsetsum_integerexpressionmembership.rs @@ -6,7 +6,7 @@ use crate::rules::test_helpers::assert_satisfaction_round_trip_from_satisfaction use crate::rules::traits::ReductionResult; use crate::rules::ReduceTo; use crate::solvers::BruteForce; -#[cfg(feature = "example-db")] +use crate::solvers::SolveOutcome; use crate::traits::Problem; fn issue_example_source() -> SubsetSum { @@ -48,14 +48,24 @@ fn test_subsetsum_to_integerexpressionmembership_extract_solution_matches_choice assert_eq!( reduction - .extract_solution(&issue_example_target_config()) - .unwrap(), + .recover_result( + &source, + SolveOutcome::optimal( + reduction.target_problem(), + issue_example_target_config().clone() + ) + .unwrap() + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"), issue_example_source_config() ); // Selecting 1 and 8 does not reach the source target 11. - assert!( - !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![true, false, false, true]), Ok(value) if { value.is_valid() }) - ); + assert!(!ReductionResult::target_problem(&reduction) + .evaluate(&vec![true, false, false, true]) + .unwrap() + .is_valid()); } #[test] diff --git a/src/unit_tests/rules/subsetsum_partition.rs b/src/unit_tests/rules/subsetsum_partition.rs index 663dbd54e..0b173f0e4 100644 --- a/src/unit_tests/rules/subsetsum_partition.rs +++ b/src/unit_tests/rules/subsetsum_partition.rs @@ -5,6 +5,7 @@ use crate::rules::test_helpers::assert_satisfaction_round_trip_from_satisfaction use crate::rules::traits::ReductionResult; use crate::rules::ReduceTo; use crate::solvers::BruteForce; +use crate::solvers::SolveOutcome; #[cfg(feature = "example-db")] use crate::traits::Problem; @@ -32,14 +33,32 @@ fn test_subsetsum_to_partition_sigma_greater_than_two_t_extraction() { assert_eq!(reduction.target_problem().sizes(), &[10, 20, 30, 40]); assert_eq!( reduction - .extract_solution(&vec![true, false, false, true]) - .unwrap(), + .recover_result( + &source, + SolveOutcome::optimal( + reduction.target_problem(), + vec![true, false, false, true].clone() + ) + .unwrap() + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"), vec![true, false, false] ); assert_eq!( reduction - .extract_solution(&vec![false, true, true, false]) - .unwrap(), + .recover_result( + &source, + SolveOutcome::optimal( + reduction.target_problem(), + vec![false, true, true, false].clone() + ) + .unwrap() + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"), vec![true, false, false] ); } @@ -52,8 +71,17 @@ fn test_subsetsum_to_partition_sigma_equals_two_t_extraction() { assert_eq!(reduction.target_problem().sizes(), &[3, 5, 2, 6]); assert_eq!( reduction - .extract_solution(&vec![true, true, false, false]) - .unwrap(), + .recover_result( + &source, + SolveOutcome::optimal( + reduction.target_problem(), + vec![true, true, false, false].clone() + ) + .unwrap() + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"), vec![true, true, false, false] ); } diff --git a/src/unit_tests/rules/sumofsquarespartition_ilp.rs b/src/unit_tests/rules/sumofsquarespartition_ilp.rs index 862423737..9a4cae80e 100644 --- a/src/unit_tests/rules/sumofsquarespartition_ilp.rs +++ b/src/unit_tests/rules/sumofsquarespartition_ilp.rs @@ -1,4 +1,5 @@ use super::*; +use crate::solvers::SolveOutcome; use crate::solvers::{BruteForce, ILPSolver}; use crate::traits::Problem; use crate::types::Min; @@ -41,7 +42,14 @@ fn test_sumofsquarespartition_to_ilp_bf_vs_ilp() { ReduceTo::>::reduce_to(&problem).expect("reduction should succeed"); let ilp = reduction.target_problem(); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be feasible"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let ilp_value = problem.evaluate(&extracted).unwrap(); assert_eq!( ilp_value, bf_value, @@ -71,7 +79,14 @@ fn test_solution_extraction() { } } } - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted, vec![0, 1, 1, 0]); } @@ -88,7 +103,14 @@ fn test_sumofsquarespartition_to_ilp_trivial() { let ilp_solver = ILPSolver::new(); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be feasible"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let value = problem.evaluate(&extracted).unwrap(); // Optimal: {1},{2} -> 1+4=5 assert_eq!(value, Min(Some(5))); @@ -102,7 +124,14 @@ fn test_sumofsquarespartition_to_ilp_requires_exact_mip_optimality() { let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should prove the exact optimum"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(problem.evaluate(&extracted).unwrap(), Min(Some(74129))); } diff --git a/src/unit_tests/rules/threedimensionalmatching_ilp.rs b/src/unit_tests/rules/threedimensionalmatching_ilp.rs index 30eebc232..8b3d5da9e 100644 --- a/src/unit_tests/rules/threedimensionalmatching_ilp.rs +++ b/src/unit_tests/rules/threedimensionalmatching_ilp.rs @@ -3,6 +3,7 @@ use crate::models::algebraic::{Comparison, ObjectiveSense, ILP}; use crate::models::misc::{ResourceConstrainedScheduling, ThreePartition}; use crate::models::set::ThreeDimensionalMatching; use crate::rules::{ReduceTo, ReductionGraph, ReductionResult}; +use crate::solvers::SolveOutcome; use crate::solvers::{BruteForce, ILPSolver}; use crate::traits::Problem; use crate::types::Or; @@ -96,7 +97,14 @@ fn test_threedimensionalmatching_to_ilp_closed_loop() { let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("direct ILP should be feasible"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted, vec![true, true, true, false, false]); assert_eq!(problem.evaluate(&extracted).unwrap(), Or(true)); @@ -136,7 +144,14 @@ fn test_threedimensionalmatching_to_ilp_direct_path_beats_indirect_chain() { let direct_solution = solver .solve(direct.target_problem()) .expect("direct ILP should solve"); - let direct_source = direct.extract_solution(&direct_solution).unwrap(); + let direct_source = direct + .recover_result( + &problem, + SolveOutcome::optimal(direct.target_problem(), direct_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(problem.evaluate(&direct_source).unwrap(), Or(true)); assert!(direct.target_problem().num_vars() < indirect.target_problem().num_vars()); diff --git a/src/unit_tests/rules/threedimensionalmatching_minimumweightdecoding.rs b/src/unit_tests/rules/threedimensionalmatching_minimumweightdecoding.rs index 3ce6a03dd..92c4db023 100644 --- a/src/unit_tests/rules/threedimensionalmatching_minimumweightdecoding.rs +++ b/src/unit_tests/rules/threedimensionalmatching_minimumweightdecoding.rs @@ -2,7 +2,10 @@ use super::*; use crate::models::algebraic::MinimumWeightDecoding; use crate::models::set::ThreeDimensionalMatching; use crate::rules::test_helpers::assert_satisfaction_round_trip_from_optimization_target; +use crate::rules::ReductionResult; use crate::solvers::BruteForce; +use crate::solvers::SolveOutcome; +use crate::traits::EvaluationError::InvalidConfiguration; use crate::traits::Problem; use crate::types::Min; @@ -117,7 +120,14 @@ fn test_threedimensionalmatching_to_minimumweightdecoding_sentinel_q_zero() { for witness in &target_witnesses { // Sentinel codeword is the all-zero vector of length 1. assert_eq!(witness, &vec![false]); - let extracted = reduction.extract_solution(witness).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), (witness).clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); // Source has 0 triples → extracted vector has length 0. assert_eq!(extracted.len(), source.num_triples()); assert_eq!(extracted, Vec::::new()); @@ -139,7 +149,23 @@ fn test_threedimensionalmatching_to_minimumweightdecoding_sentinel_no_triples() let target_witnesses = solver.find_all_witnesses(target).unwrap(); assert!(!target_witnesses.is_empty()); for witness in &target_witnesses { - let extracted = reduction.extract_solution(witness).unwrap(); + let extracted = reduction.map_solution(witness).unwrap(); + assert_eq!( + reduction + .recover_result( + &source, + SolveOutcome::optimal(target, (witness).clone()).unwrap() + ) + .unwrap(), + SolveOutcome::Infeasible + ); + assert!(matches!( + reduction.recover_result( + &source, + SolveOutcome::feasible(target, (witness).clone()).unwrap() + ), + Err(crate::rules::ExtractionError::InsufficientSolutionQuality) + )); assert_eq!(extracted.len(), source.num_triples()); // Empty triple set cannot cover non-empty universe. assert!( @@ -167,7 +193,14 @@ fn test_threedimensionalmatching_to_minimumweightdecoding_solution_extraction_id assert!(!target_witnesses.is_empty()); for witness in &target_witnesses { - let extracted = reduction.extract_solution(witness).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), (witness).clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted, *witness); assert!( source_witnesses.contains(&extracted), @@ -175,7 +208,8 @@ fn test_threedimensionalmatching_to_minimumweightdecoding_solution_extraction_id ); } - assert!( - !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![false, true, false]), Ok(value) if { value.is_valid() }) - ); + assert!(matches!( + ReductionResult::target_problem(&reduction).evaluate(&vec![false, true, false]), + Err(InvalidConfiguration(_)) + )); } diff --git a/src/unit_tests/rules/threedimensionalmatching_threepartition.rs b/src/unit_tests/rules/threedimensionalmatching_threepartition.rs index f4379897f..99902e069 100644 --- a/src/unit_tests/rules/threedimensionalmatching_threepartition.rs +++ b/src/unit_tests/rules/threedimensionalmatching_threepartition.rs @@ -2,7 +2,10 @@ use super::*; use crate::models::misc::ThreePartition; use crate::models::set::ThreeDimensionalMatching; use crate::rules::ReduceTo; +use crate::rules::ReductionResult; use crate::solvers::BruteForce; +use crate::solvers::SolveOutcome; +use crate::traits::EvaluationError::InvalidConfiguration; use crate::traits::Problem; fn reduce( @@ -64,7 +67,14 @@ fn test_threedimensionalmatching_to_threepartition_extracts_manual_q1_witness() .0 ); - let extracted = reduction.extract_solution(&target_config).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), target_config.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted, vec![true]); assert!(source.evaluate(&extracted).unwrap().0); } @@ -81,7 +91,14 @@ fn test_threedimensionalmatching_to_threepartition_closed_loop_from_known_matchi .unwrap() .0 ); - let extracted = reduction.extract_solution(&target_solution).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), target_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted, vec![true]); assert!(source.evaluate(&extracted).unwrap().0); } @@ -99,7 +116,14 @@ fn test_threedimensionalmatching_to_threepartition_round_trip_q2_minimal_matchin .0 ); - let extracted = reduction.extract_solution(&target_solution).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), target_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted, vec![true, true]); assert!(source.evaluate(&extracted).unwrap().0); } @@ -134,12 +158,29 @@ fn test_threedimensionalmatching_to_threepartition_extracts_noncanonical_partiti 4, 0, 0, 4, 2, 1, 3, 3, 6, 0, 6, 4, 5, 5, 2, 1, 3, 1, 6, 2, 5, ]; assert!(reduction.target_problem().evaluate(&witness).unwrap().0); - let extracted = reduction.extract_solution(&witness).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), witness.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted, vec![true]); assert!(source.evaluate(&extracted).unwrap().0); // Group labels have no mathematical significance. - let relabeled = witness.iter().map(|group| 6 - group).collect(); - assert_eq!(reduction.extract_solution(&relabeled).unwrap(), extracted); + let relabeled: Vec<_> = witness.iter().map(|group| 6 - group).collect(); + assert_eq!( + reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), relabeled.clone()).unwrap() + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"), + extracted + ); } #[test] @@ -157,7 +198,15 @@ fn test_threedimensionalmatching_to_threepartition_equal_size_permutations() { { witness.swap(left, right); assert!(target.evaluate(&witness).unwrap().0); - let extracted = reduction.extract_solution(&witness).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), witness.clone()) + .unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!(source.evaluate(&extracted).unwrap().0); exchanges += 1; } @@ -171,20 +220,24 @@ fn test_threedimensionalmatching_to_threepartition_equal_size_permutations() { fn test_threedimensionalmatching_to_threepartition_rejects_invalid_partitions() { let (_, reduction) = reduce(1, &[(0, 0, 0)]); let valid = reduction.build_target_witness(&[1]); - assert!( - !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![]), Ok(value) if { value.is_valid() }) - ); + assert!(matches!( + ReductionResult::target_problem(&reduction).evaluate(&vec![]), + Err(InvalidConfiguration(_)) + )); let mut invalid = valid.clone(); invalid[0] = reduction.target_problem().num_groups(); - assert!( - !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &invalid), Ok(value) if { value.is_valid() }) - ); - assert!( - !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &vec![0; valid.len()]), Ok(value) if { value.is_valid() }) - ); + assert!(matches!( + ReductionResult::target_problem(&reduction).evaluate(&invalid), + Err(InvalidConfiguration(_)) + )); + assert!(!ReductionResult::target_problem(&reduction) + .evaluate(&vec![0; valid.len()]) + .unwrap() + .is_valid()); let mut wrong_sum = valid; wrong_sum.swap(0, 2); - assert!( - !matches!(crate::traits::Problem::evaluate(crate::rules::ReductionResult::target_problem(&reduction), &wrong_sum), Ok(value) if { value.is_valid() }) - ); + assert!(!ReductionResult::target_problem(&reduction) + .evaluate(&wrong_sum) + .unwrap() + .is_valid()); } diff --git a/src/unit_tests/rules/threepartition_resourceconstrainedscheduling.rs b/src/unit_tests/rules/threepartition_resourceconstrainedscheduling.rs index 33bac8549..f17bfcb27 100644 --- a/src/unit_tests/rules/threepartition_resourceconstrainedscheduling.rs +++ b/src/unit_tests/rules/threepartition_resourceconstrainedscheduling.rs @@ -2,6 +2,7 @@ use super::*; use crate::models::misc::{ResourceConstrainedScheduling, ThreePartition}; use crate::rules::test_helpers::assert_satisfaction_round_trip_from_satisfaction_target; use crate::solvers::BruteForce; +use crate::solvers::SolveOutcome; use crate::traits::Problem; fn reduce_three_partition( @@ -65,7 +66,14 @@ fn test_threepartition_to_resourceconstrainedscheduling_solution_extraction() { let target_solutions = solver.find_all_witnesses(target).unwrap(); for sol in &target_solutions { - let extracted = reduction.extract_solution(sol).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), (sol).clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted.len(), source.num_elements()); let target_valid = target.evaluate(sol).unwrap(); let source_valid = source.evaluate(&extracted).unwrap(); diff --git a/src/unit_tests/rules/threepartition_sequencingwithreleasetimesanddeadlines.rs b/src/unit_tests/rules/threepartition_sequencingwithreleasetimesanddeadlines.rs index 015232d58..f130dc31b 100644 --- a/src/unit_tests/rules/threepartition_sequencingwithreleasetimesanddeadlines.rs +++ b/src/unit_tests/rules/threepartition_sequencingwithreleasetimesanddeadlines.rs @@ -2,6 +2,7 @@ use super::*; use crate::models::misc::{SequencingWithReleaseTimesAndDeadlines, ThreePartition}; use crate::rules::test_helpers::assert_satisfaction_round_trip_from_satisfaction_target; use crate::solvers::BruteForce; +use crate::solvers::SolveOutcome; use crate::traits::Problem; fn reduce(sizes: Vec, bound: i64) -> (ThreePartition, ReductionThreePartitionToSRTD) { @@ -74,7 +75,14 @@ fn test_threepartition_to_sequencingwithreleasetimesanddeadlines_solution_extrac let target_solutions = solver.find_all_witnesses(target).unwrap(); for sol in &target_solutions { - let extracted = reduction.extract_solution(sol).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), (sol).clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(extracted.len(), source.num_elements()); let source_valid = source.evaluate(&extracted).unwrap(); assert!( diff --git a/src/unit_tests/rules/timetabledesign_ilp.rs b/src/unit_tests/rules/timetabledesign_ilp.rs index eac518e1c..efdf77e6d 100644 --- a/src/unit_tests/rules/timetabledesign_ilp.rs +++ b/src/unit_tests/rules/timetabledesign_ilp.rs @@ -1,6 +1,7 @@ use super::*; use crate::models::algebraic::ILP; use crate::rules::test_helpers::assert_bf_vs_ilp; +use crate::solvers::SolveOutcome; use crate::solvers::{BruteForce, ILPSolver}; use crate::traits::Problem; use crate::types::Or; @@ -42,7 +43,14 @@ fn test_timetabledesign_to_ilp_bf_vs_ilp() { let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(problem.evaluate(&extracted).unwrap(), Or(true)); } @@ -73,7 +81,14 @@ fn test_timetabledesign_to_ilp_identity_extraction() { let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!( extracted diff --git a/src/unit_tests/rules/traits.rs b/src/unit_tests/rules/traits.rs index df2815fd0..464778530 100644 --- a/src/unit_tests/rules/traits.rs +++ b/src/unit_tests/rules/traits.rs @@ -1,7 +1,5 @@ -use crate::rules::traits::{ - AggregateReductionResult, DynAggregateReductionResult, ReduceTo, ReduceToAggregate, - ReductionResult, -}; +use crate::rules::traits::{DynReductionResult, ReduceTo, ReductionResult}; +use crate::solvers::{downcast_outcome, erase_outcome, SolveOutcome}; use crate::traits::Problem; use crate::types::Sum; use serde_json::json; @@ -75,10 +73,33 @@ impl ReductionResult for TestReduction { fn target_problem(&self) -> &TargetProblem { &self.target } - fn extract_solution( + fn recover_result( &self, - target_config: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { + source: &Self::Source, + target: crate::solvers::ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + crate::solvers::SolveOutcome::Infeasible => { + Ok(crate::solvers::SolveOutcome::Infeasible) + } + crate::solvers::SolveOutcome::Optimal { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(crate::solvers::SolveOutcome::optimal(source, solution)?) + } + crate::solvers::SolveOutcome::Feasible { solution, .. } => { + let solution = self.map_solution(&solution)?; + Ok(crate::solvers::SolveOutcome::feasible(source, solution)?) + } + } + } +} + +impl TestReduction { + fn map_solution( + &self, + target_config: &<::Target as Problem>::Solution, + ) -> crate::rules::ExtractionResult<<::Source as Problem>::Solution> + { Ok(target_config.to_vec()) } } @@ -102,7 +123,18 @@ fn test_reduction() { target.evaluate(&vec![1, 1]).unwrap(), crate::types::Max(Some(2)) ); - assert_eq!(result.extract_solution(&vec![1, 0]).unwrap(), vec![1, 0]); + assert_eq!( + result + .recover_result( + &source, + crate::solvers::SolveOutcome::optimal(result.target_problem(), vec![1, 0].clone()) + .unwrap() + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"), + vec![1, 0] + ); } #[test] @@ -126,15 +158,32 @@ fn aggregate_value_from_solution_keeps_evaluation_errors_distinct_from_false() { }) .unwrap(); let step = (edge.reduce_fn.unwrap())(&source).unwrap(); - let interpret = step.interpret_optimum.as_ref().unwrap(); - let value = interpret(&vec![true, false]).unwrap(); - assert!(!value); + let target = step + .witness + .target_result_from_json(SolveOutcome::Optimal { + solution: json!([true, false]), + evaluation: String::new(), + }) + .unwrap(); assert!(matches!( - interpret(&vec![true]), + step.witness.recover_result_dyn(&source, target).unwrap(), + SolveOutcome::Infeasible + )); + assert!(matches!( + step.witness.target_result_from_json(SolveOutcome::Optimal { + solution: json!([true]), + evaluation: String::new(), + }), Err(ExtractionError::Evaluation(_)) )); assert!(matches!( - interpret(&vec![1i64, 0]), + step.witness.recover_result_dyn( + &source, + erase_outcome(SolveOutcome::Optimal { + solution: vec![1i64, 0], + evaluation: crate::types::Min(Some(1i64)), + }) + ), Err(ExtractionError::InvalidTargetSolution(_)) )); } @@ -200,7 +249,7 @@ struct TestAggregateReduction { offset: u64, } -impl AggregateReductionResult for TestAggregateReduction { +impl ReductionResult for TestAggregateReduction { type Source = AggregateSourceProblem; type Target = AggregateTargetProblem; @@ -208,15 +257,29 @@ impl AggregateReductionResult for TestAggregateReduction { &self.target } - fn extract_value(&self, target_value: Sum) -> Sum { - Sum(target_value.0 + self.offset) + fn recover_result( + &self, + source: &Self::Source, + target: crate::solvers::ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + Ok(match target { + SolveOutcome::Optimal { mut solution, .. } => { + solution[0] += self.offset as usize; + SolveOutcome::optimal(source, solution)? + } + SolveOutcome::Feasible { mut solution, .. } => { + solution[0] += self.offset as usize; + SolveOutcome::feasible(source, solution)? + } + SolveOutcome::Infeasible => SolveOutcome::Infeasible, + }) } } -impl ReduceToAggregate for AggregateSourceProblem { +impl ReduceTo for AggregateSourceProblem { type Result = TestAggregateReduction; - fn reduce_to_aggregate(&self) -> Result { + fn reduce_to(&self) -> Result { Ok(TestAggregateReduction { target: AggregateTargetProblem, offset: 3, @@ -227,13 +290,21 @@ impl ReduceToAggregate for AggregateSourceProblem { #[test] fn test_aggregate_reduction_extracts_value() { let source = AggregateSourceProblem; - let result = - >::reduce_to_aggregate( - &source, - ) + let result = >::reduce_to(&source) .expect("reduction should succeed"); - assert_eq!(result.extract_value(Sum(7)), Sum(10)); + assert_eq!( + result + .recover_result( + &source, + SolveOutcome::optimal(result.target_problem(), vec![7]).unwrap() + ) + .unwrap(), + SolveOutcome::Optimal { + solution: vec![10], + evaluation: Sum(10) + } + ); } #[test] @@ -242,11 +313,26 @@ fn test_dyn_aggregate_reduction_result_extracts_value() { target: AggregateTargetProblem, offset: 2, }; - let dyn_result: &dyn DynAggregateReductionResult = &result; + let dyn_result: &dyn DynReductionResult = &result; assert!(dyn_result .target_problem_any() .downcast_ref::() .is_some()); - assert_eq!(dyn_result.extract_value_dyn(json!(7)), json!(9)); + let target = dyn_result + .target_result_from_json(SolveOutcome::Optimal { + solution: json!([7]), + evaluation: "Sum(7)".into(), + }) + .unwrap(); + let recovered = dyn_result + .recover_result_dyn(&AggregateSourceProblem, target) + .unwrap(); + assert_eq!( + downcast_outcome::, Sum>(recovered).unwrap(), + SolveOutcome::Optimal { + solution: vec![9], + evaluation: Sum(9) + } + ); } diff --git a/src/unit_tests/rules/travelingsalesman_ilp.rs b/src/unit_tests/rules/travelingsalesman_ilp.rs index 394a33dea..1faa38fe2 100644 --- a/src/unit_tests/rules/travelingsalesman_ilp.rs +++ b/src/unit_tests/rules/travelingsalesman_ilp.rs @@ -1,4 +1,5 @@ use super::*; +use crate::solvers::SolveOutcome; use crate::solvers::{BruteForce, ILPSolver}; use crate::topology::SimpleGraph; use crate::traits::Problem; @@ -40,7 +41,14 @@ fn test_reduction_c4_closed_loop() { let ilp_solver = ILPSolver::new(); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); // Verify extracted solution is valid on source problem let metric = problem.evaluate(&extracted).unwrap(); @@ -59,7 +67,14 @@ fn test_reduction_k4_weighted_closed_loop() { let ilp = reduction.target_problem(); let ilp_solver = ILPSolver::new(); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); // Solve via brute force for cross-check let bf = BruteForce::new(); @@ -87,7 +102,14 @@ fn test_reduction_c5_unweighted_closed_loop() { let ilp = reduction.target_problem(); let ilp_solver = ILPSolver::new(); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let metric = problem.evaluate(&extracted).unwrap(); assert!(metric.is_valid()); @@ -128,7 +150,14 @@ fn test_solution_extraction_structure() { let ilp_solver = ILPSolver::new(); let ilp_solution = ilp_solver.solve(ilp).expect("ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); // Should have one value per edge assert_eq!(extracted.len(), 4); diff --git a/src/unit_tests/rules/travelingsalesman_qubo.rs b/src/unit_tests/rules/travelingsalesman_qubo.rs index 25fc70a65..b0bf970da 100644 --- a/src/unit_tests/rules/travelingsalesman_qubo.rs +++ b/src/unit_tests/rules/travelingsalesman_qubo.rs @@ -1,6 +1,7 @@ use super::*; use crate::solvers::BruteForce; use crate::solvers::BruteForceProblem as _; +use crate::solvers::SolveOutcome; use crate::traits::Problem; use crate::types::Min; @@ -17,7 +18,14 @@ fn test_travelingsalesman_to_qubo_closed_loop() { // All QUBO solutions should extract to valid TSP solutions for sol in &qubo_solutions { - let extracted = reduction.extract_solution(sol).unwrap(); + let extracted = reduction + .recover_result( + &tsp, + SolveOutcome::optimal(reduction.target_problem(), (sol).clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let metric = tsp.evaluate(&extracted).unwrap(); assert!(metric.is_valid(), "Extracted solution should be valid"); // K3 has only one Hamiltonian cycle (all 3 edges), cost = 1+2+3 = 6 @@ -45,7 +53,14 @@ fn test_travelingsalesman_to_qubo_k4() { // Every Hamiltonian cycle in K4 uses exactly 4 edges, so cost = 4 for sol in &qubo_solutions { - let extracted = reduction.extract_solution(sol).unwrap(); + let extracted = reduction + .recover_result( + &tsp, + SolveOutcome::optimal(reduction.target_problem(), (sol).clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let metric = tsp.evaluate(&extracted).unwrap(); assert!(metric.is_valid(), "Extracted solution should be valid"); assert_eq!(metric, Min(Some(4))); @@ -132,38 +147,46 @@ fn signed_and_small_tours_recover_all_optima_or_infeasibility() { .unwrap(); assert!(!solutions.is_empty()); for solution in solutions { - let completed = crate::solvers::complete_reduction( - &source, - &chain, - &crate::solvers::SolveOutcome::Optimal { - solution: serde_json::to_value(&solution).unwrap(), - evaluation: String::new(), - }, - ) - .unwrap(); + let completed = chain + .recover_result_json( + &source, + SolveOutcome::Optimal { + solution: serde_json::to_value(&solution).unwrap(), + evaluation: String::new(), + }, + ) + .unwrap(); assert_eq!( - matches!(completed, crate::solvers::SolveOutcome::Optimal { .. }), + matches!(completed, SolveOutcome::Optimal { .. }), expected.is_some() ); assert_eq!( - crate::rules::AggregateReductionResult::extract_value( - &reduction, - reduction.target_problem().evaluate(&solution).unwrap() - ), + reduction.map_value(reduction.target_problem().evaluate(&solution).unwrap()), Min(expected) ); if expected.is_some() { assert_eq!( source - .evaluate(&reduction.extract_solution(&solution).unwrap()) + .evaluate( + &reduction + .recover_result( + &source, + SolveOutcome::optimal( + reduction.target_problem(), + solution.clone() + ) + .unwrap() + ) + .map(|result| result.into_solution().expect( + "qualifying target result must recover a source solution" + )) + .unwrap() + ) .unwrap(), Min(expected) ); } } - assert_eq!( - crate::rules::AggregateReductionResult::extract_value(&reduction, Min(None)), - Min(None) - ); + assert_eq!(reduction.map_value(Min(None)), Min(None)); } } diff --git a/src/unit_tests/rules/undirectedflowlowerbounds_ilp.rs b/src/unit_tests/rules/undirectedflowlowerbounds_ilp.rs index 98ee7436d..7a7fa516f 100644 --- a/src/unit_tests/rules/undirectedflowlowerbounds_ilp.rs +++ b/src/unit_tests/rules/undirectedflowlowerbounds_ilp.rs @@ -1,5 +1,6 @@ use super::*; use crate::models::algebraic::{ObjectiveSense, ILP}; +use crate::solvers::SolveOutcome; use crate::solvers::{BruteForce, ILPSolver}; use crate::topology::SimpleGraph; use crate::traits::Problem; @@ -61,7 +62,14 @@ fn test_undirectedflowlowerbounds_to_ilp_closed_loop() { let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be feasible"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); // extract_solution returns edge orientations z_e assert_eq!(extracted.len(), 2); @@ -92,7 +100,14 @@ fn test_undirectedflowlowerbounds_to_ilp_extract_solution() { // 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 let target_solution = vec![1, 0, 1, 0, 1, 1]; - let extracted = reduction.extract_solution(&target_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), target_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); // z_0=1, z_1=1 → extracted = [1-1, 1-1] = [0, 0] (both u→v = 0→1 and 1→2) assert_eq!(extracted, vec![false, false]); assert!( diff --git a/src/unit_tests/rules/undirectedtwocommodityintegralflow_ilp.rs b/src/unit_tests/rules/undirectedtwocommodityintegralflow_ilp.rs index 9fa8be746..43200e2f0 100644 --- a/src/unit_tests/rules/undirectedtwocommodityintegralflow_ilp.rs +++ b/src/unit_tests/rules/undirectedtwocommodityintegralflow_ilp.rs @@ -1,5 +1,6 @@ use super::*; use crate::models::algebraic::{ObjectiveSense, ILP}; +use crate::solvers::SolveOutcome; use crate::solvers::{BruteForce, ILPSolver}; use crate::topology::SimpleGraph; use crate::traits::Problem; @@ -103,7 +104,14 @@ fn test_undirectedtwocommodityintegralflow_to_ilp_closed_loop() { let ilp_solution = ILPSolver::new() .solve(reduction.target_problem()) .expect("ILP should be feasible"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!( problem.evaluate(&extracted).unwrap().0, @@ -161,7 +169,14 @@ fn test_undirectedtwocommodityintegralflow_to_ilp_extract_solution() { 0, 1, // d1_1=0, d2_1=1 1, 1, // d1_2=1, d2_2=1 ]; - let extracted = reduction.extract_solution(&target_solution).unwrap(); + let extracted = reduction + .recover_result( + &problem, + SolveOutcome::optimal(reduction.target_problem(), target_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); // extract_solution returns first 4*3=12 flow variables assert_eq!(extracted.len(), 12); assert!( diff --git a/src/unit_tests/solvers/registry.rs b/src/unit_tests/solvers/registry.rs index 734706e6c..528d67c5d 100644 --- a/src/unit_tests/solvers/registry.rs +++ b/src/unit_tests/solvers/registry.rs @@ -86,22 +86,42 @@ fn generic_decision_ilp_reports_no_but_preserves_extraction_errors() { self.0.inner() } - fn extract_solution(&self, _: &Vec) -> crate::rules::ExtractionResult> { - Err(ExtractionError::invalid("broken witness decoder")) + fn recover_result( + &self, + source: &Self::Source, + target: crate::solvers::ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + match target { + crate::solvers::SolveOutcome::Infeasible => { + Ok(crate::solvers::SolveOutcome::Infeasible) + } + crate::solvers::SolveOutcome::Optimal { + solution, + evaluation, + } => { + if !crate::types::OptimizationValue::meets_bound(&evaluation, source.bound()) { + return Ok(crate::solvers::SolveOutcome::Infeasible); + } + let solution = self.map_solution(&solution)?; + Ok(crate::solvers::SolveOutcome::optimal(source, solution)?) + } + crate::solvers::SolveOutcome::Feasible { + solution, + evaluation, + } => { + if !crate::types::OptimizationValue::meets_bound(&evaluation, source.bound()) { + return Err(ExtractionError::InsufficientSolutionQuality); + } + let solution = self.map_solution(&solution)?; + Ok(crate::solvers::SolveOutcome::feasible(source, solution)?) + } + } } } - impl crate::rules::AggregateReductionResult for BrokenExtractor { - type Source = Decision; - type Target = Inner; - fn target_problem(&self) -> &Inner { - self.0.inner() - } - fn extract_value(&self, value: crate::types::Min) -> crate::types::Or { - crate::types::Or(crate::types::OptimizationValue::meets_bound( - &value, - self.0.bound(), - )) + impl BrokenExtractor { + fn map_solution(&self, _: &Vec) -> crate::rules::ExtractionResult> { + Err(ExtractionError::invalid("broken witness decoder")) } } @@ -121,22 +141,7 @@ fn generic_decision_ilp_reports_no_but_preserves_extraction_errors() { pipeline.reducers[0] = |source| { let source = source.downcast_ref::>().unwrap(); let result = std::rc::Rc::new(BrokenExtractor(source.clone())); - Ok(crate::rules::registry::ExecutedStep { - aggregate: Some(result.clone()), - interpret_optimum: Some({ - let result = result.clone(); - std::rc::Rc::new(move |solution: &dyn std::any::Any| { - let solution = solution.downcast_ref::>().unwrap(); - let value = result.0.inner().evaluate(solution)?; - Ok(crate::rules::AggregateReductionResult::extract_value( - result.as_ref(), - value, - ) - .is_valid()) - }) - }), - witness: result, - }) + Ok(crate::rules::registry::ExecutedStep { witness: result }) }; let inner = Inner::new(SimpleGraph::new(2, vec![(0, 1)]), vec![1i64; 2]); assert!(matches!( @@ -604,10 +609,10 @@ fn solver_capability_registry_ambiguous_exact_edge_is_rejected() { #[test] fn native_terminal_dispatch_rejects_non_ilp_values() { - assert_eq!( + assert!(matches!( solve_ilp_terminal(&42_i64, &HighsAdapter::new(None)), Err(crate::solvers::ILPSolveError::UnsupportedProblemType) - ); + )); } #[test] diff --git a/src/unit_tests/solvers/resolver.rs b/src/unit_tests/solvers/resolver.rs index 5d10db5c3..f604f1b10 100644 --- a/src/unit_tests/solvers/resolver.rs +++ b/src/unit_tests/solvers/resolver.rs @@ -54,6 +54,9 @@ fn decision_reductions_check_target_optimum_before_extracting_witness() { ("Or(true)".into(), true) ); } + SolveOutcome::Feasible { .. } => { + panic!("exact solver returned only a feasible incumbent") + } SolveOutcome::Infeasible => assert!(!expected, "{name}, {backend:?}"), } } @@ -426,7 +429,7 @@ fn solve_outcome_has_disjoint_json_states() { }) ); assert_eq!( - serde_json::to_value(SolveOutcome::Infeasible).unwrap(), + serde_json::to_value(SolveOutcome::::Infeasible).unwrap(), serde_json::json!({"status": "infeasible"}) ); } @@ -610,3 +613,62 @@ fn unit_dominating_decision_ilp_matches_all_three_vertex_graphs() { } } } + +#[test] +fn decision_closest_vector_solver_preserves_bound_after_serialization() { + use crate::models::algebraic::ClosestVectorProblem; + use crate::models::decision::Decision; + use crate::models::misc::SubsetSum; + use crate::rules::{ReduceTo, ReductionResult}; + + for (weights, sum, expected) in [(vec![1u32, 2], 3u32, true), (vec![2, 4], 3, false)] { + let source = SubsetSum::new(weights, sum); + let reduction = + ReduceTo::>>::reduce_to(&source).unwrap(); + let target = reduction.target_problem(); + let loaded = load_dyn( + >>::NAME, + &BTreeMap::from([("target".into(), "i64".into())]), + serde_json::to_value(target).unwrap(), + ) + .unwrap(); + let result = solve(&loaded, SolverRequest::Default).unwrap(); + assert_eq!( + result.solver, + SolverExecution::Customized { + implementation: "cvp-sphere-enumeration" + } + ); + match result.outcome { + SolveOutcome::Feasible { .. } => { + panic!("exact solver returned only a feasible incumbent") + } + SolveOutcome::Infeasible => assert!(!expected), + SolveOutcome::Optimal { solution, .. } => { + assert!(expected); + let solution = serde_json::from_value(solution).unwrap(); + assert!(target.evaluate(&solution).unwrap().0); + assert!( + source + .evaluate( + &reduction + .recover_result( + &source, + SolveOutcome::optimal( + reduction.target_problem(), + solution.clone() + ) + .unwrap() + ) + .map(|result| result.into_solution().expect( + "qualifying target result must recover a source solution" + )) + .unwrap() + ) + .unwrap() + .0 + ); + } + } + } +} diff --git a/tests/suites/ksatisfiability_simultaneous_incongruences.rs b/tests/suites/ksatisfiability_simultaneous_incongruences.rs index 09cdbeb98..f1339b5ff 100644 --- a/tests/suites/ksatisfiability_simultaneous_incongruences.rs +++ b/tests/suites/ksatisfiability_simultaneous_incongruences.rs @@ -2,6 +2,7 @@ use problemreductions::models::algebraic::SimultaneousIncongruences; use problemreductions::models::formula::{CNFClause, KSatisfiability}; use problemreductions::rules::{ReduceTo, ReductionResult}; use problemreductions::solvers::BruteForce; +use problemreductions::solvers::SolveOutcome; use problemreductions::variant::K3; use problemreductions::Problem; @@ -27,7 +28,14 @@ fn test_ksatisfiability_to_simultaneous_incongruences_closed_loop() { .solve(target) .unwrap() .expect("target should be satisfiable"); - let extracted = reduction.extract_solution(&target_solution).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), target_solution).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!(source.evaluate(&extracted).unwrap()); } diff --git a/tests/suites/reductions.rs b/tests/suites/reductions.rs index c238b034b..25f075a89 100644 --- a/tests/suites/reductions.rs +++ b/tests/suites/reductions.rs @@ -9,6 +9,7 @@ use problemreductions::prelude::*; use problemreductions::rules::ReductionGraph; use problemreductions::solvers::BruteForceProblem as _; use problemreductions::solvers::ILPSolver; +use problemreductions::solvers::SolveOutcome; use problemreductions::topology::{Graph, SimpleGraph}; use problemreductions::types::{Min, Or}; use problemreductions::variant::{K2, K3}; @@ -39,7 +40,14 @@ mod is_vc_reductions { let vc_solutions = solver.find_all_witnesses(vc_problem).unwrap(); // Extract back to IS solution - let is_solution = result.extract_solution(&vc_solutions[0]).unwrap(); + let is_solution = result + .recover_result( + &is_problem, + SolveOutcome::optimal(result.target_problem(), vc_solutions[0].clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); // Solution should be valid for original problem assert!(is_problem.evaluate(&is_solution).unwrap().is_valid()); @@ -67,7 +75,14 @@ mod is_vc_reductions { let is_solutions = solver.find_all_witnesses(is_problem).unwrap(); // Extract back to VC solution - let vc_solution = result.extract_solution(&is_solutions[0]).unwrap(); + let vc_solution = result + .recover_result( + &vc_problem, + SolveOutcome::optimal(result.target_problem(), is_solutions[0].clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); // Solution should be valid for original problem assert!(vc_problem.evaluate(&vc_solution).unwrap().is_valid()); @@ -102,8 +117,22 @@ mod is_vc_reductions { let solutions = solver.find_all_witnesses(final_is).unwrap(); // Extract through the chain - let intermediate_sol = back_to_is.extract_solution(&solutions[0]).unwrap(); - let original_sol = to_vc.extract_solution(&intermediate_sol).unwrap(); + let intermediate_sol = back_to_is + .recover_result( + vc_problem, + SolveOutcome::optimal(back_to_is.target_problem(), solutions[0].clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); + let original_sol = to_vc + .recover_result( + &original, + SolveOutcome::optimal(to_vc.target_problem(), intermediate_sol.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); // Should be valid assert!(original.evaluate(&original_sol).unwrap().is_valid()); @@ -169,7 +198,14 @@ mod is_sp_reductions { let sp_solutions = solver.find_all_witnesses(sp_problem).unwrap(); // Extract to IS solution - let is_solution = result.extract_solution(&sp_solutions[0]).unwrap(); + let is_solution = result + .recover_result( + &is_problem, + SolveOutcome::optimal(result.target_problem(), sp_solutions[0].clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!(is_problem.evaluate(&is_solution).unwrap().is_valid()); } @@ -192,7 +228,14 @@ mod is_sp_reductions { let is_solutions = solver.find_all_witnesses(is_problem).unwrap(); // Extract to SP solution - let sp_solution = result.extract_solution(&is_solutions[0]).unwrap(); + let sp_solution = result + .recover_result( + &sp_problem, + SolveOutcome::optimal(result.target_problem(), is_solutions[0].clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); // All sets can be packed (disjoint) assert_eq!(sp_solution.iter().filter(|&&selected| selected).count(), 3); @@ -216,7 +259,14 @@ mod is_sp_reductions { let sp_solutions = solver.find_all_witnesses(sp_problem).unwrap(); // Extract to IS solution - let is_solution = to_sp.extract_solution(&sp_solutions[0]).unwrap(); + let is_solution = to_sp + .recover_result( + &original, + SolveOutcome::optimal(to_sp.target_problem(), sp_solutions[0].clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); // Valid for original assert!(original.evaluate(&is_solution).unwrap().is_valid()); @@ -253,7 +303,14 @@ mod sg_qubo_reductions { let qubo_solutions = solver.find_all_witnesses(qubo).unwrap(); // Extract to SG solution - let sg_solution = result.extract_solution(&qubo_solutions[0]).unwrap(); + let sg_solution = result + .recover_result( + &sg, + SolveOutcome::optimal(result.target_problem(), qubo_solutions[0].clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(sg_solution.len(), 2); } @@ -273,7 +330,14 @@ mod sg_qubo_reductions { let sg_solutions = solver.find_all_witnesses(sg).unwrap(); // Extract to QUBO solution - let qubo_solution = result.extract_solution(&sg_solutions[0]).unwrap(); + let qubo_solution = result + .recover_result( + &qubo, + SolveOutcome::optimal(result.target_problem(), sg_solutions[0].clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(qubo_solution.len(), 2); } @@ -297,7 +361,14 @@ mod sg_qubo_reductions { let qubo_solutions = solver.find_all_witnesses(qubo).unwrap(); // Extract QUBO solution back to SG - let extracted = result.extract_solution(&qubo_solutions[0]).unwrap(); + let extracted = result + .recover_result( + &sg, + SolveOutcome::optimal(result.target_problem(), qubo_solutions[0].clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); // Should be among optimal SG solutions (or equivalent) let sg_energy = sg.compute_energy(&sg_solutions[0]).unwrap(); @@ -325,7 +396,14 @@ mod minimum_covering_by_cliques_ilp_reductions { let ilp_solution = ILPSolver::new() .solve(ilp) .expect("MinimumCoveringByCliques -> ILP should be solvable"); - let extracted = reduction.extract_solution(&ilp_solution).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), ilp_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(source.evaluate(&extracted).unwrap(), Min(Some(3))); } @@ -339,15 +417,24 @@ mod partition_into_cliques_covering_by_cliques_reductions { fn test_partition_into_cliques_to_covering_by_cliques_closed_loop() { let source = PartitionIntoCliques::new(SimpleGraph::empty(1), 1); - let reduction = ReduceTo::>::reduce_to(&source) - .expect("reduction should succeed"); + let reduction = ReduceTo::< + problemreductions::models::decision::Decision>, + >::reduce_to(&source) + .expect("reduction should succeed"); let target = reduction.target_problem(); let target_solution = BruteForce::new() .solve(target) .unwrap() .expect("target should be solvable"); - let extracted = reduction.extract_solution(&target_solution).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), target_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(source.evaluate(&extracted).unwrap(), Or(true)); } @@ -355,12 +442,14 @@ mod partition_into_cliques_covering_by_cliques_reductions { #[test] fn test_partition_into_cliques_to_covering_by_cliques_orlin_issue_counts() { let source = PartitionIntoCliques::new(SimpleGraph::new(3, vec![(0, 1)]), 2); - let reduction = ReduceTo::>::reduce_to(&source) - .expect("reduction should succeed"); + let reduction = ReduceTo::< + problemreductions::models::decision::Decision>, + >::reduce_to(&source) + .expect("reduction should succeed"); let target = reduction.target_problem(); - assert_eq!(target.graph().num_vertices(), 14); - assert_eq!(target.graph().num_edges(), 53); + assert_eq!(target.inner().graph().num_vertices(), 14); + assert_eq!(target.inner().graph().num_edges(), 53); } } @@ -389,7 +478,15 @@ mod max2sat_maxcut_reductions { let solver = BruteForce::new(); let target_solutions = solver.find_all_witnesses(target).unwrap(); - let extracted = reduction.extract_solution(&target_solutions[0]).unwrap(); + let extracted = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), target_solutions[0].clone()) + .unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(source.evaluate(&extracted).unwrap(), Max(Some(5))); } @@ -421,7 +518,15 @@ mod sg_maxcut_reductions { let maxcut_solutions = solver.find_all_witnesses(maxcut).unwrap(); // Extract to SG solution - let sg_solution = result.extract_solution(&maxcut_solutions[0]).unwrap(); + let sg_solution = result + .recover_result( + &sg, + SolveOutcome::optimal(result.target_problem(), maxcut_solutions[0].clone()) + .unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(sg_solution.len(), 3); } @@ -444,7 +549,14 @@ mod sg_maxcut_reductions { let sg_solutions = solver.find_all_witnesses(sg).unwrap(); // Extract to MaxCut solution - let maxcut_solution = result.extract_solution(&sg_solutions[0]).unwrap(); + let maxcut_solution = result + .recover_result( + &maxcut, + SolveOutcome::optimal(result.target_problem(), sg_solutions[0].clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!(maxcut_solution.len(), 3); } @@ -469,7 +581,15 @@ mod sg_maxcut_reductions { let maxcut_solutions = solver.find_all_witnesses(maxcut).unwrap(); // Extract MaxCut solution back to SG - let extracted = result.extract_solution(&maxcut_solutions[0]).unwrap(); + let extracted = result + .recover_result( + &sg, + SolveOutcome::optimal(result.target_problem(), maxcut_solutions[0].clone()) + .unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); // Should have same energy as directly solved SG let direct_energy = sg.compute_energy(&sg_solutions[0]).unwrap(); @@ -578,13 +698,25 @@ mod qubo_reductions { // All QUBO optimal solutions should extract to valid IS solutions for sol in &solutions { - let extracted = chain.extract_solution(sol).unwrap(); + let extracted = chain + .recover_result::, QUBO>( + &is, + SolveOutcome::optimal(qubo, (sol).clone()).unwrap(), + ) + .map(|outcome| outcome.into_solution().unwrap()) + .unwrap(); assert!(is.evaluate(&extracted).unwrap().is_valid()); } // Optimal IS size should match ground truth let gt_is_size: usize = data.qubo_optimal.configs[0].iter().sum(); - let our_is_solution: Vec = chain.extract_solution(&solutions[0]).unwrap(); + let our_is_solution: Vec = chain + .recover_result::, QUBO>( + &is, + SolveOutcome::optimal(qubo, solutions[0].clone()).unwrap(), + ) + .map(|outcome| outcome.into_solution().unwrap()) + .unwrap(); let our_is_size = our_is_solution.iter().filter(|&&selected| selected).count(); assert_eq!(our_is_size, gt_is_size); } @@ -614,7 +746,9 @@ mod qubo_reductions { data.source.num_vertices, data.source.edges, )); - let reduction = ReduceTo::::reduce_to(&kc).expect("reduction should succeed"); + let reduction = + ReduceTo::>::reduce_to(&kc) + .expect("reduction should succeed"); let qubo = reduction.target_problem(); assert_eq!(qubo.num_variables().unwrap(), data.qubo_num_vars); @@ -623,7 +757,14 @@ mod qubo_reductions { let solutions = solver.find_all_witnesses(qubo).unwrap(); for sol in &solutions { - let extracted = reduction.extract_solution(sol).unwrap(); + let extracted = reduction + .recover_result( + &kc, + SolveOutcome::optimal(reduction.target_problem(), (sol).clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!(kc.evaluate(&extracted).unwrap()); } @@ -660,15 +801,27 @@ mod qubo_reductions { let solutions = solver.find_all_witnesses(qubo).unwrap(); for sol in &solutions { - let extracted = reduction.extract_solution(sol).unwrap(); + let extracted = reduction + .recover_result( + &sp, + SolveOutcome::optimal(reduction.target_problem(), (sol).clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!(sp.evaluate(&extracted).unwrap().is_valid()); } // Optimal packing should match ground truth let gt_selected: usize = data.qubo_optimal.configs[0].iter().sum(); let our_selected: usize = reduction - .extract_solution(&solutions[0]) + .recover_result( + &sp, + SolveOutcome::optimal(reduction.target_problem(), solutions[0].clone()).unwrap(), + ) .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution") .iter() .filter(|&&selected| selected) .count(); @@ -721,7 +874,9 @@ mod qubo_reductions { .collect(); let ksat = KSatisfiability::::new(data.source.num_variables, clauses); - let reduction = ReduceTo::::reduce_to(&ksat).expect("reduction should succeed"); + let reduction = + ReduceTo::>::reduce_to(&ksat) + .expect("reduction should succeed"); let qubo = reduction.target_problem(); assert_eq!(qubo.num_variables().unwrap(), data.qubo_num_vars); @@ -730,13 +885,27 @@ mod qubo_reductions { let solutions = solver.find_all_witnesses(qubo).unwrap(); for sol in &solutions { - let extracted = reduction.extract_solution(sol).unwrap(); + let extracted = reduction + .recover_result( + &ksat, + SolveOutcome::optimal(reduction.target_problem(), (sol).clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!(ksat.evaluate(&extracted).unwrap()); } // Verify extracted solution matches ground truth assignment let gt_config = &data.qubo_optimal.configs[0]; - let our_config = reduction.extract_solution(&solutions[0]).unwrap(); + let our_config = reduction + .recover_result( + &ksat, + SolveOutcome::optimal(reduction.target_problem(), solutions[0].clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!( our_config, gt_config @@ -818,13 +987,27 @@ mod qubo_reductions { let solutions = solver.find_all_witnesses(qubo).unwrap(); for sol in &solutions { - let extracted = reduction.extract_solution(sol).unwrap(); + let extracted = reduction + .recover_result( + &ilp, + SolveOutcome::optimal(reduction.target_problem(), (sol).clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert!(ilp.evaluate(&extracted).unwrap().is_valid()); } // Optimal assignment should match ground truth let gt_config = &data.qubo_optimal.configs[0]; - let our_config = reduction.extract_solution(&solutions[0]).unwrap(); + let our_config = reduction + .recover_result( + &ilp, + SolveOutcome::optimal(reduction.target_problem(), solutions[0].clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); assert_eq!( &our_config, >_config @@ -894,12 +1077,24 @@ mod qubo_reductions { // Extract back through the full chain to get VC solution for sol in &solutions { - let vc_sol = chain.extract_solution(sol).unwrap(); + let vc_sol = chain + .recover_result::, QUBO>( + &vc, + SolveOutcome::optimal(qubo, (sol).clone()).unwrap(), + ) + .map(|outcome| outcome.into_solution().unwrap()) + .unwrap(); assert!(vc.evaluate(&vc_sol).unwrap().is_valid()); } // Optimal VC size should match ground truth - let vc_sol: Vec = chain.extract_solution(&solutions[0]).unwrap(); + let vc_sol: Vec = chain + .recover_result::, QUBO>( + &vc, + SolveOutcome::optimal(qubo, solutions[0].clone()).unwrap(), + ) + .map(|outcome| outcome.into_solution().unwrap()) + .unwrap(); let gt_vc_size: usize = data.qubo_optimal.configs[0].iter().sum(); let our_vc_size = vc_sol.iter().filter(|&&selected| selected).count(); assert_eq!(our_vc_size, gt_vc_size); @@ -988,7 +1183,14 @@ mod end_to_end { .expect("reduction should succeed"); let vc = to_vc.target_problem(); let vc_solutions = solver.find_all_witnesses(vc).unwrap(); - let vc_extracted = to_vc.extract_solution(&vc_solutions[0]).unwrap(); + let vc_extracted = to_vc + .recover_result( + &is, + SolveOutcome::optimal(to_vc.target_problem(), vc_solutions[0].clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let via_vc_size = vc_extracted.iter().filter(|&&selected| selected).count(); // Reduce to MaximumSetPacking and solve @@ -996,7 +1198,14 @@ mod end_to_end { ReduceTo::>::reduce_to(&is).expect("reduction should succeed"); let sp = to_sp.target_problem(); let sp_solutions = solver.find_all_witnesses(sp).unwrap(); - let sp_extracted = to_sp.extract_solution(&sp_solutions[0]).unwrap(); + let sp_extracted = to_sp + .recover_result( + &is, + SolveOutcome::optimal(to_sp.target_problem(), sp_solutions[0].clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let via_sp_size = sp_extracted.iter().filter(|&&selected| selected).count(); // All should give same optimal size @@ -1025,7 +1234,15 @@ mod end_to_end { ReduceTo::>::reduce_to(&sg).expect("reduction should succeed"); let maxcut = to_maxcut.target_problem(); let maxcut_solutions = solver.find_all_witnesses(maxcut).unwrap(); - let maxcut_extracted = to_maxcut.extract_solution(&maxcut_solutions[0]).unwrap(); + let maxcut_extracted = to_maxcut + .recover_result( + &sg, + SolveOutcome::optimal(to_maxcut.target_problem(), maxcut_solutions[0].clone()) + .unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); let via_maxcut_energy = sg.compute_energy(&maxcut_extracted).unwrap(); @@ -1054,8 +1271,22 @@ mod end_to_end { let vc_solutions = solver.find_all_witnesses(vc).unwrap(); // Extract back through chain - let is_sol = is_to_vc.extract_solution(&vc_solutions[0]).unwrap(); - let sp_sol = sp_to_is.extract_solution(&is_sol).unwrap(); + let is_sol = is_to_vc + .recover_result( + is, + SolveOutcome::optimal(is_to_vc.target_problem(), vc_solutions[0].clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); + let sp_sol = sp_to_is + .recover_result( + &sp, + SolveOutcome::optimal(sp_to_is.target_problem(), is_sol.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); // Should be valid MaximumSetPacking assert!(sp.evaluate(&sp_sol).unwrap().is_valid()); diff --git a/tests/suites/register_assignment_reductions.rs b/tests/suites/register_assignment_reductions.rs index 04afcc570..307bbb770 100644 --- a/tests/suites/register_assignment_reductions.rs +++ b/tests/suites/register_assignment_reductions.rs @@ -4,6 +4,7 @@ use problemreductions::models::misc::FeasibleRegisterAssignment; use problemreductions::prelude::*; use problemreductions::rules::{ReductionGraph, ReductionPath}; use problemreductions::solvers::ILPSolver; +use problemreductions::solvers::SolveOutcome; use problemreductions::types::Or; use problemreductions::variant::K3; @@ -69,10 +70,22 @@ fn test_ksat_to_fra_structure_and_closed_loop_via_ilp() { let ilp_solution = ILPSolver::new() .solve(ilp) .expect("satisfiable FRA instance should reduce to a feasible ILP"); - let fra_solution = fra_chain.extract_solution(&ilp_solution).unwrap(); + let fra_solution = fra_chain + .recover_result::>( + fra, + SolveOutcome::optimal(ilp, ilp_solution).unwrap(), + ) + .map(|outcome| outcome.into_solution().unwrap()) + .unwrap(); assert_eq!(fra.evaluate(&fra_solution).unwrap(), Or(true)); - let sat_solution = ksat_chain.extract_solution(&fra_solution).unwrap(); + let sat_solution = ksat_chain + .recover_result::, FeasibleRegisterAssignment>( + &source, + SolveOutcome::optimal(fra, fra_solution).unwrap(), + ) + .map(|outcome| outcome.into_solution().unwrap()) + .unwrap(); assert_eq!(source.evaluate(&sat_solution).unwrap(), Or(true)); } From d9c671598f9c8862c18dcdda572dd993878d15e0 Mon Sep 17 00:00:00 2001 From: Xiwei Pan Date: Tue, 15 Sep 2026 00:47:53 +0800 Subject: [PATCH 06/42] refactor: remove unused metadata trait and narrow helper visibility --- src/models/graph/minimum_metric_dimension.rs | 2 +- src/registry/info.rs | 45 ++++++-------------- src/registry/mod.rs | 27 ++++-------- src/rules/ilp_helpers.rs | 12 +++--- 4 files changed, 28 insertions(+), 58 deletions(-) diff --git a/src/models/graph/minimum_metric_dimension.rs b/src/models/graph/minimum_metric_dimension.rs index c97b1d286..911bc2b24 100644 --- a/src/models/graph/minimum_metric_dimension.rs +++ b/src/models/graph/minimum_metric_dimension.rs @@ -32,7 +32,7 @@ inventory::submit! { /// /// Returns a vector where `dist[v]` is the shortest-path distance from /// `source` to `v`, or `usize::MAX` if `v` is unreachable. -pub fn bfs_distances(graph: &G, source: usize) -> Vec { +pub(crate) fn bfs_distances(graph: &G, source: usize) -> Vec { let n = graph.num_vertices(); let mut dist = vec![usize::MAX; n]; dist[source] = 0; diff --git a/src/registry/info.rs b/src/registry/info.rs index d39ca69c7..eb078537c 100644 --- a/src/registry/info.rs +++ b/src/registry/info.rs @@ -4,7 +4,9 @@ //! //! - [`ComplexityClass`] - Computational complexity (P, NP-complete, etc.) //! - [`ProblemInfo`] - Rich metadata about a problem type -//! - [`ProblemMetadata`] - Trait for problems to provide their metadata +//! +//! Registered models are queried through [`super::ProblemType`]; [`ProblemInfo`] +//! is a standalone description and does not register a model. //! //! # Example //! @@ -19,6 +21,15 @@ //! assert!(info.is_np_complete()); //! assert_eq!(info.all_names().len(), 3); //! ``` +//! +//! # Query a registered model +//! +//! ```rust +//! use problemreductions::registry::find_problem_type; +//! +//! let info = find_problem_type("MaximumIndependentSet").unwrap(); +//! assert_eq!(info.canonical_name, "MaximumIndependentSet"); +//! ``` use std::fmt; @@ -217,38 +228,6 @@ pub struct FieldInfo { pub description: &'static str, } -/// Trait for problems that provide static metadata. -/// -/// Implement this trait to enable introspection and discovery for problem types. -/// -/// # Example -/// -/// ```rust -/// use problemreductions::registry::{ -/// ProblemMetadata, ProblemInfo, ComplexityClass -/// }; -/// -/// struct MyProblem; -/// -/// impl ProblemMetadata for MyProblem { -/// fn problem_info() -> ProblemInfo { -/// ProblemInfo::new("My Problem", "Description") -/// .with_complexity(ComplexityClass::NpComplete) -/// } -/// } -/// -/// // Get problem metadata -/// let info = MyProblem::problem_info(); -/// assert_eq!(info.name, "My Problem"); -/// ``` -pub trait ProblemMetadata { - /// Returns the problem info for this problem type. - /// - /// This includes the problem name, description, aliases, complexity class, - /// and known reductions. - fn problem_info() -> ProblemInfo; -} - #[cfg(test)] #[path = "../unit_tests/registry/info.rs"] mod tests; diff --git a/src/registry/mod.rs b/src/registry/mod.rs index b76eb4199..0b0606f41 100644 --- a/src/registry/mod.rs +++ b/src/registry/mod.rs @@ -5,7 +5,7 @@ //! # Overview //! //! - [`ProblemInfo`] - Rich metadata (name, description, complexity, reductions) -//! - [`ProblemMetadata`] - Trait for problems to provide their own metadata +//! - [`ProblemType`] - Catalog metadata for registered models //! - [`ComplexityClass`] - Computational complexity classification //! //! # Example @@ -22,26 +22,17 @@ //! assert!(info.is_np_complete()); //! ``` //! -//! # Implementing for Custom Problems +//! # Querying Registered Problems //! -//! Problems can implement [`ProblemMetadata`] to provide introspection: +//! Models declare their catalog metadata through [`ProblemSchemaEntry`]. +//! Query those entries by name or alias: //! //! ```rust -//! use problemreductions::registry::{ -//! ProblemMetadata, ProblemInfo, ComplexityClass -//! }; +//! use problemreductions::registry::find_problem_type; //! -//! struct MyProblem; -//! -//! impl ProblemMetadata for MyProblem { -//! fn problem_info() -> ProblemInfo { -//! ProblemInfo::new("My Problem", "Description") -//! .with_complexity(ComplexityClass::NpComplete) -//! } -//! } -//! -//! let info = MyProblem::problem_info(); -//! println!("Problem: {}", info.name); +//! let info = find_problem_type("MaximumIndependentSet").unwrap(); +//! assert_eq!(info.canonical_name, "MaximumIndependentSet"); +//! println!("Problem: {}", info.display_name); //! ``` mod dyn_problem; @@ -52,7 +43,7 @@ mod schema; pub mod variant; pub use dyn_problem::{format_metric, DynProblem, LoadedDynProblem}; -pub use info::{ComplexityClass, FieldInfo, ProblemInfo, ProblemMetadata}; +pub use info::{ComplexityClass, FieldInfo, ProblemInfo}; pub use problem_ref::{parse_catalog_problem_ref, require_graph_variant, ProblemRef}; pub use problem_type::{find_problem_type, find_problem_type_by_alias, problem_types, ProblemType}; pub use schema::{ diff --git a/src/rules/ilp_helpers.rs b/src/rules/ilp_helpers.rs index 48d8dd728..ac80fd75a 100644 --- a/src/rules/ilp_helpers.rs +++ b/src/rules/ilp_helpers.rs @@ -3,7 +3,7 @@ use crate::models::algebraic::LinearConstraint; /// Convert exact ILP integer values into a source model's `usize` representation. -pub fn decode_usize_values(values: &[i64]) -> crate::rules::ExtractionResult> { +pub(crate) fn decode_usize_values(values: &[i64]) -> crate::rules::ExtractionResult> { values .iter() .enumerate() @@ -18,7 +18,7 @@ pub fn decode_usize_values(values: &[i64]) -> crate::rules::ExtractionResult>( +pub(crate) fn mccormick_product>( y_idx: usize, x_a: usize, x_b: usize, @@ -44,7 +44,7 @@ pub fn mccormick_product>( } /// Decode a column-major assignment whose constraints select one item per slot. -pub fn one_hot_decode( +pub(crate) fn one_hot_decode( solution: &[i64], num_items: usize, num_slots: usize, @@ -60,7 +60,7 @@ pub fn one_hot_decode( } /// Decode a row-major assignment whose constraints select one column per row. -pub fn one_hot_decode_rows( +pub(crate) fn one_hot_decode_rows( solution: &[i64], num_rows: usize, num_columns: usize, @@ -77,7 +77,7 @@ pub fn one_hot_decode_rows( /// Convert a permutation to Lehmer code. #[cfg(test)] -pub fn permutation_to_lehmer(permutation: &[usize]) -> Vec { +pub(crate) fn permutation_to_lehmer(permutation: &[usize]) -> Vec { (0..permutation.len()) .map(|index| { (index + 1..permutation.len()) @@ -88,7 +88,7 @@ pub fn permutation_to_lehmer(permutation: &[usize]) -> Vec { } /// Constrain each item to exactly one slot and each slot to at most one item. -pub fn one_hot_assignment_constraints( +pub(crate) fn one_hot_assignment_constraints( num_items: usize, num_slots: usize, var_offset: usize, From efc111f0503e87bd3334eecb8e960c39a55a45b4 Mon Sep 17 00:00:00 2001 From: Xiwei Pan Date: Tue, 15 Sep 2026 03:31:53 +0800 Subject: [PATCH 07/42] fix: validate solve results and preserve evaluations through recovery Reject constraint-violating candidates and feedback-arc incumbents that do not satisfy the recovery premise. Carry complete ILP results through dispatch and evaluate external target results once at the recovery boundary. Validation: 6,523 workspace tests passed; clippy and formatting passed; changed-line coverage 96.98%. --- .claude/CLAUDE.md | 3 +- docs/src/design.md | 24 +++- problemreductions-cli/src/commands/extract.rs | 20 +-- problemreductions-cli/src/dispatch.rs | 4 +- problemreductions-cli/tests/cli_tests.rs | 5 +- src/lib.rs | 4 +- src/registry/dyn_problem.rs | 5 +- src/rules/graph.rs | 14 +- ...inimumvertexcover_minimumfeedbackarcset.rs | 22 +-- src/rules/traits.rs | 46 ++---- src/solvers/mod.rs | 2 +- src/solvers/outcome.rs | 69 ++++++++- src/solvers/registry.rs | 59 ++++---- src/solvers/resolver.rs | 51 ++++--- src/traits.rs | 15 +- src/types.rs | 24 ++++ src/unit_tests/example_db.rs | 7 +- src/unit_tests/registry/dispatch.rs | 2 +- src/unit_tests/rules/graph.rs | 23 +-- .../ksatisfiability_quadraticcongruences.rs | 31 ++-- src/unit_tests/rules/maximumsetpacking_ilp.rs | 3 +- ...mumdiscreteplanarinversekinematics_qubo.rs | 3 +- ...inimumvertexcover_minimumfeedbackarcset.rs | 39 +++++ src/unit_tests/rules/traits.rs | 47 ++++-- .../rules/travelingsalesman_qubo.rs | 3 +- src/unit_tests/solvers/outcome.rs | 136 ++++++++++++++++++ src/unit_tests/solvers/registry.rs | 7 +- src/unit_tests/solvers/resolver.rs | 64 +++++++++ src/unit_tests/traits.rs | 10 +- 29 files changed, 537 insertions(+), 205 deletions(-) create mode 100644 src/unit_tests/solvers/outcome.rs diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index f3c25a420..145fcb60c 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -125,7 +125,7 @@ Problem (core trait — all problems must implement) │ ├── const NAME: &'static str // e.g., "MaximumIndependentSet" ├── type Solution // mathematical witness representation -├── type Value: Clone // per-solution evaluation value +├── type Value: EvaluationValue // per-solution value with is_valid() ├── fn parameter_names() // canonical problem-owned parameter schema ├── fn parameters(&self) -> ProblemParameters // concrete instance parameter values ├── fn evaluate(&self, solution) -> Result @@ -166,6 +166,7 @@ Max, Min, Sum, Or, And, Extremum, ExtremumSense - `BruteForce::solve()` returns `Result, SolveError>`; `None` means exhaustive search proved infeasibility - `BruteForce::find_all_witnesses()` is a reference-testing helper for collecting every optimal or satisfying solution - Each executed step constructs one result and shares it through `Rc`. Document the rule's domain, witness premise, source guarantee, and infeasibility interpretation; all tied qualifying optima must map correctly. +- `EvaluationValue` exposes candidate feasibility through `is_valid()`; `Min`, `Max`, `Or`, and `Extremum` implement it. `SolveOutcome::optimal()` and `feasible()` evaluate once and reject constraint-violating candidates with `EvaluationError::ConstraintViolation`. This does not prove problem infeasibility or optimality. - `SolutionAggregate` belongs to `solvers::BruteForce` witness selection. Models, pure reduction mappings, dynamic evaluation, and non-enumerative solving do not require it. See [executed lifecycle](../docs/src/design.md#executed-reduction-lifecycle). - `ReductionResult` provides `target_problem()` and mandatory `recover_result(source, target_outcome)`. Recovery returns typed `Optimal`, `Feasible`, or `Infeasible` outcomes, including solution and evaluation. Each rule handles all statuses explicitly; no optional completion callback or separate value-only path exists. - `pred solve bundle.json` and `pred extract bundle.json --result target-result.json` use the same complete recovery. External results declare their status; the transport boundary validates target feasibility, while the external solver supplies the optimality claim. Insufficient witness quality is an error, never evidence of source infeasibility. diff --git a/docs/src/design.md b/docs/src/design.md index caeef8b29..1344425e2 100644 --- a/docs/src/design.md +++ b/docs/src/design.md @@ -27,7 +27,7 @@ Every problem implements `Problem`. The associated `Value` type is the per-confi trait Problem: Clone { const NAME: &'static str; // e.g., "MaximumIndependentSet" type Solution; // e.g., Vec, permutation, tuple - type Value: Clone; // e.g., Max, Or, Sum + type Value: EvaluationValue; // e.g., Max, Or fn parameter_names() -> &'static [&'static str]; fn parameters(&self) -> ProblemParameters; fn evaluate(&self, solution: &Self::Solution) -> Result; @@ -37,6 +37,7 @@ trait Problem: Clone { ``` - **`Problem`** — the base trait. Every problem declares a mathematical `Solution` type, evaluates that type directly, and reports its canonical instance parameters. For example, a 4-vertex MIS uses `Vec`; `evaluate(&[true, false, true, false])` returns `Ok(Max(Some(2)))` if vertices 0 and 2 form an independent set, or `Ok(Max(None))` if they share an edge. Inherent getters such as `num_vertices()` and `num_edges()` supply the named parameters used by reduction expressions. +- **`EvaluationValue`** — requires `Clone` and `is_valid()`, expressing whether one candidate satisfies the model constraints. `Min`, `Max`, `Or`, and `Extremum` implement it. Custom evaluation types implement this check without needing aggregation or solver capabilities. An invalid candidate does not establish that the problem is infeasible. - **`BruteForceProblem`** — the reference-solver capability for registered variants with a finite Cartesian coordinate space. Its fallible `num_variables()` and `dimension(variable)` methods describe coordinates without allocating their vector. These methods and the Cartesian iterator belong to the brute-force solver, not to the mathematical `Problem` contract. - **Objective problems** — typically use `Max`, `Min`, or `Extremum` as `Value`. - **Feasibility problems** — typically use `Or`. @@ -128,9 +129,24 @@ optional interpretation callback or separate value-only execution path. | Evaluation, decoding, or execution error | Preserve the error; never turn it into `Infeasible` | `ProblemOutcome

` retains `P::Solution` and `P::Value` as concrete Rust types. -`SolveOutcome::optimal` evaluates an already established optimum; it does not -prove optimality. The solver or external caller supplies that conclusion. -Likewise, `SolveOutcome::feasible` packages an established feasible witness. +`SolveOutcome::optimal` and `SolveOutcome::feasible` evaluate the candidate once +and check `EvaluationValue::is_valid()`. A candidate violating the constraints +returns `EvaluationError::ConstraintViolation`, not `SolveOutcome::Infeasible`: +rejecting one candidate does not prove that the problem has no solution. +Evaluation failures propagate unchanged. `optimal` does not prove optimality; +the solver or external caller supplies that conclusion. Direct enum construction +and deserialization do not perform these checks. + +The ILP pipeline carries complete results through recovery and JSON serialization; +it does not discard and recompute the source evaluation. Backend infeasibility +becomes a mathematical result at the terminal boundary. Only the typed +`ILPSolver::solve()` outlet converts it to `ILPSolveError::Infeasible` to satisfy +that method's solution-returning contract. + +`ReductionChain::recover_result_json()` returns `(source_result, target_result)`. +It decodes and evaluates the external target once, then retains the model-computed +target evaluation for output while recovering the source. CLI callers use both +returned results; they do not independently evaluate the external candidate. For example, an independent set of size 2 in a four-vertex graph maps to a vertex cover of size 2. Recovery complements the solution and evaluates the diff --git a/problemreductions-cli/src/commands/extract.rs b/problemreductions-cli/src/commands/extract.rs index 997944a3c..223c06950 100644 --- a/problemreductions-cli/src/commands/extract.rs +++ b/problemreductions-cli/src/commands/extract.rs @@ -8,27 +8,9 @@ use std::path::Path; pub fn extract(input: &Path, result_path: &Path, out: &OutputConfig) -> Result<()> { let bundle: ReductionBundle = serde_json::from_str(&read_input(input)?) .context("pred extract requires a reduction bundle produced by pred reduce")?; - let mut target: SolveOutcome = serde_json::from_str(&read_input(result_path)?) + let target: SolveOutcome = serde_json::from_str(&read_input(result_path)?) .context("Target result must declare optimal, feasible, or infeasible status")?; let replay = BundleReplay::prepare(&bundle)?; - match &mut target { - SolveOutcome::Optimal { - solution, - evaluation, - } - | SolveOutcome::Feasible { - solution, - evaluation, - } => { - let (value, feasible) = replay.target.evaluate_dyn(solution)?; - anyhow::ensure!( - feasible, - "external result contains an infeasible target solution" - ); - *evaluation = value; - } - SolveOutcome::Infeasible => {} - } let result = replay.recover_result(target, SolverExecution::External)?; out.emit( || { diff --git a/problemreductions-cli/src/dispatch.rs b/problemreductions-cli/src/dispatch.rs index 3261fe6e3..f2e9b51f6 100644 --- a/problemreductions-cli/src/dispatch.rs +++ b/problemreductions-cli/src/dispatch.rs @@ -298,9 +298,9 @@ impl BundleReplay { target_outcome: SolveOutcome, solver: SolverExecution, ) -> Result { - let source_outcome = self + let (source_outcome, target_outcome) = self .chain - .recover_result_json(self.source.as_any(), target_outcome.clone())?; + .recover_result_json(self.source.as_any(), target_outcome)?; Ok(BundleSolveResult { source_name: self.source_name.clone(), target_name: self.target_name.clone(), diff --git a/problemreductions-cli/tests/cli_tests.rs b/problemreductions-cli/tests/cli_tests.rs index 816772633..616acbb1a 100644 --- a/problemreductions-cli/tests/cli_tests.rs +++ b/problemreductions-cli/tests/cli_tests.rs @@ -10228,7 +10228,7 @@ fn test_extract_preserves_feasible_status_and_rejects_invalid_witnesses() { std::fs::write( &result_file, serde_json::json!({ - "status":"feasible", "solution":solution, "evaluation":"", + "status":"feasible", "solution":solution, "evaluation":"untrusted external evaluation", }) .to_string(), ) @@ -10257,7 +10257,8 @@ fn test_extract_preserves_feasible_status_and_rejects_invalid_witnesses() { assert_eq!(result["intermediate"]["status"], "feasible"); assert_eq!(result["intermediate"]["evaluation"], "Min(3)"); } else { - assert!(String::from_utf8_lossy(&output.stderr).contains("infeasible target solution")); + assert!(String::from_utf8_lossy(&output.stderr) + .contains("candidate solution violates the problem constraints")); } } std::fs::remove_dir_all(directory).unwrap(); diff --git a/src/lib.rs b/src/lib.rs index 2ab0c154b..e84718db2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -102,7 +102,7 @@ pub mod prelude { // Core traits pub use crate::rules::{ReduceTo, ReductionResult}; pub use crate::solvers::{BruteForce, ProblemOutcome, SolveOutcome}; - pub use crate::traits::Problem; + pub use crate::traits::{EvaluationValue, Problem}; // Types pub use crate::error::{ProblemError, Result}; @@ -120,7 +120,7 @@ pub use expr::{ pub use growth::Growth; pub use registry::{ComplexityClass, ProblemInfo}; pub use solvers::BruteForce; -pub use traits::Problem; +pub use traits::{EvaluationValue, Problem}; pub use types::{ And, Extremum, ExtremumSense, Max, Min, NumericSize, One, Or, ProblemParameters, Sum, WeightElement, diff --git a/src/registry/dyn_problem.rs b/src/registry/dyn_problem.rs index 16f4595e7..1bdd77708 100644 --- a/src/registry/dyn_problem.rs +++ b/src/registry/dyn_problem.rs @@ -54,7 +54,10 @@ macro_rules! impl_dyn_problem { )) })?; let value = <$ty as $crate::traits::Problem>::evaluate(self, &solution)?; - Ok(($crate::registry::format_metric(&value), value.is_valid())) + Ok(( + $crate::registry::format_metric(&value), + $crate::traits::EvaluationValue::is_valid(&value), + )) } fn evaluate_json( diff --git a/src/rules/graph.rs b/src/rules/graph.rs index b09ac6873..ad79ea1fc 100644 --- a/src/rules/graph.rs +++ b/src/rules/graph.rs @@ -1610,16 +1610,22 @@ impl ReductionChain { recover_steps(&self.steps, source, target) } - /// JSON transport for the same complete-result recovery used by typed callers. + /// Recover the source result and return it with the validated target result. + /// The returned pair is `(source, target)`; target evaluation is computed once + /// from the model, never trusted from the incoming display string. pub fn recover_result_json( &self, source: &dyn Any, target: crate::solvers::SolveOutcome, - ) -> crate::rules::ExtractionResult { + ) -> crate::rules::ExtractionResult<(crate::solvers::SolveOutcome, crate::solvers::SolveOutcome)> + { let last = self.steps.last().expect("ReductionChain has no steps"); - let target = last.witness.target_result_from_json(target)?; + let (target, target_json) = last.witness.target_result_from_json(target)?; let source = recover_steps(&self.steps, source, target)?; - self.steps[0].witness.source_result_json(source) + Ok(( + self.steps[0].witness.source_result_json(source)?, + target_json, + )) } } diff --git a/src/rules/minimumvertexcover_minimumfeedbackarcset.rs b/src/rules/minimumvertexcover_minimumfeedbackarcset.rs index 06134a3f4..0542efef5 100644 --- a/src/rules/minimumvertexcover_minimumfeedbackarcset.rs +++ b/src/rules/minimumvertexcover_minimumfeedbackarcset.rs @@ -32,7 +32,9 @@ impl ReductionResult for ReductionVCToFAS { } /// Extract solution: internal arcs are at positions 0..n in the FAS config. - /// If internal arc i is in the FAS (config[i] = 1), vertex i is in the cover. + /// If internal arc i is in the FAS, vertex i is in the cover. + /// Only optimal target results qualify: a feasible incumbent may remove + /// crossing arcs, whose removal does not select any source vertex. fn recover_result( &self, source: &Self::Source, @@ -41,28 +43,16 @@ impl ReductionResult for ReductionVCToFAS { match target { SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; + let solution = solution[..self.num_source_vertices].to_vec(); Ok(SolveOutcome::optimal(source, solution)?) } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) + SolveOutcome::Feasible { .. } => { + Err(crate::rules::ExtractionError::InsufficientSolutionQuality) } } } } -impl ReductionVCToFAS { - fn map_solution( - &self, - target_solution: &<::Target as crate::traits::Problem>::Solution, - ) -> crate::rules::ExtractionResult< - <::Source as crate::traits::Problem>::Solution, - > { - Ok(target_solution[..self.num_source_vertices].to_vec()) - } -} - #[reduction( transform = exact { num_vertices = "2 * num_vertices", diff --git a/src/rules/traits.rs b/src/rules/traits.rs index 582f388ca..63bbd7f08 100644 --- a/src/rules/traits.rs +++ b/src/rules/traits.rs @@ -281,16 +281,16 @@ where /// Type erasure for executed reduction results. Mathematical recovery remains typed. pub trait DynReductionResult { fn target_problem_any(&self) -> &dyn Any; - fn source_solution_json(&self, solution: &dyn Any) -> ExtractionResult; fn recover_result_dyn( &self, source: &dyn Any, target: crate::solvers::ErasedOutcome, ) -> ExtractionResult; + /// Decode and evaluate once, returning the typed result and its canonical JSON. fn target_result_from_json( &self, target: crate::solvers::SolveOutcome, - ) -> ExtractionResult; + ) -> ExtractionResult<(crate::solvers::ErasedOutcome, crate::solvers::SolveOutcome)>; fn source_result_json( &self, source: crate::solvers::ErasedOutcome, @@ -301,8 +301,8 @@ impl DynReductionResult for R where R::Source: 'static, R::Target: 'static, - ::Solution: serde::de::DeserializeOwned + 'static, - ::Value: 'static, + ::Solution: serde::de::DeserializeOwned + serde::Serialize + 'static, + ::Value: std::fmt::Display + 'static, ::Solution: serde::Serialize + 'static, ::Value: std::fmt::Display + 'static, { @@ -310,13 +310,6 @@ where self.target_problem() } - fn source_solution_json(&self, solution: &dyn Any) -> ExtractionResult { - let solution = solution - .downcast_ref::<::Solution>() - .ok_or_else(|| ExtractionError::invalid("source solution type mismatch"))?; - serde_json::to_value(solution).map_err(|error| ExtractionError::invalid(error.to_string())) - } - fn recover_result_dyn( &self, source: &dyn Any, @@ -334,12 +327,12 @@ where fn target_result_from_json( &self, target: crate::solvers::SolveOutcome, - ) -> ExtractionResult { + ) -> ExtractionResult<(crate::solvers::ErasedOutcome, crate::solvers::SolveOutcome)> { use crate::solvers::SolveOutcome; // Numeric evaluation is model-owned, not parsed from a display string. let decode = |solution| { serde_json::from_value(solution).map_err(|error| { - ExtractionError::invalid(format!("target solution deserialization failed: {error}")) + ExtractionError::invalid(format!("invalid solution JSON: {error}")) }) }; let target = match target { @@ -351,38 +344,17 @@ where } SolveOutcome::Infeasible => SolveOutcome::Infeasible, }; - Ok(crate::solvers::erase_outcome(target)) + let target_json = crate::solvers::outcome_to_json(&target)?; + Ok((crate::solvers::erase_outcome(target), target_json)) } fn source_result_json( &self, source: crate::solvers::ErasedOutcome, ) -> ExtractionResult { - use crate::solvers::SolveOutcome; let source: crate::solvers::ProblemOutcome = crate::solvers::downcast_outcome(source)?; - let encode = |solution| { - serde_json::to_value(solution).map_err(|error| { - ExtractionError::invalid(format!("source solution serialization failed: {error}")) - }) - }; - Ok(match source { - SolveOutcome::Optimal { - solution, - evaluation, - } => SolveOutcome::Optimal { - solution: encode(solution)?, - evaluation: evaluation.to_string(), - }, - SolveOutcome::Feasible { - solution, - evaluation, - } => SolveOutcome::Feasible { - solution: encode(solution)?, - evaluation: evaluation.to_string(), - }, - SolveOutcome::Infeasible => SolveOutcome::Infeasible, - }) + crate::solvers::outcome_to_json(&source) } } diff --git a/src/solvers/mod.rs b/src/solvers/mod.rs index eb26a5357..496b7b575 100644 --- a/src/solvers/mod.rs +++ b/src/solvers/mod.rs @@ -7,7 +7,7 @@ mod outcome; mod pipelines; mod registry; mod resolver; -pub(crate) use outcome::{downcast_outcome, erase_outcome, ErasedOutcome}; +pub(crate) use outcome::{downcast_outcome, erase_outcome, outcome_to_json, ErasedOutcome}; pub use outcome::{ProblemOutcome, SolveOutcome}; pub mod ilp; diff --git a/src/solvers/outcome.rs b/src/solvers/outcome.rs index e3b009f7c..c0c1677ff 100644 --- a/src/solvers/outcome.rs +++ b/src/solvers/outcome.rs @@ -1,6 +1,6 @@ //! Mathematical solve results, shared by solvers and reduction recovery. -use crate::traits::{EvaluationError, Problem}; +use crate::traits::{EvaluationError, EvaluationValue, Problem}; use serde::{Deserialize, Serialize}; use std::any::Any; @@ -18,24 +18,42 @@ pub enum SolveOutcome { pub type ProblemOutcome

= SolveOutcome<

::Solution,

::Value>; impl SolveOutcome { - /// Package an optimum established by the caller and evaluate its objective. + /// Evaluate and validate a candidate whose optimality is established by the caller. + /// + /// Returns an evaluation error if evaluation fails or the candidate violates + /// the constraints. This checks feasibility, not optimality. pub fn optimal>( problem: &P, solution: S, - ) -> Result { + ) -> Result + where + V: EvaluationValue, + { let evaluation = problem.evaluate(&solution)?; + if !evaluation.is_valid() { + return Err(EvaluationError::ConstraintViolation); + } Ok(Self::Optimal { solution, evaluation, }) } - /// Package a feasible witness established by the caller without claiming optimality. + /// Evaluate and validate a candidate without claiming optimality. + /// + /// Returns an evaluation error if evaluation fails or the candidate violates + /// the constraints; this does not establish problem infeasibility. pub fn feasible>( problem: &P, solution: S, - ) -> Result { + ) -> Result + where + V: EvaluationValue, + { let evaluation = problem.evaluate(&solution)?; + if !evaluation.is_valid() { + return Err(EvaluationError::ConstraintViolation); + } Ok(Self::Feasible { solution, evaluation, @@ -57,6 +75,43 @@ impl SolveOutcome { } } +/// Serialize a model-owned result without evaluating its solution again. +pub(crate) fn outcome_to_json( + outcome: &SolveOutcome, +) -> crate::rules::ExtractionResult { + if let SolveOutcome::Optimal { evaluation, .. } | SolveOutcome::Feasible { evaluation, .. } = + outcome + { + if !evaluation.is_valid() { + return Err(EvaluationError::ConstraintViolation.into()); + } + } + let encode = |solution| { + serde_json::to_value(solution).map_err(|error| { + crate::rules::ExtractionError::invalid(format!( + "solution serialization failed: {error}" + )) + }) + }; + Ok(match outcome { + SolveOutcome::Optimal { + solution, + evaluation, + } => SolveOutcome::Optimal { + solution: encode(solution)?, + evaluation: evaluation.to_string(), + }, + SolveOutcome::Feasible { + solution, + evaluation, + } => SolveOutcome::Feasible { + solution: encode(solution)?, + evaluation: evaluation.to_string(), + }, + SolveOutcome::Infeasible => SolveOutcome::Infeasible, + }) +} + pub(crate) type ErasedOutcome = SolveOutcome, Box>; pub(crate) fn erase_outcome(outcome: SolveOutcome) -> ErasedOutcome { @@ -115,3 +170,7 @@ pub(crate) fn downcast_outcome( SolveOutcome::Infeasible => SolveOutcome::Infeasible, }) } + +#[cfg(test)] +#[path = "../unit_tests/solvers/outcome.rs"] +mod tests; diff --git a/src/solvers/registry.rs b/src/solvers/registry.rs index b7a189795..c1e41a9c4 100644 --- a/src/solvers/registry.rs +++ b/src/solvers/registry.rs @@ -18,9 +18,12 @@ fn solve_ilp_terminal( macro_rules! dispatch { ($($v:ty, $c:ty);* $(;)?) => { $( if let Some(ilp) = source.downcast_ref::>() { - let solution = adapter.solve(ilp)?; - let outcome = super::SolveOutcome::optimal(ilp, solution) - .map_err(crate::rules::ExtractionError::from)?; + let outcome = match adapter.solve(ilp) { + Ok(solution) => super::SolveOutcome::optimal(ilp, solution) + .map_err(crate::rules::ExtractionError::from)?, + Err(super::ilp::adapter::IlpBackendError::Infeasible) => super::SolveOutcome::Infeasible, + Err(error) => return Err(error.into()), + }; return Ok(super::erase_outcome(outcome)); } )* }; @@ -144,7 +147,7 @@ impl CompiledIlpPipeline { source: &dyn Any, adapter: &HighsAdapter, finish: impl FnOnce( - Box, + super::ErasedOutcome, Option<&dyn DynReductionResult>, ) -> Result, ) -> Result { @@ -159,24 +162,16 @@ impl CompiledIlpPipeline { let target_problem = chain .as_ref() .map_or(source, |chain| chain.target_problem_any()); - let target = match solve_ilp_terminal(target_problem, adapter) { - Ok(outcome) => outcome, - Err(super::ILPSolveError::Infeasible) => super::SolveOutcome::Infeasible, - Err(error) => return Err(error), - }; + let target = solve_ilp_terminal(target_problem, adapter)?; let recovered = match &chain { Some(chain) => chain.recover_erased(source, target)?, None => target, }; - let source_solution = match recovered { - super::SolveOutcome::Optimal { solution, .. } => solution, - super::SolveOutcome::Infeasible => return Err(super::ILPSolveError::Infeasible), - super::SolveOutcome::Feasible { .. } => { - return Err(crate::rules::ExtractionError::InsufficientSolutionQuality.into()) - } - }; + if matches!(recovered, super::SolveOutcome::Feasible { .. }) { + return Err(crate::rules::ExtractionError::InsufficientSolutionQuality.into()); + } finish( - source_solution, + recovered, chain.as_ref().map(|chain| chain.steps[0].witness.as_ref()), ) } @@ -185,19 +180,22 @@ impl CompiledIlpPipeline { &self, source: &dyn Any, adapter: &HighsAdapter, - ) -> Result { - self.solve_with(source, adapter, |solution, first_reduction| { + ) -> Result { + self.solve_with(source, adapter, |outcome, first_reduction| { if let Some(reduction) = first_reduction { - return reduction - .source_solution_json(solution.as_ref()) - .map_err(super::ILPSolveError::from); + return Ok(reduction.source_result_json(outcome)?); + } + // The terminal already validated the native ILP type. Recover its + // coefficient type here to format the stored evaluation. + if source.is::>() || source.is::>() { + let outcome = + super::downcast_outcome::, crate::types::Extremum>(outcome)?; + Ok(super::outcome_to_json(&outcome)?) + } else { + let outcome = + super::downcast_outcome::, crate::types::Extremum>(outcome)?; + Ok(super::outcome_to_json(&outcome)?) } - Ok(serde_json::to_value( - *solution - .downcast::>() - .expect("ILP backend returned the wrong solution type"), - ) - .expect("ILP solution serialization failed")) }) } @@ -210,7 +208,10 @@ impl CompiledIlpPipeline { P: crate::traits::Problem + 'static, P::Solution: 'static, { - self.solve_with(source, adapter, |solution, _| { + self.solve_with(source, adapter, |outcome, _| { + let solution = outcome + .into_solution() + .ok_or(super::ILPSolveError::Infeasible)?; let solution = solution .downcast::() .map_err(|_| super::ILPSolveError::PipelineTypeMismatch(self.path[0].label()))?; diff --git a/src/solvers/resolver.rs b/src/solvers/resolver.rs index 73640e386..72f0a6197 100644 --- a/src/solvers/resolver.rs +++ b/src/solvers/resolver.rs @@ -37,18 +37,27 @@ fn problem_key(problem: &LoadedDynProblem) -> ExactProblemKey { ExactProblemKey::new(problem.problem_name(), problem.variant_map()) } +/// Check the candidate returned by an exact solver before publishing its result. +fn optimal_outcome( + problem: &LoadedDynProblem, + solution: serde_json::Value, +) -> Result { + let (evaluation, feasible) = problem.evaluate_dyn(&solution)?; + if !feasible { + return Err(crate::traits::EvaluationError::ConstraintViolation.into()); + } + Ok(SolveOutcome::Optimal { + solution, + evaluation, + }) +} + fn solve_customized( problem: &LoadedDynProblem, registration: &'static CustomizedSolverRegistration, ) -> Result { let outcome = match (registration.solve_fn)(problem.as_any())? { - Some(solution) => { - let (evaluation, _) = problem.evaluate_dyn(&solution)?; - SolveOutcome::Optimal { - evaluation, - solution, - } - } + Some(solution) => optimal_outcome(problem, solution)?, None => SolveOutcome::Infeasible, }; Ok(SolveResult { @@ -63,25 +72,15 @@ fn solve_ilp( problem: &LoadedDynProblem, pipeline: &CompiledIlpPipeline, ) -> Result { - let outcome = match pipeline.solve( - problem.as_any(), - &super::ilp::adapter::HighsAdapter::new(None), - ) { - Ok(solution) => { - let (evaluation, _) = problem.evaluate_dyn(&solution)?; - SolveOutcome::Optimal { - evaluation, - solution, - } - } - Err(super::ILPSolveError::Infeasible) => SolveOutcome::Infeasible, - Err(source) => { - return Err(super::SolveError::IlpSolve { - problem: problem_key(problem).label(), - source, - }); - } - }; + let outcome = pipeline + .solve( + problem.as_any(), + &super::ilp::adapter::HighsAdapter::new(None), + ) + .map_err(|source| super::SolveError::IlpSolve { + problem: problem_key(problem).label(), + source, + })?; Ok(SolveResult { solver: SolverExecution::Ilp { reduction_path: pipeline.path_labels(), diff --git a/src/traits.rs b/src/traits.rs index 4c1cf333d..60eaa1f72 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -2,11 +2,14 @@ use crate::types::ProblemParameters; -/// Failure while evaluating one configuration of a valid problem instance. +/// Failure while evaluating a candidate or validating its claimed feasibility. #[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] pub enum EvaluationError { #[error("invalid configuration: {0}")] InvalidConfiguration(String), + /// A candidate being labeled feasible or optimal violates the problem constraints. + #[error("candidate solution violates the problem constraints")] + ConstraintViolation, #[error("integer overflow while {0}")] IntegerOverflow(String), #[error("inexact integer-to-float conversion while {0}")] @@ -15,6 +18,14 @@ pub enum EvaluationError { NonFiniteResult(String), } +/// Feasibility of one candidate, as determined by the model's evaluation. +/// +/// Independent of aggregation and solver capabilities. An invalid value rejects +/// this candidate; it does not establish that the problem has no feasible solution. +pub trait EvaluationValue: Clone { + fn is_valid(&self) -> bool; +} + /// Minimal problem trait — a problem maps a solution to a value or an /// evaluation error. /// @@ -26,7 +37,7 @@ pub trait Problem: Clone { /// Mathematical witness type for this problem. type Solution; /// The evaluation value type. - type Value: Clone; + type Value: EvaluationValue; /// Canonical parameter names for this problem model. fn parameter_names() -> &'static [&'static str]; /// Measure the complete canonical parameters of this concrete instance. diff --git a/src/types.rs b/src/types.rs index 9de4d611d..7b19d3e4e 100644 --- a/src/types.rs +++ b/src/types.rs @@ -343,6 +343,12 @@ impl fmt::Display for Max { } } +impl crate::traits::EvaluationValue for Max { + fn is_valid(&self) -> bool { + Max::is_valid(self) + } +} + impl Max { pub fn is_valid(&self) -> bool { self.0.is_some() @@ -394,6 +400,12 @@ impl fmt::Display for Min { } } +impl crate::traits::EvaluationValue for Min { + fn is_valid(&self) -> bool { + Min::is_valid(self) + } +} + impl Min { pub fn is_valid(&self) -> bool { self.0.is_some() @@ -464,6 +476,12 @@ impl fmt::Display for Sum { #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] pub struct Or(pub bool); +impl crate::traits::EvaluationValue for Or { + fn is_valid(&self) -> bool { + Or::is_valid(self) + } +} + impl Or { pub fn is_valid(&self) -> bool { self.0 @@ -550,6 +568,12 @@ pub struct Extremum { pub value: Option, } +impl crate::traits::EvaluationValue for Extremum { + fn is_valid(&self) -> bool { + Extremum::is_valid(self) + } +} + impl Extremum { pub fn maximize(value: Option) -> Self { Self { diff --git a/src/unit_tests/example_db.rs b/src/unit_tests/example_db.rs index 715324f9a..8b077d7d1 100644 --- a/src/unit_tests/example_db.rs +++ b/src/unit_tests/example_db.rs @@ -715,7 +715,7 @@ fn rule_specs_solution_pairs_are_consistent() { evaluation: target_eval.0.clone(), }, ) - .map(|outcome| outcome.into_solution().unwrap()) + .map(|(outcome, _)| outcome.into_solution().unwrap()) .unwrap(); let extracted_val = source .evaluate_json(&extracted) @@ -731,14 +731,15 @@ fn rule_specs_solution_pairs_are_consistent() { assert_eq!( chain .recover_result_json(source.as_any(), SolveOutcome::Infeasible) - .unwrap(), + .unwrap() + .0, SolveOutcome::Infeasible, "Rule {label}: target infeasibility must propagate" ); match chain.recover_result_json(source.as_any(), SolveOutcome::Feasible { solution: pair.target_config.clone(), evaluation: target_eval.0.clone(), }) { - Ok(SolveOutcome::Feasible { solution, evaluation }) => { + Ok((SolveOutcome::Feasible { solution, evaluation }, _)) => { let (actual, valid) = source.evaluate_dyn(&solution).unwrap(); assert!(valid, "Rule {label}: feasible recovery returned an invalid source witness"); assert_eq!(evaluation, actual); diff --git a/src/unit_tests/registry/dispatch.rs b/src/unit_tests/registry/dispatch.rs index b3fa582df..9452c9cd6 100644 --- a/src/unit_tests/registry/dispatch.rs +++ b/src/unit_tests/registry/dispatch.rs @@ -507,7 +507,7 @@ struct DirectEvaluation; #[derive(Clone, serde::Serialize)] struct DirectValue(bool); -impl DirectValue { +impl crate::traits::EvaluationValue for DirectValue { fn is_valid(&self) -> bool { self.0 } diff --git a/src/unit_tests/rules/graph.rs b/src/unit_tests/rules/graph.rs index 3b6400541..5f768be7e 100644 --- a/src/unit_tests/rules/graph.rs +++ b/src/unit_tests/rules/graph.rs @@ -15,7 +15,7 @@ use crate::rules::traits::ReductionResult; use crate::solvers::SolveOutcome; use crate::topology::SimpleGraph; use crate::traits::Problem; -use crate::types::{One, ProblemParameters, Sum}; +use crate::types::{Min, One, ProblemParameters}; use petgraph::graph::DiGraph; use serde_json::json; use std::any::Any; @@ -82,7 +82,7 @@ struct NaturalVariantProblem; impl Problem for AggregateChainSource { const NAME: &'static str = "AggregateChainSource"; type Solution = Vec; - type Value = Sum; + type Value = Min; fn parameter_names() -> &'static [&'static str] { &["num_variables"] @@ -95,7 +95,7 @@ impl Problem for AggregateChainSource { &self, config: &Self::Solution, ) -> Result { - Ok(Sum(config.iter().sum::() as u64)) + Ok(Min(Some(config.iter().sum::() as u64))) } fn variant() -> Vec<(&'static str, &'static str)> { @@ -116,7 +116,7 @@ impl crate::solvers::BruteForceProblem for AggregateChainSource { impl Problem for AggregateChainMiddle { const NAME: &'static str = "AggregateChainMiddle"; type Solution = Vec; - type Value = Sum; + type Value = Min; fn parameter_names() -> &'static [&'static str] { &["num_variables"] @@ -129,7 +129,7 @@ impl Problem for AggregateChainMiddle { &self, config: &Self::Solution, ) -> Result { - Ok(Sum(config.iter().sum::() as u64)) + Ok(Min(Some(config.iter().sum::() as u64))) } fn variant() -> Vec<(&'static str, &'static str)> { @@ -150,7 +150,7 @@ impl crate::solvers::BruteForceProblem for AggregateChainMiddle { impl Problem for AggregateChainTarget { const NAME: &'static str = "AggregateChainTarget"; type Solution = Vec; - type Value = Sum; + type Value = Min; fn parameter_names() -> &'static [&'static str] { &["num_variables"] @@ -163,7 +163,7 @@ impl Problem for AggregateChainTarget { &self, config: &Self::Solution, ) -> Result { - Ok(Sum(config.iter().sum::() as u64)) + Ok(Min(Some(config.iter().sum::() as u64))) } fn variant() -> Vec<(&'static str, &'static str)> { @@ -563,7 +563,7 @@ fn execute_paths_executes_a_shared_prefix_once() { &AggregateChainSource, SolveOutcome::Optimal { solution: vec![1usize], - evaluation: Sum(1) + evaluation: Min(Some(1)) } ) .unwrap() @@ -853,7 +853,7 @@ fn test_aggregate_reduction_chain_extracts_value_backwards() { .unwrap(), SolveOutcome::Optimal { solution: vec![12], - evaluation: Sum(12) + evaluation: Min(Some(12)) } ); } @@ -2127,7 +2127,7 @@ fn witness_and_value_mapping_share_one_executed_construction() { .unwrap(), SolveOutcome::Optimal { solution: witness, - evaluation: Sum(7) + evaluation: Min(Some(7)) } ); assert_eq!(CONSTRUCTIONS.load(Ordering::SeqCst), 1); @@ -2207,7 +2207,8 @@ fn composed_witness_agrees_across_direct_chain_path_and_json() { evaluation: String::new(), } ) - .unwrap(), + .unwrap() + .0, SolveOutcome::Optimal { solution: json!(expected), evaluation: "Min(1)".into() diff --git a/src/unit_tests/rules/ksatisfiability_quadraticcongruences.rs b/src/unit_tests/rules/ksatisfiability_quadraticcongruences.rs index 077308f55..344b01e0b 100644 --- a/src/unit_tests/rules/ksatisfiability_quadraticcongruences.rs +++ b/src/unit_tests/rules/ksatisfiability_quadraticcongruences.rs @@ -69,27 +69,30 @@ fn test_native_clauses_and_arbitrary_crt_signs() { .collect(); let witness = witness_value_from_alphas(&signs, &construction.thetas); let valid = reduction.target_problem().evaluate(&witness).unwrap().0; + let target = SolveOutcome::optimal(reduction.target_problem(), witness.clone()); + if !valid { + assert_eq!( + target, + Err(crate::traits::EvaluationError::ConstraintViolation) + ); + continue; + } let extracted = reduction - .recover_result( - &source, - SolveOutcome::optimal(reduction.target_problem(), witness.clone()).unwrap(), - ) + .recover_result(&source, target.unwrap()) .map(|result| { result .into_solution() .expect("qualifying target result must recover a source solution") }); - if valid { - let extracted = extracted.unwrap(); - assert_eq!(source.evaluate(&extracted).unwrap(), Or(true)); - for (i, &original) in construction.active_to_source.iter().enumerate() { - assert_eq!( - extracted[original], - signs[0] != signs[2 * construction.clauses.len() + i + 1] - ); - } - recovered.insert(extracted); + let extracted = extracted.unwrap(); + assert_eq!(source.evaluate(&extracted).unwrap(), Or(true)); + for (i, &original) in construction.active_to_source.iter().enumerate() { + assert_eq!( + extracted[original], + signs[0] != signs[2 * construction.clauses.len() + i + 1] + ); } + recovered.insert(extracted); } // Enumerate only appearing variables; unused coordinates are free. let mut expected = BTreeSet::new(); diff --git a/src/unit_tests/rules/maximumsetpacking_ilp.rs b/src/unit_tests/rules/maximumsetpacking_ilp.rs index d1b6103ad..63d5f351e 100644 --- a/src/unit_tests/rules/maximumsetpacking_ilp.rs +++ b/src/unit_tests/rules/maximumsetpacking_ilp.rs @@ -238,7 +238,8 @@ fn extraction_maps_feasible_witnesses_through_typed_and_dynamic_paths() { evaluation: String::new(), } ) - .unwrap(), + .unwrap() + .0, SolveOutcome::Feasible { solution: json!([false]), evaluation: "Max(0)".into() diff --git a/src/unit_tests/rules/minimumdiscreteplanarinversekinematics_qubo.rs b/src/unit_tests/rules/minimumdiscreteplanarinversekinematics_qubo.rs index 816e44782..ef4b4fd82 100644 --- a/src/unit_tests/rules/minimumdiscreteplanarinversekinematics_qubo.rs +++ b/src/unit_tests/rules/minimumdiscreteplanarinversekinematics_qubo.rs @@ -186,7 +186,8 @@ fn optimum_energy_recovers_distance_and_infeasibility() { evaluation: String::new(), }, ) - .unwrap(); + .unwrap() + .0; assert_eq!( matches!(completed, SolveOutcome::Optimal { .. }), expected.is_some() diff --git a/src/unit_tests/rules/minimumvertexcover_minimumfeedbackarcset.rs b/src/unit_tests/rules/minimumvertexcover_minimumfeedbackarcset.rs index 7bf0f902f..b1e44c93a 100644 --- a/src/unit_tests/rules/minimumvertexcover_minimumfeedbackarcset.rs +++ b/src/unit_tests/rules/minimumvertexcover_minimumfeedbackarcset.rs @@ -172,3 +172,42 @@ fn test_canonical_rule_example_spec_builds() { assert_eq!(source_metric, source.evaluate(&best_source).unwrap()); assert_eq!(target_metric, target.evaluate(&best_target).unwrap()); } + +#[test] +fn feasible_feedback_arc_set_does_not_establish_a_vertex_cover() { + use crate::rules::{DynReductionResult, ExtractionError}; + use crate::traits::Problem; + use crate::types::Min; + + let source = triangle_source(); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + // Removing one internal arc and one crossing arc breaks every cycle, + // but selecting only the corresponding vertex leaves edge (0, 1) uncovered. + let candidate = vec![false, false, true, true, false, false, false, false, false]; + assert!(reduction + .target_problem() + .evaluate(&candidate) + .unwrap() + .is_valid()); + assert_eq!( + source.evaluate(&candidate[..3].to_vec()).unwrap(), + Min(None) + ); + let target = SolveOutcome::feasible(reduction.target_problem(), candidate.clone()).unwrap(); + assert!(matches!( + reduction.recover_result(&source, target), + Err(ExtractionError::InsufficientSolutionQuality), + )); + // The external JSON boundary must report the same rule-level failure. + let target = reduction + .target_result_from_json(SolveOutcome::Feasible { + solution: serde_json::json!(candidate), + evaluation: String::new(), + }) + .unwrap() + .0; + assert!(matches!( + reduction.recover_result_dyn(&source, target), + Err(ExtractionError::InsufficientSolutionQuality), + )); +} diff --git a/src/unit_tests/rules/traits.rs b/src/unit_tests/rules/traits.rs index 464778530..6186173d0 100644 --- a/src/unit_tests/rules/traits.rs +++ b/src/unit_tests/rules/traits.rs @@ -1,7 +1,7 @@ use crate::rules::traits::{DynReductionResult, ReduceTo, ReductionResult}; use crate::solvers::{downcast_outcome, erase_outcome, SolveOutcome}; use crate::traits::Problem; -use crate::types::Sum; +use crate::types::Min; use serde_json::json; #[derive(Clone)] @@ -24,16 +24,19 @@ impl TargetProblem { impl Problem for SourceProblem { const NAME: &'static str = "Source"; type Solution = Vec; - type Value = i64; + type Value = Min; crate::problem_parameters![("num_variables", num_variables)]; - fn evaluate(&self, config: &Self::Solution) -> Result { + fn evaluate( + &self, + config: &Self::Solution, + ) -> Result { if config.len() != 2 || config.iter().any(|&value| value >= 2) { return Err(crate::traits::EvaluationError::InvalidConfiguration( "expected two binary target values".to_string(), )); } - Ok((config[0] + config[1]) as i64) + Ok(Min(Some((config[0] + config[1]) as i64))) } fn variant() -> Vec<(&'static str, &'static str)> { vec![("graph", "SimpleGraph"), ("weight", "i64")] @@ -164,7 +167,8 @@ fn aggregate_value_from_solution_keeps_evaluation_errors_distinct_from_false() { solution: json!([true, false]), evaluation: String::new(), }) - .unwrap(); + .unwrap() + .0; assert!(matches!( step.witness.recover_result_dyn(&source, target).unwrap(), SolveOutcome::Infeasible @@ -194,6 +198,10 @@ struct AggregateSourceProblem; #[derive(Clone)] struct AggregateTargetProblem; +thread_local! { + static TARGET_EVALUATIONS: std::cell::Cell = const { std::cell::Cell::new(0) }; +} + impl AggregateSourceProblem { fn num_variables(&self) -> usize { 1 @@ -209,7 +217,7 @@ impl AggregateTargetProblem { impl Problem for AggregateSourceProblem { const NAME: &'static str = "AggregateSource"; type Solution = Vec; - type Value = Sum; + type Value = Min; crate::problem_parameters![("num_variables", num_variables)]; @@ -217,7 +225,7 @@ impl Problem for AggregateSourceProblem { &self, config: &Self::Solution, ) -> Result { - Ok(Sum(config.iter().sum::() as u64)) + Ok(Min(Some(config.iter().sum::() as u64))) } fn variant() -> Vec<(&'static str, &'static str)> { @@ -228,7 +236,7 @@ impl Problem for AggregateSourceProblem { impl Problem for AggregateTargetProblem { const NAME: &'static str = "AggregateTarget"; type Solution = Vec; - type Value = Sum; + type Value = Min; crate::problem_parameters![("num_variables", num_variables)]; @@ -236,7 +244,8 @@ impl Problem for AggregateTargetProblem { &self, config: &Self::Solution, ) -> Result { - Ok(Sum(config.iter().sum::() as u64)) + TARGET_EVALUATIONS.with(|count| count.set(count.get() + 1)); + Ok(Min(Some(config.iter().sum::() as u64))) } fn variant() -> Vec<(&'static str, &'static str)> { @@ -302,7 +311,7 @@ fn test_aggregate_reduction_extracts_value() { .unwrap(), SolveOutcome::Optimal { solution: vec![10], - evaluation: Sum(10) + evaluation: Min(Some(10)) } ); } @@ -319,20 +328,30 @@ fn test_dyn_aggregate_reduction_result_extracts_value() { .target_problem_any() .downcast_ref::() .is_some()); - let target = dyn_result + TARGET_EVALUATIONS.with(|count| count.set(0)); + let (target, target_json) = dyn_result .target_result_from_json(SolveOutcome::Optimal { solution: json!([7]), - evaluation: "Sum(7)".into(), + evaluation: "untrusted external evaluation".into(), }) .unwrap(); + assert_eq!( + target_json, + SolveOutcome::Optimal { + solution: json!([7]), + evaluation: "Min(7)".into(), + } + ); + assert_eq!(TARGET_EVALUATIONS.with(|count| count.get()), 1); let recovered = dyn_result .recover_result_dyn(&AggregateSourceProblem, target) .unwrap(); + assert_eq!(TARGET_EVALUATIONS.with(|count| count.get()), 1); assert_eq!( - downcast_outcome::, Sum>(recovered).unwrap(), + downcast_outcome::, Min>(recovered).unwrap(), SolveOutcome::Optimal { solution: vec![9], - evaluation: Sum(9) + evaluation: Min(Some(9)) } ); } diff --git a/src/unit_tests/rules/travelingsalesman_qubo.rs b/src/unit_tests/rules/travelingsalesman_qubo.rs index b0bf970da..685911a25 100644 --- a/src/unit_tests/rules/travelingsalesman_qubo.rs +++ b/src/unit_tests/rules/travelingsalesman_qubo.rs @@ -155,7 +155,8 @@ fn signed_and_small_tours_recover_all_optima_or_infeasibility() { evaluation: String::new(), }, ) - .unwrap(); + .unwrap() + .0; assert_eq!( matches!(completed, SolveOutcome::Optimal { .. }), expected.is_some() diff --git a/src/unit_tests/solvers/outcome.rs b/src/unit_tests/solvers/outcome.rs new file mode 100644 index 000000000..5bae0e1ca --- /dev/null +++ b/src/unit_tests/solvers/outcome.rs @@ -0,0 +1,136 @@ +use super::SolveOutcome; +use crate::traits::{EvaluationError, EvaluationValue, Problem}; +use crate::types::{Extremum, Max, Min, Or, ProblemParameters}; +use std::cell::Cell; + +#[derive(Clone)] +struct Evaluated { + value: Result, + evaluations: Cell, +} + +impl Problem for Evaluated { + const NAME: &'static str = "Evaluated"; + type Solution = (); + type Value = V; + + fn parameter_names() -> &'static [&'static str] { + &[] + } + fn parameters(&self) -> ProblemParameters { + ProblemParameters::default() + } + fn variant() -> Vec<(&'static str, &'static str)> { + vec![] + } + fn evaluate(&self, _: &()) -> Result { + self.evaluations.set(self.evaluations.get() + 1); + self.value.clone() + } +} + +fn check_candidate(value: V, valid: bool) { + let problem = Evaluated { + value: Ok(value.clone()), + evaluations: Cell::new(0), + }; + let optimal = SolveOutcome::optimal(&problem, ()); + assert_eq!(problem.evaluations.get(), 1); + let feasible = SolveOutcome::feasible(&problem, ()); + assert_eq!(problem.evaluations.get(), 2); + if valid { + assert_eq!( + optimal, + Ok(SolveOutcome::Optimal { + solution: (), + evaluation: value.clone() + }) + ); + assert_eq!( + feasible, + Ok(SolveOutcome::Feasible { + solution: (), + evaluation: value + }) + ); + } else { + assert_eq!(optimal, Err(EvaluationError::ConstraintViolation)); + assert_eq!(feasible, Err(EvaluationError::ConstraintViolation)); + } +} + +#[test] +fn constructors_validate_candidate_values_with_one_evaluation() { + check_candidate(Min(Some(0)), true); + check_candidate(Min::(None), false); + check_candidate(Max(Some(-1)), true); + check_candidate(Max::(None), false); + check_candidate(Or(true), true); + check_candidate(Or(false), false); + check_candidate(Extremum::minimize(Some(0)), true); + check_candidate(Extremum::::minimize(None), false); + check_candidate(Extremum::maximize(Some(-1)), true); + check_candidate(Extremum::::maximize(None), false); +} + +#[test] +fn constructors_preserve_evaluation_failures() { + for error in [ + EvaluationError::InvalidConfiguration("wrong solution length".into()), + EvaluationError::IntegerOverflow("summing weights".into()), + ] { + let problem = Evaluated::> { + value: Err(error.clone()), + evaluations: Cell::new(0), + }; + assert_eq!(SolveOutcome::optimal(&problem, ()), Err(error.clone())); + assert_eq!(SolveOutcome::feasible(&problem, ()), Err(error)); + assert_eq!(problem.evaluations.get(), 2); + } +} + +#[test] +fn invalid_candidate_does_not_establish_problem_infeasibility() { + use crate::models::graph::MinimumVertexCover; + use crate::topology::SimpleGraph; + + let problem = MinimumVertexCover::new(SimpleGraph::new(2, vec![(0, 1)]), vec![1i64; 2]); + let invalid = vec![false, false]; + assert_eq!(problem.evaluate(&invalid).unwrap(), Min(None)); + assert_eq!( + SolveOutcome::feasible(&problem, invalid.clone()), + Err(EvaluationError::ConstraintViolation), + ); + assert_eq!( + SolveOutcome::optimal(&problem, invalid), + Err(EvaluationError::ConstraintViolation), + ); + assert_eq!( + SolveOutcome::feasible(&problem, vec![true, false]).unwrap(), + SolveOutcome::Feasible { + solution: vec![true, false], + evaluation: Min(Some(1)), + }, + ); +} + +#[test] +fn result_serialization_rejects_invalid_stored_evaluations() { + for outcome in [ + SolveOutcome::Optimal { + solution: (), + evaluation: Or(false), + }, + SolveOutcome::Feasible { + solution: (), + evaluation: Or(false), + }, + ] { + assert!(matches!( + super::outcome_to_json(&outcome), + Err(crate::rules::ExtractionError::Evaluation( + EvaluationError::ConstraintViolation + )), + )); + } +} diff --git a/src/unit_tests/solvers/registry.rs b/src/unit_tests/solvers/registry.rs index 528d67c5d..a41cd89a0 100644 --- a/src/unit_tests/solvers/registry.rs +++ b/src/unit_tests/solvers/registry.rs @@ -54,12 +54,13 @@ fn generic_decision_ilp_respects_maximization_bounds() { if bound > 1 { assert!(matches!( result, - Err(crate::solvers::ILPSolveError::Infeasible) + Ok(crate::solvers::SolveOutcome::Infeasible) )); assert!(BruteForce::new().solve(&decision).unwrap().is_none()); continue; } - let solution: Vec = serde_json::from_value(result.unwrap()).unwrap(); + let solution: Vec = + serde_json::from_value(result.unwrap().into_solution().unwrap()).unwrap(); assert_eq!( crate::traits::Problem::evaluate(&decision, &solution).unwrap(), crate::types::Or(true) @@ -146,7 +147,7 @@ fn generic_decision_ilp_reports_no_but_preserves_extraction_errors() { let inner = Inner::new(SimpleGraph::new(2, vec![(0, 1)]), vec![1i64; 2]); assert!(matches!( pipeline.solve(&Decision::new(inner.clone(), 0), &HighsAdapter::new(None)), - Err(ILPSolveError::Infeasible) + Ok(crate::solvers::SolveOutcome::Infeasible) )); assert!(matches!( pipeline.solve(&Decision::new(inner, 1), &HighsAdapter::new(None)), diff --git a/src/unit_tests/solvers/resolver.rs b/src/unit_tests/solvers/resolver.rs index f604f1b10..e94fab704 100644 --- a/src/unit_tests/solvers/resolver.rs +++ b/src/unit_tests/solvers/resolver.rs @@ -672,3 +672,67 @@ fn decision_closest_vector_solver_preserves_bound_after_serialization() { } } } + +#[test] +fn customized_dispatch_rejects_a_constraint_violating_candidate() { + use crate::solvers::registry::CustomizedSolverRegistration; + use crate::solvers::SolveError; + use crate::traits::EvaluationError; + + static INVALID_SOLVER: CustomizedSolverRegistration = CustomizedSolverRegistration { + source_name: "MinimumVertexCover", + source_variant_fn: || vec![("graph", "SimpleGraph"), ("weight", "i64")], + implementation: "invalid-candidate", + solve_fn: |_| Ok(Some(serde_json::json!([false, false]))), + }; + let problem = load_dyn( + "MinimumVertexCover", + &BTreeMap::from([ + ("graph".into(), "SimpleGraph".into()), + ("weight".into(), "i64".into()), + ]), + serde_json::json!({"graph": {"num_vertices": 2, "edges": [[0, 1]]}, "weights": [1, 1]}), + ) + .unwrap(); + assert!(matches!( + super::solve_customized(&problem, &INVALID_SOLVER), + Err(SolveError::Evaluation(EvaluationError::ConstraintViolation)), + )); + // The model is feasible; the error concerns only the solver's candidate. + assert!( + problem + .evaluate_dyn(&serde_json::json!([true, false])) + .unwrap() + .1 + ); +} + +#[test] +fn native_float_ilp_preserves_solution_and_fractional_evaluation() { + let problem = + ILP::::new(1, vec![], vec![(0, 0.5)], ObjectiveSense::Maximize).unwrap(); + // Both variable domains accept this same explicitly bounded [0, 1] instance. + let data = serde_json::to_value(problem).unwrap(); + for variable in ["bool", "i64"] { + let loaded = load_dyn( + "ILP", + &BTreeMap::from([ + ("variable".into(), variable.into()), + ("coefficient".into(), "f64".into()), + ]), + data.clone(), + ) + .unwrap(); + let result = solve(&loaded, SolverRequest::Ilp).unwrap(); + let solution = serde_json::json!([1]); + let (evaluation, valid) = loaded.evaluate_dyn(&solution).unwrap(); + assert!(valid); + assert_eq!( + result.outcome, + SolveOutcome::Optimal { + solution, + evaluation + } + ); + } +} diff --git a/src/unit_tests/traits.rs b/src/unit_tests/traits.rs index 2fb2e851c..1dfaeae53 100644 --- a/src/unit_tests/traits.rs +++ b/src/unit_tests/traits.rs @@ -1,6 +1,6 @@ use crate::solvers::BruteForceProblem as _; use crate::traits::Problem; -use crate::types::{Max, Min, Or, Sum}; +use crate::types::{Max, Min, Or}; #[derive(Clone)] struct TestSatProblem { @@ -183,7 +183,7 @@ struct MultiDimProblem { impl Problem for MultiDimProblem { const NAME: &'static str = "MultiDim"; type Solution = Vec; - type Value = Sum; + type Value = Min; fn parameter_names() -> &'static [&'static str] { &["num_variables"] @@ -196,7 +196,7 @@ impl Problem for MultiDimProblem { &self, config: &Self::Solution, ) -> Result { - Ok(Sum(config.iter().map(|&c| c as i64).sum())) + Ok(Min(Some(config.iter().map(|&c| c as i64).sum()))) } fn variant() -> Vec<(&'static str, &'static str)> { @@ -225,8 +225,8 @@ fn test_multi_dim_problem() { vec![2, 3, 4] ); assert_eq!(p.num_variables().unwrap(), 3); - assert_eq!(p.evaluate(&vec![0, 0, 0]).unwrap(), Sum(0)); - assert_eq!(p.evaluate(&vec![1, 2, 3]).unwrap(), Sum(6)); + assert_eq!(p.evaluate(&vec![0, 0, 0]).unwrap(), Min(Some(0))); + assert_eq!(p.evaluate(&vec![1, 2, 3]).unwrap(), Min(Some(6))); } #[test] From dc9af667edeea713915ddd44e9cfe4a8ae5816e8 Mon Sep 17 00:00:00 2001 From: Xiwei Pan Date: Tue, 15 Sep 2026 03:52:30 +0800 Subject: [PATCH 08/42] fix: share flow construction validation across input boundaries Use the model constructors for flow create specifications and deserialization. Reject negative internal multipliers and bundle requirements consistently, and test malformed terminals, capacities, bundles, and homologous arc indices through both input paths. Validation: 6,526 workspace tests passed; clippy and formatting passed. Local PR changed-line coverage against origin/main is 97.54%; changed lines in the three flow models have 100% coverage. --- src/models/graph/integral_flow_bundles.rs | 61 +++---------------- .../graph/integral_flow_homologous_arcs.rs | 28 +++------ .../graph/integral_flow_with_multipliers.rs | 38 +++--------- .../models/graph/integral_flow_bundles.rs | 48 +++++++++++++++ .../graph/integral_flow_homologous_arcs.rs | 49 +++++++++++++++ .../graph/integral_flow_with_multipliers.rs | 43 +++++++++++++ 6 files changed, 165 insertions(+), 102 deletions(-) diff --git a/src/models/graph/integral_flow_bundles.rs b/src/models/graph/integral_flow_bundles.rs index f7f05f792..c5e161dc0 100644 --- a/src/models/graph/integral_flow_bundles.rs +++ b/src/models/graph/integral_flow_bundles.rs @@ -92,57 +92,14 @@ impl TryFrom for IntegralFlowBundles { if count < inferred { return Err("num_vertices is too small".into()); } - if spec.source >= count || spec.sink >= count { - return Err("source and sink must be valid vertices".into()); - } - if spec.source == spec.sink { - return Err("source and sink must be distinct".into()); - } - if spec.bundles.len() != spec.bundle_capacities.len() { - return Err("bundles length must match bundle_capacities length".into()); - } - if spec.requirement == 0 { - return Err("requirement must be positive".into()); - } - let mut covered = vec![false; spec.arcs.len()]; - let mut upper = vec![i64::MAX; spec.arcs.len()]; - for (i, (bundle, &capacity)) in spec.bundles.iter().zip(&spec.bundle_capacities).enumerate() - { - if capacity == 0 { - return Err(format!("bundle capacity {i} must be positive").into()); - } - let mut seen = BTreeSet::new(); - for &arc in bundle { - if arc >= spec.arcs.len() { - return Err(format!("bundle {i} arc is out of range").into()); - } - if !seen.insert(arc) { - return Err(format!("bundle {i} contains duplicate arc").into()); - } - covered[arc] = true; - upper[arc] = upper[arc].min(capacity); - } - } - for (arc, &is_covered) in covered.iter().enumerate() { - if !is_covered { - return Err(format!("arc {arc} must belong to a bundle").into()); - } - if usize::try_from(upper[arc]) - .ok() - .and_then(|v| v.checked_add(1)) - .is_none() - { - return Err(format!("arc {arc} upper bound is too large").into()); - } - } - Ok(Self { - graph: DirectedGraph::new(count, spec.arcs), - source: spec.source, - sink: spec.sink, - bundles: spec.bundles, - bundle_capacities: spec.bundle_capacities, - requirement: spec.requirement, - }) + Self::try_new( + DirectedGraph::new(count, spec.arcs), + spec.source, + spec.sink, + spec.bundles, + spec.bundle_capacities, + spec.requirement, + ) } } @@ -201,7 +158,7 @@ impl IntegralFlowBundles { let mut seen = BTreeSet::new(); for &arc_index in bundle { if !(arc_index < num_arcs) { - return Err(format!("bundle {bundle_index} references arc {arc_index}, but num_arcs is {num_arcs}").into()); + return Err(format!("bundle {bundle_index} arc is out of range: index {arc_index}, num_arcs {num_arcs}").into()); } if !(seen.insert(arc_index)) { return Err(format!( diff --git a/src/models/graph/integral_flow_homologous_arcs.rs b/src/models/graph/integral_flow_homologous_arcs.rs index d17a92824..6f175c49b 100644 --- a/src/models/graph/integral_flow_homologous_arcs.rs +++ b/src/models/graph/integral_flow_homologous_arcs.rs @@ -98,28 +98,14 @@ impl TryFrom for IntegralFlowHomologousArc return Err("num_vertices is too small".into()); } let capacities = spec.capacities.unwrap_or_else(|| vec![1; spec.arcs.len()]); - if capacities.len() != spec.arcs.len() { - return Err("capacities length must match arcs length".into()); - } - if spec.source >= count || spec.sink >= count { - return Err("source and sink must be valid vertices".into()); - } - for &(a, b) in &spec.homologous_pairs { - if a >= spec.arcs.len() || b >= spec.arcs.len() { - return Err("homologous pair arc index is out of range".into()); - } - } - if capacities.iter().any(|&capacity| capacity < 0) { - return Err("capacities must be nonnegative".into()); - } - Ok(Self { - graph: DirectedGraph::new(count, spec.arcs), + Self::try_new( + DirectedGraph::new(count, spec.arcs), capacities, - source: spec.source, - sink: spec.sink, - requirement: spec.requirement, - homologous_pairs: spec.homologous_pairs, - }) + spec.source, + spec.sink, + spec.requirement, + spec.homologous_pairs, + ) } } diff --git a/src/models/graph/integral_flow_with_multipliers.rs b/src/models/graph/integral_flow_with_multipliers.rs index e06ec3da1..cbbcb3531 100644 --- a/src/models/graph/integral_flow_with_multipliers.rs +++ b/src/models/graph/integral_flow_with_multipliers.rs @@ -91,34 +91,14 @@ impl TryFrom for IntegralFlowWithMultipli if count < inferred { return Err("num_vertices is too small".into()); } - if spec.capacities.len() != spec.arcs.len() { - return Err("capacities length must match arcs length".into()); - } - if spec.multipliers.len() != count { - return Err("multipliers length must match num_vertices".into()); - } - if spec.source >= count || spec.sink >= count { - return Err("source and sink must be valid vertices".into()); - } - if spec.source == spec.sink { - return Err("source and sink must be distinct".into()); - } - for (v, &m) in spec.multipliers.iter().enumerate() { - if v != spec.source && v != spec.sink && m == 0 { - return Err("non-terminal multipliers must be positive".into()); - } - } - if spec.capacities.iter().any(|&capacity| capacity < 0) { - return Err("capacities must be nonnegative".into()); - } - Ok(Self { - graph: DirectedGraph::new(count, spec.arcs), - source: spec.source, - sink: spec.sink, - multipliers: spec.multipliers, - capacities: spec.capacities, - requirement: spec.requirement, - }) + Self::try_new( + DirectedGraph::new(count, spec.arcs), + spec.source, + spec.sink, + spec.multipliers, + spec.capacities, + spec.requirement, + ) } } @@ -147,7 +127,7 @@ impl IntegralFlowWithMultipliers { return Err("capacities length must match graph num_arcs".into()); } if multipliers.len() != graph.num_vertices() { - return Err("multipliers length must match graph num_vertices".into()); + return Err("multipliers length must match num_vertices".into()); } let num_vertices = graph.num_vertices(); diff --git a/src/unit_tests/models/graph/integral_flow_bundles.rs b/src/unit_tests/models/graph/integral_flow_bundles.rs index 31c7a1b2f..af4d99c6a 100644 --- a/src/unit_tests/models/graph/integral_flow_bundles.rs +++ b/src/unit_tests/models/graph/integral_flow_bundles.rs @@ -116,3 +116,51 @@ fn test_integral_flow_bundles_problem_name() { "IntegralFlowBundles" ); } + +#[test] +fn creation_and_deserialization_enforce_the_same_flow_constraints() { + let input = serde_json::json!({ + "arcs": [[0, 1], [1, 2]], "num_vertices": 3, + "source": 0, "sink": 2, "requirement": 1, + "bundles": [[0], [1]], "bundle_capacities": [1, 1], + }); + let problem = IntegralFlowBundles::try_from( + serde_json::from_value::(input.clone()).unwrap(), + ) + .unwrap(); + let persisted = serde_json::to_value(&problem).unwrap(); + let restored: IntegralFlowBundles = serde_json::from_value(persisted.clone()).unwrap(); + assert_eq!( + restored.evaluate(&vec![1, 1]).unwrap(), + crate::types::Or(true) + ); + + for (field, value, message) in [ + ("source", serde_json::json!(3), "source"), + ("sink", serde_json::json!(3), "sink"), + ("sink", serde_json::json!(0), "distinct"), + ("bundle_capacities", serde_json::json!([1]), "length"), + ("requirement", serde_json::json!(0), "positive"), + ("requirement", serde_json::json!(-1), "positive"), + ("bundle_capacities", serde_json::json!([0, 1]), "positive"), + ("bundle_capacities", serde_json::json!([-1, 1]), "positive"), + ("bundles", serde_json::json!([[2], [1]]), "out of range"), + ("bundles", serde_json::json!([[0, 0], [1]]), "duplicate"), + ( + "bundles", + serde_json::json!([[0], []]), + "at least one bundle", + ), + ] { + let mut invalid_input = input.clone(); + invalid_input[field] = value.clone(); + let spec = serde_json::from_value::(invalid_input).unwrap(); + let error = IntegralFlowBundles::try_from(spec).unwrap_err(); + assert!(error.to_string().contains(message), "{field}: {error}"); + + let mut invalid_persisted = persisted.clone(); + invalid_persisted[field] = value; + let error = serde_json::from_value::(invalid_persisted).unwrap_err(); + assert!(error.to_string().contains(message), "{field}: {error}"); + } +} diff --git a/src/unit_tests/models/graph/integral_flow_homologous_arcs.rs b/src/unit_tests/models/graph/integral_flow_homologous_arcs.rs index 7e2367016..53dab6324 100644 --- a/src/unit_tests/models/graph/integral_flow_homologous_arcs.rs +++ b/src/unit_tests/models/graph/integral_flow_homologous_arcs.rs @@ -170,3 +170,52 @@ fn test_integral_flow_homologous_arcs_paper_example() { .iter() .all(|solution| problem.evaluate(solution).unwrap().0)); } + +#[test] +fn creation_and_deserialization_enforce_the_same_flow_constraints() { + let input = serde_json::json!({ + "arcs": [[0, 1], [1, 2]], "num_vertices": 3, + "source": 0, "sink": 2, "requirement": 1, + "capacities": [1, 1], "homologous_pairs": [[0, 1]], + }); + let problem = IntegralFlowHomologousArcs::try_from( + serde_json::from_value::(input.clone()).unwrap(), + ) + .unwrap(); + let persisted = serde_json::to_value(&problem).unwrap(); + let restored: IntegralFlowHomologousArcs = serde_json::from_value(persisted.clone()).unwrap(); + assert_eq!( + restored.evaluate(&vec![1, 1]).unwrap(), + crate::types::Or(true) + ); + + for (field, value, message) in [ + ("capacities", serde_json::json!([1]), "length"), + ("source", serde_json::json!(3), "source"), + ("sink", serde_json::json!(3), "sink"), + ( + "homologous_pairs", + serde_json::json!([[2, 1]]), + "out of range", + ), + ( + "homologous_pairs", + serde_json::json!([[0, 2]]), + "out of range", + ), + ("capacities", serde_json::json!([-1, 1]), "nonnegative"), + ] { + let mut invalid_input = input.clone(); + invalid_input[field] = value.clone(); + let spec = + serde_json::from_value::(invalid_input).unwrap(); + let error = IntegralFlowHomologousArcs::try_from(spec).unwrap_err(); + assert!(error.to_string().contains(message), "{field}: {error}"); + + let mut invalid_persisted = persisted.clone(); + invalid_persisted[field] = value; + let error = + serde_json::from_value::(invalid_persisted).unwrap_err(); + assert!(error.to_string().contains(message), "{field}: {error}"); + } +} 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 54e2f3e1d..e43b34bba 100644 --- a/src/unit_tests/models/graph/integral_flow_with_multipliers.rs +++ b/src/unit_tests/models/graph/integral_flow_with_multipliers.rs @@ -174,3 +174,46 @@ fn test_integral_flow_with_multipliers_paper_example() { let all_solutions = solver.find_all_witnesses(&problem).unwrap(); assert!(all_solutions.iter().any(|solution| solution == &config)); } + +#[test] +fn creation_and_deserialization_enforce_the_same_flow_constraints() { + let input = serde_json::json!({ + "arcs": [[0, 1], [1, 2]], "num_vertices": 3, + "source": 0, "sink": 2, "requirement": 1, + "multipliers": [1, 1, 1], "capacities": [1, 1], + }); + let problem = IntegralFlowWithMultipliers::try_from( + serde_json::from_value::(input.clone()).unwrap(), + ) + .unwrap(); + let persisted = serde_json::to_value(&problem).unwrap(); + let restored: IntegralFlowWithMultipliers = serde_json::from_value(persisted.clone()).unwrap(); + assert_eq!( + restored.evaluate(&vec![1, 1]).unwrap(), + crate::types::Or(true) + ); + + for (field, value, message) in [ + ("capacities", serde_json::json!([1]), "length"), + ("multipliers", serde_json::json!([1, 1]), "length"), + ("source", serde_json::json!(3), "source"), + ("sink", serde_json::json!(3), "sink"), + ("sink", serde_json::json!(0), "distinct"), + ("multipliers", serde_json::json!([1, 0, 1]), "positive"), + ("multipliers", serde_json::json!([1, -1, 1]), "positive"), + ("capacities", serde_json::json!([-1, 1]), "nonnegative"), + ] { + let mut invalid_input = input.clone(); + invalid_input[field] = value.clone(); + let spec = + serde_json::from_value::(invalid_input).unwrap(); + let error = IntegralFlowWithMultipliers::try_from(spec).unwrap_err(); + assert!(error.to_string().contains(message), "{field}: {error}"); + + let mut invalid_persisted = persisted.clone(); + invalid_persisted[field] = value; + let error = + serde_json::from_value::(invalid_persisted).unwrap_err(); + assert!(error.to_string().contains(message), "{field}: {error}"); + } +} From 019cf7d065cf31ec716f3b8d136967e3548a47dd Mon Sep 17 00:00:00 2001 From: Xiwei Pan Date: Tue, 15 Sep 2026 13:57:46 +0800 Subject: [PATCH 09/42] refactor: share explicit status-preserving recovery across rules --- .claude/skills/add-rule/SKILL.md | 84 ++++++++++++------- src/rules/acyclicpartition_ilp.rs | 14 +--- .../balancedcompletebipartitesubgraph_ilp.rs | 15 +--- src/rules/bicliquecover_bmf.rs | 28 +------ src/rules/biconnectivityaugmentation_ilp.rs | 14 +--- src/rules/binpacking_ilp.rs | 28 +------ src/rules/bmf_bicliquecover.rs | 28 +------ src/rules/bmf_ilp.rs | 15 +--- src/rules/bottlenecktravelingsalesman_ilp.rs | 15 +--- .../boundedcomponentspanningforest_ilp.rs | 27 +----- src/rules/capacityassignment_ilp.rs | 15 +--- src/rules/circuit_ilp.rs | 15 +--- src/rules/circuit_sat.rs | 27 +----- src/rules/circuit_spinglass.rs | 15 +--- src/rules/closeststring_ilp.rs | 15 +--- src/rules/closestsubstring_ilp.rs | 15 +--- src/rules/closestvectorproblem_qubo.rs | 15 +--- src/rules/clustering_ilp.rs | 15 +--- src/rules/coloring_ilp.rs | 15 +--- src/rules/coloring_qubo.rs | 15 +--- src/rules/consecutiveblockminimization_ilp.rs | 15 +--- .../consecutiveonesmatrixaugmentation_ilp.rs | 14 +--- src/rules/consecutiveonessubmatrix_ilp.rs | 14 +--- ...onsistencyofdatabasefrequencytables_ilp.rs | 15 +--- ...ximumindependentset_integralflowbundles.rs | 31 ++----- ...imumdominatingset_minimumsummulticenter.rs | 15 +--- ...nminimumdominatingset_minmaxmulticenter.rs | 28 +------ ...onminimumvertexcover_hamiltoniancircuit.rs | 15 +--- src/rules/directedhamiltonianpath_ilp.rs | 15 +--- .../directedtwocommodityintegralflow_ilp.rs | 15 +--- src/rules/disjointconnectingpaths_ilp.rs | 15 +--- src/rules/ensemblecomputation_ilp.rs | 15 +--- src/rules/eulerianpath_ilp.rs | 15 +--- ...overby3sets_boundeddiameterspanningtree.rs | 15 +--- src/rules/exactcoverby3sets_ilp.rs | 28 +------ .../exactcoverby3sets_staffscheduling.rs | 28 +------ src/rules/expectedretrievalcost_ilp.rs | 15 +--- src/rules/factoring_circuit.rs | 15 +--- src/rules/factoring_ilp.rs | 15 +--- src/rules/feasibleregisterassignment_ilp.rs | 15 +--- src/rules/flowshopscheduling_ilp.rs | 15 +--- src/rules/graphpartitioning_ilp.rs | 15 +--- ...oniancircuit_biconnectivityaugmentation.rs | 15 +--- .../hamiltoniancircuit_hamiltonianpath.rs | 15 +--- .../hamiltoniancircuit_longestcircuit.rs | 15 +--- .../hamiltoniancircuit_quadraticassignment.rs | 15 +--- src/rules/hamiltoniancircuit_ruralpostman.rs | 15 +--- src/rules/hamiltoniancircuit_stackercrane.rs | 15 +--- ...ncircuit_strongconnectivityaugmentation.rs | 15 +--- ...onianpath_degreeconstrainedspanningtree.rs | 15 +--- src/rules/hamiltonianpath_ilp.rs | 15 +--- ...onianpathbetweentwovertices_longestpath.rs | 15 +--- src/rules/highlyconnecteddeletion_ilp.rs | 15 +--- src/rules/ilp_i64_ilp_bool.rs | 15 +--- src/rules/integerknapsack_ilp.rs | 15 +--- src/rules/integralflowbundles_ilp.rs | 15 +--- src/rules/integralflowhomologousarcs_ilp.rs | 15 +--- src/rules/integralflowwithmultipliers_ilp.rs | 15 +--- src/rules/isomorphicspanningtree_ilp.rs | 15 +--- ...lique_balancedcompletebipartitesubgraph.rs | 15 +--- src/rules/kclique_conjunctivebooleanquery.rs | 15 +--- src/rules/kclique_ilp.rs | 28 +------ src/rules/kclique_subgraphisomorphism.rs | 15 +--- src/rules/kcoloring_bicliquecover.rs | 15 +--- src/rules/kcoloring_clustering.rs | 28 +------ ...kcoloring_twodimensionalconsecutivesets.rs | 15 +--- src/rules/knapsack_ilp.rs | 28 +------ src/rules/ksatisfiability_acyclicpartition.rs | 15 +--- src/rules/ksatisfiability_bicliquecover.rs | 15 +--- src/rules/ksatisfiability_cyclicordering.rs | 15 +--- ...tisfiability_decisionminimumvertexcover.rs | 30 +------ ...bility_directedtwocommodityintegralflow.rs | 15 +--- ...tisfiability_feasibleregisterassignment.rs | 14 +--- src/rules/ksatisfiability_kclique.rs | 15 +--- src/rules/ksatisfiability_kernel.rs | 15 +--- .../ksatisfiability_monochromatictriangle.rs | 14 +--- ...satisfiability_oneinthreesatisfiability.rs | 15 +--- .../ksatisfiability_quadraticcongruences.rs | 15 +--- ...fiability_quadraticdiophantineequations.rs | 15 +--- src/rules/ksatisfiability_qubo.rs | 53 ++---------- .../ksatisfiability_registersufficiency.rs | 14 +--- ...atisfiability_simultaneousincongruences.rs | 15 +--- src/rules/ksatisfiability_subsetsum.rs | 15 +--- src/rules/ksatisfiability_timetabledesign.rs | 15 +--- src/rules/lengthboundeddisjointpaths_ilp.rs | 15 +--- src/rules/longestcircuit_ilp.rs | 15 +--- src/rules/longestcommonsubsequence_ilp.rs | 15 +--- ...commonsubsequence_maximumindependentset.rs | 15 +--- src/rules/longestpath_ilp.rs | 15 +--- src/rules/maxcut_minimumcutintoboundedsets.rs | 28 +------ src/rules/maximalis_ilp.rs | 28 +------ src/rules/maximum2satisfiability_ilp.rs | 15 +--- src/rules/maximum2satisfiability_maxcut.rs | 15 +--- src/rules/maximumclique_ilp.rs | 28 +------ src/rules/maximumcokplex_ilp.rs | 31 +------ src/rules/maximumcommonedgesubgraph_ilp.rs | 15 +--- src/rules/maximumcontactmapoverlap_ilp.rs | 15 +--- src/rules/maximumdomaticnumber_ilp.rs | 15 +--- src/rules/maximumedgeweightedkclique_ilp.rs | 15 +--- src/rules/maximumleafspanningtree_ilp.rs | 15 +--- src/rules/maximumlikelihoodranking_ilp.rs | 15 +--- src/rules/maximummatching_ilp.rs | 28 +------ src/rules/maximumsetpacking_ilp.rs | 28 +------ .../minimumcapacitatedspanningtree_ilp.rs | 15 +--- ...mcostmaximumflow_minimumcostcirculation.rs | 28 +------ src/rules/minimumcoveringbycliques_ilp.rs | 15 +--- ...bycliques_minimumintersectiongraphbasis.rs | 15 +--- src/rules/minimumcutintoboundedsets_ilp.rs | 15 +--- src/rules/minimumdominatingset_ilp.rs | 28 +------ src/rules/minimumedgecostflow_ilp.rs | 15 +--- ...minimumexternalmacrodatacompression_ilp.rs | 14 +--- src/rules/minimumfaultdetectiontestset_ilp.rs | 15 +--- src/rules/minimumfeedbackarcset_ilp.rs | 15 +--- src/rules/minimumfeedbackvertexset_ilp.rs | 15 +--- ...minimumcodegenerationunlimitedregisters.rs | 14 +--- src/rules/minimumgraphbandwidth_ilp.rs | 15 +--- src/rules/minimumhittingset_ilp.rs | 28 +------ ...minimuminternalmacrodatacompression_ilp.rs | 14 +--- src/rules/minimummatrixcover_ilp.rs | 15 +--- src/rules/minimummaximalmatching_ilp.rs | 28 +------ ...maximalmatching_maximumachromaticnumber.rs | 15 +--- ...maximalmatching_minimummatrixdomination.rs | 15 +--- src/rules/minimummetricdimension_ilp.rs | 28 +------ src/rules/minimummultiwaycut_ilp.rs | 15 +--- src/rules/minimumsetcovering_ilp.rs | 28 +------ src/rules/minimumsummulticenter_ilp.rs | 15 +--- src/rules/minimumtardinesssequencing_ilp.rs | 27 +----- .../minimumvertexcover_ensemblecomputation.rs | 15 +--- ...mumvertexcover_longestcommonsubsequence.rs | 15 +--- ...inimumvertexcover_maximumindependentset.rs | 59 ++----------- ...imumvertexcover_minimumweightandorgraph.rs | 15 +--- src/rules/minimumweightdecoding_ilp.rs | 15 +--- src/rules/minmaxmulticenter_ilp.rs | 15 +--- src/rules/mixedchinesepostman_ilp.rs | 15 +--- src/rules/monochromatictriangle_ilp.rs | 28 +------ src/rules/multiplechoicebranching_ilp.rs | 15 +--- src/rules/multiplecopyfileallocation_ilp.rs | 15 +--- src/rules/multiprocessorscheduling_ilp.rs | 15 +--- src/rules/naesatisfiability_ilp.rs | 28 +------ src/rules/naesatisfiability_maxcut.rs | 15 +--- ...fiability_partitionintoperfectmatchings.rs | 15 +--- src/rules/naesatisfiability_setsplitting.rs | 28 +------ ...atching_numericalmatchingwithtargetsums.rs | 15 +--- .../numericalmatchingwithtargetsums_ilp.rs | 15 +--- src/rules/openshopscheduling_ilp.rs | 15 +--- ...ement_consecutiveonesmatrixaugmentation.rs | 15 +--- src/rules/optimallineararrangement_ilp.rs | 15 +--- ...uencingtominimizeweightedcompletiontime.rs | 14 +--- .../optimumcommunicationspanningtree_ilp.rs | 15 +--- src/rules/paintshop_ilp.rs | 14 +--- src/rules/partiallyorderedknapsack_ilp.rs | 28 +------ .../partition_integralflowwithmultipliers.rs | 15 +--- .../partition_multiprocessorscheduling.rs | 15 +--- src/rules/partition_openshopscheduling.rs | 15 +--- src/rules/partition_productionplanning.rs | 15 +--- ...ion_sequencingtominimizetardytaskweight.rs | 15 +--- src/rules/partitionintocliques_ilp.rs | 15 +--- ...ionintocliques_minimumcoveringbycliques.rs | 15 +--- src/rules/partitionintopathsoflength2_ilp.rs | 15 +--- src/rules/partitionintotriangles_ilp.rs | 15 +--- src/rules/pathconstrainednetworkflow_ilp.rs | 15 +--- .../precedenceconstrainedscheduling_ilp.rs | 15 +--- src/rules/preemptivescheduling_ilp.rs | 15 +--- ...rizecollectingsteinerforest_steinertree.rs | 15 +--- src/rules/quadraticassignment_ilp.rs | 15 +--- src/rules/qubo_ilp.rs | 15 +--- .../rectilinearpicturecompression_ilp.rs | 28 +------ src/rules/registersufficiency_ilp.rs | 15 +--- .../resourceconstrainedscheduling_ilp.rs | 15 +--- ...arrangement_rootedtreestorageassignment.rs | 15 +--- src/rules/rootedtreestorageassignment_ilp.rs | 27 +----- src/rules/ruralpostman_ilp.rs | 15 +--- src/rules/sat_circuitsat.rs | 15 +--- src/rules/sat_coloring.rs | 15 +--- src/rules/sat_ksat.rs | 27 +----- src/rules/sat_maximumindependentset.rs | 15 +--- src/rules/sat_minimumdominatingset.rs | 15 +--- ...tisfiability_integralflowhomologousarcs.rs | 15 +--- .../satisfiability_maximum2satisfiability.rs | 28 +------ src/rules/satisfiability_naesatisfiability.rs | 15 +--- ...ingtominimizeweightedcompletiontime_ilp.rs | 15 +--- .../schedulingwithindividualdeadlines_ilp.rs | 15 +--- ...cingtominimizemaximumcumulativecost_ilp.rs | 15 +--- ...sequencingtominimizetardytaskweight_ilp.rs | 15 +--- ...ingtominimizeweightedcompletiontime_ilp.rs | 15 +--- ...quencingtominimizeweightedtardiness_ilp.rs | 15 +--- ...equencingwithdeadlinesandsetuptimes_ilp.rs | 15 +--- src/rules/sequencingwithinintervals_ilp.rs | 14 +--- ...uencingwithreleasetimesanddeadlines_ilp.rs | 15 +--- src/rules/setsplitting_betweenness.rs | 15 +--- src/rules/setsplitting_ilp.rs | 28 +------ src/rules/shortestcommonsupersequence_ilp.rs | 14 +--- .../shortestweightconstrainedpath_ilp.rs | 15 +--- src/rules/sparsematrixcompression_ilp.rs | 14 +--- src/rules/spinglass_maxcut.rs | 50 ++--------- src/rules/spinglass_qubo.rs | 40 ++------- src/rules/stackercrane_ilp.rs | 15 +--- src/rules/steinertree_ilp.rs | 15 +--- src/rules/stringtostringcorrection_ilp.rs | 14 +--- .../strongconnectivityaugmentation_ilp.rs | 14 +--- src/rules/subgraphisomorphism_ilp.rs | 15 +--- src/rules/subsetsum_closestvectorproblem.rs | 15 +--- .../subsetsum_integerexpressionmembership.rs | 15 +--- src/rules/subsetsum_partition.rs | 15 +--- src/rules/sumofsquarespartition_ilp.rs | 15 +--- src/rules/threedimensionalmatching_ilp.rs | 28 +------ ...threedimensionalmatching_threepartition.rs | 15 +--- ..._sequencingwithreleasetimesanddeadlines.rs | 15 +--- src/rules/timetabledesign_ilp.rs | 15 +--- src/rules/traits.rs | 23 +++++ src/rules/travelingsalesman_ilp.rs | 15 +--- src/rules/undirectedflowlowerbounds_ilp.rs | 15 +--- .../undirectedtwocommodityintegralflow_ilp.rs | 14 +--- src/unit_tests/rules/traits.rs | 68 +++++++++++++++ 214 files changed, 655 insertions(+), 3280 deletions(-) diff --git a/.claude/skills/add-rule/SKILL.md b/.claude/skills/add-rule/SKILL.md index dd54ca36b..24fc87579 100644 --- a/.claude/skills/add-rule/SKILL.md +++ b/.claude/skills/add-rule/SKILL.md @@ -38,12 +38,12 @@ If any item is missing, ask the user to provide it. Put a high standard on item ## Step 0.5: Mathematical and API Contract -Read [the canonical witness/aggregate contract](../../../docs/src/design.md#witness-and-aggregate-reductions). +Read [the canonical complete-result recovery contract](../../../docs/src/design.md#complete-result-recovery). Resolve the source and target's concrete `Solution` and `Value` types from their implementations and check the construction, extraction preconditions, and objective relationship. Different optimization directions or numeric value types do not by themselves invalidate a witness reduction. Use the existing -witness, aggregate, or Turing capability required by the actual operation. +complete-result, proof-only, or Turing capability required by the actual operation. Report a concrete mathematical or Rust implementation mismatch if one exists; do not apply a wrapper-pair whitelist. @@ -68,7 +68,7 @@ Read these first to understand the patterns: - **Reduction rule:** `src/rules/minimumvertexcover_maximumindependentset.rs` - **Reduction tests:** `src/unit_tests/rules/minimumvertexcover_maximumindependentset.rs` - **Paper entry:** search `docs/paper/reductions.typ` for `MinimumVertexCover` `MaximumIndependentSet` -- **Traits:** `src/rules/traits.rs` (`ReduceTo`, `ReduceToAggregate`, `ReductionResult`, `AggregateReductionResult`) +- **Traits:** `src/rules/traits.rs` (`ReduceTo`, `ReductionResult`) ## Step 1: Mathematical Verification (default, skip with `--no-verify`) @@ -89,7 +89,7 @@ Create `src/rules/_.rs` (all lowercase, no underscores between w ```rust // Required structure: // 1. ReductionResult struct (holds the target problem + mapping state) -// 2. ReductionResult trait impl (target_problem + extract_solution) +// 2. ReductionResult trait impl (target_problem + mandatory recover_result) // 3. #[reduction(transform = exact { ... })] on ReduceTo impl // 4. ReduceTo trait impl (reduce_to method) // 5. #[cfg(test)] #[path = "..."] mod tests; @@ -102,27 +102,60 @@ Key elements: #[derive(Debug, Clone)] pub struct ReductionXToY { target: TargetType, - // any additional mapping state needed for extract_solution + // any additional mapping state needed for recovery } ``` **ReductionResult trait impl:** + +`recover_result` is mandatory; it has no default implementation. For a rule whose +proof establishes all three implications (target optimal -> source optimal, +target feasible -> source feasible, target infeasible -> source infeasible), +explicitly use the internal helper: + ```rust +use crate::rules::traits::recover_preserving_status; +use crate::solvers::ProblemOutcome; + impl ReductionResult for ReductionXToY { type Source = SourceType; type Target = TargetType; fn target_problem(&self) -> &Self::Target { &self.target } - fn extract_solution( + + fn recover_result( &self, - target_solution: &::Solution, - ) -> crate::rules::ExtractionResult<::Solution> { - let source_solution = /* translate the verified mathematical mapping exactly */; - Ok(source_solution) + source: &Self::Source, + target: ProblemOutcome, + ) -> crate::rules::ExtractionResult> { + recover_preserving_status(source, target, |solution| { + self.map_solution(solution) + }) } } ``` -Follow the canonical [extraction contract](../../../docs/src/design.md#witness-and-aggregate-reductions). Document the mathematical premises and implement the mapping directly. The adapter accepts solver output; external callers supply witnesses under the same mathematical contract. Extraction does not validate feasibility or optimality. Do not recheck constraints or add errors for states excluded by construction. Solver orchestration uses aggregate mappings to handle required thresholds before witness extraction; do not independently certify optimality or compensate for a rule bug with source revalidation. +Keep the mathematical mapping in a private `map_solution` method, or inline a +short mapping in the closure. Do not add a forwarding method solely to call the +helper. It preserves the declared status, maps the witness, evaluates the source +candidate once, and propagates mapping/evaluation errors. The old target value +is not reused as the source value. `Infeasible` does not invoke the mapping. + +**Choose this helper only when the proof supports all three implications.** It +cannot prove the premise, optimality, or source infeasibility from an invalid +mapped candidate. When recovery needs an optimum threshold, rejects feasible +incumbents, or has another mathematical interpretation, write an explicit +`match target` in `recover_result` instead. Examples: MVC -> FeedbackArcSet +rejects merely feasible targets; ILP -> QUBO interprets the optimum penalty. +Insufficient witness quality returns `ExtractionError::InsufficientSolutionQuality`, +never `SolveOutcome::Infeasible` without a proof. + +Follow the canonical [recovery contract](../../../docs/src/design.md#complete-result-recovery). +Document the instance domain, witness premises, source guarantee, and +infeasibility interpretation. Target validation belongs to the solver/transport +boundary; keep the existing source evaluation when constructing recovered +outcomes. Do not duplicate model constraint checks inside the mapping or add +fallbacks for inputs excluded by its premises. Do not introduce default recovery, +policy flags, macros, or a new public mapping trait to remove this explicit choice. **ReduceTo with `#[reduction]` macro** (a parameter relation is **required**): ```rust @@ -139,7 +172,9 @@ impl ReduceTo for SourceType { Each primitive reduction is determined by the exact source/target variant pair. Keep one primitive registration per endpoint pair and declare `transform = exact`, `upper_bound`, or `unavailable` according to the actual parameter relationship; follow `.claude/CLAUDE.md` for metadata requirements. -**Aggregate-only reductions:** when the rule preserves aggregate values but cannot recover a source witness from a target witness, implement `AggregateReductionResult` + `ReduceToAggregate` instead of `ReductionResult` + `ReduceTo`. Those edges are not auto-registered by `#[reduction]` yet; register them manually with `ReductionEntry { reduce_aggregate_fn: ..., capabilities: EdgeCapabilities::aggregate_only(), ... }`. See `src/unit_tests/rules/traits.rs` and `src/unit_tests/rules/graph.rs` for the reference pattern. +A construction that cannot recover complete source results must not be registered +as a complete-result edge. Use the existing proof-only or Turing capability when +it describes the actual reduction; do not invent an aggregate-only recovery API. ## Step 3: Register in mod.rs @@ -155,8 +190,8 @@ Create `src/unit_tests/rules/_.rs`: ```rust // 1. Create source problem instance // 2. Reduce: let reduction = ReduceTo::::reduce_to(&source).unwrap(); -// 3. Solve target: solver.find_all_witnesses(reduction.target_problem()) -// 4. Extract: reduction.extract_solution(&target_sol) +// 3. Solve target; wrap each proven optimum with SolveOutcome::optimal(target, solution) +// 4. Recover: reduction.recover_result(&source, target_outcome) // 5. Verify: extracted solution is valid and optimal for source ``` @@ -167,12 +202,7 @@ Additional recommended tests: - Edge cases (empty graph, single vertex, etc.) - Weight preservation (if applicable) -Test the mathematical mapping for witnesses satisfying its premises, including all tied optima on suitable small instances. Malformed witnesses do not impose rejection requirements on extraction. Keep necessary parsing/type-conversion tests at the transport boundary. - -For aggregate-only reductions, replace the closed-loop witness test with value-chain tests: -- Solve the target with `Solver::solve()` -- Map the aggregate value back with `extract_value()` -- If testing a path, use `ReductionGraph::reduce_aggregate_along_path(...)` +Test the mathematical mapping for witnesses satisfying its premises, including all tied optima on suitable small instances. Also exercise feasible incumbents and infeasibility when reachable, checking the rule's declared implications or rejection. Keep necessary parsing/type-conversion tests at the transport boundary. Link via `#[cfg(test)] #[path = "..."] mod tests;` at the bottom of the rule file. @@ -266,11 +296,6 @@ Adding a witness-preserving reduction rule does NOT require CLI changes -- the r `ExtractionError` already propagates through `pred extract` and bundle `pred solve`; add a rule-specific CLI test only when the CLI surface changes. -Aggregate-only reductions currently have a narrower CLI surface: -- `pred solve ` can still compute direct aggregate values for aggregate-only problems -- `pred reduce` and `pred solve bundle.json` remain witness-only workflows and reject aggregate-only paths -- Manual aggregate-edge registration affects runtime graph search and internal value extraction, but not bundle solving - ## File Naming - Rule file: `src/rules/_.rs` -- no underscores within a problem name @@ -283,11 +308,11 @@ Aggregate-only reductions currently have a narrower CLI surface: | Mistake | Fix | |---------|-----| | Forgetting `#[reduction(...)]` macro | Required for compile-time registration in the reduction graph | -| Using `#[reduction]` for an aggregate-only rule | `#[reduction]` currently registers witness/config edges only; aggregate-only rules need manual `ReductionEntry` wiring with `reduce_aggregate_fn` | +| Registering proof-only or Turing reductions as complete-result edges | Register the actual capability; `#[reduction]` requires complete source-result recovery | | Wrong overhead expression | Must accurately reflect the size relationship | | Adding extra reduction metadata or duplicate primitive endpoint registration | Keep one primitive registration per endpoint pair and use only the `overhead` form of `#[reduction]` | -| Missing `extract_solution` mapping state | Store any index maps needed in the ReductionResult struct | -| Permissive extraction | Map witnesses satisfying the documented premises directly; do not validate feasibility or optimality | +| Missing recovery mapping state | Store any index maps needed in the ReductionResult struct | +| Using status-preserving recovery without its premises | Prove all three status implications, or interpret outcomes explicitly in `recover_result` | | Not adding a canonical example | Add the rule-local spec and include it from `src/rules/mod.rs` | | Not regenerating reduction graph | Run `cargo run --example export_graph` after adding a rule | | Skipping Step 6 (paper documentation) | **Every rule MUST have a `reduction-rule` entry in the paper. This is mandatory, not optional. PRs without documentation will be rejected.** | @@ -303,8 +328,7 @@ and infeasibility interpretation. Check every qualifying tied optimum in small exhaustive cases where ties are relevant. A witness flag alone does not prove complete solvability or that adjacent path premises compose. -Construct each executed result once and share target, witness, value, and -completion state. Outcome interpretation uses the rule's mathematical relation; +Construct each executed result once and share the target and mapping state. Outcome interpretation uses the rule's mathematical relation; ordinary extraction assumes its premises. Keep necessary dynamic/JSON conversion and reachable representation failures, but no checked/unchecked extraction or pure forwarding wrappers. Do not add `SolutionAggregate` bounds to models or diff --git a/src/rules/acyclicpartition_ilp.rs b/src/rules/acyclicpartition_ilp.rs index b5a058256..004983192 100644 --- a/src/rules/acyclicpartition_ilp.rs +++ b/src/rules/acyclicpartition_ilp.rs @@ -8,7 +8,7 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::AcyclicPartition; use crate::reduction; use crate::rules::ilp_helpers::mccormick_product; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; use crate::solvers::SolveOutcome; @@ -32,17 +32,7 @@ impl ReductionResult for ReductionAcyclicPartitionToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/balancedcompletebipartitesubgraph_ilp.rs b/src/rules/balancedcompletebipartitesubgraph_ilp.rs index 07c1dfa9e..d94eab139 100644 --- a/src/rules/balancedcompletebipartitesubgraph_ilp.rs +++ b/src/rules/balancedcompletebipartitesubgraph_ilp.rs @@ -7,9 +7,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::BalancedCompleteBipartiteSubgraph; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use std::collections::HashSet; #[derive(Debug, Clone)] @@ -31,17 +30,7 @@ impl ReductionResult for ReductionBCBSToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/bicliquecover_bmf.rs b/src/rules/bicliquecover_bmf.rs index 53774e380..ae0bee102 100644 --- a/src/rules/bicliquecover_bmf.rs +++ b/src/rules/bicliquecover_bmf.rs @@ -15,9 +15,8 @@ use crate::models::algebraic::BMF; use crate::models::graph::BicliqueCover; use crate::reduction; use crate::rules::bmf_bicliquecover::config_bmf_to_bc; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; /// Result of reducing BicliqueCover to BMF. #[derive(Debug, Clone)] @@ -43,28 +42,9 @@ impl ReductionResult for ReductionBicliqueCoverToBMF { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } - } -} - -impl ReductionBicliqueCoverToBMF { - fn map_solution( - &self, - target_solution: &<::Target as crate::traits::Problem>::Solution, - ) -> crate::rules::ExtractionResult< - <::Source as crate::traits::Problem>::Solution, - > { - Ok(config_bmf_to_bc(target_solution, self.m, self.n, self.k)) + recover_preserving_status(source, target, |solution| { + Ok(config_bmf_to_bc(solution, self.m, self.n, self.k)) + }) } } diff --git a/src/rules/biconnectivityaugmentation_ilp.rs b/src/rules/biconnectivityaugmentation_ilp.rs index bdda0dbef..165add188 100644 --- a/src/rules/biconnectivityaugmentation_ilp.rs +++ b/src/rules/biconnectivityaugmentation_ilp.rs @@ -7,7 +7,7 @@ use crate::models::algebraic::{IntegerVariable, LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::BiconnectivityAugmentation; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; @@ -61,17 +61,7 @@ impl ReductionResult for ReductionBiconnAugToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/binpacking_ilp.rs b/src/rules/binpacking_ilp.rs index 3a40b4029..8cd8f5995 100644 --- a/src/rules/binpacking_ilp.rs +++ b/src/rules/binpacking_ilp.rs @@ -10,9 +10,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::BinPacking; use crate::reduction; use crate::rules::ilp_helpers::one_hot_decode_rows; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; /// Result of reducing BinPacking to ILP. /// @@ -44,28 +43,9 @@ impl ReductionResult for ReductionBPToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } - } -} - -impl ReductionBPToILP { - fn map_solution( - &self, - target_solution: &<::Target as crate::traits::Problem>::Solution, - ) -> crate::rules::ExtractionResult< - <::Source as crate::traits::Problem>::Solution, - > { - Ok(one_hot_decode_rows(target_solution, self.n, self.n, 0)) + recover_preserving_status(source, target, |solution| { + Ok(one_hot_decode_rows(solution, self.n, self.n, 0)) + }) } } diff --git a/src/rules/bmf_bicliquecover.rs b/src/rules/bmf_bicliquecover.rs index d188af4c1..30dfc69b2 100644 --- a/src/rules/bmf_bicliquecover.rs +++ b/src/rules/bmf_bicliquecover.rs @@ -17,9 +17,8 @@ use crate::models::algebraic::BMF; use crate::models::graph::BicliqueCover; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::topology::BipartiteGraph; /// Convert one vertex-membership row per biclique into BMF factors. @@ -89,28 +88,9 @@ impl ReductionResult for ReductionBMFToBicliqueCover { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } - } -} - -impl ReductionBMFToBicliqueCover { - fn map_solution( - &self, - target_solution: &<::Target as crate::traits::Problem>::Solution, - ) -> crate::rules::ExtractionResult< - <::Source as crate::traits::Problem>::Solution, - > { - Ok(config_bc_to_bmf(target_solution, self.m, self.n, self.k)) + recover_preserving_status(source, target, |solution| { + Ok(config_bc_to_bmf(solution, self.m, self.n, self.k)) + }) } } diff --git a/src/rules/bmf_ilp.rs b/src/rules/bmf_ilp.rs index 1f93e9292..67e028069 100644 --- a/src/rules/bmf_ilp.rs +++ b/src/rules/bmf_ilp.rs @@ -7,9 +7,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, BMF, ILP}; use crate::reduction; use crate::rules::ilp_helpers::mccormick_product; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; #[derive(Debug, Clone)] pub struct ReductionBMFToILP { @@ -32,17 +31,7 @@ impl ReductionResult for ReductionBMFToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/bottlenecktravelingsalesman_ilp.rs b/src/rules/bottlenecktravelingsalesman_ilp.rs index 93ad39051..8f5b6b81d 100644 --- a/src/rules/bottlenecktravelingsalesman_ilp.rs +++ b/src/rules/bottlenecktravelingsalesman_ilp.rs @@ -3,9 +3,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::BottleneckTravelingSalesman; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::topology::Graph; /// A tour is encoded by positions and distinct directed uses of source edges. @@ -30,17 +29,7 @@ impl ReductionResult for ReductionBTSPToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/boundedcomponentspanningforest_ilp.rs b/src/rules/boundedcomponentspanningforest_ilp.rs index db31d3ad0..e6389c000 100644 --- a/src/rules/boundedcomponentspanningforest_ilp.rs +++ b/src/rules/boundedcomponentspanningforest_ilp.rs @@ -8,7 +8,7 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::BoundedComponentSpanningForest; use crate::reduction; use crate::rules::ilp_helpers::one_hot_decode_rows; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; @@ -34,28 +34,9 @@ impl ReductionResult for ReductionBCSFToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } - } -} - -impl ReductionBCSFToILP { - fn map_solution( - &self, - target_solution: &<::Target as crate::traits::Problem>::Solution, - ) -> crate::rules::ExtractionResult< - <::Source as crate::traits::Problem>::Solution, - > { - Ok(one_hot_decode_rows(target_solution, self.n, self.k, 0)) + recover_preserving_status(source, target, |solution| { + Ok(one_hot_decode_rows(solution, self.n, self.k, 0)) + }) } } diff --git a/src/rules/capacityassignment_ilp.rs b/src/rules/capacityassignment_ilp.rs index d44f48109..c5b5b027a 100644 --- a/src/rules/capacityassignment_ilp.rs +++ b/src/rules/capacityassignment_ilp.rs @@ -10,9 +10,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::CapacityAssignment; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; /// Result of reducing CapacityAssignment to ILP. /// @@ -41,17 +40,7 @@ impl ReductionResult for ReductionCAToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/circuit_ilp.rs b/src/rules/circuit_ilp.rs index 0b83cc5e2..a2826353c 100644 --- a/src/rules/circuit_ilp.rs +++ b/src/rules/circuit_ilp.rs @@ -17,9 +17,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::formula::{BooleanExpr, BooleanOp, CircuitSAT}; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use std::collections::HashMap; /// Result of reducing CircuitSAT to ILP. @@ -43,17 +42,7 @@ impl ReductionResult for ReductionCircuitToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/circuit_sat.rs b/src/rules/circuit_sat.rs index 53a4b9a3d..b7e18850f 100644 --- a/src/rules/circuit_sat.rs +++ b/src/rules/circuit_sat.rs @@ -5,7 +5,7 @@ use crate::models::formula::{ }; use crate::reduction; use crate::rules::sat_helpers::SatVariableAllocator; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; use crate::solvers::SolveOutcome; use std::collections::HashMap; @@ -296,28 +296,9 @@ impl ReductionResult for ReductionCircuitSATToSAT { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } - } -} - -impl ReductionCircuitSATToSAT { - fn map_solution( - &self, - target_solution: &<::Target as crate::traits::Problem>::Solution, - ) -> crate::rules::ExtractionResult< - <::Source as crate::traits::Problem>::Solution, - > { - Ok(target_solution[..self.source_var_count].to_vec()) + recover_preserving_status(source, target, |solution| { + Ok(solution[..self.source_var_count].to_vec()) + }) } } diff --git a/src/rules/circuit_spinglass.rs b/src/rules/circuit_spinglass.rs index 264dc49c6..6a25e1ac4 100644 --- a/src/rules/circuit_spinglass.rs +++ b/src/rules/circuit_spinglass.rs @@ -10,9 +10,8 @@ use crate::models::decision::Decision; use crate::models::formula::{Assignment, BooleanExpr, BooleanOp, CircuitSAT}; use crate::models::graph::SpinGlass; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::topology::SimpleGraph; use crate::types::WeightElement; use num_traits::Zero; @@ -232,17 +231,7 @@ impl ReductionResult for ReductionCircuitToSG { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/closeststring_ilp.rs b/src/rules/closeststring_ilp.rs index 7165627c6..4fd355ea4 100644 --- a/src/rules/closeststring_ilp.rs +++ b/src/rules/closeststring_ilp.rs @@ -24,9 +24,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::ClosestString; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; /// Result of reducing ClosestString to ILP. /// @@ -58,17 +57,7 @@ impl ReductionResult for ReductionClosestStringToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/closestsubstring_ilp.rs b/src/rules/closestsubstring_ilp.rs index 2dd7a9fdc..4fe72379b 100644 --- a/src/rules/closestsubstring_ilp.rs +++ b/src/rules/closestsubstring_ilp.rs @@ -32,9 +32,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::ClosestSubstring; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; /// Result of reducing ClosestSubstring to ILP. /// @@ -78,17 +77,7 @@ impl ReductionResult for ReductionClosestSubstringToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/closestvectorproblem_qubo.rs b/src/rules/closestvectorproblem_qubo.rs index 1092b0d30..6b39695c5 100644 --- a/src/rules/closestvectorproblem_qubo.rs +++ b/src/rules/closestvectorproblem_qubo.rs @@ -8,9 +8,8 @@ use crate::export::SolutionPair; use crate::models::algebraic::{ClosestVectorProblem, QUBO}; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use num_bigint::BigInt; use num_traits::Zero; @@ -44,17 +43,7 @@ impl ReductionResult for ReductionCVPToQUBO { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/clustering_ilp.rs b/src/rules/clustering_ilp.rs index ac8c020ef..19ff99343 100644 --- a/src/rules/clustering_ilp.rs +++ b/src/rules/clustering_ilp.rs @@ -8,9 +8,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::Clustering; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; /// Result of reducing Clustering to ILP. #[derive(Debug, Clone)] @@ -33,17 +32,7 @@ impl ReductionResult for ReductionClusteringToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/coloring_ilp.rs b/src/rules/coloring_ilp.rs index cba925a47..adfde3a0b 100644 --- a/src/rules/coloring_ilp.rs +++ b/src/rules/coloring_ilp.rs @@ -11,9 +11,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::KColoring; use crate::reduction; use crate::rules::ilp_helpers::one_hot_decode_rows; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; use crate::variant::{KValue, K1, K2, K3, K4, KN}; @@ -51,17 +50,7 @@ where source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/coloring_qubo.rs b/src/rules/coloring_qubo.rs index ca6f63300..ac50bf60a 100644 --- a/src/rules/coloring_qubo.rs +++ b/src/rules/coloring_qubo.rs @@ -12,9 +12,8 @@ use crate::models::algebraic::QUBO; use crate::models::decision::Decision; use crate::models::graph::KColoring; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; use crate::variant::{KValue, K2, K3, KN}; @@ -44,17 +43,7 @@ impl ReductionResult for ReductionKColoringToQUBO { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/consecutiveblockminimization_ilp.rs b/src/rules/consecutiveblockminimization_ilp.rs index a94e53aee..48d41a176 100644 --- a/src/rules/consecutiveblockminimization_ilp.rs +++ b/src/rules/consecutiveblockminimization_ilp.rs @@ -8,9 +8,8 @@ use crate::models::algebraic::{ }; use crate::reduction; use crate::rules::ilp_helpers::{one_hot_assignment_constraints, one_hot_decode}; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; #[derive(Debug, Clone)] pub struct ReductionCBMToILP { @@ -31,17 +30,7 @@ impl ReductionResult for ReductionCBMToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/consecutiveonesmatrixaugmentation_ilp.rs b/src/rules/consecutiveonesmatrixaugmentation_ilp.rs index 120aa1a7c..145284717 100644 --- a/src/rules/consecutiveonesmatrixaugmentation_ilp.rs +++ b/src/rules/consecutiveonesmatrixaugmentation_ilp.rs @@ -9,7 +9,7 @@ use crate::models::algebraic::{ }; use crate::reduction; use crate::rules::ilp_helpers::{one_hot_assignment_constraints, one_hot_decode}; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; use crate::solvers::SolveOutcome; @@ -32,17 +32,7 @@ impl ReductionResult for ReductionCOMAToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/consecutiveonessubmatrix_ilp.rs b/src/rules/consecutiveonessubmatrix_ilp.rs index c457af937..15f47c45b 100644 --- a/src/rules/consecutiveonessubmatrix_ilp.rs +++ b/src/rules/consecutiveonessubmatrix_ilp.rs @@ -6,7 +6,7 @@ use crate::models::algebraic::{ConsecutiveOnesSubmatrix, LinearConstraint, ObjectiveSense, ILP}; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; use crate::solvers::SolveOutcome; @@ -29,17 +29,7 @@ impl ReductionResult for ReductionCOSToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/consistencyofdatabasefrequencytables_ilp.rs b/src/rules/consistencyofdatabasefrequencytables_ilp.rs index 1872c47a7..c06bb870a 100644 --- a/src/rules/consistencyofdatabasefrequencytables_ilp.rs +++ b/src/rules/consistencyofdatabasefrequencytables_ilp.rs @@ -10,9 +10,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::ConsistencyOfDatabaseFrequencyTables; use crate::reduction; use crate::rules::ilp_helpers::mccormick_product; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; /// Result of reducing ConsistencyOfDatabaseFrequencyTables to ILP. #[derive(Debug, Clone)] @@ -98,17 +97,7 @@ impl ReductionResult for ReductionCDFTToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/decisionmaximumindependentset_integralflowbundles.rs b/src/rules/decisionmaximumindependentset_integralflowbundles.rs index 3147eb2c9..7e8ad154a 100644 --- a/src/rules/decisionmaximumindependentset_integralflowbundles.rs +++ b/src/rules/decisionmaximumindependentset_integralflowbundles.rs @@ -8,7 +8,7 @@ use crate::models::decision::Decision; use crate::models::graph::{IntegralFlowBundles, MaximumIndependentSet}; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; @@ -34,30 +34,11 @@ impl ReductionResult for ReductionDecisionMISToIFB { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } - } -} - -impl ReductionDecisionMISToIFB { - fn map_solution( - &self, - target_solution: &<::Target as crate::traits::Problem>::Solution, - ) -> crate::rules::ExtractionResult< - <::Source as crate::traits::Problem>::Solution, - > { - Ok((0..self.num_source_vertices) - .map(|i| target_solution[2 * i + 1] == 1) - .collect()) + recover_preserving_status(source, target, |solution| { + Ok((0..self.num_source_vertices) + .map(|i| solution[2 * i + 1] == 1) + .collect()) + }) } } diff --git a/src/rules/decisionminimumdominatingset_minimumsummulticenter.rs b/src/rules/decisionminimumdominatingset_minimumsummulticenter.rs index 6ab2ece88..9a7dae9c6 100644 --- a/src/rules/decisionminimumdominatingset_minimumsummulticenter.rs +++ b/src/rules/decisionminimumdominatingset_minimumsummulticenter.rs @@ -8,9 +8,8 @@ use crate::models::decision::Decision; use crate::models::graph::{MinimumDominatingSet, MinimumSumMulticenter}; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; use crate::types::One; @@ -34,17 +33,7 @@ impl ReductionResult for ReductionDecisionMinimumDominatingSetToMinimumSumMultic source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/decisionminimumdominatingset_minmaxmulticenter.rs b/src/rules/decisionminimumdominatingset_minmaxmulticenter.rs index ef7dc2606..f8afe385e 100644 --- a/src/rules/decisionminimumdominatingset_minmaxmulticenter.rs +++ b/src/rules/decisionminimumdominatingset_minmaxmulticenter.rs @@ -7,9 +7,8 @@ use crate::models::decision::Decision; use crate::models::graph::{MinMaxMulticenter, MinimumDominatingSet}; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; use crate::types::One; @@ -33,28 +32,9 @@ impl ReductionResult for ReductionDecisionMinimumDominatingSetToMinMaxMulticente source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } - } -} - -impl ReductionDecisionMinimumDominatingSetToMinMaxMulticenter { - fn map_solution( - &self, - target_solution: &<::Target as crate::traits::Problem>::Solution, - ) -> crate::rules::ExtractionResult< - <::Source as crate::traits::Problem>::Solution, - > { - Ok(target_solution[..self.source_num_vertices].to_vec()) + recover_preserving_status(source, target, |solution| { + Ok(solution[..self.source_num_vertices].to_vec()) + }) } } diff --git a/src/rules/decisionminimumvertexcover_hamiltoniancircuit.rs b/src/rules/decisionminimumvertexcover_hamiltoniancircuit.rs index 1fac88148..b1f931e2a 100644 --- a/src/rules/decisionminimumvertexcover_hamiltoniancircuit.rs +++ b/src/rules/decisionminimumvertexcover_hamiltoniancircuit.rs @@ -6,9 +6,8 @@ use crate::models::decision::Decision; use crate::models::graph::{HamiltonianCircuit, MinimumVertexCover}; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; use crate::types::One; use std::collections::BTreeSet; @@ -249,17 +248,7 @@ impl ReductionResult for ReductionDecisionMinimumVertexCoverToHamiltonianCircuit source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/directedhamiltonianpath_ilp.rs b/src/rules/directedhamiltonianpath_ilp.rs index da61f518d..1dbe99cee 100644 --- a/src/rules/directedhamiltonianpath_ilp.rs +++ b/src/rules/directedhamiltonianpath_ilp.rs @@ -10,9 +10,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::DirectedHamiltonianPath; use crate::reduction; use crate::rules::ilp_helpers::{one_hot_assignment_constraints, one_hot_decode}; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; /// Result of reducing DirectedHamiltonianPath to ILP. /// @@ -37,17 +36,7 @@ impl ReductionResult for ReductionDirectedHamiltonianPathToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/directedtwocommodityintegralflow_ilp.rs b/src/rules/directedtwocommodityintegralflow_ilp.rs index 8d1472158..b0845d575 100644 --- a/src/rules/directedtwocommodityintegralflow_ilp.rs +++ b/src/rules/directedtwocommodityintegralflow_ilp.rs @@ -15,9 +15,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::DirectedTwoCommodityIntegralFlow; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; /// Result of reducing DirectedTwoCommodityIntegralFlow to `ILP`. /// @@ -44,17 +43,7 @@ impl ReductionResult for ReductionD2CIFToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/disjointconnectingpaths_ilp.rs b/src/rules/disjointconnectingpaths_ilp.rs index 20b641317..d3f81e52c 100644 --- a/src/rules/disjointconnectingpaths_ilp.rs +++ b/src/rules/disjointconnectingpaths_ilp.rs @@ -6,9 +6,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::DisjointConnectingPaths; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::topology::SimpleGraph; use std::collections::VecDeque; @@ -43,17 +42,7 @@ impl ReductionResult for ReductionDCPToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/ensemblecomputation_ilp.rs b/src/rules/ensemblecomputation_ilp.rs index 2524ea5e4..a61199023 100644 --- a/src/rules/ensemblecomputation_ilp.rs +++ b/src/rules/ensemblecomputation_ilp.rs @@ -3,9 +3,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::EnsembleComputation; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; #[derive(Debug, Clone)] pub struct ReductionEnsembleComputationToILP { @@ -45,17 +44,7 @@ impl ReductionResult for ReductionEnsembleComputationToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/eulerianpath_ilp.rs b/src/rules/eulerianpath_ilp.rs index 51a3e0697..24c5b20b3 100644 --- a/src/rules/eulerianpath_ilp.rs +++ b/src/rules/eulerianpath_ilp.rs @@ -26,9 +26,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::EulerianPath; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; /// Result of reducing EulerianPath to `ILP`. /// @@ -76,17 +75,7 @@ impl ReductionResult for ReductionEulerianPathToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/exactcoverby3sets_boundeddiameterspanningtree.rs b/src/rules/exactcoverby3sets_boundeddiameterspanningtree.rs index 60a51ba95..4bf1bd7c0 100644 --- a/src/rules/exactcoverby3sets_boundeddiameterspanningtree.rs +++ b/src/rules/exactcoverby3sets_boundeddiameterspanningtree.rs @@ -34,9 +34,8 @@ use crate::models::graph::BoundedDiameterSpanningTree; use crate::models::set::ExactCoverBy3Sets; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::topology::SimpleGraph; use std::collections::HashSet; @@ -101,17 +100,7 @@ impl ReductionResult for ReductionX3CToBoundedDiameterSpanningTree { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/exactcoverby3sets_ilp.rs b/src/rules/exactcoverby3sets_ilp.rs index 1b6796cea..ef0a968ce 100644 --- a/src/rules/exactcoverby3sets_ilp.rs +++ b/src/rules/exactcoverby3sets_ilp.rs @@ -6,9 +6,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::set::ExactCoverBy3Sets; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; #[derive(Debug, Clone)] pub struct ReductionX3CToILP { @@ -28,28 +27,9 @@ impl ReductionResult for ReductionX3CToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } - } -} - -impl ReductionX3CToILP { - fn map_solution( - &self, - target_solution: &<::Target as crate::traits::Problem>::Solution, - ) -> crate::rules::ExtractionResult< - <::Source as crate::traits::Problem>::Solution, - > { - Ok(target_solution.iter().map(|&value| value == 1).collect()) + recover_preserving_status(source, target, |solution| { + Ok(solution.iter().map(|&value| value == 1).collect()) + }) } } diff --git a/src/rules/exactcoverby3sets_staffscheduling.rs b/src/rules/exactcoverby3sets_staffscheduling.rs index 40c221a93..8d0f8a6f4 100644 --- a/src/rules/exactcoverby3sets_staffscheduling.rs +++ b/src/rules/exactcoverby3sets_staffscheduling.rs @@ -13,9 +13,8 @@ use crate::models::misc::StaffScheduling; use crate::models::set::ExactCoverBy3Sets; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; /// Result of reducing ExactCoverBy3Sets to StaffScheduling. #[derive(Debug, Clone)] @@ -40,28 +39,9 @@ impl ReductionResult for ReductionXC3SToStaffScheduling { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } - } -} - -impl ReductionXC3SToStaffScheduling { - fn map_solution( - &self, - target_solution: &<::Target as crate::traits::Problem>::Solution, - ) -> crate::rules::ExtractionResult< - <::Source as crate::traits::Problem>::Solution, - > { - Ok(target_solution.iter().map(|&count| count > 0).collect()) + recover_preserving_status(source, target, |solution| { + Ok(solution.iter().map(|&count| count > 0).collect()) + }) } } diff --git a/src/rules/expectedretrievalcost_ilp.rs b/src/rules/expectedretrievalcost_ilp.rs index b6a6931db..bf365e7aa 100644 --- a/src/rules/expectedretrievalcost_ilp.rs +++ b/src/rules/expectedretrievalcost_ilp.rs @@ -18,9 +18,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::ExpectedRetrievalCost; use crate::reduction; use crate::rules::ilp_helpers::{mccormick_product, one_hot_decode_rows}; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; /// Result of reducing ExpectedRetrievalCost to ILP. /// @@ -61,17 +60,7 @@ impl ReductionResult for ReductionERCToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/factoring_circuit.rs b/src/rules/factoring_circuit.rs index 147c6f9d0..caab8403b 100644 --- a/src/rules/factoring_circuit.rs +++ b/src/rules/factoring_circuit.rs @@ -10,9 +10,8 @@ use crate::models::formula::{Assignment, BooleanExpr, Circuit, CircuitSAT}; use crate::models::misc::Factoring; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use num_bigint::BigUint; use num_traits::{One, Zero}; /// Result of reducing Factoring to CircuitSAT. @@ -50,17 +49,7 @@ impl ReductionResult for ReductionFactoringToCircuit { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/factoring_ilp.rs b/src/rules/factoring_ilp.rs index ea99ebe09..c89568aca 100644 --- a/src/rules/factoring_ilp.rs +++ b/src/rules/factoring_ilp.rs @@ -23,9 +23,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::Factoring; use crate::reduction; use crate::rules::ilp_helpers::mccormick_product; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use std::cmp::min; /// Result of reducing Factoring to ILP. @@ -83,17 +82,7 @@ impl ReductionResult for ReductionFactoringToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/feasibleregisterassignment_ilp.rs b/src/rules/feasibleregisterassignment_ilp.rs index 4de2b6e5f..6139482de 100644 --- a/src/rules/feasibleregisterassignment_ilp.rs +++ b/src/rules/feasibleregisterassignment_ilp.rs @@ -13,9 +13,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::FeasibleRegisterAssignment; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; #[derive(Debug, Clone)] pub struct ReductionFeasibleRegisterAssignmentToILP { @@ -36,17 +35,7 @@ impl ReductionResult for ReductionFeasibleRegisterAssignmentToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/flowshopscheduling_ilp.rs b/src/rules/flowshopscheduling_ilp.rs index 72744f075..4dbbb2a8d 100644 --- a/src/rules/flowshopscheduling_ilp.rs +++ b/src/rules/flowshopscheduling_ilp.rs @@ -8,9 +8,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::FlowShopScheduling; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; /// Result of reducing FlowShopScheduling to `ILP`. /// @@ -42,17 +41,7 @@ impl ReductionResult for ReductionFSSToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/graphpartitioning_ilp.rs b/src/rules/graphpartitioning_ilp.rs index 5e539440d..dfb46b391 100644 --- a/src/rules/graphpartitioning_ilp.rs +++ b/src/rules/graphpartitioning_ilp.rs @@ -8,9 +8,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::GraphPartitioning; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; /// Result of reducing GraphPartitioning to ILP. @@ -37,17 +36,7 @@ impl ReductionResult for ReductionGraphPartitioningToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/hamiltoniancircuit_biconnectivityaugmentation.rs b/src/rules/hamiltoniancircuit_biconnectivityaugmentation.rs index 2ef296bfe..cf2cef02b 100644 --- a/src/rules/hamiltoniancircuit_biconnectivityaugmentation.rs +++ b/src/rules/hamiltoniancircuit_biconnectivityaugmentation.rs @@ -23,9 +23,8 @@ use crate::models::graph::{BiconnectivityAugmentation, HamiltonianCircuit}; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; /// Result of reducing HamiltonianCircuit to BiconnectivityAugmentation. @@ -54,17 +53,7 @@ impl ReductionResult for ReductionHamiltonianCircuitToBiconnectivityAugmentation source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/hamiltoniancircuit_hamiltonianpath.rs b/src/rules/hamiltoniancircuit_hamiltonianpath.rs index 78123c127..2c0580048 100644 --- a/src/rules/hamiltoniancircuit_hamiltonianpath.rs +++ b/src/rules/hamiltoniancircuit_hamiltonianpath.rs @@ -14,9 +14,8 @@ use crate::models::graph::{HamiltonianCircuit, HamiltonianPath}; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; /// Result of reducing HamiltonianCircuit to HamiltonianPath. @@ -43,17 +42,7 @@ impl ReductionResult for ReductionHamiltonianCircuitToHamiltonianPath { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/hamiltoniancircuit_longestcircuit.rs b/src/rules/hamiltoniancircuit_longestcircuit.rs index 5c7f9d37d..67a490e10 100644 --- a/src/rules/hamiltoniancircuit_longestcircuit.rs +++ b/src/rules/hamiltoniancircuit_longestcircuit.rs @@ -7,9 +7,8 @@ use crate::models::decision::Decision; use crate::models::graph::{HamiltonianCircuit, LongestCircuit}; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; /// Result of reducing HamiltonianCircuit to LongestCircuit. @@ -31,17 +30,7 @@ impl ReductionResult for ReductionHamiltonianCircuitToLongestCircuit { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/hamiltoniancircuit_quadraticassignment.rs b/src/rules/hamiltoniancircuit_quadraticassignment.rs index a9ca7bf52..13fcb1802 100644 --- a/src/rules/hamiltoniancircuit_quadraticassignment.rs +++ b/src/rules/hamiltoniancircuit_quadraticassignment.rs @@ -9,9 +9,8 @@ use crate::models::algebraic::QuadraticAssignment; use crate::models::decision::Decision; use crate::models::graph::HamiltonianCircuit; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; /// Result of reducing HamiltonianCircuit to QuadraticAssignment. @@ -33,17 +32,7 @@ impl ReductionResult for ReductionHamiltonianCircuitToQuadraticAssignment { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/hamiltoniancircuit_ruralpostman.rs b/src/rules/hamiltoniancircuit_ruralpostman.rs index ba90b8d1f..300ef86fb 100644 --- a/src/rules/hamiltoniancircuit_ruralpostman.rs +++ b/src/rules/hamiltoniancircuit_ruralpostman.rs @@ -26,9 +26,8 @@ use crate::models::decision::Decision; use crate::models::graph::{HamiltonianCircuit, RuralPostman}; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; /// Result of reducing HamiltonianCircuit to RuralPostman. @@ -54,17 +53,7 @@ impl ReductionResult for ReductionHamiltonianCircuitToRuralPostman { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/hamiltoniancircuit_stackercrane.rs b/src/rules/hamiltoniancircuit_stackercrane.rs index b89cd5a28..f262c1108 100644 --- a/src/rules/hamiltoniancircuit_stackercrane.rs +++ b/src/rules/hamiltoniancircuit_stackercrane.rs @@ -16,9 +16,8 @@ use crate::models::decision::Decision; use crate::models::graph::HamiltonianCircuit; use crate::models::misc::StackerCrane; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; /// Result of reducing HamiltonianCircuit to StackerCrane. @@ -40,17 +39,7 @@ impl ReductionResult for ReductionHamiltonianCircuitToStackerCrane { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/hamiltoniancircuit_strongconnectivityaugmentation.rs b/src/rules/hamiltoniancircuit_strongconnectivityaugmentation.rs index 1f028bda9..fae3b2ffb 100644 --- a/src/rules/hamiltoniancircuit_strongconnectivityaugmentation.rs +++ b/src/rules/hamiltoniancircuit_strongconnectivityaugmentation.rs @@ -9,9 +9,8 @@ use crate::models::graph::{HamiltonianCircuit, StrongConnectivityAugmentation}; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::topology::{DirectedGraph, Graph, SimpleGraph}; /// Result of reducing HamiltonianCircuit to StrongConnectivityAugmentation. @@ -34,17 +33,7 @@ impl ReductionResult for ReductionHamiltonianCircuitToStrongConnectivityAugmenta source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/hamiltonianpath_degreeconstrainedspanningtree.rs b/src/rules/hamiltonianpath_degreeconstrainedspanningtree.rs index 79a13e39a..3f4d1938d 100644 --- a/src/rules/hamiltonianpath_degreeconstrainedspanningtree.rs +++ b/src/rules/hamiltonianpath_degreeconstrainedspanningtree.rs @@ -4,9 +4,8 @@ use crate::models::graph::{DegreeConstrainedSpanningTree, HamiltonianPath}; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; /// Result of reducing HamiltonianPath to DegreeConstrainedSpanningTree. @@ -28,17 +27,7 @@ impl ReductionResult for ReductionHamiltonianPathToDegreeConstrainedSpanningTree source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/hamiltonianpath_ilp.rs b/src/rules/hamiltonianpath_ilp.rs index 3a5d9cccc..dd50ef1ca 100644 --- a/src/rules/hamiltonianpath_ilp.rs +++ b/src/rules/hamiltonianpath_ilp.rs @@ -12,9 +12,8 @@ use crate::reduction; use crate::rules::ilp_helpers::{ mccormick_product, one_hot_assignment_constraints, one_hot_decode, }; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; /// Result of reducing HamiltonianPath to ILP. @@ -42,17 +41,7 @@ impl ReductionResult for ReductionHamiltonianPathToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/hamiltonianpathbetweentwovertices_longestpath.rs b/src/rules/hamiltonianpathbetweentwovertices_longestpath.rs index 46a5b1c3d..c8c722cfa 100644 --- a/src/rules/hamiltonianpathbetweentwovertices_longestpath.rs +++ b/src/rules/hamiltonianpathbetweentwovertices_longestpath.rs @@ -8,9 +8,8 @@ use crate::models::decision::Decision; use crate::models::graph::{HamiltonianPathBetweenTwoVertices, LongestPath}; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; use crate::types::One; @@ -37,17 +36,7 @@ impl ReductionResult for ReductionHPBTVToLP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/highlyconnecteddeletion_ilp.rs b/src/rules/highlyconnecteddeletion_ilp.rs index 8d5c0ec1b..68986c6ef 100644 --- a/src/rules/highlyconnecteddeletion_ilp.rs +++ b/src/rules/highlyconnecteddeletion_ilp.rs @@ -28,9 +28,8 @@ 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::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; /// Result of reducing HighlyConnectedDeletion to ILP. @@ -67,17 +66,7 @@ impl ReductionResult for ReductionHighlyConnectedDeletionToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/ilp_i64_ilp_bool.rs b/src/rules/ilp_i64_ilp_bool.rs index bf21b37f6..88b1a8e8a 100644 --- a/src/rules/ilp_i64_ilp_bool.rs +++ b/src/rules/ilp_i64_ilp_bool.rs @@ -2,10 +2,9 @@ use crate::models::algebraic::{Comparison, LinearConstraint, ILP}; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::rules::ReductionError; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; #[derive(Debug, Clone)] struct VarEncoding { @@ -87,17 +86,7 @@ impl ReductionResult for ReductionIntILPToBinaryILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/integerknapsack_ilp.rs b/src/rules/integerknapsack_ilp.rs index 9f2151de0..c2c25b876 100644 --- a/src/rules/integerknapsack_ilp.rs +++ b/src/rules/integerknapsack_ilp.rs @@ -7,9 +7,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::set::IntegerKnapsack; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; #[derive(Debug, Clone)] pub struct ReductionIntegerKnapsackToILP { @@ -29,17 +28,7 @@ impl ReductionResult for ReductionIntegerKnapsackToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/integralflowbundles_ilp.rs b/src/rules/integralflowbundles_ilp.rs index e75a9af2a..c5644f7e6 100644 --- a/src/rules/integralflowbundles_ilp.rs +++ b/src/rules/integralflowbundles_ilp.rs @@ -7,9 +7,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::IntegralFlowBundles; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; /// Result of reducing IntegralFlowBundles to ILP. #[derive(Debug, Clone)] @@ -30,17 +29,7 @@ impl ReductionResult for ReductionIFBToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/integralflowhomologousarcs_ilp.rs b/src/rules/integralflowhomologousarcs_ilp.rs index ebe49d6bc..c4e98cae2 100644 --- a/src/rules/integralflowhomologousarcs_ilp.rs +++ b/src/rules/integralflowhomologousarcs_ilp.rs @@ -6,9 +6,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::IntegralFlowHomologousArcs; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; /// Result of reducing IntegralFlowHomologousArcs to ILP. #[derive(Debug, Clone)] @@ -29,17 +28,7 @@ impl ReductionResult for ReductionIFHAToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/integralflowwithmultipliers_ilp.rs b/src/rules/integralflowwithmultipliers_ilp.rs index 2895519bd..f8f73f6cd 100644 --- a/src/rules/integralflowwithmultipliers_ilp.rs +++ b/src/rules/integralflowwithmultipliers_ilp.rs @@ -6,9 +6,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::IntegralFlowWithMultipliers; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; /// Result of reducing IntegralFlowWithMultipliers to ILP. #[derive(Debug, Clone)] @@ -29,17 +28,7 @@ impl ReductionResult for ReductionIFWMToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/isomorphicspanningtree_ilp.rs b/src/rules/isomorphicspanningtree_ilp.rs index 7220e6010..34de4e431 100644 --- a/src/rules/isomorphicspanningtree_ilp.rs +++ b/src/rules/isomorphicspanningtree_ilp.rs @@ -6,9 +6,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::IsomorphicSpanningTree; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; #[derive(Debug, Clone)] @@ -31,17 +30,7 @@ impl ReductionResult for ReductionISTToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/kclique_balancedcompletebipartitesubgraph.rs b/src/rules/kclique_balancedcompletebipartitesubgraph.rs index dcdc10542..cc076a650 100644 --- a/src/rules/kclique_balancedcompletebipartitesubgraph.rs +++ b/src/rules/kclique_balancedcompletebipartitesubgraph.rs @@ -7,9 +7,8 @@ use crate::models::graph::{BalancedCompleteBipartiteSubgraph, KClique}; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::topology::{BipartiteGraph, Graph, SimpleGraph}; /// Result of reducing KClique to BalancedCompleteBipartiteSubgraph. @@ -41,17 +40,7 @@ impl ReductionResult for ReductionKCliqueToBCBS { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/kclique_conjunctivebooleanquery.rs b/src/rules/kclique_conjunctivebooleanquery.rs index cef642bb0..7882dc6d3 100644 --- a/src/rules/kclique_conjunctivebooleanquery.rs +++ b/src/rules/kclique_conjunctivebooleanquery.rs @@ -11,9 +11,8 @@ use crate::models::graph::KClique; use crate::models::misc::{CbqRelation, ConjunctiveBooleanQuery, QueryArg}; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; /// Result of reducing KClique to ConjunctiveBooleanQuery. @@ -41,17 +40,7 @@ impl ReductionResult for ReductionKCliqueToCBQ { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/kclique_ilp.rs b/src/rules/kclique_ilp.rs index 0fa879525..dbcf83d6d 100644 --- a/src/rules/kclique_ilp.rs +++ b/src/rules/kclique_ilp.rs @@ -12,9 +12,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::KClique; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; /// Result of reducing KClique to ILP. @@ -46,28 +45,9 @@ impl ReductionResult for ReductionKCliqueToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } - } -} - -impl ReductionKCliqueToILP { - fn map_solution( - &self, - target_solution: &<::Target as crate::traits::Problem>::Solution, - ) -> crate::rules::ExtractionResult< - <::Source as crate::traits::Problem>::Solution, - > { - Ok(target_solution.iter().map(|&value| value == 1).collect()) + recover_preserving_status(source, target, |solution| { + Ok(solution.iter().map(|&value| value == 1).collect()) + }) } } diff --git a/src/rules/kclique_subgraphisomorphism.rs b/src/rules/kclique_subgraphisomorphism.rs index 02e8a032f..2d97aebd8 100644 --- a/src/rules/kclique_subgraphisomorphism.rs +++ b/src/rules/kclique_subgraphisomorphism.rs @@ -7,9 +7,8 @@ use crate::models::graph::{KClique, SubgraphIsomorphism}; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::topology::SimpleGraph; /// Result of reducing KClique to SubgraphIsomorphism. @@ -41,17 +40,7 @@ impl ReductionResult for ReductionKCliqueToSubIso { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/kcoloring_bicliquecover.rs b/src/rules/kcoloring_bicliquecover.rs index 90f791d25..238e78519 100644 --- a/src/rules/kcoloring_bicliquecover.rs +++ b/src/rules/kcoloring_bicliquecover.rs @@ -31,9 +31,8 @@ use crate::models::graph::{BicliqueCover, KColoring}; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::topology::{BipartiteGraph, Graph, SimpleGraph}; use crate::variant::KN; use std::collections::BTreeSet; @@ -72,17 +71,7 @@ impl ReductionResult for ReductionKColoringToBicliqueCover { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/kcoloring_clustering.rs b/src/rules/kcoloring_clustering.rs index 8726d488d..25bf3ade2 100644 --- a/src/rules/kcoloring_clustering.rs +++ b/src/rules/kcoloring_clustering.rs @@ -7,9 +7,8 @@ use crate::models::graph::KColoring; use crate::models::misc::Clustering; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; use crate::variant::K3; @@ -35,28 +34,9 @@ impl ReductionResult for ReductionKColoringToClustering { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } - } -} - -impl ReductionKColoringToClustering { - fn map_solution( - &self, - target_solution: &<::Target as crate::traits::Problem>::Solution, - ) -> crate::rules::ExtractionResult< - <::Source as crate::traits::Problem>::Solution, - > { - Ok(target_solution[..self.source_num_vertices].to_vec()) + recover_preserving_status(source, target, |solution| { + Ok(solution[..self.source_num_vertices].to_vec()) + }) } } diff --git a/src/rules/kcoloring_twodimensionalconsecutivesets.rs b/src/rules/kcoloring_twodimensionalconsecutivesets.rs index 8e35fe1e4..2c3554180 100644 --- a/src/rules/kcoloring_twodimensionalconsecutivesets.rs +++ b/src/rules/kcoloring_twodimensionalconsecutivesets.rs @@ -15,9 +15,8 @@ use crate::models::graph::KColoring; use crate::models::set::TwoDimensionalConsecutiveSets; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; use crate::variant::K3; @@ -48,17 +47,7 @@ impl ReductionResult for ReductionKColoringToTDCS { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/knapsack_ilp.rs b/src/rules/knapsack_ilp.rs index 51514e5e1..31fd9be36 100644 --- a/src/rules/knapsack_ilp.rs +++ b/src/rules/knapsack_ilp.rs @@ -8,9 +8,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::Knapsack; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; /// Result of reducing Knapsack to ILP. #[derive(Debug, Clone)] @@ -31,28 +30,9 @@ impl ReductionResult for ReductionKnapsackToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } - } -} - -impl ReductionKnapsackToILP { - fn map_solution( - &self, - target_solution: &<::Target as crate::traits::Problem>::Solution, - ) -> crate::rules::ExtractionResult< - <::Source as crate::traits::Problem>::Solution, - > { - Ok(target_solution.iter().map(|&value| value == 1).collect()) + recover_preserving_status(source, target, |solution| { + Ok(solution.iter().map(|&value| value == 1).collect()) + }) } } diff --git a/src/rules/ksatisfiability_acyclicpartition.rs b/src/rules/ksatisfiability_acyclicpartition.rs index f3af8049a..554843ceb 100644 --- a/src/rules/ksatisfiability_acyclicpartition.rs +++ b/src/rules/ksatisfiability_acyclicpartition.rs @@ -9,9 +9,8 @@ use crate::models::formula::KSatisfiability; use crate::models::graph::{AcyclicPartition, KClique}; use crate::reduction; use crate::rules::ksatisfiability_kclique::Reduction3SATToKClique; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::topology::{DirectedGraph, Graph, SimpleGraph}; use crate::variant::K3; @@ -36,17 +35,7 @@ impl ReductionResult for Reduction3SATToAcyclicPartition { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/ksatisfiability_bicliquecover.rs b/src/rules/ksatisfiability_bicliquecover.rs index a165ce657..9dadcd3d7 100644 --- a/src/rules/ksatisfiability_bicliquecover.rs +++ b/src/rules/ksatisfiability_bicliquecover.rs @@ -48,9 +48,8 @@ use crate::models::formula::CNFClause; use crate::models::formula::KSatisfiability; use crate::models::graph::BicliqueCover; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::topology::BipartiteGraph; use crate::variant::K3; use std::collections::BTreeSet; @@ -97,17 +96,7 @@ impl ReductionResult for ReductionKSatisfiabilityToBicliqueCover { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/ksatisfiability_cyclicordering.rs b/src/rules/ksatisfiability_cyclicordering.rs index 2186d3b18..eae615b82 100644 --- a/src/rules/ksatisfiability_cyclicordering.rs +++ b/src/rules/ksatisfiability_cyclicordering.rs @@ -20,9 +20,8 @@ use crate::models::formula::KSatisfiability; use crate::models::misc::CyclicOrdering; use crate::reduction; use crate::rules::sat_helpers::SatVariableAllocator; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::variant::K3; use std::collections::BTreeSet; @@ -46,17 +45,7 @@ impl ReductionResult for Reduction3SATToCyclicOrdering { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/ksatisfiability_decisionminimumvertexcover.rs b/src/rules/ksatisfiability_decisionminimumvertexcover.rs index d05c6f51b..eef000b06 100644 --- a/src/rules/ksatisfiability_decisionminimumvertexcover.rs +++ b/src/rules/ksatisfiability_decisionminimumvertexcover.rs @@ -15,9 +15,8 @@ use crate::models::decision::Decision; use crate::models::formula::KSatisfiability; use crate::models::graph::MinimumVertexCover; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::topology::SimpleGraph; use crate::variant::K3; @@ -48,30 +47,9 @@ impl ReductionResult for Reduction3SATToDecisionMVC { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } - } -} - -impl Reduction3SATToDecisionMVC { - fn map_solution( - &self, - target_solution: &<::Target as crate::traits::Problem>::Solution, - ) -> crate::rules::ExtractionResult< - <::Source as crate::traits::Problem>::Solution, - > { - Ok((0..self.source_num_vars) - .map(|i| target_solution[2 * i]) - .collect()) + recover_preserving_status(source, target, |solution| { + Ok((0..self.source_num_vars).map(|i| solution[2 * i]).collect()) + }) } } diff --git a/src/rules/ksatisfiability_directedtwocommodityintegralflow.rs b/src/rules/ksatisfiability_directedtwocommodityintegralflow.rs index a91db59fb..accd44459 100644 --- a/src/rules/ksatisfiability_directedtwocommodityintegralflow.rs +++ b/src/rules/ksatisfiability_directedtwocommodityintegralflow.rs @@ -10,9 +10,8 @@ use crate::models::formula::KSatisfiability; use crate::models::graph::DirectedTwoCommodityIntegralFlow; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::topology::DirectedGraph; use crate::variant::K3; @@ -178,17 +177,7 @@ impl ReductionResult for Reduction3SATToDirectedTwoCommodityIntegralFlow { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/ksatisfiability_feasibleregisterassignment.rs b/src/rules/ksatisfiability_feasibleregisterassignment.rs index e0e5f8c16..55ba3cd7a 100644 --- a/src/rules/ksatisfiability_feasibleregisterassignment.rs +++ b/src/rules/ksatisfiability_feasibleregisterassignment.rs @@ -15,7 +15,7 @@ use crate::models::formula::KSatisfiability; use crate::models::misc::FeasibleRegisterAssignment; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; use crate::solvers::SolveOutcome; use crate::variant::K3; @@ -81,17 +81,7 @@ impl ReductionResult for Reduction3SATToFeasibleRegisterAssignment { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/ksatisfiability_kclique.rs b/src/rules/ksatisfiability_kclique.rs index 0a8f50fc5..6d6715cb1 100644 --- a/src/rules/ksatisfiability_kclique.rs +++ b/src/rules/ksatisfiability_kclique.rs @@ -9,9 +9,8 @@ use crate::models::formula::KSatisfiability; use crate::models::graph::KClique; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::topology::SimpleGraph; use crate::variant::K3; @@ -36,17 +35,7 @@ impl ReductionResult for Reduction3SATToKClique { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/ksatisfiability_kernel.rs b/src/rules/ksatisfiability_kernel.rs index 2600b7395..83281275b 100644 --- a/src/rules/ksatisfiability_kernel.rs +++ b/src/rules/ksatisfiability_kernel.rs @@ -8,9 +8,8 @@ use crate::models::formula::KSatisfiability; use crate::models::graph::Kernel; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::topology::DirectedGraph; use crate::variant::K3; use std::collections::BTreeSet; @@ -36,17 +35,7 @@ impl ReductionResult for Reduction3SatToKernel { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/ksatisfiability_monochromatictriangle.rs b/src/rules/ksatisfiability_monochromatictriangle.rs index 28e945dff..9fa59f1a8 100644 --- a/src/rules/ksatisfiability_monochromatictriangle.rs +++ b/src/rules/ksatisfiability_monochromatictriangle.rs @@ -13,7 +13,7 @@ use crate::models::graph::MonochromaticTriangle; use crate::reduction; use crate::rules::sat_helpers::SatVariableAllocator; use crate::rules::satisfiability_naesatisfiability::ReductionSATToNAESAT; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; use crate::solvers::SolveOutcome; use crate::topology::SimpleGraph; @@ -58,17 +58,7 @@ impl ReductionResult for Reduction3SATToMonochromaticTriangle { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/ksatisfiability_oneinthreesatisfiability.rs b/src/rules/ksatisfiability_oneinthreesatisfiability.rs index 71b0a550e..f1f3931c0 100644 --- a/src/rules/ksatisfiability_oneinthreesatisfiability.rs +++ b/src/rules/ksatisfiability_oneinthreesatisfiability.rs @@ -8,9 +8,8 @@ use crate::models::formula::{CNFClause, KSatisfiability, OneInThreeSatisfiability}; use crate::reduction; use crate::rules::sat_helpers::SatVariableAllocator; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::variant::K3; use std::collections::BTreeSet; @@ -34,17 +33,7 @@ impl ReductionResult for Reduction3SATToOneInThreeSAT { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/ksatisfiability_quadraticcongruences.rs b/src/rules/ksatisfiability_quadraticcongruences.rs index d393171f3..8f4b83ce7 100644 --- a/src/rules/ksatisfiability_quadraticcongruences.rs +++ b/src/rules/ksatisfiability_quadraticcongruences.rs @@ -7,13 +7,12 @@ //! orients every sign by the distinguished odd coordinate. use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use std::collections::{BTreeMap, BTreeSet}; use crate::models::algebraic::QuadraticCongruences; use crate::models::formula::KSatisfiability; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::variant::K3; use num_bigint::{BigInt, BigUint}; #[cfg(any(test, feature = "example-db"))] @@ -43,17 +42,7 @@ impl ReductionResult for Reduction3SATToQuadraticCongruences { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/ksatisfiability_quadraticdiophantineequations.rs b/src/rules/ksatisfiability_quadraticdiophantineequations.rs index 29650df7c..d4973c318 100644 --- a/src/rules/ksatisfiability_quadraticdiophantineequations.rs +++ b/src/rules/ksatisfiability_quadraticdiophantineequations.rs @@ -8,9 +8,8 @@ use crate::models::algebraic::{QuadraticCongruences, QuadraticDiophantineEquatio use crate::models::formula::KSatisfiability; use crate::reduction; use crate::rules::ksatisfiability_quadraticcongruences::Reduction3SATToQuadraticCongruences; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::variant::K3; use num_bigint::BigUint; use num_traits::One; @@ -35,17 +34,7 @@ impl ReductionResult for Reduction3SATToQuadraticDiophantineEquations { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/ksatisfiability_qubo.rs b/src/rules/ksatisfiability_qubo.rs index e2022805b..eb472d622 100644 --- a/src/rules/ksatisfiability_qubo.rs +++ b/src/rules/ksatisfiability_qubo.rs @@ -16,9 +16,8 @@ use crate::models::algebraic::QUBO; use crate::models::decision::Decision; use crate::models::formula::KSatisfiability; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::variant::{K2, K3}; /// Result of reducing KSatisfiability to QUBO. #[derive(Debug, Clone)] @@ -40,28 +39,9 @@ impl ReductionResult for ReductionKSatToQUBO { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } - } -} - -impl ReductionKSatToQUBO { - fn map_solution( - &self, - target_solution: &<::Target as crate::traits::Problem>::Solution, - ) -> crate::rules::ExtractionResult< - <::Source as crate::traits::Problem>::Solution, - > { - Ok(target_solution[..self.source_num_vars].to_vec()) + recover_preserving_status(source, target, |solution| { + Ok(solution[..self.source_num_vars].to_vec()) + }) } } @@ -85,28 +65,9 @@ impl ReductionResult for Reduction3SATToQUBO { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } - } -} - -impl Reduction3SATToQUBO { - fn map_solution( - &self, - target_solution: &<::Target as crate::traits::Problem>::Solution, - ) -> crate::rules::ExtractionResult< - <::Source as crate::traits::Problem>::Solution, - > { - Ok(target_solution[..self.source_num_vars].to_vec()) + recover_preserving_status(source, target, |solution| { + Ok(solution[..self.source_num_vars].to_vec()) + }) } } diff --git a/src/rules/ksatisfiability_registersufficiency.rs b/src/rules/ksatisfiability_registersufficiency.rs index 7177a80b7..91950c4be 100644 --- a/src/rules/ksatisfiability_registersufficiency.rs +++ b/src/rules/ksatisfiability_registersufficiency.rs @@ -12,7 +12,7 @@ use crate::models::formula::KSatisfiability; use crate::models::misc::RegisterSufficiency; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; use crate::solvers::SolveOutcome; use crate::variant::K3; @@ -299,17 +299,7 @@ impl ReductionResult for Reduction3SATToRegisterSufficiency { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/ksatisfiability_simultaneousincongruences.rs b/src/rules/ksatisfiability_simultaneousincongruences.rs index 44bab5993..0b0a6fa29 100644 --- a/src/rules/ksatisfiability_simultaneousincongruences.rs +++ b/src/rules/ksatisfiability_simultaneousincongruences.rs @@ -5,13 +5,12 @@ //! residue class via the Chinese Remainder Theorem. use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use std::collections::BTreeMap; use crate::models::algebraic::SimultaneousIncongruences; use crate::models::formula::{ksat::first_n_odd_primes, CNFClause, KSatisfiability}; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::variant::K3; #[derive(Debug, Clone)] @@ -33,17 +32,7 @@ impl ReductionResult for Reduction3SATToSimultaneousIncongruences { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/ksatisfiability_subsetsum.rs b/src/rules/ksatisfiability_subsetsum.rs index 3cb4cc35c..3f11fb0ea 100644 --- a/src/rules/ksatisfiability_subsetsum.rs +++ b/src/rules/ksatisfiability_subsetsum.rs @@ -15,9 +15,8 @@ use crate::models::formula::KSatisfiability; use crate::models::misc::SubsetSum; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::variant::K3; use num_bigint::BigUint; use num_traits::Zero; @@ -42,17 +41,7 @@ impl ReductionResult for Reduction3SATToSubsetSum { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/ksatisfiability_timetabledesign.rs b/src/rules/ksatisfiability_timetabledesign.rs index 0f6620d30..e00805f0d 100644 --- a/src/rules/ksatisfiability_timetabledesign.rs +++ b/src/rules/ksatisfiability_timetabledesign.rs @@ -24,9 +24,8 @@ use crate::models::formula::{CNFClause, KSatisfiability}; use crate::models::misc::TimetableDesign; use crate::reduction; use crate::rules::sat_helpers::SatVariableAllocator; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::variant::K3; use std::collections::VecDeque; @@ -751,17 +750,7 @@ impl ReductionResult for Reduction3SATToTimetableDesign { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/lengthboundeddisjointpaths_ilp.rs b/src/rules/lengthboundeddisjointpaths_ilp.rs index 0fd1ac4de..83fd02427 100644 --- a/src/rules/lengthboundeddisjointpaths_ilp.rs +++ b/src/rules/lengthboundeddisjointpaths_ilp.rs @@ -6,9 +6,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::LengthBoundedDisjointPaths; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; use std::collections::VecDeque; @@ -43,17 +42,7 @@ impl ReductionResult for ReductionLBDPToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/longestcircuit_ilp.rs b/src/rules/longestcircuit_ilp.rs index 82c470c0a..7b4330a94 100644 --- a/src/rules/longestcircuit_ilp.rs +++ b/src/rules/longestcircuit_ilp.rs @@ -11,9 +11,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::LongestCircuit; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; /// Result of reducing LongestCircuit to ILP. @@ -43,17 +42,7 @@ impl ReductionResult for ReductionLongestCircuitToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/longestcommonsubsequence_ilp.rs b/src/rules/longestcommonsubsequence_ilp.rs index ea4895526..f5d4725aa 100644 --- a/src/rules/longestcommonsubsequence_ilp.rs +++ b/src/rules/longestcommonsubsequence_ilp.rs @@ -13,9 +13,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::LongestCommonSubsequence; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; /// Result of reducing LongestCommonSubsequence to ILP. #[derive(Debug, Clone)] @@ -38,17 +37,7 @@ impl ReductionResult for ReductionLCSToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/longestcommonsubsequence_maximumindependentset.rs b/src/rules/longestcommonsubsequence_maximumindependentset.rs index 08190daa6..36fddd49c 100644 --- a/src/rules/longestcommonsubsequence_maximumindependentset.rs +++ b/src/rules/longestcommonsubsequence_maximumindependentset.rs @@ -13,9 +13,8 @@ use crate::models::graph::MaximumIndependentSet; use crate::models::misc::LongestCommonSubsequence; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::topology::SimpleGraph; use crate::types::One; @@ -53,17 +52,7 @@ impl ReductionResult for ReductionLCSToIS { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/longestpath_ilp.rs b/src/rules/longestpath_ilp.rs index cf31f4f82..1c9615d9a 100644 --- a/src/rules/longestpath_ilp.rs +++ b/src/rules/longestpath_ilp.rs @@ -8,9 +8,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::LongestPath; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; #[derive(Debug, Clone)] @@ -38,17 +37,7 @@ impl ReductionResult for ReductionLongestPathToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/maxcut_minimumcutintoboundedsets.rs b/src/rules/maxcut_minimumcutintoboundedsets.rs index 964f6a8e4..69b7693bc 100644 --- a/src/rules/maxcut_minimumcutintoboundedsets.rs +++ b/src/rules/maxcut_minimumcutintoboundedsets.rs @@ -9,9 +9,8 @@ use crate::models::graph::{MaxCut, MinimumCutIntoBoundedSets}; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; /// Result of reducing MaxCut to MinimumCutIntoBoundedSets. @@ -37,28 +36,9 @@ impl ReductionResult for ReductionMaxCutToMinCutBounded { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } - } -} - -impl ReductionMaxCutToMinCutBounded { - fn map_solution( - &self, - target_solution: &<::Target as crate::traits::Problem>::Solution, - ) -> crate::rules::ExtractionResult< - <::Source as crate::traits::Problem>::Solution, - > { - Ok(target_solution[..self.original_n].to_vec()) + recover_preserving_status(source, target, |solution| { + Ok(solution[..self.original_n].to_vec()) + }) } } diff --git a/src/rules/maximalis_ilp.rs b/src/rules/maximalis_ilp.rs index 935043426..eeee3258a 100644 --- a/src/rules/maximalis_ilp.rs +++ b/src/rules/maximalis_ilp.rs @@ -6,9 +6,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::MaximalIS; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; #[derive(Debug, Clone)] @@ -29,28 +28,9 @@ impl ReductionResult for ReductionMxISToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } - } -} - -impl ReductionMxISToILP { - fn map_solution( - &self, - target_solution: &<::Target as crate::traits::Problem>::Solution, - ) -> crate::rules::ExtractionResult< - <::Source as crate::traits::Problem>::Solution, - > { - Ok(target_solution.iter().map(|&value| value == 1).collect()) + recover_preserving_status(source, target, |solution| { + Ok(solution.iter().map(|&value| value == 1).collect()) + }) } } diff --git a/src/rules/maximum2satisfiability_ilp.rs b/src/rules/maximum2satisfiability_ilp.rs index 2165cec4b..d8d0279ef 100644 --- a/src/rules/maximum2satisfiability_ilp.rs +++ b/src/rules/maximum2satisfiability_ilp.rs @@ -10,9 +10,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::formula::Maximum2Satisfiability; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; /// Result of reducing Maximum2Satisfiability to ILP. #[derive(Debug, Clone)] @@ -34,17 +33,7 @@ impl ReductionResult for ReductionMaximum2SatisfiabilityToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/maximum2satisfiability_maxcut.rs b/src/rules/maximum2satisfiability_maxcut.rs index 1d7b4597c..525b4ea69 100644 --- a/src/rules/maximum2satisfiability_maxcut.rs +++ b/src/rules/maximum2satisfiability_maxcut.rs @@ -14,9 +14,8 @@ use crate::models::formula::Maximum2Satisfiability; use crate::models::graph::MaxCut; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::topology::SimpleGraph; use std::collections::BTreeMap; @@ -40,17 +39,7 @@ impl ReductionResult for ReductionMaximum2SatisfiabilityToMaxCut { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/maximumclique_ilp.rs b/src/rules/maximumclique_ilp.rs index 0cbd42b38..df9a7e1f9 100644 --- a/src/rules/maximumclique_ilp.rs +++ b/src/rules/maximumclique_ilp.rs @@ -9,9 +9,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::MaximumClique; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; /// Result of reducing MaximumClique to ILP. @@ -42,28 +41,9 @@ impl ReductionResult for ReductionCliqueToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } - } -} - -impl ReductionCliqueToILP { - fn map_solution( - &self, - target_solution: &<::Target as crate::traits::Problem>::Solution, - ) -> crate::rules::ExtractionResult< - <::Source as crate::traits::Problem>::Solution, - > { - Ok(target_solution.iter().map(|&value| value == 1).collect()) + recover_preserving_status(source, target, |solution| { + Ok(solution.iter().map(|&value| value == 1).collect()) + }) } } diff --git a/src/rules/maximumcokplex_ilp.rs b/src/rules/maximumcokplex_ilp.rs index a9c7af796..c839676e4 100644 --- a/src/rules/maximumcokplex_ilp.rs +++ b/src/rules/maximumcokplex_ilp.rs @@ -8,9 +8,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::MaximumCoKPlex; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; use crate::types::{One, WeightElement}; use crate::variant::{VariantParam, KN}; @@ -38,31 +37,9 @@ where source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } - } -} - -impl ReductionCoKPlexToILP -where - W: WeightElement + VariantParam, -{ - fn map_solution( - &self, - target_solution: &<::Target as crate::traits::Problem>::Solution, - ) -> crate::rules::ExtractionResult< - <::Source as crate::traits::Problem>::Solution, - > { - Ok(target_solution.iter().map(|&value| value == 1).collect()) + recover_preserving_status(source, target, |solution| { + Ok(solution.iter().map(|&value| value == 1).collect()) + }) } } diff --git a/src/rules/maximumcommonedgesubgraph_ilp.rs b/src/rules/maximumcommonedgesubgraph_ilp.rs index 04ac7e19f..de4d5a401 100644 --- a/src/rules/maximumcommonedgesubgraph_ilp.rs +++ b/src/rules/maximumcommonedgesubgraph_ilp.rs @@ -16,9 +16,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::MaximumCommonEdgeSubgraph; use crate::reduction; use crate::rules::ilp_helpers::mccormick_product; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; /// Result of reducing MaximumCommonEdgeSubgraph to ILP. /// @@ -50,17 +49,7 @@ impl ReductionResult for ReductionMCESToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/maximumcontactmapoverlap_ilp.rs b/src/rules/maximumcontactmapoverlap_ilp.rs index a56ddf299..eb8e7885d 100644 --- a/src/rules/maximumcontactmapoverlap_ilp.rs +++ b/src/rules/maximumcontactmapoverlap_ilp.rs @@ -17,9 +17,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::MaximumContactMapOverlap; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; /// Result of reducing MaximumContactMapOverlap to ILP. /// @@ -53,17 +52,7 @@ impl ReductionResult for ReductionCMOToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/maximumdomaticnumber_ilp.rs b/src/rules/maximumdomaticnumber_ilp.rs index b84960e24..e3a573531 100644 --- a/src/rules/maximumdomaticnumber_ilp.rs +++ b/src/rules/maximumdomaticnumber_ilp.rs @@ -11,9 +11,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::MaximumDomaticNumber; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; /// Result of reducing MaximumDomaticNumber to ILP. @@ -43,17 +42,7 @@ impl ReductionResult for ReductionDomaticNumberToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/maximumedgeweightedkclique_ilp.rs b/src/rules/maximumedgeweightedkclique_ilp.rs index 1bfc5b9e8..7b50ee252 100644 --- a/src/rules/maximumedgeweightedkclique_ilp.rs +++ b/src/rules/maximumedgeweightedkclique_ilp.rs @@ -26,9 +26,8 @@ use crate::models::algebraic::{ILPCoefficient, LinearConstraint, ObjectiveSense, use crate::models::graph::MaximumEdgeWeightedKClique; use crate::reduction; use crate::rules::ilp_helpers::mccormick_product; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::topology::Graph; use crate::variant::VariantParam; @@ -65,17 +64,7 @@ where source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/maximumleafspanningtree_ilp.rs b/src/rules/maximumleafspanningtree_ilp.rs index 69253298a..a66f5dfd3 100644 --- a/src/rules/maximumleafspanningtree_ilp.rs +++ b/src/rules/maximumleafspanningtree_ilp.rs @@ -21,9 +21,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::MaximumLeafSpanningTree; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; /// Result of reducing MaximumLeafSpanningTree to ILP. @@ -46,17 +45,7 @@ impl ReductionResult for ReductionMaximumLeafSpanningTreeToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/maximumlikelihoodranking_ilp.rs b/src/rules/maximumlikelihoodranking_ilp.rs index a1fdc2957..5e07fffda 100644 --- a/src/rules/maximumlikelihoodranking_ilp.rs +++ b/src/rules/maximumlikelihoodranking_ilp.rs @@ -13,9 +13,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::MaximumLikelihoodRanking; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; /// Result of reducing MaximumLikelihoodRanking to ILP. #[derive(Debug, Clone)] @@ -46,17 +45,7 @@ impl ReductionResult for ReductionMaximumLikelihoodRankingToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/maximummatching_ilp.rs b/src/rules/maximummatching_ilp.rs index 5f613c00c..45db439c9 100644 --- a/src/rules/maximummatching_ilp.rs +++ b/src/rules/maximummatching_ilp.rs @@ -9,9 +9,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::MaximumMatching; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; /// Result of reducing MaximumMatching to ILP. @@ -42,28 +41,9 @@ impl ReductionResult for ReductionMatchingToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } - } -} - -impl ReductionMatchingToILP { - fn map_solution( - &self, - target_solution: &<::Target as crate::traits::Problem>::Solution, - ) -> crate::rules::ExtractionResult< - <::Source as crate::traits::Problem>::Solution, - > { - Ok(target_solution.iter().map(|&value| value == 1).collect()) + recover_preserving_status(source, target, |solution| { + Ok(solution.iter().map(|&value| value == 1).collect()) + }) } } diff --git a/src/rules/maximumsetpacking_ilp.rs b/src/rules/maximumsetpacking_ilp.rs index 0ccf2813f..5bd7f07f1 100644 --- a/src/rules/maximumsetpacking_ilp.rs +++ b/src/rules/maximumsetpacking_ilp.rs @@ -8,9 +8,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::set::MaximumSetPacking; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; /// Result of reducing MaximumSetPacking to ILP. /// @@ -36,28 +35,9 @@ impl ReductionResult for ReductionSPToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } - } -} - -impl ReductionSPToILP { - fn map_solution( - &self, - target_solution: &<::Target as crate::traits::Problem>::Solution, - ) -> crate::rules::ExtractionResult< - <::Source as crate::traits::Problem>::Solution, - > { - Ok(target_solution.iter().map(|&value| value == 1).collect()) + recover_preserving_status(source, target, |solution| { + Ok(solution.iter().map(|&value| value == 1).collect()) + }) } } diff --git a/src/rules/minimumcapacitatedspanningtree_ilp.rs b/src/rules/minimumcapacitatedspanningtree_ilp.rs index 5bc77d66c..aaad5073c 100644 --- a/src/rules/minimumcapacitatedspanningtree_ilp.rs +++ b/src/rules/minimumcapacitatedspanningtree_ilp.rs @@ -27,9 +27,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::MinimumCapacitatedSpanningTree; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; use crate::types::WeightElement; @@ -53,17 +52,7 @@ impl ReductionResult for ReductionMinimumCapacitatedSpanningTreeToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/minimumcostmaximumflow_minimumcostcirculation.rs b/src/rules/minimumcostmaximumflow_minimumcostcirculation.rs index 44a987531..8adc15bbe 100644 --- a/src/rules/minimumcostmaximumflow_minimumcostcirculation.rs +++ b/src/rules/minimumcostmaximumflow_minimumcostcirculation.rs @@ -17,9 +17,8 @@ use crate::models::graph::{MinimumCostCirculation, MinimumCostMaximumFlow}; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::topology::DirectedGraph; /// Result of reducing MinimumCostMaximumFlow to MinimumCostCirculation. @@ -50,28 +49,9 @@ impl ReductionResult for ReductionMCMFToMCC { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } - } -} - -impl ReductionMCMFToMCC { - fn map_solution( - &self, - target_solution: &<::Target as crate::traits::Problem>::Solution, - ) -> crate::rules::ExtractionResult< - <::Source as crate::traits::Problem>::Solution, - > { - Ok(target_solution[..self.num_original_arcs].to_vec()) + recover_preserving_status(source, target, |solution| { + Ok(solution[..self.num_original_arcs].to_vec()) + }) } } diff --git a/src/rules/minimumcoveringbycliques_ilp.rs b/src/rules/minimumcoveringbycliques_ilp.rs index 0edaa3019..9fe221631 100644 --- a/src/rules/minimumcoveringbycliques_ilp.rs +++ b/src/rules/minimumcoveringbycliques_ilp.rs @@ -21,9 +21,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::MinimumCoveringByCliques; use crate::reduction; use crate::rules::ilp_helpers::mccormick_product; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; #[derive(Debug, Clone)] @@ -46,17 +45,7 @@ impl ReductionResult for ReductionMinimumCoveringByCliquesToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/minimumcoveringbycliques_minimumintersectiongraphbasis.rs b/src/rules/minimumcoveringbycliques_minimumintersectiongraphbasis.rs index 6e79165de..98125df22 100644 --- a/src/rules/minimumcoveringbycliques_minimumintersectiongraphbasis.rs +++ b/src/rules/minimumcoveringbycliques_minimumintersectiongraphbasis.rs @@ -6,9 +6,8 @@ use crate::models::graph::{MinimumCoveringByCliques, MinimumIntersectionGraphBasis}; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; use std::collections::BTreeMap; @@ -71,17 +70,7 @@ impl ReductionResult for ReductionMinimumCoveringByCliquesToMinimumIntersectionG source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/minimumcutintoboundedsets_ilp.rs b/src/rules/minimumcutintoboundedsets_ilp.rs index fe8c59536..b444ba3ab 100644 --- a/src/rules/minimumcutintoboundedsets_ilp.rs +++ b/src/rules/minimumcutintoboundedsets_ilp.rs @@ -9,9 +9,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::MinimumCutIntoBoundedSets; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; #[derive(Debug, Clone)] @@ -33,17 +32,7 @@ impl ReductionResult for ReductionMinCutBSToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/minimumdominatingset_ilp.rs b/src/rules/minimumdominatingset_ilp.rs index a43157ce1..6c5c59f4e 100644 --- a/src/rules/minimumdominatingset_ilp.rs +++ b/src/rules/minimumdominatingset_ilp.rs @@ -9,9 +9,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::MinimumDominatingSet; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; /// Result of reducing MinimumDominatingSet to ILP. @@ -43,28 +42,9 @@ impl ReductionResult for ReductionDSToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } - } -} - -impl ReductionDSToILP { - fn map_solution( - &self, - target_solution: &<::Target as crate::traits::Problem>::Solution, - ) -> crate::rules::ExtractionResult< - <::Source as crate::traits::Problem>::Solution, - > { - Ok(target_solution.iter().map(|&value| value == 1).collect()) + recover_preserving_status(source, target, |solution| { + Ok(solution.iter().map(|&value| value == 1).collect()) + }) } } diff --git a/src/rules/minimumedgecostflow_ilp.rs b/src/rules/minimumedgecostflow_ilp.rs index f2f80b945..b6a9b9e4a 100644 --- a/src/rules/minimumedgecostflow_ilp.rs +++ b/src/rules/minimumedgecostflow_ilp.rs @@ -21,9 +21,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::MinimumEdgeCostFlow; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; /// Result of reducing MinimumEdgeCostFlow to `ILP`. /// @@ -50,17 +49,7 @@ impl ReductionResult for ReductionMECFToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/minimumexternalmacrodatacompression_ilp.rs b/src/rules/minimumexternalmacrodatacompression_ilp.rs index aee0f9685..dded452a9 100644 --- a/src/rules/minimumexternalmacrodatacompression_ilp.rs +++ b/src/rules/minimumexternalmacrodatacompression_ilp.rs @@ -16,7 +16,7 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::MinimumExternalMacroDataCompression; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; use crate::solvers::SolveOutcome; @@ -128,17 +128,7 @@ impl ReductionResult for ReductionEMDCToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/minimumfaultdetectiontestset_ilp.rs b/src/rules/minimumfaultdetectiontestset_ilp.rs index 3ffe50944..c39ccb0e2 100644 --- a/src/rules/minimumfaultdetectiontestset_ilp.rs +++ b/src/rules/minimumfaultdetectiontestset_ilp.rs @@ -8,9 +8,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::MinimumFaultDetectionTestSet; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use std::collections::VecDeque; /// Result of reducing MinimumFaultDetectionTestSet to `ILP`. @@ -34,17 +33,7 @@ impl ReductionResult for ReductionMFDTSToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/minimumfeedbackarcset_ilp.rs b/src/rules/minimumfeedbackarcset_ilp.rs index c6449d9e5..9f1cd2f0e 100644 --- a/src/rules/minimumfeedbackarcset_ilp.rs +++ b/src/rules/minimumfeedbackarcset_ilp.rs @@ -12,9 +12,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::MinimumFeedbackArcSet; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; /// Result of reducing MinimumFeedbackArcSet to ILP. /// @@ -48,17 +47,7 @@ impl ReductionResult for ReductionFASToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/minimumfeedbackvertexset_ilp.rs b/src/rules/minimumfeedbackvertexset_ilp.rs index be58bb3d2..8afd8f9ea 100644 --- a/src/rules/minimumfeedbackvertexset_ilp.rs +++ b/src/rules/minimumfeedbackvertexset_ilp.rs @@ -9,9 +9,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::MinimumFeedbackVertexSet; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; /// Result of reducing MinimumFeedbackVertexSet to ILP. /// @@ -45,17 +44,7 @@ impl ReductionResult for ReductionMFVSToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/minimumfeedbackvertexset_minimumcodegenerationunlimitedregisters.rs b/src/rules/minimumfeedbackvertexset_minimumcodegenerationunlimitedregisters.rs index cdf595399..a79ad763d 100644 --- a/src/rules/minimumfeedbackvertexset_minimumcodegenerationunlimitedregisters.rs +++ b/src/rules/minimumfeedbackvertexset_minimumcodegenerationunlimitedregisters.rs @@ -9,7 +9,7 @@ use crate::models::graph::MinimumFeedbackVertexSet; use crate::models::misc::MinimumCodeGenerationUnlimitedRegisters; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; use crate::solvers::SolveOutcome; use crate::types::One; @@ -37,17 +37,7 @@ impl ReductionResult for ReductionFVSToCodeGen { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/minimumgraphbandwidth_ilp.rs b/src/rules/minimumgraphbandwidth_ilp.rs index df2fa6029..103fc9d8e 100644 --- a/src/rules/minimumgraphbandwidth_ilp.rs +++ b/src/rules/minimumgraphbandwidth_ilp.rs @@ -10,9 +10,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::MinimumGraphBandwidth; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; /// Result of reducing MinimumGraphBandwidth to ILP. @@ -41,17 +40,7 @@ impl ReductionResult for ReductionMGBToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/minimumhittingset_ilp.rs b/src/rules/minimumhittingset_ilp.rs index 1856ee278..127caef46 100644 --- a/src/rules/minimumhittingset_ilp.rs +++ b/src/rules/minimumhittingset_ilp.rs @@ -6,9 +6,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::set::MinimumHittingSet; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; #[derive(Debug, Clone)] pub struct ReductionHSToILP { @@ -28,28 +27,9 @@ impl ReductionResult for ReductionHSToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } - } -} - -impl ReductionHSToILP { - fn map_solution( - &self, - target_solution: &<::Target as crate::traits::Problem>::Solution, - ) -> crate::rules::ExtractionResult< - <::Source as crate::traits::Problem>::Solution, - > { - Ok(target_solution.iter().map(|&value| value == 1).collect()) + recover_preserving_status(source, target, |solution| { + Ok(solution.iter().map(|&value| value == 1).collect()) + }) } } diff --git a/src/rules/minimuminternalmacrodatacompression_ilp.rs b/src/rules/minimuminternalmacrodatacompression_ilp.rs index 89dc5af41..ae953dcc7 100644 --- a/src/rules/minimuminternalmacrodatacompression_ilp.rs +++ b/src/rules/minimuminternalmacrodatacompression_ilp.rs @@ -18,7 +18,7 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::MinimumInternalMacroDataCompression; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; use crate::solvers::SolveOutcome; @@ -102,17 +102,7 @@ impl ReductionResult for ReductionIMDCToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/minimummatrixcover_ilp.rs b/src/rules/minimummatrixcover_ilp.rs index 2d3d4874b..5bfe9c398 100644 --- a/src/rules/minimummatrixcover_ilp.rs +++ b/src/rules/minimummatrixcover_ilp.rs @@ -11,9 +11,8 @@ use crate::models::algebraic::MinimumMatrixCover; use crate::models::algebraic::{ObjectiveSense, ILP}; use crate::reduction; use crate::rules::ilp_helpers::mccormick_product; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; /// Result of reducing MinimumMatrixCover to ILP. #[derive(Debug, Clone)] @@ -35,17 +34,7 @@ impl ReductionResult for ReductionMinimumMatrixCoverToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/minimummaximalmatching_ilp.rs b/src/rules/minimummaximalmatching_ilp.rs index b70a1de86..e6b7a150c 100644 --- a/src/rules/minimummaximalmatching_ilp.rs +++ b/src/rules/minimummaximalmatching_ilp.rs @@ -10,9 +10,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::MinimumMaximalMatching; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; /// Result of reducing MinimumMaximalMatching to ILP. @@ -45,28 +44,9 @@ impl ReductionResult for ReductionMMMToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } - } -} - -impl ReductionMMMToILP { - fn map_solution( - &self, - target_solution: &<::Target as crate::traits::Problem>::Solution, - ) -> crate::rules::ExtractionResult< - <::Source as crate::traits::Problem>::Solution, - > { - Ok(target_solution.iter().map(|&value| value == 1).collect()) + recover_preserving_status(source, target, |solution| { + Ok(solution.iter().map(|&value| value == 1).collect()) + }) } } diff --git a/src/rules/minimummaximalmatching_maximumachromaticnumber.rs b/src/rules/minimummaximalmatching_maximumachromaticnumber.rs index 0b4bd6889..946671c14 100644 --- a/src/rules/minimummaximalmatching_maximumachromaticnumber.rs +++ b/src/rules/minimummaximalmatching_maximumachromaticnumber.rs @@ -9,9 +9,8 @@ use crate::models::graph::{MaximumAchromaticNumber, MinimumMaximalMatching}; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::topology::{BipartiteGraph, Graph, SimpleGraph}; /// Result of reducing `MinimumMaximalMatching` to @@ -49,17 +48,7 @@ impl ReductionResult for ReductionMMMToAchromatic { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/minimummaximalmatching_minimummatrixdomination.rs b/src/rules/minimummaximalmatching_minimummatrixdomination.rs index 325044a91..59fab764d 100644 --- a/src/rules/minimummaximalmatching_minimummatrixdomination.rs +++ b/src/rules/minimummaximalmatching_minimummatrixdomination.rs @@ -36,9 +36,8 @@ use crate::models::algebraic::MinimumMatrixDomination; use crate::models::graph::MinimumMaximalMatching; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::topology::{BipartiteGraph, Graph}; /// Result of reducing `MinimumMaximalMatching` to @@ -99,17 +98,7 @@ impl ReductionResult for ReductionMMMToMatrixDomination { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/minimummetricdimension_ilp.rs b/src/rules/minimummetricdimension_ilp.rs index 0c21edc4e..645e86ad8 100644 --- a/src/rules/minimummetricdimension_ilp.rs +++ b/src/rules/minimummetricdimension_ilp.rs @@ -11,9 +11,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::minimum_metric_dimension::bfs_distances; use crate::models::graph::MinimumMetricDimension; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; /// Result of reducing MinimumMetricDimension to ILP. @@ -45,28 +44,9 @@ impl ReductionResult for ReductionMDToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } - } -} - -impl ReductionMDToILP { - fn map_solution( - &self, - target_solution: &<::Target as crate::traits::Problem>::Solution, - ) -> crate::rules::ExtractionResult< - <::Source as crate::traits::Problem>::Solution, - > { - Ok(target_solution.iter().map(|&value| value == 1).collect()) + recover_preserving_status(source, target, |solution| { + Ok(solution.iter().map(|&value| value == 1).collect()) + }) } } diff --git a/src/rules/minimummultiwaycut_ilp.rs b/src/rules/minimummultiwaycut_ilp.rs index 14e1bcca4..e6108e162 100644 --- a/src/rules/minimummultiwaycut_ilp.rs +++ b/src/rules/minimummultiwaycut_ilp.rs @@ -9,9 +9,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::MinimumMultiwayCut; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; /// Result of reducing MinimumMultiwayCut to ILP. @@ -49,17 +48,7 @@ impl ReductionResult for ReductionMMCToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/minimumsetcovering_ilp.rs b/src/rules/minimumsetcovering_ilp.rs index e4712fc06..ca95faa05 100644 --- a/src/rules/minimumsetcovering_ilp.rs +++ b/src/rules/minimumsetcovering_ilp.rs @@ -8,9 +8,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::set::MinimumSetCovering; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; /// Result of reducing MinimumSetCovering to ILP. /// @@ -40,28 +39,9 @@ impl ReductionResult for ReductionSCToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } - } -} - -impl ReductionSCToILP { - fn map_solution( - &self, - target_solution: &<::Target as crate::traits::Problem>::Solution, - ) -> crate::rules::ExtractionResult< - <::Source as crate::traits::Problem>::Solution, - > { - Ok(target_solution.iter().map(|&value| value == 1).collect()) + recover_preserving_status(source, target, |solution| { + Ok(solution.iter().map(|&value| value == 1).collect()) + }) } } diff --git a/src/rules/minimumsummulticenter_ilp.rs b/src/rules/minimumsummulticenter_ilp.rs index 898691161..c30ffaf86 100644 --- a/src/rules/minimumsummulticenter_ilp.rs +++ b/src/rules/minimumsummulticenter_ilp.rs @@ -23,9 +23,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::MinimumSumMulticenter; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; /// Result of reducing MinimumSumMulticenter to ILP. @@ -48,17 +47,7 @@ impl ReductionResult for ReductionMSMCToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/minimumtardinesssequencing_ilp.rs b/src/rules/minimumtardinesssequencing_ilp.rs index 5d6d0da93..63649c3e1 100644 --- a/src/rules/minimumtardinesssequencing_ilp.rs +++ b/src/rules/minimumtardinesssequencing_ilp.rs @@ -8,9 +8,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::MinimumTardinessSequencing; use crate::reduction; use crate::rules::ilp_helpers::one_hot_decode; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::types::One; /// Result of reducing MinimumTardinessSequencing to `ILP`. @@ -33,17 +32,7 @@ impl ReductionResult for ReductionMTSToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } @@ -82,17 +71,7 @@ impl ReductionResult for ReductionMTSWeightedToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/minimumvertexcover_ensemblecomputation.rs b/src/rules/minimumvertexcover_ensemblecomputation.rs index b0f3e8e88..d3a3540e4 100644 --- a/src/rules/minimumvertexcover_ensemblecomputation.rs +++ b/src/rules/minimumvertexcover_ensemblecomputation.rs @@ -15,9 +15,8 @@ use crate::models::graph::MinimumVertexCover; use crate::models::misc::EnsembleComputation; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; use crate::types::One; @@ -47,17 +46,7 @@ impl ReductionResult for ReductionVCToEC { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/minimumvertexcover_longestcommonsubsequence.rs b/src/rules/minimumvertexcover_longestcommonsubsequence.rs index 080d67bea..775f99efd 100644 --- a/src/rules/minimumvertexcover_longestcommonsubsequence.rs +++ b/src/rules/minimumvertexcover_longestcommonsubsequence.rs @@ -3,9 +3,8 @@ use crate::models::graph::MinimumVertexCover; use crate::models::misc::LongestCommonSubsequence; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; use crate::types::One; @@ -28,17 +27,7 @@ impl ReductionResult for ReductionVCToLCS { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/minimumvertexcover_maximumindependentset.rs b/src/rules/minimumvertexcover_maximumindependentset.rs index b636a6289..6fdebd7b6 100644 --- a/src/rules/minimumvertexcover_maximumindependentset.rs +++ b/src/rules/minimumvertexcover_maximumindependentset.rs @@ -4,9 +4,8 @@ use crate::models::graph::{MaximumIndependentSet, MinimumVertexCover}; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; use crate::types::WeightElement; @@ -34,31 +33,9 @@ where source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } - } -} - -impl ReductionISToVC -where - W: WeightElement + crate::variant::VariantParam, -{ - fn map_solution( - &self, - target_solution: &<::Target as crate::traits::Problem>::Solution, - ) -> crate::rules::ExtractionResult< - <::Source as crate::traits::Problem>::Solution, - > { - Ok(target_solution.iter().map(|&x| !x).collect()) + recover_preserving_status(source, target, |solution| { + Ok(solution.iter().map(|&x| !x).collect()) + }) } } @@ -103,31 +80,9 @@ where source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } - } -} - -impl ReductionVCToIS -where - W: WeightElement + crate::variant::VariantParam, -{ - fn map_solution( - &self, - target_solution: &<::Target as crate::traits::Problem>::Solution, - ) -> crate::rules::ExtractionResult< - <::Source as crate::traits::Problem>::Solution, - > { - Ok(target_solution.iter().map(|&x| !x).collect()) + recover_preserving_status(source, target, |solution| { + Ok(solution.iter().map(|&x| !x).collect()) + }) } } diff --git a/src/rules/minimumvertexcover_minimumweightandorgraph.rs b/src/rules/minimumvertexcover_minimumweightandorgraph.rs index ae00726b7..ff779e51e 100644 --- a/src/rules/minimumvertexcover_minimumweightandorgraph.rs +++ b/src/rules/minimumvertexcover_minimumweightandorgraph.rs @@ -3,9 +3,8 @@ use crate::models::graph::MinimumVertexCover; use crate::models::misc::MinimumWeightAndOrGraph; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::topology::Graph; use crate::topology::SimpleGraph; @@ -30,17 +29,7 @@ impl ReductionResult for ReductionVCToAndOrGraph { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/minimumweightdecoding_ilp.rs b/src/rules/minimumweightdecoding_ilp.rs index 49d5c7977..94403b0ee 100644 --- a/src/rules/minimumweightdecoding_ilp.rs +++ b/src/rules/minimumweightdecoding_ilp.rs @@ -18,9 +18,8 @@ use crate::models::algebraic::MinimumWeightDecoding; use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; /// Result of reducing MinimumWeightDecoding to `ILP`. /// @@ -47,17 +46,7 @@ impl ReductionResult for ReductionMinimumWeightDecodingToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/minmaxmulticenter_ilp.rs b/src/rules/minmaxmulticenter_ilp.rs index de26ef4ed..d9569c159 100644 --- a/src/rules/minmaxmulticenter_ilp.rs +++ b/src/rules/minmaxmulticenter_ilp.rs @@ -27,9 +27,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::MinMaxMulticenter; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; /// Result of reducing MinMaxMulticenter to ILP. @@ -52,17 +51,7 @@ impl ReductionResult for ReductionMMCToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/mixedchinesepostman_ilp.rs b/src/rules/mixedchinesepostman_ilp.rs index cb3810a5f..c47301065 100644 --- a/src/rules/mixedchinesepostman_ilp.rs +++ b/src/rules/mixedchinesepostman_ilp.rs @@ -8,9 +8,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::MixedChinesePostman; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::types::WeightElement; /// Result of reducing MixedChinesePostman to ILP. @@ -33,17 +32,7 @@ impl ReductionResult for ReductionMCPToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/monochromatictriangle_ilp.rs b/src/rules/monochromatictriangle_ilp.rs index d81e8dbe1..2f010ff70 100644 --- a/src/rules/monochromatictriangle_ilp.rs +++ b/src/rules/monochromatictriangle_ilp.rs @@ -7,9 +7,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::MonochromaticTriangle; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; use std::collections::HashMap; @@ -32,28 +31,9 @@ impl ReductionResult for ReductionMonochromaticTriangleToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } - } -} - -impl ReductionMonochromaticTriangleToILP { - fn map_solution( - &self, - target_solution: &<::Target as crate::traits::Problem>::Solution, - ) -> crate::rules::ExtractionResult< - <::Source as crate::traits::Problem>::Solution, - > { - Ok(target_solution.iter().map(|&value| value == 1).collect()) + recover_preserving_status(source, target, |solution| { + Ok(solution.iter().map(|&value| value == 1).collect()) + }) } } diff --git a/src/rules/multiplechoicebranching_ilp.rs b/src/rules/multiplechoicebranching_ilp.rs index 4ae6ad2bb..bf198c17a 100644 --- a/src/rules/multiplechoicebranching_ilp.rs +++ b/src/rules/multiplechoicebranching_ilp.rs @@ -3,9 +3,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::MultipleChoiceBranching; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; #[derive(Debug, Clone)] pub struct ReductionMultipleChoiceBranchingToILP { @@ -26,17 +25,7 @@ impl ReductionResult for ReductionMultipleChoiceBranchingToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/multiplecopyfileallocation_ilp.rs b/src/rules/multiplecopyfileallocation_ilp.rs index df2107ed9..b5b982a86 100644 --- a/src/rules/multiplecopyfileallocation_ilp.rs +++ b/src/rules/multiplecopyfileallocation_ilp.rs @@ -17,9 +17,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::MultipleCopyFileAllocation; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; use std::collections::VecDeque; @@ -43,17 +42,7 @@ impl ReductionResult for ReductionMCFAToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/multiprocessorscheduling_ilp.rs b/src/rules/multiprocessorscheduling_ilp.rs index fa9c2084d..23a59a005 100644 --- a/src/rules/multiprocessorscheduling_ilp.rs +++ b/src/rules/multiprocessorscheduling_ilp.rs @@ -9,9 +9,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::MultiprocessorScheduling; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; /// Result of reducing MultiprocessorScheduling to ILP. /// @@ -40,17 +39,7 @@ impl ReductionResult for ReductionMSToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/naesatisfiability_ilp.rs b/src/rules/naesatisfiability_ilp.rs index af7631ada..9472c3910 100644 --- a/src/rules/naesatisfiability_ilp.rs +++ b/src/rules/naesatisfiability_ilp.rs @@ -11,9 +11,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::formula::NAESatisfiability; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; #[derive(Debug, Clone)] pub struct ReductionNAESATToILP { @@ -33,28 +32,9 @@ impl ReductionResult for ReductionNAESATToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } - } -} - -impl ReductionNAESATToILP { - fn map_solution( - &self, - target_solution: &<::Target as crate::traits::Problem>::Solution, - ) -> crate::rules::ExtractionResult< - <::Source as crate::traits::Problem>::Solution, - > { - Ok(target_solution.iter().map(|&value| value == 1).collect()) + recover_preserving_status(source, target, |solution| { + Ok(solution.iter().map(|&value| value == 1).collect()) + }) } } diff --git a/src/rules/naesatisfiability_maxcut.rs b/src/rules/naesatisfiability_maxcut.rs index a4c49e590..9455f32af 100644 --- a/src/rules/naesatisfiability_maxcut.rs +++ b/src/rules/naesatisfiability_maxcut.rs @@ -15,9 +15,8 @@ use crate::models::decision::Decision; use crate::models::formula::NAESatisfiability; use crate::models::graph::MaxCut; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::topology::SimpleGraph; /// Result of reducing NAESatisfiability to MaxCut. @@ -45,17 +44,7 @@ impl ReductionResult for ReductionNAESATToMaxCut { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/naesatisfiability_partitionintoperfectmatchings.rs b/src/rules/naesatisfiability_partitionintoperfectmatchings.rs index 202d93223..92a44b545 100644 --- a/src/rules/naesatisfiability_partitionintoperfectmatchings.rs +++ b/src/rules/naesatisfiability_partitionintoperfectmatchings.rs @@ -7,9 +7,8 @@ use crate::models::formula::NAESatisfiability; use crate::models::graph::PartitionIntoPerfectMatchings; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::topology::SimpleGraph; #[derive(Debug, Clone, Copy)] @@ -72,17 +71,7 @@ impl ReductionResult for ReductionNAESATToPartitionIntoPerfectMatchings { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/naesatisfiability_setsplitting.rs b/src/rules/naesatisfiability_setsplitting.rs index c78b8e738..fc52aec65 100644 --- a/src/rules/naesatisfiability_setsplitting.rs +++ b/src/rules/naesatisfiability_setsplitting.rs @@ -9,9 +9,8 @@ use crate::models::formula::NAESatisfiability; use crate::models::set::SetSplitting; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; #[derive(Debug, Clone)] pub struct ReductionNAESATToSetSplitting { @@ -32,28 +31,9 @@ impl ReductionResult for ReductionNAESATToSetSplitting { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } - } -} - -impl ReductionNAESATToSetSplitting { - fn map_solution( - &self, - target_solution: &<::Target as crate::traits::Problem>::Solution, - ) -> crate::rules::ExtractionResult< - <::Source as crate::traits::Problem>::Solution, - > { - Ok(target_solution[..self.num_source_variables].to_vec()) + recover_preserving_status(source, target, |solution| { + Ok(solution[..self.num_source_variables].to_vec()) + }) } } diff --git a/src/rules/numerical3dimensionalmatching_numericalmatchingwithtargetsums.rs b/src/rules/numerical3dimensionalmatching_numericalmatchingwithtargetsums.rs index 0b1bf1cf4..aa17d447b 100644 --- a/src/rules/numerical3dimensionalmatching_numericalmatchingwithtargetsums.rs +++ b/src/rules/numerical3dimensionalmatching_numericalmatchingwithtargetsums.rs @@ -7,9 +7,8 @@ use crate::models::misc::{Numerical3DimensionalMatching, NumericalMatchingWithTargetSums}; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; /// Result of reducing Numerical3DimensionalMatching to NumericalMatchingWithTargetSums. #[derive(Debug, Clone)] @@ -30,17 +29,7 @@ impl ReductionResult for ReductionN3DMToNMTS { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/numericalmatchingwithtargetsums_ilp.rs b/src/rules/numericalmatchingwithtargetsums_ilp.rs index d08724e25..20f98715f 100644 --- a/src/rules/numericalmatchingwithtargetsums_ilp.rs +++ b/src/rules/numericalmatchingwithtargetsums_ilp.rs @@ -14,9 +14,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::NumericalMatchingWithTargetSums; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; /// A compatible triple (i, j, k) where s(x_i) + s(y_j) = B_k. #[derive(Debug, Clone)] @@ -51,17 +50,7 @@ impl ReductionResult for ReductionNMTSToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/openshopscheduling_ilp.rs b/src/rules/openshopscheduling_ilp.rs index 086305ec7..31acc1a07 100644 --- a/src/rules/openshopscheduling_ilp.rs +++ b/src/rules/openshopscheduling_ilp.rs @@ -29,9 +29,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::OpenShopScheduling; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; /// Result of reducing OpenShopScheduling to `ILP`. /// @@ -94,17 +93,7 @@ impl ReductionResult for ReductionOSSToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/optimallineararrangement_consecutiveonesmatrixaugmentation.rs b/src/rules/optimallineararrangement_consecutiveonesmatrixaugmentation.rs index 5faa66243..f290e4382 100644 --- a/src/rules/optimallineararrangement_consecutiveonesmatrixaugmentation.rs +++ b/src/rules/optimallineararrangement_consecutiveonesmatrixaugmentation.rs @@ -8,9 +8,8 @@ use crate::models::algebraic::ConsecutiveOnesMatrixAugmentation; use crate::models::decision::Decision; use crate::models::graph::OptimalLinearArrangement; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; /// The target incidence matrix, or a fixed infeasible matrix when the source @@ -33,17 +32,7 @@ impl ReductionResult for ReductionOptimalLinearArrangementToConsecutiveOnesMatri source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/optimallineararrangement_ilp.rs b/src/rules/optimallineararrangement_ilp.rs index bc7d4002e..b04fbe53d 100644 --- a/src/rules/optimallineararrangement_ilp.rs +++ b/src/rules/optimallineararrangement_ilp.rs @@ -10,9 +10,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::OptimalLinearArrangement; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; /// Result of reducing OptimalLinearArrangement to ILP. @@ -41,17 +40,7 @@ impl ReductionResult for ReductionOLAToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/optimallineararrangement_sequencingtominimizeweightedcompletiontime.rs b/src/rules/optimallineararrangement_sequencingtominimizeweightedcompletiontime.rs index b7894b1ac..49b8654b8 100644 --- a/src/rules/optimallineararrangement_sequencingtominimizeweightedcompletiontime.rs +++ b/src/rules/optimallineararrangement_sequencingtominimizeweightedcompletiontime.rs @@ -14,7 +14,7 @@ use crate::models::graph::OptimalLinearArrangement; use crate::models::misc::SequencingToMinimizeWeightedCompletionTime; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; @@ -39,17 +39,7 @@ impl ReductionResult for ReductionOLAToSequencingToMinimizeWeightedCompletionTim source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/optimumcommunicationspanningtree_ilp.rs b/src/rules/optimumcommunicationspanningtree_ilp.rs index 301dfaad2..56cc41263 100644 --- a/src/rules/optimumcommunicationspanningtree_ilp.rs +++ b/src/rules/optimumcommunicationspanningtree_ilp.rs @@ -10,9 +10,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::OptimumCommunicationSpanningTree; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; /// Result of reducing OptimumCommunicationSpanningTree to ILP. /// @@ -40,17 +39,7 @@ impl ReductionResult for ReductionOptimumCommunicationSpanningTreeToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/paintshop_ilp.rs b/src/rules/paintshop_ilp.rs index 58268ae37..dc2fdecd4 100644 --- a/src/rules/paintshop_ilp.rs +++ b/src/rules/paintshop_ilp.rs @@ -7,7 +7,7 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::PaintShop; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; use crate::solvers::SolveOutcome; @@ -31,17 +31,7 @@ impl ReductionResult for ReductionPaintShopToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/partiallyorderedknapsack_ilp.rs b/src/rules/partiallyorderedknapsack_ilp.rs index 8c939b71d..01b546c3a 100644 --- a/src/rules/partiallyorderedknapsack_ilp.rs +++ b/src/rules/partiallyorderedknapsack_ilp.rs @@ -6,9 +6,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::PartiallyOrderedKnapsack; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; #[derive(Debug, Clone)] pub struct ReductionPOKToILP { @@ -28,28 +27,9 @@ impl ReductionResult for ReductionPOKToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } - } -} - -impl ReductionPOKToILP { - fn map_solution( - &self, - target_solution: &<::Target as crate::traits::Problem>::Solution, - ) -> crate::rules::ExtractionResult< - <::Source as crate::traits::Problem>::Solution, - > { - Ok(target_solution.iter().map(|&value| value == 1).collect()) + recover_preserving_status(source, target, |solution| { + Ok(solution.iter().map(|&value| value == 1).collect()) + }) } } diff --git a/src/rules/partition_integralflowwithmultipliers.rs b/src/rules/partition_integralflowwithmultipliers.rs index a35fa854d..de9313f9e 100644 --- a/src/rules/partition_integralflowwithmultipliers.rs +++ b/src/rules/partition_integralflowwithmultipliers.rs @@ -8,9 +8,8 @@ use crate::models::graph::IntegralFlowWithMultipliers; use crate::models::misc::Partition; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::topology::DirectedGraph; /// Result of reducing Partition to IntegralFlowWithMultipliers. @@ -33,17 +32,7 @@ impl ReductionResult for ReductionPartitionToIntegralFlowWithMultipliers { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/partition_multiprocessorscheduling.rs b/src/rules/partition_multiprocessorscheduling.rs index 89335301d..f77c5c35b 100644 --- a/src/rules/partition_multiprocessorscheduling.rs +++ b/src/rules/partition_multiprocessorscheduling.rs @@ -14,9 +14,8 @@ use crate::models::misc::{MultiprocessorScheduling, Partition}; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; /// Result of reducing Partition to MultiprocessorScheduling. #[derive(Debug, Clone)] @@ -39,17 +38,7 @@ impl ReductionResult for ReductionPartitionToMPS { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/partition_openshopscheduling.rs b/src/rules/partition_openshopscheduling.rs index d75241746..b028221e2 100644 --- a/src/rules/partition_openshopscheduling.rs +++ b/src/rules/partition_openshopscheduling.rs @@ -3,9 +3,8 @@ use crate::models::decision::Decision; use crate::models::misc::{OpenShopScheduling, Partition}; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; #[derive(Debug, Clone)] pub struct ReductionPartitionToOpenShopScheduling { @@ -25,17 +24,7 @@ impl ReductionResult for ReductionPartitionToOpenShopScheduling { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/partition_productionplanning.rs b/src/rules/partition_productionplanning.rs index 4996231ce..4c0201093 100644 --- a/src/rules/partition_productionplanning.rs +++ b/src/rules/partition_productionplanning.rs @@ -2,9 +2,8 @@ use crate::models::misc::{Partition, ProductionPlanning}; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; #[derive(Debug, Clone)] pub struct ReductionPartitionToProductionPlanning { @@ -24,17 +23,7 @@ impl ReductionResult for ReductionPartitionToProductionPlanning { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/partition_sequencingtominimizetardytaskweight.rs b/src/rules/partition_sequencingtominimizetardytaskweight.rs index 764011009..7a37075a0 100644 --- a/src/rules/partition_sequencingtominimizetardytaskweight.rs +++ b/src/rules/partition_sequencingtominimizetardytaskweight.rs @@ -3,9 +3,8 @@ use crate::models::decision::Decision; use crate::models::misc::{Partition, SequencingToMinimizeTardyTaskWeight}; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; /// Result of reducing Partition to SequencingToMinimizeTardyTaskWeight. #[derive(Debug, Clone)] @@ -26,17 +25,7 @@ impl ReductionResult for ReductionPartitionToSequencingToMinimizeTardyTaskWeight source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/partitionintocliques_ilp.rs b/src/rules/partitionintocliques_ilp.rs index 4d9329a2b..698427740 100644 --- a/src/rules/partitionintocliques_ilp.rs +++ b/src/rules/partitionintocliques_ilp.rs @@ -3,9 +3,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::PartitionIntoCliques; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; #[derive(Debug, Clone)] @@ -28,17 +27,7 @@ impl ReductionResult for ReductionPartitionIntoCliquesToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/partitionintocliques_minimumcoveringbycliques.rs b/src/rules/partitionintocliques_minimumcoveringbycliques.rs index 7d7e50862..6057b2dd0 100644 --- a/src/rules/partitionintocliques_minimumcoveringbycliques.rs +++ b/src/rules/partitionintocliques_minimumcoveringbycliques.rs @@ -11,9 +11,8 @@ use crate::models::decision::Decision; use crate::models::graph::{MinimumCoveringByCliques, PartitionIntoCliques}; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; use std::collections::BTreeMap; @@ -158,17 +157,7 @@ impl ReductionResult for ReductionPartitionIntoCliquesToMinimumCoveringByCliques source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/partitionintopathsoflength2_ilp.rs b/src/rules/partitionintopathsoflength2_ilp.rs index 8c2d70b0f..f9ec02c93 100644 --- a/src/rules/partitionintopathsoflength2_ilp.rs +++ b/src/rules/partitionintopathsoflength2_ilp.rs @@ -20,9 +20,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::PartitionIntoPathsOfLength2; use crate::reduction; use crate::rules::ilp_helpers::mccormick_product; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; /// Result of reducing PartitionIntoPathsOfLength2 to ILP. @@ -51,17 +50,7 @@ impl ReductionResult for ReductionPIPL2ToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/partitionintotriangles_ilp.rs b/src/rules/partitionintotriangles_ilp.rs index 72567f3c7..2704c7c52 100644 --- a/src/rules/partitionintotriangles_ilp.rs +++ b/src/rules/partitionintotriangles_ilp.rs @@ -12,9 +12,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::PartitionIntoTriangles; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; /// Result of reducing PartitionIntoTriangles to ILP. @@ -44,17 +43,7 @@ impl ReductionResult for ReductionPITToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/pathconstrainednetworkflow_ilp.rs b/src/rules/pathconstrainednetworkflow_ilp.rs index e0e1ff096..c6843ff15 100644 --- a/src/rules/pathconstrainednetworkflow_ilp.rs +++ b/src/rules/pathconstrainednetworkflow_ilp.rs @@ -6,9 +6,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::PathConstrainedNetworkFlow; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; /// Result of reducing PathConstrainedNetworkFlow to ILP. #[derive(Debug, Clone)] @@ -29,17 +28,7 @@ impl ReductionResult for ReductionPCNFToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/precedenceconstrainedscheduling_ilp.rs b/src/rules/precedenceconstrainedscheduling_ilp.rs index 6d414cf67..6d533413a 100644 --- a/src/rules/precedenceconstrainedscheduling_ilp.rs +++ b/src/rules/precedenceconstrainedscheduling_ilp.rs @@ -13,9 +13,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::PrecedenceConstrainedScheduling; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; /// Result of reducing PrecedenceConstrainedScheduling to `ILP`. /// @@ -45,17 +44,7 @@ impl ReductionResult for ReductionPCSToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/preemptivescheduling_ilp.rs b/src/rules/preemptivescheduling_ilp.rs index 07b496635..e906f13cf 100644 --- a/src/rules/preemptivescheduling_ilp.rs +++ b/src/rules/preemptivescheduling_ilp.rs @@ -24,9 +24,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::PreemptiveScheduling; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; /// Result of reducing PreemptiveScheduling to `ILP`. /// @@ -58,17 +57,7 @@ impl ReductionResult for ReductionPSToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/prizecollectingsteinerforest_steinertree.rs b/src/rules/prizecollectingsteinerforest_steinertree.rs index ee06eaf0c..51e6008b8 100644 --- a/src/rules/prizecollectingsteinerforest_steinertree.rs +++ b/src/rules/prizecollectingsteinerforest_steinertree.rs @@ -35,9 +35,8 @@ use crate::models::graph::{PrizeCollectingSteinerForest, SteinerTree}; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; /// Result of reducing PCSF to SteinerTree. @@ -78,17 +77,7 @@ impl ReductionResult for ReductionPCSFToSteinerTree { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/quadraticassignment_ilp.rs b/src/rules/quadraticassignment_ilp.rs index 37101a692..1e1229010 100644 --- a/src/rules/quadraticassignment_ilp.rs +++ b/src/rules/quadraticassignment_ilp.rs @@ -11,9 +11,8 @@ use crate::models::algebraic::QuadraticAssignment; use crate::models::algebraic::{ObjectiveSense, ILP}; use crate::reduction; use crate::rules::ilp_helpers::{mccormick_product, one_hot_assignment_constraints}; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; /// Result of reducing QuadraticAssignment to ILP. /// @@ -41,17 +40,7 @@ impl ReductionResult for ReductionQAPToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/qubo_ilp.rs b/src/rules/qubo_ilp.rs index 638667784..4f39a59cb 100644 --- a/src/rules/qubo_ilp.rs +++ b/src/rules/qubo_ilp.rs @@ -17,9 +17,8 @@ use crate::models::algebraic::{ILPCoefficient, ObjectiveSense, ILP, QUBO}; use crate::reduction; use crate::rules::ilp_helpers::mccormick_product; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; /// Result of reducing QUBO to ILP. #[derive(Debug, Clone)] @@ -44,17 +43,7 @@ where source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/rectilinearpicturecompression_ilp.rs b/src/rules/rectilinearpicturecompression_ilp.rs index ac4d931aa..6ebf2c56a 100644 --- a/src/rules/rectilinearpicturecompression_ilp.rs +++ b/src/rules/rectilinearpicturecompression_ilp.rs @@ -6,9 +6,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::RectilinearPictureCompression; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; #[derive(Debug, Clone)] pub struct ReductionRPCToILP { @@ -28,28 +27,9 @@ impl ReductionResult for ReductionRPCToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } - } -} - -impl ReductionRPCToILP { - fn map_solution( - &self, - target_solution: &<::Target as crate::traits::Problem>::Solution, - ) -> crate::rules::ExtractionResult< - <::Source as crate::traits::Problem>::Solution, - > { - Ok(target_solution.iter().map(|&value| value == 1).collect()) + recover_preserving_status(source, target, |solution| { + Ok(solution.iter().map(|&value| value == 1).collect()) + }) } } diff --git a/src/rules/registersufficiency_ilp.rs b/src/rules/registersufficiency_ilp.rs index 77dfa62af..2d5233759 100644 --- a/src/rules/registersufficiency_ilp.rs +++ b/src/rules/registersufficiency_ilp.rs @@ -10,9 +10,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::RegisterSufficiency; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; #[derive(Debug, Clone)] pub struct ReductionRegisterSufficiencyToILP { @@ -33,17 +32,7 @@ impl ReductionResult for ReductionRegisterSufficiencyToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/resourceconstrainedscheduling_ilp.rs b/src/rules/resourceconstrainedscheduling_ilp.rs index 9ebf01c5f..c05f65408 100644 --- a/src/rules/resourceconstrainedscheduling_ilp.rs +++ b/src/rules/resourceconstrainedscheduling_ilp.rs @@ -7,9 +7,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::ResourceConstrainedScheduling; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; /// Result of reducing ResourceConstrainedScheduling to `ILP`. /// @@ -36,17 +35,7 @@ impl ReductionResult for ReductionRCSToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/rootedtreearrangement_rootedtreestorageassignment.rs b/src/rules/rootedtreearrangement_rootedtreestorageassignment.rs index 9a4534166..9de5919e4 100644 --- a/src/rules/rootedtreearrangement_rootedtreestorageassignment.rs +++ b/src/rules/rootedtreearrangement_rootedtreestorageassignment.rs @@ -12,9 +12,8 @@ use crate::models::graph::RootedTreeArrangement; use crate::models::set::RootedTreeStorageAssignment; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; /// Result of reducing RootedTreeArrangement to RootedTreeStorageAssignment. @@ -43,17 +42,7 @@ impl ReductionResult for ReductionRootedTreeArrangementToRootedTreeStorageAssign source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/rootedtreestorageassignment_ilp.rs b/src/rules/rootedtreestorageassignment_ilp.rs index 63985e29b..4f6123bb1 100644 --- a/src/rules/rootedtreestorageassignment_ilp.rs +++ b/src/rules/rootedtreestorageassignment_ilp.rs @@ -8,7 +8,7 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::set::RootedTreeStorageAssignment; use crate::reduction; use crate::rules::ilp_helpers::{mccormick_product, one_hot_decode_rows}; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; use crate::solvers::SolveOutcome; @@ -79,28 +79,9 @@ impl ReductionResult for ReductionRTSAToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } - } -} - -impl ReductionRTSAToILP { - fn map_solution( - &self, - target_solution: &<::Target as crate::traits::Problem>::Solution, - ) -> crate::rules::ExtractionResult< - <::Source as crate::traits::Problem>::Solution, - > { - Ok(one_hot_decode_rows(target_solution, self.n, self.n, 0)) + recover_preserving_status(source, target, |solution| { + Ok(one_hot_decode_rows(solution, self.n, self.n, 0)) + }) } } diff --git a/src/rules/ruralpostman_ilp.rs b/src/rules/ruralpostman_ilp.rs index 6ef6a505c..0430c0d35 100644 --- a/src/rules/ruralpostman_ilp.rs +++ b/src/rules/ruralpostman_ilp.rs @@ -7,9 +7,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::RuralPostman; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; use crate::types::WeightElement; @@ -33,17 +32,7 @@ impl ReductionResult for ReductionRPToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/sat_circuitsat.rs b/src/rules/sat_circuitsat.rs index 5baa3ff09..3e15c5894 100644 --- a/src/rules/sat_circuitsat.rs +++ b/src/rules/sat_circuitsat.rs @@ -6,9 +6,8 @@ use crate::models::formula::Satisfiability; use crate::models::formula::{Assignment, BooleanExpr, Circuit, CircuitSAT}; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use std::collections::HashSet; /// Result of reducing SAT to CircuitSAT. @@ -32,17 +31,7 @@ impl ReductionResult for ReductionSATToCircuit { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/sat_coloring.rs b/src/rules/sat_coloring.rs index 754ffd30b..47647f67f 100644 --- a/src/rules/sat_coloring.rs +++ b/src/rules/sat_coloring.rs @@ -12,9 +12,8 @@ use crate::models::formula::Satisfiability; use crate::models::graph::KColoring; use crate::reduction; use crate::rules::sat_maximumindependentset::BoolVar; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::topology::SimpleGraph; use crate::variant::K3; use std::collections::HashMap; @@ -247,17 +246,7 @@ impl ReductionResult for ReductionSATToColoring { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/sat_ksat.rs b/src/rules/sat_ksat.rs index 76ee4d55f..988c54d07 100644 --- a/src/rules/sat_ksat.rs +++ b/src/rules/sat_ksat.rs @@ -9,9 +9,8 @@ use crate::models::formula::{CNFClause, KSatisfiability, Satisfiability}; use crate::reduction; use crate::rules::sat_helpers::SatVariableAllocator; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::variant::{KValue, K2, K3, KN}; /// Result of reducing general SAT to K-SAT. @@ -39,17 +38,7 @@ impl ReductionResult for ReductionSATToKSAT { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } @@ -209,17 +198,7 @@ impl ReductionResult for ReductionKSATToSAT { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/sat_maximumindependentset.rs b/src/rules/sat_maximumindependentset.rs index 0d4c7d65c..60e3aac7a 100644 --- a/src/rules/sat_maximumindependentset.rs +++ b/src/rules/sat_maximumindependentset.rs @@ -12,9 +12,8 @@ use crate::models::decision::Decision; use crate::models::formula::Satisfiability; use crate::models::graph::MaximumIndependentSet; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::topology::SimpleGraph; use crate::types::One; @@ -84,17 +83,7 @@ impl ReductionResult for ReductionSATToIS { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/sat_minimumdominatingset.rs b/src/rules/sat_minimumdominatingset.rs index cc94c8109..87c8122e5 100644 --- a/src/rules/sat_minimumdominatingset.rs +++ b/src/rules/sat_minimumdominatingset.rs @@ -19,9 +19,8 @@ use crate::models::formula::Satisfiability; use crate::models::graph::MinimumDominatingSet; use crate::reduction; use crate::rules::sat_maximumindependentset::BoolVar; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::topology::SimpleGraph; use std::collections::BTreeMap; @@ -63,17 +62,7 @@ impl ReductionResult for ReductionSATToDS { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/satisfiability_integralflowhomologousarcs.rs b/src/rules/satisfiability_integralflowhomologousarcs.rs index 72c221f45..7c5a5fd00 100644 --- a/src/rules/satisfiability_integralflowhomologousarcs.rs +++ b/src/rules/satisfiability_integralflowhomologousarcs.rs @@ -9,9 +9,8 @@ use crate::models::formula::Satisfiability; use crate::models::graph::IntegralFlowHomologousArcs; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::topology::DirectedGraph; #[derive(Debug, Clone)] @@ -109,17 +108,7 @@ impl ReductionResult for ReductionSATToIntegralFlowHomologousArcs { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/satisfiability_maximum2satisfiability.rs b/src/rules/satisfiability_maximum2satisfiability.rs index 936274d5d..769946ec7 100644 --- a/src/rules/satisfiability_maximum2satisfiability.rs +++ b/src/rules/satisfiability_maximum2satisfiability.rs @@ -4,9 +4,8 @@ use crate::models::decision::Decision; use crate::models::formula::{CNFClause, Maximum2Satisfiability, Satisfiability}; use crate::reduction; use crate::rules::sat_helpers::SatVariableAllocator; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; /// Result of reducing SAT to MAX-2-SAT. #[derive(Debug, Clone)] @@ -28,28 +27,9 @@ impl ReductionResult for ReductionSatisfiabilityToMaximum2Satisfiability { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } - } -} - -impl ReductionSatisfiabilityToMaximum2Satisfiability { - fn map_solution( - &self, - target_solution: &<::Target as crate::traits::Problem>::Solution, - ) -> crate::rules::ExtractionResult< - <::Source as crate::traits::Problem>::Solution, - > { - Ok(target_solution[..self.source_num_vars].to_vec()) + recover_preserving_status(source, target, |solution| { + Ok(solution[..self.source_num_vars].to_vec()) + }) } } diff --git a/src/rules/satisfiability_naesatisfiability.rs b/src/rules/satisfiability_naesatisfiability.rs index 177bbd548..902cd27c9 100644 --- a/src/rules/satisfiability_naesatisfiability.rs +++ b/src/rules/satisfiability_naesatisfiability.rs @@ -10,9 +10,8 @@ use crate::models::formula::{CNFClause, NAESatisfiability, Satisfiability}; use crate::reduction; use crate::rules::sat_helpers::SatVariableAllocator; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; /// Result of reducing Satisfiability to NAE-Satisfiability. #[derive(Debug, Clone)] @@ -36,17 +35,7 @@ impl ReductionResult for ReductionSATToNAESAT { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/schedulingtominimizeweightedcompletiontime_ilp.rs b/src/rules/schedulingtominimizeweightedcompletiontime_ilp.rs index 5d238488f..d1f0eb2ef 100644 --- a/src/rules/schedulingtominimizeweightedcompletiontime_ilp.rs +++ b/src/rules/schedulingtominimizeweightedcompletiontime_ilp.rs @@ -9,9 +9,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::SchedulingToMinimizeWeightedCompletionTime; use crate::reduction; use crate::rules::ilp_helpers::one_hot_decode_rows; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; /// Result of reducing SchedulingToMinimizeWeightedCompletionTime to ILP. /// @@ -59,17 +58,7 @@ impl ReductionResult for ReductionSMWCTToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/schedulingwithindividualdeadlines_ilp.rs b/src/rules/schedulingwithindividualdeadlines_ilp.rs index a9fff7fab..87d4e3a0a 100644 --- a/src/rules/schedulingwithindividualdeadlines_ilp.rs +++ b/src/rules/schedulingwithindividualdeadlines_ilp.rs @@ -15,9 +15,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::SchedulingWithIndividualDeadlines; use crate::reduction; use crate::rules::ilp_helpers::one_hot_decode_rows; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; /// Result of reducing SchedulingWithIndividualDeadlines to `ILP`. /// @@ -46,17 +45,7 @@ impl ReductionResult for ReductionSWIDToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/sequencingtominimizemaximumcumulativecost_ilp.rs b/src/rules/sequencingtominimizemaximumcumulativecost_ilp.rs index 08823d93c..2d18f590d 100644 --- a/src/rules/sequencingtominimizemaximumcumulativecost_ilp.rs +++ b/src/rules/sequencingtominimizemaximumcumulativecost_ilp.rs @@ -8,9 +8,8 @@ use crate::models::algebraic::{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}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; /// Result of reducing SequencingToMinimizeMaximumCumulativeCost to `ILP`. /// @@ -38,17 +37,7 @@ impl ReductionResult for ReductionSTMMCCToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/sequencingtominimizetardytaskweight_ilp.rs b/src/rules/sequencingtominimizetardytaskweight_ilp.rs index b2b06fb99..b7e8a89ad 100644 --- a/src/rules/sequencingtominimizetardytaskweight_ilp.rs +++ b/src/rules/sequencingtominimizetardytaskweight_ilp.rs @@ -8,9 +8,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::SequencingToMinimizeTardyTaskWeight; use crate::reduction; use crate::rules::ilp_helpers::one_hot_decode; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; /// Result of reducing SequencingToMinimizeTardyTaskWeight to `ILP`. #[derive(Debug, Clone)] @@ -32,17 +31,7 @@ impl ReductionResult for ReductionSTMTTWToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/sequencingtominimizeweightedcompletiontime_ilp.rs b/src/rules/sequencingtominimizeweightedcompletiontime_ilp.rs index e3617b856..5c3945526 100644 --- a/src/rules/sequencingtominimizeweightedcompletiontime_ilp.rs +++ b/src/rules/sequencingtominimizeweightedcompletiontime_ilp.rs @@ -8,9 +8,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::SequencingToMinimizeWeightedCompletionTime; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; #[derive(Debug, Clone)] pub struct ReductionSTMWCTToILP { @@ -44,17 +43,7 @@ impl ReductionResult for ReductionSTMWCTToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/sequencingtominimizeweightedtardiness_ilp.rs b/src/rules/sequencingtominimizeweightedtardiness_ilp.rs index a4a86ff47..7c45b8090 100644 --- a/src/rules/sequencingtominimizeweightedtardiness_ilp.rs +++ b/src/rules/sequencingtominimizeweightedtardiness_ilp.rs @@ -7,9 +7,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::SequencingToMinimizeWeightedTardiness; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; /// Result of reducing SequencingToMinimizeWeightedTardiness to `ILP`. /// @@ -40,17 +39,7 @@ impl ReductionResult for ReductionSTMWTToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/sequencingwithdeadlinesandsetuptimes_ilp.rs b/src/rules/sequencingwithdeadlinesandsetuptimes_ilp.rs index ab5726d01..cfe6ef8ba 100644 --- a/src/rules/sequencingwithdeadlinesandsetuptimes_ilp.rs +++ b/src/rules/sequencingwithdeadlinesandsetuptimes_ilp.rs @@ -19,9 +19,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::SequencingWithDeadlinesAndSetUpTimes; use crate::reduction; use crate::rules::ilp_helpers::one_hot_decode; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; /// Result of reducing SequencingWithDeadlinesAndSetUpTimes to `ILP`. #[derive(Debug, Clone)] @@ -43,17 +42,7 @@ impl ReductionResult for ReductionSWDSTToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/sequencingwithinintervals_ilp.rs b/src/rules/sequencingwithinintervals_ilp.rs index 20355e37a..d64c88756 100644 --- a/src/rules/sequencingwithinintervals_ilp.rs +++ b/src/rules/sequencingwithinintervals_ilp.rs @@ -18,7 +18,7 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::SequencingWithinIntervals; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; use crate::solvers::SolveOutcome; @@ -50,17 +50,7 @@ impl ReductionResult for ReductionSWIToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/sequencingwithreleasetimesanddeadlines_ilp.rs b/src/rules/sequencingwithreleasetimesanddeadlines_ilp.rs index d12e06e68..103e72167 100644 --- a/src/rules/sequencingwithreleasetimesanddeadlines_ilp.rs +++ b/src/rules/sequencingwithreleasetimesanddeadlines_ilp.rs @@ -7,9 +7,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::SequencingWithReleaseTimesAndDeadlines; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; /// Result of reducing SequencingWithReleaseTimesAndDeadlines to `ILP`. /// @@ -36,17 +35,7 @@ impl ReductionResult for ReductionSWRTDToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/setsplitting_betweenness.rs b/src/rules/setsplitting_betweenness.rs index b48c69045..67241143e 100644 --- a/src/rules/setsplitting_betweenness.rs +++ b/src/rules/setsplitting_betweenness.rs @@ -10,9 +10,8 @@ use crate::models::misc::Betweenness; use crate::models::set::SetSplitting; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; /// Result of reducing SetSplitting to Betweenness. #[derive(Debug, Clone)] @@ -35,17 +34,7 @@ impl ReductionResult for ReductionSetSplittingToBetweenness { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/setsplitting_ilp.rs b/src/rules/setsplitting_ilp.rs index 23ebce3fd..0ee1066a7 100644 --- a/src/rules/setsplitting_ilp.rs +++ b/src/rules/setsplitting_ilp.rs @@ -12,9 +12,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::set::SetSplitting; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; /// Result of reducing SetSplitting to ILP. #[derive(Debug, Clone)] @@ -35,28 +34,9 @@ impl ReductionResult for ReductionSetSplittingToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } - } -} - -impl ReductionSetSplittingToILP { - fn map_solution( - &self, - target_solution: &<::Target as crate::traits::Problem>::Solution, - ) -> crate::rules::ExtractionResult< - <::Source as crate::traits::Problem>::Solution, - > { - Ok(target_solution.iter().map(|&value| value == 1).collect()) + recover_preserving_status(source, target, |solution| { + Ok(solution.iter().map(|&value| value == 1).collect()) + }) } } diff --git a/src/rules/shortestcommonsupersequence_ilp.rs b/src/rules/shortestcommonsupersequence_ilp.rs index aca74e33a..23a7940a0 100644 --- a/src/rules/shortestcommonsupersequence_ilp.rs +++ b/src/rules/shortestcommonsupersequence_ilp.rs @@ -8,7 +8,7 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::ShortestCommonSupersequence; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; use crate::solvers::SolveOutcome; @@ -34,17 +34,7 @@ impl ReductionResult for ReductionSCSToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/shortestweightconstrainedpath_ilp.rs b/src/rules/shortestweightconstrainedpath_ilp.rs index daf30cb48..a996f6128 100644 --- a/src/rules/shortestweightconstrainedpath_ilp.rs +++ b/src/rules/shortestweightconstrainedpath_ilp.rs @@ -9,9 +9,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::ShortestWeightConstrainedPath; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; use crate::types::WeightElement; @@ -47,17 +46,7 @@ impl ReductionResult for ReductionSWCPToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/sparsematrixcompression_ilp.rs b/src/rules/sparsematrixcompression_ilp.rs index fd256607b..eb0b01063 100644 --- a/src/rules/sparsematrixcompression_ilp.rs +++ b/src/rules/sparsematrixcompression_ilp.rs @@ -5,7 +5,7 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, SparseMatrixCompression, ILP}; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; use crate::solvers::SolveOutcome; @@ -29,17 +29,7 @@ impl ReductionResult for ReductionSMCToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/spinglass_maxcut.rs b/src/rules/spinglass_maxcut.rs index 8e56561f4..df7de8703 100644 --- a/src/rules/spinglass_maxcut.rs +++ b/src/rules/spinglass_maxcut.rs @@ -6,9 +6,8 @@ use crate::models::graph::MaxCut; use crate::models::graph::SpinGlass; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; use crate::types::WeightElement; use num_traits::Zero; @@ -42,38 +41,9 @@ where source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } - } -} - -impl ReductionMaxCutToSG -where - W: WeightElement - + crate::variant::VariantParam - + PartialOrd - + num_traits::Num - + num_traits::Zero - + num_traits::Bounded - + std::ops::AddAssign - + std::ops::Mul, -{ - fn map_solution( - &self, - target_solution: &<::Target as crate::traits::Problem>::Solution, - ) -> crate::rules::ExtractionResult< - <::Source as crate::traits::Problem>::Solution, - > { - Ok(target_solution.iter().map(|&spin| spin == 1).collect()) + recover_preserving_status(source, target, |solution| { + Ok(solution.iter().map(|&spin| spin == 1).collect()) + }) } } @@ -158,17 +128,7 @@ where source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/spinglass_qubo.rs b/src/rules/spinglass_qubo.rs index 0410881f5..26dbc1742 100644 --- a/src/rules/spinglass_qubo.rs +++ b/src/rules/spinglass_qubo.rs @@ -8,9 +8,8 @@ use crate::models::algebraic::QUBO; use crate::models::graph::SpinGlass; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::topology::SimpleGraph; /// Result of reducing QUBO to SpinGlass. @@ -33,28 +32,9 @@ impl ReductionResult for ReductionQUBOToSG { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } - } -} - -impl ReductionQUBOToSG { - fn map_solution( - &self, - target_solution: &<::Target as crate::traits::Problem>::Solution, - ) -> crate::rules::ExtractionResult< - <::Source as crate::traits::Problem>::Solution, - > { - Ok(target_solution.iter().map(|&spin| spin == 1).collect()) + recover_preserving_status(source, target, |solution| { + Ok(solution.iter().map(|&spin| spin == 1).collect()) + }) } } @@ -143,17 +123,7 @@ where source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/stackercrane_ilp.rs b/src/rules/stackercrane_ilp.rs index b91d45c91..93996eec7 100644 --- a/src/rules/stackercrane_ilp.rs +++ b/src/rules/stackercrane_ilp.rs @@ -8,9 +8,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::StackerCrane; use crate::reduction; use crate::rules::ilp_helpers::{mccormick_product, one_hot_decode}; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; /// Result of reducing StackerCrane to ILP. /// @@ -38,17 +37,7 @@ impl ReductionResult for ReductionSCToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/steinertree_ilp.rs b/src/rules/steinertree_ilp.rs index 15104a9eb..7f7eeaf0c 100644 --- a/src/rules/steinertree_ilp.rs +++ b/src/rules/steinertree_ilp.rs @@ -7,9 +7,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::SteinerTree; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; /// Binary layout: m edge selectors, n vertex selectors, then 2m flow arcs @@ -33,17 +32,7 @@ impl ReductionResult for ReductionSteinerTreeToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/stringtostringcorrection_ilp.rs b/src/rules/stringtostringcorrection_ilp.rs index 56ef689bc..58e1eccb9 100644 --- a/src/rules/stringtostringcorrection_ilp.rs +++ b/src/rules/stringtostringcorrection_ilp.rs @@ -7,7 +7,7 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::StringToStringCorrection; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; use crate::solvers::SolveOutcome; @@ -61,17 +61,7 @@ impl ReductionResult for ReductionSTSCToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/strongconnectivityaugmentation_ilp.rs b/src/rules/strongconnectivityaugmentation_ilp.rs index 77f1c56cb..7f63a8b55 100644 --- a/src/rules/strongconnectivityaugmentation_ilp.rs +++ b/src/rules/strongconnectivityaugmentation_ilp.rs @@ -7,7 +7,7 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::StrongConnectivityAugmentation; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; use crate::solvers::SolveOutcome; @@ -30,17 +30,7 @@ impl ReductionResult for ReductionSCAToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/subgraphisomorphism_ilp.rs b/src/rules/subgraphisomorphism_ilp.rs index 8f699a50e..fd67e5128 100644 --- a/src/rules/subgraphisomorphism_ilp.rs +++ b/src/rules/subgraphisomorphism_ilp.rs @@ -11,9 +11,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::SubgraphIsomorphism; use crate::reduction; use crate::rules::ilp_helpers::{one_hot_assignment_constraints, one_hot_decode_rows}; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::topology::Graph; /// Result of reducing SubgraphIsomorphism to ILP. @@ -41,17 +40,7 @@ impl ReductionResult for ReductionSubIsoToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/subsetsum_closestvectorproblem.rs b/src/rules/subsetsum_closestvectorproblem.rs index 9e6421309..023bb31f5 100644 --- a/src/rules/subsetsum_closestvectorproblem.rs +++ b/src/rules/subsetsum_closestvectorproblem.rs @@ -4,9 +4,8 @@ use crate::models::algebraic::ClosestVectorProblem; use crate::models::decision::Decision; use crate::models::misc::SubsetSum; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use num_rational::BigRational; /// Result of reducing SubsetSum to ClosestVectorProblem. @@ -29,17 +28,7 @@ impl ReductionResult for ReductionSubsetSumToClosestVectorProblem { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/subsetsum_integerexpressionmembership.rs b/src/rules/subsetsum_integerexpressionmembership.rs index f5ecfdff7..cd7c65c69 100644 --- a/src/rules/subsetsum_integerexpressionmembership.rs +++ b/src/rules/subsetsum_integerexpressionmembership.rs @@ -1,9 +1,8 @@ use crate::models::misc::SubsetSum; use crate::models::misc::{IntExpr, IntegerExpressionMembership}; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use num_traits::ToPrimitive; #[derive(Debug, Clone)] @@ -24,17 +23,7 @@ impl ReductionResult for ReductionSubsetSumToIntegerExpressionMembership { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/subsetsum_partition.rs b/src/rules/subsetsum_partition.rs index efb2de6cb..6d085be04 100644 --- a/src/rules/subsetsum_partition.rs +++ b/src/rules/subsetsum_partition.rs @@ -2,9 +2,8 @@ use crate::models::misc::{Partition, SubsetSum}; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use num_bigint::BigUint; use num_traits::ToPrimitive; use std::cmp::Ordering; @@ -37,17 +36,7 @@ impl ReductionResult for ReductionSubsetSumToPartition { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/sumofsquarespartition_ilp.rs b/src/rules/sumofsquarespartition_ilp.rs index a8debff85..36da2363e 100644 --- a/src/rules/sumofsquarespartition_ilp.rs +++ b/src/rules/sumofsquarespartition_ilp.rs @@ -20,9 +20,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::SumOfSquaresPartition; use crate::reduction; use crate::rules::ilp_helpers::mccormick_product; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; /// Result of reducing SumOfSquaresPartition to ILP. /// @@ -64,17 +63,7 @@ impl ReductionResult for ReductionSSPToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/threedimensionalmatching_ilp.rs b/src/rules/threedimensionalmatching_ilp.rs index 790f3968f..3f62365a2 100644 --- a/src/rules/threedimensionalmatching_ilp.rs +++ b/src/rules/threedimensionalmatching_ilp.rs @@ -3,9 +3,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::set::ThreeDimensionalMatching; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; #[derive(Debug, Clone)] pub struct ReductionThreeDimensionalMatchingToILP { @@ -25,28 +24,9 @@ impl ReductionResult for ReductionThreeDimensionalMatchingToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } - } -} - -impl ReductionThreeDimensionalMatchingToILP { - fn map_solution( - &self, - target_solution: &<::Target as crate::traits::Problem>::Solution, - ) -> crate::rules::ExtractionResult< - <::Source as crate::traits::Problem>::Solution, - > { - Ok(target_solution.iter().map(|&value| value == 1).collect()) + recover_preserving_status(source, target, |solution| { + Ok(solution.iter().map(|&value| value == 1).collect()) + }) } } diff --git a/src/rules/threedimensionalmatching_threepartition.rs b/src/rules/threedimensionalmatching_threepartition.rs index 7fe25a94d..de788656d 100644 --- a/src/rules/threedimensionalmatching_threepartition.rs +++ b/src/rules/threedimensionalmatching_threepartition.rs @@ -8,9 +8,8 @@ use crate::models::misc::ThreePartition; use crate::models::set::ThreeDimensionalMatching; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; #[derive(Debug, Clone, Copy)] enum Step2Item { @@ -269,17 +268,7 @@ impl ReductionResult for ReductionThreeDimensionalMatchingToThreePartition { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/threepartition_sequencingwithreleasetimesanddeadlines.rs b/src/rules/threepartition_sequencingwithreleasetimesanddeadlines.rs index b20cc2e91..605fa3809 100644 --- a/src/rules/threepartition_sequencingwithreleasetimesanddeadlines.rs +++ b/src/rules/threepartition_sequencingwithreleasetimesanddeadlines.rs @@ -13,9 +13,8 @@ use crate::models::misc::{SequencingWithReleaseTimesAndDeadlines, ThreePartition}; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; /// Number of element tasks (= source.num_elements() = 3m). fn num_element_tasks(source: &ThreePartition) -> usize { @@ -54,17 +53,7 @@ impl ReductionResult for ReductionThreePartitionToSRTD { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/timetabledesign_ilp.rs b/src/rules/timetabledesign_ilp.rs index 06dcf1113..2df404575 100644 --- a/src/rules/timetabledesign_ilp.rs +++ b/src/rules/timetabledesign_ilp.rs @@ -7,9 +7,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::misc::TimetableDesign; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; /// Result of reducing TimetableDesign to `ILP`. /// @@ -38,17 +37,7 @@ impl ReductionResult for ReductionTDToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/traits.rs b/src/rules/traits.rs index 63bbd7f08..65e773528 100644 --- a/src/rules/traits.rs +++ b/src/rules/traits.rs @@ -1,5 +1,6 @@ //! Core traits for problem reductions. +use crate::solvers::{ProblemOutcome, SolveOutcome}; use crate::traits::Problem; use std::any::Any; use std::marker::PhantomData; @@ -184,6 +185,28 @@ pub trait ReductionResult { ) -> ExtractionResult>; } +/// Recover using a rule that preserves optimality, feasibility, and infeasibility. +/// +/// The caller must establish all three implications for its mathematical mapping. +/// Rules requiring optimum thresholds or rejecting feasible incumbents must instead +/// interpret those outcomes in their own `recover_result` implementation. +/// Source evaluation errors propagate; they never establish source infeasibility. +pub(super) fn recover_preserving_status( + source: &P, + target: SolveOutcome, + map_solution: impl FnOnce(&S) -> ExtractionResult, +) -> ExtractionResult> { + match target { + SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => { + Ok(SolveOutcome::optimal(source, map_solution(&solution)?)?) + } + SolveOutcome::Feasible { solution, .. } => { + Ok(SolveOutcome::feasible(source, map_solution(&solution)?)?) + } + } +} + /// Trait for problems that can be reduced to target type T. /// /// # Example diff --git a/src/rules/travelingsalesman_ilp.rs b/src/rules/travelingsalesman_ilp.rs index 55be91ad0..b8b6aa257 100644 --- a/src/rules/travelingsalesman_ilp.rs +++ b/src/rules/travelingsalesman_ilp.rs @@ -9,9 +9,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::TravelingSalesman; use crate::reduction; use crate::rules::ilp_helpers::{mccormick_product, one_hot_decode}; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; /// Result of reducing TravelingSalesman to ILP. @@ -39,17 +38,7 @@ impl ReductionResult for ReductionTSPToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/undirectedflowlowerbounds_ilp.rs b/src/rules/undirectedflowlowerbounds_ilp.rs index bacaf2206..76bd7f77f 100644 --- a/src/rules/undirectedflowlowerbounds_ilp.rs +++ b/src/rules/undirectedflowlowerbounds_ilp.rs @@ -26,9 +26,8 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::UndirectedFlowLowerBounds; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::topology::Graph; /// Result of reducing UndirectedFlowLowerBounds to `ILP`. @@ -61,17 +60,7 @@ impl ReductionResult for ReductionUFLBToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/undirectedtwocommodityintegralflow_ilp.rs b/src/rules/undirectedtwocommodityintegralflow_ilp.rs index 2dddb38b2..0b2126d36 100644 --- a/src/rules/undirectedtwocommodityintegralflow_ilp.rs +++ b/src/rules/undirectedtwocommodityintegralflow_ilp.rs @@ -27,7 +27,7 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, ILP}; use crate::models::graph::UndirectedTwoCommodityIntegralFlow; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; use crate::solvers::SolveOutcome; use crate::topology::Graph; @@ -58,17 +58,7 @@ impl ReductionResult for ReductionU2CIFToILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/unit_tests/rules/traits.rs b/src/unit_tests/rules/traits.rs index 6186173d0..71ebfa6f0 100644 --- a/src/unit_tests/rules/traits.rs +++ b/src/unit_tests/rules/traits.rs @@ -4,6 +4,74 @@ use crate::traits::Problem; use crate::types::Min; use serde_json::json; +#[test] +fn recovery_preserves_status_and_evaluates_the_mapped_solution() { + use crate::rules::traits::recover_preserving_status; + + let source = SourceProblem; + let target = TargetProblem; + let complement = + |solution: &Vec| Ok(solution.iter().map(|value| 1 - value).collect::>()); + assert_eq!( + recover_preserving_status( + &source, + SolveOutcome::optimal(&target, vec![1, 1]).unwrap(), + complement, + ) + .unwrap(), + SolveOutcome::Optimal { + solution: vec![0, 0], + evaluation: Min(Some(0)), + } + ); + assert_eq!( + recover_preserving_status( + &source, + SolveOutcome::feasible(&target, vec![0, 0]).unwrap(), + complement, + ) + .unwrap(), + SolveOutcome::Feasible { + solution: vec![1, 1], + evaluation: Min(Some(2)), + } + ); + assert_eq!( + recover_preserving_status( + &source, + crate::solvers::ProblemOutcome::::Infeasible, + |_| panic!("infeasibility has no witness to map"), + ) + .unwrap(), + SolveOutcome::Infeasible + ); +} + +#[test] +fn recovery_propagates_mapping_and_evaluation_failures() { + use crate::rules::traits::recover_preserving_status; + use crate::rules::ExtractionError; + use crate::traits::EvaluationError; + + for outcome in [ + SolveOutcome::optimal(&TargetProblem, vec![1, 1]).unwrap(), + SolveOutcome::feasible(&TargetProblem, vec![1, 1]).unwrap(), + ] { + assert!(matches!( + recover_preserving_status(&SourceProblem, outcome.clone(), |_| { + Err(ExtractionError::InsufficientSolutionQuality) + }), + Err(ExtractionError::InsufficientSolutionQuality) + )); + assert!(matches!( + recover_preserving_status(&SourceProblem, outcome, |_| Ok(vec![2, 0])), + Err(ExtractionError::Evaluation( + EvaluationError::InvalidConfiguration(_) + )) + )); + } +} + #[derive(Clone)] struct SourceProblem; #[derive(Clone)] From e99c577c2965702ba8127a9f9b36a32783c65dda Mon Sep 17 00:00:00 2001 From: Xiwei Pan Date: Tue, 15 Sep 2026 15:37:22 +0800 Subject: [PATCH 10/42] refactor: separate integer and numerical CVP variants --- .claude/CLAUDE.md | 14 + docs/paper/reductions.typ | 14 +- docs/src/design.md | 20 +- .../src/commands/create/tests.rs | 12 +- problemreductions-cli/tests/cli_tests.rs | 64 ++- .../algebraic/closest_vector_problem.rs | 395 +++++++++--------- src/models/algebraic/mod.rs | 2 +- src/rules/closestvectorproblem_casts.rs | 35 -- src/rules/closestvectorproblem_qubo.rs | 6 +- src/rules/mod.rs | 1 - src/rules/subsetsum_closestvectorproblem.rs | 8 +- .../customized/closest_vector_problem.rs | 211 ++++++++-- src/solvers/customized/solver.rs | 54 ++- src/solvers/registry.rs | 2 +- src/solvers/resolver.rs | 20 +- .../algebraic/closest_vector_problem.rs | 174 +++++--- .../rules/closestvectorproblem_casts.rs | 40 -- .../rules/closestvectorproblem_qubo.rs | 28 +- .../rules/subsetsum_closestvectorproblem.rs | 36 +- .../customized/closest_vector_problem.rs | 101 +++-- src/unit_tests/solvers/customized/solver.rs | 2 +- src/unit_tests/solvers/registry.rs | 4 +- src/unit_tests/solvers/resolver.rs | 8 +- 23 files changed, 774 insertions(+), 477 deletions(-) delete mode 100644 src/rules/closestvectorproblem_casts.rs delete mode 100644 src/unit_tests/rules/closestvectorproblem_casts.rs diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 145fcb60c..8628e8f35 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -273,6 +273,20 @@ in every rule. See the design document for the shared search-representation cont ## Testing Requirements +### Representative examples and validation priorities + +- Lead issues, plans, documentation, and acceptance tests with small, ordinary + instances that explain problem modeling, reduction construction, solution + recovery, and solver use. Prefer a complete source-to-target-to-source example + for reductions and a recognizable application for numerical models. +- Numeric extremes such as `i64::MAX`, huge vertex counts, or tiny floating-point + differences near `2^-50` are not the package's central use cases. Do not make + them the default examples or let them dominate task scope and acceptance criteria. +- Keep focused boundary regressions when they reproduce a concrete defect or + verify a required numeric contract. Preserve required overflow and non-finite + error handling, but do not invent extreme cases to justify extra infrastructure, + public numeric types, or stronger solver guarantees. + **No single test should take more than 5 seconds.** If a test requires solving a large instance (e.g., ILP with thousands of variables), use a smaller test instance or a faster solver. Tests that exceed 5s block CI and must be refactored. **Reference implementations — read these first:** diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index 76bbcc984..588450da9 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -5404,9 +5404,9 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| let distance-squared = range(dim).fold(0, (total, d) => total + calc.pow(bx.at(d) - target.at(d), 2)) [ #problem-def("ClosestVectorProblem")[ - Given a full-column-rank integer lattice basis $bold(B) in ZZ^(m times n)$, whose columns span $cal(L)(bold(B)) = {bold(B) bold(x) : bold(x) in ZZ^n}$, and target $bold(t) in RR^m$, find $bold(x) in ZZ^n$ minimizing $norm(bold(B) bold(x) - bold(t))_2^2$. + Given a full-column-rank lattice basis $bold(B) in RR^(m times n)$, whose columns span $cal(L)(bold(B)) = {bold(B) bold(x) : bold(x) in ZZ^n}$, and target $bold(t) in RR^m$, find $bold(x) in ZZ^n$ minimizing $norm(bold(B) bold(x) - bold(t))_2^2$. ][ - The Closest Vector Problem is a fundamental lattice problem @micciancio2002 and is NP-hard @vanemde1981. The implementation provides an integer-target variant for exact reduction data and a finite-`f64` target variant for real input; both keep the lattice basis integral and place no bounds on $bold(x)$. Its reference solver uses exact rational Gram--Schmidt projections and sphere-enumeration bounds following the recursive enumeration structure of Fincke and Pohst @fincke1985. Model evaluation returns the squared distance as an exact rational, preserving the minimizers of Euclidean distance. Finite `f64` targets are interpreted as their exact binary rational values. The solver is intended for small instances. Kannan's enumeration algorithm @kannan1987 solves CVP in $n^(O(n))$ time; Micciancio and Voulgaris @micciancio2010 improved this to deterministic $O^*(4^n)$, and Aggarwal, Dadush, and Stephens-Davidowitz @aggarwal2015 achieved randomized $O^*(2^n)$. + The Closest Vector Problem is a fundamental lattice problem @micciancio2002 and is NP-hard @vanemde1981. The implementation provides two coefficient variants: `i64` basis and target entries for exact reductions, and finite `f64` entries for numerical modeling. Both return integer coefficients. Integer evaluation uses checked integer squared distance; float evaluation uses ordinary floating-point squared distance. The integer solver uses exact rational Gram--Schmidt projections and sphere enumeration following Fincke and Pohst @fincke1985. The numerical float solver uses floating-point projections and reports a feasible candidate without claiming exact optimality. Both solvers are intended for small instances. Kannan's enumeration algorithm @kannan1987 solves CVP in $n^(O(n))$ time; Micciancio and Voulgaris @micciancio2010 improved this to deterministic $O^*(4^n)$, and Aggarwal, Dadush, and Stephens-Davidowitz @aggarwal2015 achieved randomized $O^*(2^n)$. *Example.* Consider the 2D lattice with basis #range(basis.len()).map(j => $bold(b)_#(j + 1) = #fmt-vec(basis.at(j))$).join(", ") and target $bold(t) = #fmt-vec(target)$. The point $bold(B)(#coords.map(c => str(c)).join(","))^top = (#bx.map(v => str(int(v))).join(", "))^top$ equals the target, so it is a closest lattice point with squared distance #distance-squared. @@ -16582,16 +16582,6 @@ The numerical variant embeddings below preserve individual stored coefficients o _Solution extraction._ Return the target configuration unchanged. ] -#reduction-rule("ClosestVectorProblem", "ClosestVectorProblem")[ - An integer-target CVP instance converts to the floating-target variant by embedding every target coordinate with `i64_to_exact_f64`. The integer lattice basis is copied unchanged. -][ - _Construction._ Given $(B, bold(t))$ with $B in ZZ^(m times n)$ and $bold(t) in ZZ^m$, construct $(B, bold(t)')$ with $t'_i = "f64"(t_i)$ when every target coordinate satisfies $abs(t_i) <= 2^53 - 1$, the supported conversion range. - - _Correctness._ Exact coordinate conversion gives $bold(t)' = bold(t)$ in $RR^m$. Therefore $norm(B bold(x) - bold(t)')_2^2 = norm(B bold(x) - bold(t))_2^2$ for every $bold(x) in ZZ^n$, so the minimizers coincide. - - _Solution extraction._ Return the integer coefficient vector unchanged. -] - #reduction-rule("QUBO", "QUBO")[ An integer QUBO converts to the floating-coefficient variant by embedding every matrix coefficient with `i64_to_exact_f64`. ][ diff --git a/docs/src/design.md b/docs/src/design.md index 1344425e2..53edc6157 100644 --- a/docs/src/design.md +++ b/docs/src/design.md @@ -211,13 +211,19 @@ counting preservation. coefficients. Backend feasibility tolerances must not expand the model's feasible set. A declared input convention, such as checking probability sums, is distinct from accepting a solver's returned assignment. -- CVP evaluates squared distance as `Min` through its - `squared_distance()` method. Integer coordinates enter exact integer arithmetic; - finite `f64` targets retain their stored binary rational values. For example, - the zero lattice point and target `(3, 4)` have objective `25`. The customized - solver uses the same coordinate conversion. SubsetSum compares squared distance - with its integer item count. JSON evaluation uses the dependency's rational - serialization; CLI display uses fractions such as `Min(9/16)`. +- CVP has separate `coefficient=i64` and `coefficient=f64` variants for both + basis and target. Both return integer coefficient vectors. Integer CVP evaluates + squared distance as `Min` with checked arithmetic; floating CVP evaluates + it as `Min` with ordinary rounding and non-finite-result checks. Decision + bounds use the corresponding squared-distance type. SubsetSum uses the integer + variant with bound equal to its item count. Integer sphere enumeration retains + implementation-local exact arithmetic and returns an optimal solution; numerical + float enumeration returns a feasible candidate without an optimality claim. + A numerical float decision solve returns a satisfying candidate when found; + missing its bound reports insufficient solution quality, not infeasibility. + Customized solver callbacks carry these statuses through dispatch. No exact + CVP integer-to-float edge is registered: coordinate conversion alone does not + establish preservation of rounded objective ordering. - `i64_to_exact_f64()` accepts integers in `[-(2^53-1), 2^53-1]` and rejects everything outside that supported conversion range. This is a conservative interface limit, not the set of all exactly representable f64 integers. Ordinary conversion diff --git a/problemreductions-cli/src/commands/create/tests.rs b/problemreductions-cli/src/commands/create/tests.rs index 79294e200..05c803d07 100644 --- a/problemreductions-cli/src/commands/create/tests.rs +++ b/problemreductions-cli/src/commands/create/tests.rs @@ -347,7 +347,7 @@ fn test_create_schema_driven_builds_integer_target_closest_vector_problem() { panic!("expected create command"); }; - let resolved_variant = BTreeMap::from([("target".to_string(), "i64".to_string())]); + let resolved_variant = BTreeMap::from([("coefficient".to_string(), "i64".to_string())]); let (data, variant) = create_schema_driven(&args, "ClosestVectorProblem", &resolved_variant) .expect("schema-driven create should parse"); @@ -366,9 +366,9 @@ fn test_create_schema_driven_builds_real_target_closest_vector_problem() { "create", "CVP", "--basis", - "1,0;0,1", + "1,0;0.5,0.8", "--target-vec", - "0.5,1.25", + "1.6,0.9", ]) .expect("create command parses"); @@ -376,14 +376,14 @@ fn test_create_schema_driven_builds_real_target_closest_vector_problem() { panic!("expected create command"); }; - let resolved_variant = BTreeMap::from([("target".to_string(), "f64".to_string())]); + let resolved_variant = BTreeMap::from([("coefficient".to_string(), "f64".to_string())]); let (data, variant) = create_schema_driven(&args, "ClosestVectorProblem", &resolved_variant) .expect("schema-driven create should parse"); let entry = problemreductions::registry::find_variant_entry("ClosestVectorProblem", &variant) .expect("variant entry"); (entry.factory)(data.clone()).expect("factory should deserialize generated JSON"); - assert_eq!(data["basis"], serde_json::json!([[1, 0], [0, 1]])); - assert_eq!(data["target"], serde_json::json!([0.5, 1.25])); + assert_eq!(data["basis"], serde_json::json!([[1.0, 0.0], [0.5, 0.8]])); + assert_eq!(data["target"], serde_json::json!([1.6, 0.9])); } #[test] diff --git a/problemreductions-cli/tests/cli_tests.rs b/problemreductions-cli/tests/cli_tests.rs index 616acbb1a..69679a87a 100644 --- a/problemreductions-cli/tests/cli_tests.rs +++ b/problemreductions-cli/tests/cli_tests.rs @@ -10163,8 +10163,8 @@ fn test_extract_reads_bundle_from_stdin() { } #[test] -fn test_create_decision_closest_vector_preserves_rational_bound() { - let bound = serde_json::json!([num_bigint::BigInt::from(3), num_bigint::BigInt::from(2)]); +fn test_create_decision_closest_vector_preserves_integer_bound() { + let bound = serde_json::json!(3); let output = pred() .args([ "create", @@ -10263,3 +10263,63 @@ fn test_extract_preserves_feasible_status_and_rejects_invalid_witnesses() { } std::fs::remove_dir_all(directory).unwrap(); } + +#[test] +fn test_cvp_variants_create_and_solve() { + use std::io::Write; + use std::process::Stdio; + for (variant, basis, target, status, expected) in [ + ("i64", "2,0;1,2", "3,2", "optimal", 0.0), + ("f64", "1,0;0.5,0.8", "1.6,0.9", "feasible", 0.02), + ] { + let created = pred() + .args([ + "create", + &format!("CVP/{variant}"), + "--basis", + basis, + "--target-vec", + target, + ]) + .output() + .unwrap(); + assert!( + created.status.success(), + "{}", + String::from_utf8_lossy(&created.stderr) + ); + let instance: serde_json::Value = serde_json::from_slice(&created.stdout).unwrap(); + assert_eq!(instance["variant"]["coefficient"], variant); + let mut child = pred() + .args(["solve", "-"]) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .unwrap(); + child + .stdin + .take() + .unwrap() + .write_all(&created.stdout) + .unwrap(); + let solved = child.wait_with_output().unwrap(); + assert!( + solved.status.success(), + "{}", + String::from_utf8_lossy(&solved.stderr) + ); + let result: serde_json::Value = serde_json::from_slice(&solved.stdout).unwrap(); + assert_eq!(result["status"], status); + assert_eq!(result["solution"], serde_json::json!([1, 1])); + let display = result["evaluation"].as_str().unwrap(); + let value: f64 = display + .strip_prefix("Min(") + .unwrap() + .strip_suffix(')') + .unwrap() + .parse() + .unwrap(); + assert!((value - expected).abs() < 1e-12); + } +} diff --git a/src/models/algebraic/closest_vector_problem.rs b/src/models/algebraic/closest_vector_problem.rs index 37456f6a5..436b6057a 100644 --- a/src/models/algebraic/closest_vector_problem.rs +++ b/src/models/algebraic/closest_vector_problem.rs @@ -1,65 +1,22 @@ //! Closest Vector Problem (CVP). //! -//! Given an integer lattice basis `B` and a target vector `t`, find integer +//! Given a lattice basis `B` and a target vector `t`, find integer //! coefficients `x` minimizing the squared distance `||Bx - t||_2^2`. use crate::registry::{ConstructionError, CreateSpec, ProblemSchemaEntry, VariantDimension}; use crate::traits::{EvaluationError, Problem}; use crate::types::Min; -use num_bigint::BigInt; use num_rational::BigRational; use num_traits::Zero; use serde::{Deserialize, Serialize}; -/// Target coordinate domains supported by [`ClosestVectorProblem`]. -pub trait ClosestVectorTarget: Clone + std::fmt::Debug + 'static { - /// Registered value of the `target` variant dimension. - const NAME: &'static str; - - /// Validate one stored target coordinate. - fn validate(&self, index: usize) -> Result<(), ConstructionError>; - - /// Represent a stored coordinate exactly for distance evaluation and solving. - fn to_rational(&self) -> BigRational; -} - -impl ClosestVectorTarget for i64 { - const NAME: &'static str = "i64"; - - fn validate(&self, _index: usize) -> Result<(), ConstructionError> { - Ok(()) - } - - fn to_rational(&self) -> BigRational { - BigRational::from_integer((*self).into()) - } -} - -impl ClosestVectorTarget for f64 { - const NAME: &'static str = "f64"; - - fn validate(&self, index: usize) -> Result<(), ConstructionError> { - if self.is_finite() { - Ok(()) - } else { - Err(ConstructionError::NonFiniteFloat(format!( - "target coordinate at index {index} must be finite" - ))) - } - } - - fn to_rational(&self) -> BigRational { - BigRational::from_float(*self).expect("CVP target coordinate must be finite") - } -} - macro_rules! cvp_create_spec { ($name:ident, $target:ty) => { #[derive(Debug, Deserialize, crate::CreateSpec)] struct $name { - /// Integer basis matrix as semicolon-separated column vectors. + /// Basis matrix as semicolon-separated column vectors. #[create(codec = "semicolon-separated")] - basis: Vec>, + basis: Vec>, /// Target vector. #[create(name = "target_vec", codec = "comma-separated")] target: Vec<$target>, @@ -69,7 +26,7 @@ macro_rules! cvp_create_spec { type Error = ConstructionError; fn try_from(spec: $name) -> Result { - ClosestVectorProblem::new(spec.basis, spec.target) + ClosestVectorProblem::<$target>::new(spec.basis, spec.target) } } }; @@ -83,79 +40,60 @@ inventory::submit! { name: "ClosestVectorProblem", display_name: "Closest Vector Problem", aliases: &["CVP"], - dimensions: &[VariantDimension::new("target", "i64", &["i64", "f64"])], + dimensions: &[VariantDimension::new("coefficient", "i64", &["i64", "f64"])], category: crate::registry::ProblemCategory::Algebraic, module_path: module_path!(), - description: "Find the closest point in an integer lattice to a target vector", + description: "Find the closest point in a lattice to a target vector", fields: ClosestVectorProblemI64CreateSpec::FIELDS, } } -/// Euclidean Closest Vector Problem over an integer lattice basis. +/// Euclidean Closest Vector Problem with integer or floating-point coordinates. #[derive(Debug, Clone, Serialize)] pub struct ClosestVectorProblem { /// Basis matrix stored as column vectors. - basis: Vec>, + basis: Vec>, /// Target vector in the ambient space. target: Vec, } -impl ClosestVectorProblem { - /// Construct a CVP instance with a full-column-rank integer basis. - pub fn new(basis: Vec>, target: Vec) -> Result { - let ambient_dimension = target.len(); - for (index, coordinate) in target.iter().enumerate() { - coordinate.validate(index)?; - } - for (index, column) in basis.iter().enumerate() { - if column.len() != ambient_dimension { - return Err(ConstructionError::Conversion(format!( - "basis vector {index} has length {}, expected {ambient_dimension}", - column.len() - ))); - } - } - if basis.len() > ambient_dimension { - return Err(ConstructionError::Conversion(format!( - "{} basis vectors cannot be independent in ambient dimension {ambient_dimension}", - basis.len() - ))); - } - if independent_rows(&basis, ambient_dimension).is_none() { - return Err(ConstructionError::Conversion( - "closest-vector basis columns must be linearly independent".into(), - )); - } - Ok(Self { basis, target }) - } - +impl ClosestVectorProblem { /// Number of basis vectors. pub fn num_basis_vectors(&self) -> usize { self.basis.len() } - /// Dimension of the ambient space. pub fn ambient_dimension(&self) -> usize { self.target.len() } - - /// Integer basis columns. - pub fn basis(&self) -> &[Vec] { + /// Basis columns in the variant's numeric domain. + pub fn basis(&self) -> &[Vec] { &self.basis } - /// Target coordinates. pub fn target(&self) -> &[T] { &self.target } - pub(crate) fn independent_rows(&self) -> Vec { - independent_rows(&self.basis, self.ambient_dimension()) - .expect("CVP basis columns must be independent") + fn validate_dimensions(&self) -> Result<(), ConstructionError> { + for (index, column) in self.basis.iter().enumerate() { + if column.len() != self.ambient_dimension() { + return Err(ConstructionError::Conversion(format!( + "basis vector {index} has length {}, expected {}", + column.len(), + self.ambient_dimension() + ))); + } + } + if self.num_basis_vectors() > self.ambient_dimension() { + return Err(ConstructionError::Conversion( + "more basis vectors than ambient dimensions".into(), + )); + } + Ok(()) } - /// Exact squared distance from the lattice point to the stored target. - pub fn squared_distance(&self, solution: &[i64]) -> Result { + fn validate_solution(&self, solution: &[i64]) -> Result<(), EvaluationError> { if solution.len() != self.num_basis_vectors() { return Err(EvaluationError::InvalidConfiguration(format!( "expected {} closest-vector coefficients, got {}", @@ -163,99 +101,152 @@ impl ClosestVectorProblem { solution.len() ))); } - Ok(self - .target - .iter() - .enumerate() - .map(|(row, target)| { - let coordinate: BigInt = solution - .iter() - .zip(&self.basis) - .map(|(&coefficient, column)| BigInt::from(coefficient) * column[row]) - .sum(); - let difference = BigRational::from_integer(coordinate) - target.to_rational(); - &difference * &difference - }) - .sum()) + Ok(()) } } -fn independent_rows(basis: &[Vec], ambient_dimension: usize) -> Option> { - let num_columns = basis.len(); - if num_columns == 0 { - return Some(Vec::new()); - } - - let mut matrix = (0..ambient_dimension) - .map(|row| { - basis - .iter() - .map(|column| BigInt::from(column[row])) - .collect::>() - }) - .collect::>(); - let mut previous_pivot = BigInt::from(1); - let mut row_indices = (0..ambient_dimension).collect::>(); +macro_rules! cvp_numeric_impl { + ($numeric:ty, $name:literal, $rational:expr) => { + impl ClosestVectorProblem<$numeric> { + /// Construct a CVP instance with full-column-rank basis. + pub fn new( + basis: Vec>, + target: Vec<$numeric>, + ) -> Result { + use crate::types::WeightElement; + let instance = Self { basis, target }; + instance.validate_dimensions()?; + for value in instance.basis.iter().flatten().chain(&instance.target) { + value.validate_element("CVP coordinate")?; + } + let matrix = (0..instance.ambient_dimension()) + .map(|row| { + instance + .basis + .iter() + .map(|column| ($rational)(column[row])) + .collect() + }) + .collect(); + if independent_rows(matrix, instance.num_basis_vectors()).is_none() { + return Err(ConstructionError::Conversion( + "closest-vector basis columns must be linearly independent".into(), + )); + } + Ok(instance) + } + } - for column in 0..num_columns { - let pivot_row = (column..ambient_dimension).find(|&row| !matrix[row][column].is_zero())?; - matrix.swap(column, pivot_row); - row_indices.swap(column, pivot_row); - let pivot = matrix[column][column].clone(); + impl<'de> Deserialize<'de> for ClosestVectorProblem<$numeric> { + fn deserialize>(deserializer: D) -> Result { + #[derive(Deserialize)] + struct Raw { + basis: Vec>, + target: Vec<$numeric>, + } + let raw = Raw::deserialize(deserializer)?; + Self::new(raw.basis, raw.target).map_err(serde::de::Error::custom) + } + } - for row in (column + 1)..ambient_dimension { - for next_column in (column + 1)..num_columns { - matrix[row][next_column] = (&matrix[row][next_column] * &pivot - - &matrix[row][column] * &matrix[column][next_column]) - / &previous_pivot; + impl Problem for ClosestVectorProblem<$numeric> { + const NAME: &'static str = "ClosestVectorProblem"; + type Solution = Vec; + type Value = Min<$numeric>; + crate::problem_parameters![ + ("ambient_dimension", ambient_dimension), + ("num_basis_vectors", num_basis_vectors), + ]; + fn evaluate(&self, solution: &Self::Solution) -> Result { + Ok(Min(Some(self.squared_distance(solution)?))) + } + fn variant() -> Vec<(&'static str, &'static str)> { + vec![("coefficient", $name)] } - matrix[row][column] = BigInt::zero(); } - previous_pivot = pivot; - } - row_indices.truncate(num_columns); - Some(row_indices) + }; } -impl<'de, T> Deserialize<'de> for ClosestVectorProblem -where - T: ClosestVectorTarget + Deserialize<'de>, -{ - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - #[derive(Deserialize)] - struct Raw { - basis: Vec>, - target: Vec, - } +cvp_numeric_impl!(i64, "i64", |value: i64| BigRational::from_integer( + value.into() +)); +cvp_numeric_impl!(f64, "f64", |value: f64| BigRational::from_float(value) + .expect("validated finite coordinate")); - let raw = Raw::deserialize(deserializer)?; - Self::new(raw.basis, raw.target).map_err(serde::de::Error::custom) +impl ClosestVectorProblem { + pub(crate) fn independent_rows(&self) -> Vec { + let matrix = (0..self.ambient_dimension()) + .map(|row| { + self.basis + .iter() + .map(|column| BigRational::from_integer(column[row].into())) + .collect() + }) + .collect(); + independent_rows(matrix, self.num_basis_vectors()).expect("validated independent columns") } -} - -impl Problem for ClosestVectorProblem -where - T: ClosestVectorTarget + Serialize + for<'de> Deserialize<'de>, -{ - const NAME: &'static str = "ClosestVectorProblem"; - type Solution = Vec; - type Value = Min; - crate::problem_parameters![ - ("ambient_dimension", ambient_dimension), - ("num_basis_vectors", num_basis_vectors), - ]; + /// Squared distance using checked integer arithmetic. + pub fn squared_distance(&self, solution: &[i64]) -> Result { + self.validate_solution(solution)?; + let overflow = || EvaluationError::IntegerOverflow("computing CVP squared distance".into()); + let mut squared = 0_i64; + for (row, &target) in self.target.iter().enumerate() { + let mut coordinate = 0_i64; + for (&coefficient, column) in solution.iter().zip(&self.basis) { + coordinate = coordinate + .checked_add(coefficient.checked_mul(column[row]).ok_or_else(overflow)?) + .ok_or_else(overflow)?; + } + let difference = coordinate.checked_sub(target).ok_or_else(overflow)?; + squared = squared + .checked_add(difference.checked_mul(difference).ok_or_else(overflow)?) + .ok_or_else(overflow)?; + } + Ok(squared) + } +} - fn evaluate(&self, solution: &Self::Solution) -> Result { - Ok(Min(Some(self.squared_distance(solution)?))) +impl ClosestVectorProblem { + /// Squared distance in row/column order with ordinary floating-point rounding. + pub fn squared_distance(&self, solution: &[i64]) -> Result { + self.validate_solution(solution)?; + let finite = |value: f64| { + value.is_finite().then_some(value).ok_or_else(|| { + EvaluationError::NonFiniteResult("computing CVP squared distance".into()) + }) + }; + let mut squared = 0.0; + for (row, &target) in self.target.iter().enumerate() { + let mut coordinate = 0.0; + for (&coefficient, column) in solution.iter().zip(&self.basis) { + coordinate = finite(coordinate + finite(coefficient as f64 * column[row])?)?; + } + let difference = finite(coordinate - target)?; + squared = finite(squared + finite(difference * difference)?)?; + } + Ok(squared) } +} - fn variant() -> Vec<(&'static str, &'static str)> { - vec![("target", T::NAME)] +fn independent_rows(mut matrix: Vec>, n: usize) -> Option> { + let ambient_dimension = matrix.len(); + let mut indices = (0..ambient_dimension).collect::>(); + for column in 0..n { + let pivot_row = (column..ambient_dimension).find(|&row| !matrix[row][column].is_zero())?; + matrix.swap(column, pivot_row); + indices.swap(column, pivot_row); + let (pivots, remaining) = matrix.split_at_mut(column + 1); + let pivot = &pivots[column]; + for row in remaining { + let ratio = &row[column] / &pivot[column]; + for (entry, value) in row.iter_mut().zip(pivot).skip(column + 1) { + *entry -= &ratio * value; + } + } } + indices.truncate(n); + Some(indices) } crate::declare_variants! { @@ -268,11 +259,11 @@ pub(crate) fn canonical_model_example_specs() -> Vec::new(vec![vec![2, 0], vec![1, 2]], vec![3_i64, 2]) .expect("canonical closest-vector instance must be valid"), ), optimal_config: serde_json::json!(vec![1, 1]), - optimal_value: serde_json::json!(BigRational::zero()), + optimal_value: serde_json::json!(0), }] } @@ -281,43 +272,69 @@ pub(crate) fn canonical_model_example_specs() -> Vec, "DecisionClosestVectorProblem"); +crate::decision_problem_meta!(ClosestVectorProblem, "DecisionClosestVectorProblem"); inventory::submit! { crate::registry::ProblemSchemaEntry { name: "DecisionClosestVectorProblem", display_name: "Decision ClosestVectorProblem", aliases: &[], - dimensions: &[VariantDimension::new("target", "i64", &["i64"])], category: crate::registry::ProblemCategory::Algebraic, module_path: module_path!(), + dimensions: &[VariantDimension::new("coefficient", "i64", &["i64", "f64"])], category: crate::registry::ProblemCategory::Algebraic, module_path: module_path!(), description: "Does a feasible solution meet the objective bound?", fields: &[ - crate::registry::FieldInfo { name: "basis", type_name: "Vec>", description: "Integer basis matrix as semicolon-separated column vectors." }, + crate::registry::FieldInfo { name: "basis", type_name: "Vec>", description: "Basis matrix as semicolon-separated column vectors." }, crate::registry::FieldInfo { name: "target_vec", type_name: "Vec", description: "Target vector." }, - crate::registry::FieldInfo { name: "bound", type_name: "BigRational", description: "Decision objective bound" }, + crate::registry::FieldInfo { name: "bound", type_name: "i64", description: "Decision objective bound" }, ], } } crate::declare_variants! { default crate::models::decision::Decision> => "2^(num_basis_vectors * log(num_basis_vectors))" create crate::models::decision::DecisionCreateSpec>, + crate::models::decision::Decision> => "2^(num_basis_vectors * log(num_basis_vectors))" create crate::models::decision::DecisionCreateSpec>, } crate::register_decision_variant!(@edges ClosestVectorProblem, "DecisionClosestVectorProblem"); +crate::register_decision_variant!(@edges ClosestVectorProblem, "DecisionClosestVectorProblem"); #[cfg(feature = "example-db")] pub(crate) fn decision_canonical_rule_example_specs( ) -> Vec { - vec![crate::example_db::specs::RuleExampleSpec { - id: "decision_closest_vector_problem_to_closest_vector_problem", - build: || { - let source = crate::models::decision::Decision::new( - ClosestVectorProblem::new(vec![vec![2, 0], vec![1, 2]], vec![3_i64, 2]) - .expect("canonical closest-vector instance must be valid"), - BigRational::zero(), - ); - let witness = serde_json::json!(vec![1, 1]); - crate::example_db::specs::rule_example_with_witness::<_, ClosestVectorProblem>( - source, - crate::export::SolutionPair { - source_config: witness.clone(), - target_config: witness, - }, - ) + vec![ + crate::example_db::specs::RuleExampleSpec { + id: "decision_closest_vector_problem_to_closest_vector_problem", + build: || { + let source = crate::models::decision::Decision::new( + ClosestVectorProblem::::new(vec![vec![2, 0], vec![1, 2]], vec![3_i64, 2]) + .expect("canonical closest-vector instance must be valid"), + 0, + ); + let witness = serde_json::json!(vec![1, 1]); + crate::example_db::specs::rule_example_with_witness::<_, ClosestVectorProblem>( + source, + crate::export::SolutionPair { + source_config: witness.clone(), + target_config: witness, + }, + ) + }, }, - }] + crate::example_db::specs::RuleExampleSpec { + id: "decision_closest_vector_problem_float_to_closest_vector_problem", + build: || { + let source = crate::models::decision::Decision::new( + ClosestVectorProblem::::new( + vec![vec![1.0, 0.0], vec![0.5, 0.8]], + vec![1.6, 0.9], + ) + .expect("canonical oblique-grid instance must be valid"), + 0.03, + ); + let witness = serde_json::json!(vec![1, 1]); + crate::example_db::specs::rule_example_with_witness::<_, ClosestVectorProblem>( + source, + crate::export::SolutionPair { + source_config: witness.clone(), + target_config: witness, + }, + ) + }, + }, + ] } diff --git a/src/models/algebraic/mod.rs b/src/models/algebraic/mod.rs index 328e6c21e..9568a0a8e 100644 --- a/src/models/algebraic/mod.rs +++ b/src/models/algebraic/mod.rs @@ -40,7 +40,7 @@ pub(crate) mod sparse_matrix_compression; pub use algebraic_equations_over_gf2::AlgebraicEquationsOverGF2; pub use bmf::BMF; -pub use closest_vector_problem::{ClosestVectorProblem, ClosestVectorTarget}; +pub use closest_vector_problem::ClosestVectorProblem; pub use consecutive_block_minimization::ConsecutiveBlockMinimization; pub use consecutive_ones_matrix_augmentation::ConsecutiveOnesMatrixAugmentation; pub use consecutive_ones_submatrix::ConsecutiveOnesSubmatrix; diff --git a/src/rules/closestvectorproblem_casts.rs b/src/rules/closestvectorproblem_casts.rs deleted file mode 100644 index 9e45d59df..000000000 --- a/src/rules/closestvectorproblem_casts.rs +++ /dev/null @@ -1,35 +0,0 @@ -//! Numeric variant reduction for Closest Vector Problem. - -use crate::impl_variant_reduction; -use crate::models::algebraic::ClosestVectorProblem; -use crate::rules::ReductionError; -use crate::types::i64_to_exact_f64; - -impl_variant_reduction!( - ClosestVectorProblem, - => , - fields: [ambient_dimension, num_basis_vectors], - |src| { - let target = src - .target() - .iter() - .copied() - .map(i64_to_exact_f64) - .collect::, _>>() - .map_err(|error| { - ReductionError::inexact_float_conversion::< - ClosestVectorProblem, - ClosestVectorProblem, - >(error) - })?; - ClosestVectorProblem::new(src.basis().to_vec(), target).map_err(|error| { - ReductionError::construction::, ClosestVectorProblem>( - error, - ) - })? - } -); - -#[cfg(test)] -#[path = "../unit_tests/rules/closestvectorproblem_casts.rs"] -mod tests; diff --git a/src/rules/closestvectorproblem_qubo.rs b/src/rules/closestvectorproblem_qubo.rs index 6b39695c5..55a8cb8fd 100644 --- a/src/rules/closestvectorproblem_qubo.rs +++ b/src/rules/closestvectorproblem_qubo.rs @@ -1,4 +1,4 @@ -//! Reduction from integer-target CVP to QUBO. +//! Reduction from integer CVP to QUBO. //! //! The reduction derives a finite coefficient box from the lattice basis and //! target, then expands the squared Euclidean distance over exact-range binary @@ -23,7 +23,7 @@ struct EncodingSpan { lower: i64, } -/// Result of reducing an integer-target CVP instance to QUBO. +/// Result of reducing an integer CVP instance to QUBO. #[derive(Debug, Clone)] pub struct ReductionCVPToQUBO { target: Target, @@ -333,7 +333,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, 2]) .expect("canonical closest-vector instance must be valid") } diff --git a/src/rules/mod.rs b/src/rules/mod.rs index ba3fa3acc..6cc013f6f 100644 --- a/src/rules/mod.rs +++ b/src/rules/mod.rs @@ -11,7 +11,6 @@ pub(crate) mod bicliquecover_bmf; pub(crate) mod bmf_bicliquecover; pub(crate) mod circuit_sat; pub(crate) mod circuit_spinglass; -mod closestvectorproblem_casts; mod closestvectorproblem_qubo; pub(crate) mod coloring_qubo; pub(crate) mod decisionmaximumindependentset_integralflowbundles; diff --git a/src/rules/subsetsum_closestvectorproblem.rs b/src/rules/subsetsum_closestvectorproblem.rs index 023bb31f5..defef0519 100644 --- a/src/rules/subsetsum_closestvectorproblem.rs +++ b/src/rules/subsetsum_closestvectorproblem.rs @@ -6,7 +6,6 @@ use crate::models::misc::SubsetSum; use crate::reduction; use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use num_rational::BigRational; /// Result of reducing SubsetSum to ClosestVectorProblem. #[derive(Debug, Clone)] @@ -115,13 +114,16 @@ impl ReduceTo>> for SubsetSum { for bit in 0..bits { target[rows - 1 - bit] = i64::from(self.target().bit(bit as u64)); } - let target = ClosestVectorProblem::new(basis, target).map_err( + let target = ClosestVectorProblem::::new(basis, target).map_err( >>>::target_construction, )?; Ok(ReductionSubsetSumToClosestVectorProblem { target: Decision::new( target, - BigRational::from_integer(self.num_elements().into()), + >>>::exact_i64( + n, + "representing the subset count", + )?, ), num_elements: n, }) diff --git a/src/solvers/customized/closest_vector_problem.rs b/src/solvers/customized/closest_vector_problem.rs index e222cebd2..28770812c 100644 --- a/src/solvers/customized/closest_vector_problem.rs +++ b/src/solvers/customized/closest_vector_problem.rs @@ -1,15 +1,14 @@ -//! Exact-rational CVP sphere enumeration in nearest-first (Schnorr--Euchner) order. +//! Integer and numerical CVP sphere enumeration in nearest-first (Schnorr--Euchner) order. -use crate::models::algebraic::{ClosestVectorProblem, ClosestVectorTarget}; +use crate::models::algebraic::ClosestVectorProblem; use crate::solvers::SolveError; +use num_bigint::BigInt; use num_rational::BigRational; -use num_traits::{ToPrimitive, Zero}; +use num_traits::{Signed, ToPrimitive, Zero}; type GramSchmidtData = (Vec>, Vec, Vec); -pub(crate) fn solve( - problem: &ClosestVectorProblem, -) -> Result, SolveError> { +pub(crate) fn solve(problem: &ClosestVectorProblem) -> Result, SolveError> { let n = problem.num_basis_vectors(); if n == 0 { return Ok(Vec::new()); @@ -28,13 +27,13 @@ pub(crate) fn solve( let target = problem .target() .iter() - .map(ClosestVectorTarget::to_rational) + .map(|&v| BigRational::from_integer(v.into())) .collect::>(); let (mu, norms, alpha) = gram_schmidt(&basis, &target); let mut best_squared = (0..n).map(|i| &norms[i] * &alpha[i] * &alpha[i]).sum(); - let mut coefficients = vec![0_i64; n]; + let mut coefficients = vec![BigInt::zero(); n]; let mut best = coefficients.clone(); enumerate( n - 1, @@ -45,8 +44,13 @@ pub(crate) fn solve( &mut coefficients, &mut best, &mut best_squared, - )?; - Ok(best) + ); + best.into_iter() + .map(|v| { + v.to_i64() + .ok_or_else(|| SolveError::IntegerOverflow("returning a CVP coefficient".into())) + }) + .collect() } fn gram_schmidt(basis: &[Vec], target: &[BigRational]) -> GramSchmidtData { @@ -94,23 +98,167 @@ fn enumerate( mu: &[Vec], norms: &[BigRational], alpha: &[BigRational], - coefficients: &mut [i64], - best: &mut Vec, + coefficients: &mut [BigInt], + best: &mut [BigInt], best_squared: &mut BigRational, -) -> Result<(), SolveError> { +) { if partial_squared >= *best_squared { - return Ok(()); + return; } let mut center = alpha[level].clone(); for later in (level + 1)..coefficients.len() { - center -= &mu[later][level] * BigRational::from_integer(coefficients[later].into()); + center -= &mu[later][level] * BigRational::from_integer(coefficients[later].clone()); } - let mut candidate = - center.round().to_integer().to_i64().ok_or_else(|| { - SolveError::IntegerOverflow("rounding a CVP enumeration center".into()) + let mut candidate = center.round().to_integer(); + let nearest = BigRational::from_integer(candidate.clone()); + let mut step = BigInt::from(if center > nearest { 1 } else { -1 }); + + // Visit the nearest integer, then alternate sides in increasing distance. + // The first descent tries the nearest-plane candidate; every subsequent + // branch uses the improved incumbent rather than a fixed initial interval. + loop { + coefficients[level] = candidate.clone(); + let delta = BigRational::from_integer(candidate.clone()) - ¢er; + let next_squared = &partial_squared + &norms[level] * &delta * δ + if next_squared >= *best_squared { + break; + } + if level == 0 { + *best_squared = next_squared; + best.clone_from_slice(coefficients); + break; + } + enumerate( + level - 1, + next_squared, + mu, + norms, + alpha, + coefficients, + best, + best_squared, + ); + if partial_squared >= *best_squared { + break; + } + // Differences +1,-2,+3,... (or -1,+2,-3,...) alternate around the center. + candidate += &step; + step = -&step - step.signum(); + } +} + +type FloatGramSchmidtData = (Vec>, Vec, Vec); + +pub(crate) fn solve_float(problem: &ClosestVectorProblem) -> Result, SolveError> { + let n = problem.num_basis_vectors(); + if n == 0 { + return Ok(Vec::new()); + } + + let (mu, norms, alpha) = float_gram_schmidt(problem.basis(), problem.target())?; + let mut best_squared = 0.0; + for i in 0..n { + best_squared = finite( + best_squared + norms[i] * alpha[i] * alpha[i], + "computing the initial CVP sphere radius", + )?; + } + + let mut coefficients = vec![0_i64; n]; + let mut best = coefficients.clone(); + enumerate_float( + n - 1, + 0.0, + &mu, + &norms, + &alpha, + &mut coefficients, + &mut best, + &mut best_squared, + )?; + Ok(best) +} + +fn float_gram_schmidt( + basis: &[Vec], + target: &[f64], +) -> Result { + let n = basis.len(); + let mut orthogonal = basis.to_vec(); + let mut mu = vec![vec![0.0; n]; n]; + let mut norms = vec![0.0; n]; + + for i in 0..n { + for j in 0..i { + let dot = basis[i] + .iter() + .zip(&orthogonal[j]) + .try_fold(0.0, |total, (&left, &right)| { + finite(total + left * right, "computing a CVP projection") + })?; + mu[i][j] = finite(dot / norms[j], "computing a CVP projection")?; + for row in 0..orthogonal[i].len() { + orthogonal[i][row] = finite( + orthogonal[i][row] - mu[i][j] * orthogonal[j][row], + "orthogonalizing a CVP basis", + )?; + } + } + norms[i] = orthogonal[i].iter().try_fold(0.0, |total, &value| { + finite(total + value * value, "computing a CVP Gram--Schmidt norm") })?; - let nearest = BigRational::from_integer(candidate.into()); + if norms[i] <= 0.0 { + return Err(SolveError::NonFiniteResult( + "the basis is numerically rank deficient".into(), + )); + } + } + + let alpha = orthogonal + .iter() + .zip(&norms) + .map(|(column, &norm)| { + let dot = target + .iter() + .zip(column) + .try_fold(0.0, |total, (&left, &right)| { + finite(total + left * right, "projecting the CVP target") + })?; + finite(dot / norm, "projecting the CVP target") + }) + .collect::, _>>()?; + Ok((mu, norms, alpha)) +} + +#[allow(clippy::too_many_arguments)] +fn enumerate_float( + level: usize, + partial_squared: f64, + mu: &[Vec], + norms: &[f64], + alpha: &[f64], + coefficients: &mut [i64], + best: &mut [i64], + best_squared: &mut f64, +) -> Result<(), SolveError> { + if partial_squared >= *best_squared { + return Ok(()); + } + + let mut center = alpha[level]; + for later in (level + 1)..coefficients.len() { + let coefficient = coefficients[later] as f64; + center = finite( + center - mu[later][level] * coefficient, + "computing a CVP enumeration center", + )?; + } + let mut candidate = center + .round() + .to_i64() + .ok_or_else(|| SolveError::IntegerOverflow("rounding a CVP enumeration center".into()))?; + let nearest = candidate as f64; let mut step = if center > nearest { 1_i64 } else { -1 }; // Visit the nearest integer, then alternate sides in increasing distance. @@ -118,8 +266,11 @@ fn enumerate( // branch uses the improved incumbent rather than a fixed initial interval. loop { coefficients[level] = candidate; - let delta = BigRational::from_integer(candidate.into()) - ¢er; - let next_squared = &partial_squared + &norms[level] * &delta * δ + let delta = candidate as f64 - center; + let next_squared = finite( + partial_squared + norms[level] * delta * delta, + "computing a CVP partial distance", + )?; if next_squared >= *best_squared { break; } @@ -128,7 +279,7 @@ fn enumerate( best.clone_from_slice(coefficients); break; } - enumerate( + enumerate_float( level - 1, next_squared, mu, @@ -143,18 +294,24 @@ fn enumerate( } // Differences +1,-2,+3,... (or -1,+2,-3,...) alternate around the center. candidate = candidate.checked_add(step).ok_or_else(|| { - SolveError::IntegerOverflow("advancing a CVP enumeration coefficient".into()) + SolveError::IntegerOverflow("advancing a numerical CVP coefficient".into()) })?; step = step .checked_neg() - .and_then(|value| value.checked_sub(step.signum())) - .ok_or_else(|| { - SolveError::IntegerOverflow("advancing a CVP enumeration step".into()) - })?; + .and_then(|v| v.checked_sub(step.signum())) + .ok_or_else(|| SolveError::IntegerOverflow("advancing a numerical CVP step".into()))?; } Ok(()) } +fn finite(value: f64, operation: &str) -> Result { + if value.is_finite() { + Ok(value) + } else { + Err(SolveError::NonFiniteResult(operation.into())) + } +} + #[cfg(test)] #[path = "../../unit_tests/solvers/customized/closest_vector_problem.rs"] mod tests; diff --git a/src/solvers/customized/solver.rs b/src/solvers/customized/solver.rs index 97c6c0758..128fef328 100644 --- a/src/solvers/customized/solver.rs +++ b/src/solvers/customized/solver.rs @@ -1,4 +1,4 @@ -//! Exact customized solvers and their exact-variant registrations. +//! Customized solvers and their concrete-variant outcome registrations. use super::fd_subset_search::{ self, compute_closure, find_essential_attributes, find_essential_attributes_restricted, @@ -29,12 +29,14 @@ macro_rules! register_customized_solver { let problem = any.downcast_ref::<$problem>().expect( "customized solver registration received the wrong concrete type", ); - $solve(problem).map(|solution| { - solution.map(|solution: <$problem as Problem>::Solution| { - serde_json::to_value(solution) - .expect("customized solution serialization must succeed") - }) - }) + let result: Result::Solution>, crate::solvers::SolveError> = $solve(problem); + match result? { + Some(solution) => { + let outcome = crate::solvers::SolveOutcome::optimal(problem, solution)?; + Ok(crate::solvers::outcome_to_json(&outcome)?) + } + None => Ok(crate::solvers::SolveOutcome::Infeasible), + } }, } } @@ -97,11 +99,20 @@ register_customized_solver!( "cvp-sphere-enumeration", |problem| super::closest_vector_problem::solve(problem).map(Some) ); -register_customized_solver!( - crate::models::algebraic::ClosestVectorProblem, - "cvp-sphere-enumeration", - |problem| super::closest_vector_problem::solve(problem).map(Some) -); +inventory::submit! { + CustomizedSolverRegistration { + source_name: "ClosestVectorProblem", + source_variant_fn: crate::models::algebraic::ClosestVectorProblem::::variant, + implementation: "cvp-numerical-sphere-enumeration", + solve_fn: |any| { + let problem = any.downcast_ref::>() + .expect("registered CVP float variant"); + let solution = super::closest_vector_problem::solve_float(problem)?; + let outcome = crate::solvers::SolveOutcome::feasible(problem, solution)?; + Ok(crate::solvers::outcome_to_json(&outcome)?) + }, + } +} register_customized_solver!( crate::models::decision::Decision>, @@ -114,6 +125,25 @@ register_customized_solver!( } ); +inventory::submit! { + CustomizedSolverRegistration { + source_name: "DecisionClosestVectorProblem", + source_variant_fn: crate::models::decision::Decision::>::variant, + implementation: "cvp-numerical-sphere-enumeration", + solve_fn: |any| { + let problem = any.downcast_ref::>>() + .expect("registered CVP float decision variant"); + let solution = super::closest_vector_problem::solve_float(problem.inner())?; + let evaluation = problem.evaluate(&solution)?; + if !evaluation.0 { + return Err(crate::rules::ExtractionError::InsufficientSolutionQuality.into()); + } + let outcome = crate::solvers::SolveOutcome::Feasible { solution, evaluation }; + Ok(crate::solvers::outcome_to_json(&outcome)?) + }, + } +} + /// Solve MinimumCardinalityKey: find a minimal key with smallest cardinality. /// /// Uses iterative deepening by cardinality to guarantee the first solution diff --git a/src/solvers/registry.rs b/src/solvers/registry.rs index c1e41a9c4..dfe4484a4 100644 --- a/src/solvers/registry.rs +++ b/src/solvers/registry.rs @@ -102,7 +102,7 @@ pub(crate) struct IlpPipelineRegistration { inventory::collect!(IlpPipelineRegistration); -type CustomizedSolveFn = fn(&dyn Any) -> Result, super::SolveError>; +type CustomizedSolveFn = fn(&dyn Any) -> Result; /// A dedicated solver registered for one exact problem variant. #[derive(Debug)] diff --git a/src/solvers/resolver.rs b/src/solvers/resolver.rs index 72f0a6197..9c314d8f4 100644 --- a/src/solvers/resolver.rs +++ b/src/solvers/resolver.rs @@ -37,29 +37,11 @@ fn problem_key(problem: &LoadedDynProblem) -> ExactProblemKey { ExactProblemKey::new(problem.problem_name(), problem.variant_map()) } -/// Check the candidate returned by an exact solver before publishing its result. -fn optimal_outcome( - problem: &LoadedDynProblem, - solution: serde_json::Value, -) -> Result { - let (evaluation, feasible) = problem.evaluate_dyn(&solution)?; - if !feasible { - return Err(crate::traits::EvaluationError::ConstraintViolation.into()); - } - Ok(SolveOutcome::Optimal { - solution, - evaluation, - }) -} - fn solve_customized( problem: &LoadedDynProblem, registration: &'static CustomizedSolverRegistration, ) -> Result { - let outcome = match (registration.solve_fn)(problem.as_any())? { - Some(solution) => optimal_outcome(problem, solution)?, - None => SolveOutcome::Infeasible, - }; + let outcome = (registration.solve_fn)(problem.as_any())?; Ok(SolveResult { solver: SolverExecution::Customized { implementation: registration.implementation, diff --git a/src/unit_tests/models/algebraic/closest_vector_problem.rs b/src/unit_tests/models/algebraic/closest_vector_problem.rs index 8cf23c292..4cee65938 100644 --- a/src/unit_tests/models/algebraic/closest_vector_problem.rs +++ b/src/unit_tests/models/algebraic/closest_vector_problem.rs @@ -6,32 +6,34 @@ use crate::types::Min; #[test] fn test_cvp_constructs_integer_and_real_targets() { let integer = - ClosestVectorProblem::new(vec![vec![2, 0, 0], vec![1, 2, 0]], vec![3_i64, 3, 1]).unwrap(); + ClosestVectorProblem::::new(vec![vec![2, 0, 0], vec![1, 2, 0]], vec![3_i64, 3, 1]) + .unwrap(); assert_eq!(integer.num_basis_vectors(), 2); assert_eq!(integer.ambient_dimension(), 3); assert_eq!(integer.target(), &[3, 3, 1]); assert_eq!( ClosestVectorProblem::::variant(), - vec![("target", "i64")] + vec![("coefficient", "i64")] ); - let real = ClosestVectorProblem::new(vec![vec![2, 0, 0], vec![1, 2, 0]], vec![2.5, 1.25, -0.5]) - .unwrap(); + let real = ClosestVectorProblem::::new( + vec![vec![2.0, 0.0, 0.0], vec![1.0, 2.0, 0.0]], + vec![2.5, 1.25, -0.5], + ) + .unwrap(); assert_eq!(real.target(), &[2.5, 1.25, -0.5]); assert_eq!( ClosestVectorProblem::::variant(), - vec![("target", "f64")] + vec![("coefficient", "f64")] ); } #[test] fn test_cvp_evaluates_without_coefficient_bounds() { let problem = - ClosestVectorProblem::new(vec![vec![2, 0, 0], vec![1, 2, 0]], vec![3_i64, 3, 1]).unwrap(); - assert_eq!( - problem.evaluate(&vec![1, 1]).unwrap(), - Min(Some(BigRational::from_integer(2.into()))) - ); + ClosestVectorProblem::::new(vec![vec![2, 0, 0], vec![1, 2, 0]], vec![3_i64, 3, 1]) + .unwrap(); + assert_eq!(problem.evaluate(&vec![1, 1]).unwrap(), Min(Some(2))); assert!(problem.evaluate(&vec![11, -12]).unwrap().0.is_some()); assert!(matches!( problem.evaluate(&vec![1]), @@ -41,33 +43,39 @@ fn test_cvp_evaluates_without_coefficient_bounds() { #[test] fn test_cvp_rejects_invalid_basis() { - assert!(ClosestVectorProblem::new(vec![vec![1_i64]], vec![0_i64, 0]).is_err()); + assert!(ClosestVectorProblem::::new(vec![vec![1_i64]], vec![0_i64, 0]).is_err()); assert!( - ClosestVectorProblem::new(vec![vec![1_i64, 0], vec![2_i64, 0]], vec![0_i64, 0],).is_err() + ClosestVectorProblem::::new(vec![vec![1_i64, 0], vec![2_i64, 0]], vec![0_i64, 0],) + .is_err() + ); + assert!( + ClosestVectorProblem::::new(vec![vec![1_i64], vec![2_i64]], vec![0_i64],).is_err() ); - assert!(ClosestVectorProblem::new(vec![vec![1_i64], vec![2_i64]], vec![0_i64],).is_err()); } #[test] fn test_cvp_rank_uses_exact_integer_elimination() { - let problem = - ClosestVectorProblem::new(vec![vec![i64::MAX, 1], vec![1, i64::MAX]], vec![0_i64, 0]) - .unwrap(); + let problem = ClosestVectorProblem::::new( + vec![vec![i64::MAX, 1], vec![1, i64::MAX]], + vec![0_i64, 0], + ) + .unwrap(); assert_eq!(problem.independent_rows(), vec![0, 1]); // Swapped pivots and a redundant ambient row preserve column rank. let rectangular = - ClosestVectorProblem::new(vec![vec![0, 0, 1], vec![0, 1, 0]], vec![0_i64; 3]).unwrap(); + ClosestVectorProblem::::new(vec![vec![0, 0, 1], vec![0, 1, 0]], vec![0_i64; 3]) + .unwrap(); assert_eq!(rectangular.independent_rows(), vec![2, 1]); } #[test] fn test_cvp_rejects_non_finite_real_target() { assert!(matches!( - ClosestVectorProblem::new(vec![vec![1_i64]], vec![f64::NAN]), + ClosestVectorProblem::::new(vec![vec![1.0]], vec![f64::NAN]), Err(ConstructionError::NonFiniteFloat(_)) )); assert!(matches!( - ClosestVectorProblem::new(vec![vec![1_i64]], vec![f64::INFINITY]), + ClosestVectorProblem::::new(vec![vec![1.0]], vec![f64::INFINITY]), Err(ConstructionError::NonFiniteFloat(_)) )); } @@ -75,47 +83,35 @@ fn test_cvp_rejects_non_finite_real_target() { #[test] fn test_cvp_integer_coordinates_preserve_zero_and_unit_distance() { let target = (1_i64 << 53) + 1; - let problem = ClosestVectorProblem::new(vec![vec![1]], vec![target]).unwrap(); + let problem = ClosestVectorProblem::::new(vec![vec![1]], vec![target]).unwrap(); assert_eq!( crate::solvers::customized::closest_vector_problem::solve(&problem).unwrap(), vec![target] ); - assert_eq!( - problem.squared_distance(&[target]).unwrap(), - BigRational::zero() - ); - assert_eq!( - problem.squared_distance(&[target - 1]).unwrap(), - BigRational::from_integer(1.into()) - ); - let cancellation = ClosestVectorProblem::new( + assert_eq!(problem.squared_distance(&[target]).unwrap(), 0); + assert_eq!(problem.squared_distance(&[target - 1]).unwrap(), 1); + let cancellation = ClosestVectorProblem::::new( vec![vec![i64::MAX, 1], vec![i64::MAX - 1, 1]], vec![1_i64, 0], ) .unwrap(); - assert_eq!( - cancellation.squared_distance(&[1, -1]).unwrap(), - BigRational::zero() - ); + assert_eq!(cancellation.squared_distance(&[1, -1]).unwrap(), 0); } #[test] -fn test_cvp_real_target_preserves_its_stored_rational_value() { - let problem = ClosestVectorProblem::new(vec![vec![1]], vec![0.25]).unwrap(); - assert_eq!( - problem.squared_distance(&[1]).unwrap(), - BigRational::new(9.into(), 16.into()) - ); +fn test_cvp_float_evaluation_and_solve_preserve_numeric_status() { + let problem = ClosestVectorProblem::::new(vec![vec![1.0]], vec![0.25]).unwrap(); + assert_eq!(problem.squared_distance(&[1]).unwrap(), 0.5625); let value = problem.evaluate(&vec![1]).unwrap(); let serialized = crate::registry::DynProblem::evaluate_json(&problem, &serde_json::json!([1])).unwrap(); assert_eq!( - serde_json::from_value::>(serialized).unwrap(), + serde_json::from_value::>(serialized).unwrap(), value ); assert_eq!( crate::registry::DynProblem::evaluate_dyn(&problem, &serde_json::json!([1])).unwrap(), - ("Min(9/16)".into(), true) + ("Min(0.5625)".into(), true) ); let loaded = crate::registry::LoadedDynProblem::new(Box::new(problem)); let outcome = crate::solvers::solve(&loaded, crate::solvers::SolverRequest::Default) @@ -123,23 +119,23 @@ fn test_cvp_real_target_preserves_its_stored_rational_value() { .outcome; assert_eq!( outcome, - SolveOutcome::Optimal { + SolveOutcome::Feasible { solution: serde_json::json!([0]), - evaluation: "Min(1/16)".into(), + evaluation: "Min(0.0625)".into(), } ); } #[test] fn test_cvp_serialization_round_trips_both_targets() { - let integer = ClosestVectorProblem::new(vec![vec![1_i64]], vec![2_i64]).unwrap(); + let integer = ClosestVectorProblem::::new(vec![vec![1_i64]], vec![2_i64]).unwrap(); let json = serde_json::to_string(&integer).unwrap(); assert!(!json.contains("bounds")); let decoded: ClosestVectorProblem = serde_json::from_str(&json).unwrap(); assert_eq!(decoded.basis(), integer.basis()); assert_eq!(decoded.target(), integer.target()); - let real = ClosestVectorProblem::new(vec![vec![1_i64]], vec![2.5]).unwrap(); + let real = ClosestVectorProblem::::new(vec![vec![1.0]], vec![2.5]).unwrap(); let json = serde_json::to_string(&real).unwrap(); let decoded: ClosestVectorProblem = serde_json::from_str(&json).unwrap(); assert_eq!(decoded.target(), real.target()); @@ -155,7 +151,7 @@ fn test_cvp_create_specs_have_no_bounds() { assert_eq!(integer.target(), &[2]); let real = ClosestVectorProblem::::try_from(ClosestVectorProblemF64CreateSpec { - basis: vec![vec![1]], + basis: vec![vec![1.0]], target: vec![2.5], }) .unwrap(); @@ -173,17 +169,91 @@ fn test_cvp_registers_both_target_variants() { assert_eq!( variants, vec![ - std::collections::BTreeMap::from([("target".into(), "f64".into())]), - std::collections::BTreeMap::from([("target".into(), "i64".into())]), + std::collections::BTreeMap::from([("coefficient".into(), "f64".into())]), + std::collections::BTreeMap::from([("coefficient".into(), "i64".into())]), ] ); } #[test] fn test_cvp_empty_basis_is_valid() { - let problem = ClosestVectorProblem::new(Vec::new(), vec![3_i64, 4]).unwrap(); - assert_eq!( - problem.evaluate(&Vec::new()).unwrap(), - Min(Some(BigRational::from_integer(25.into()))) + let problem = ClosestVectorProblem::::new(Vec::new(), vec![3_i64, 4]).unwrap(); + assert_eq!(problem.evaluate(&Vec::new()).unwrap(), Min(Some(25))); +} + +#[test] +fn test_cvp_oblique_grid_quantization() { + use crate::models::decision::Decision; + use crate::rules::{ReduceTo, ReductionResult}; + let problem = + ClosestVectorProblem::::new(vec![vec![1.0, 0.0], vec![0.5, 0.8]], vec![1.6, 0.9]) + .unwrap(); + let solution = + crate::solvers::customized::closest_vector_problem::solve_float(&problem).unwrap(); + assert_eq!(solution, vec![1, 1]); + assert!((problem.squared_distance(&solution).unwrap() - 0.02).abs() < 1e-12); + let decision = Decision::new(problem.clone(), 0.03); + assert!(decision.evaluate(&solution).unwrap().0); + let too_close = Decision::new(problem.clone(), 0.01); + assert!(!too_close.evaluate(&solution).unwrap().0); + let reduction = ReduceTo::>::reduce_to(&too_close).unwrap(); + assert!(matches!( + reduction.recover_result( + &too_close, + SolveOutcome::feasible(&problem, solution).unwrap() + ), + Err(crate::rules::ExtractionError::InsufficientSolutionQuality) + )); + let loaded = crate::registry::LoadedDynProblem::new(Box::new(problem)); + assert!(matches!( + crate::solvers::solve(&loaded, crate::solvers::SolverRequest::Default) + .unwrap() + .outcome, + SolveOutcome::Feasible { .. } + )); + for (bound, accepted) in [(0.03, true), (0.01, false)] { + let decision = Decision::new( + ClosestVectorProblem::::new(vec![vec![1.0, 0.0], vec![0.5, 0.8]], vec![1.6, 0.9]) + .unwrap(), + bound, + ); + let loaded = crate::registry::LoadedDynProblem::new(Box::new(decision)); + let result = crate::solvers::solve(&loaded, crate::solvers::SolverRequest::Default); + if accepted { + assert!(matches!( + result.unwrap().outcome, + SolveOutcome::Feasible { .. } + )); + } else { + assert!(matches!( + result, + Err(crate::solvers::SolveError::Extraction( + crate::rules::ExtractionError::InsufficientSolutionQuality + )) + )); + } + } + let graph = crate::rules::ReductionGraph::new(); + assert!(!graph.has_direct_reduction::, ClosestVectorProblem>()); +} + +#[test] +fn test_cvp_reports_declared_arithmetic_errors() { + let integer = ClosestVectorProblem::::new(vec![vec![2]], vec![0]).unwrap(); + assert!(matches!( + integer.evaluate(&vec![i64::MAX]), + Err(EvaluationError::IntegerOverflow(_)) + )); + let float = ClosestVectorProblem::::new(vec![vec![f64::MAX]], vec![0.0]).unwrap(); + assert!(matches!( + float.evaluate(&vec![2]), + Err(EvaluationError::NonFiniteResult(_)) + )); + assert!(float.evaluate(&vec![]).is_err()); + assert!(ClosestVectorProblem::::new(vec![vec![f64::NAN]], vec![0.0]).is_err()); + assert!(ClosestVectorProblem::::new(vec![vec![1.0], vec![2.0]], vec![0.0]).is_err()); + assert!( + ClosestVectorProblem::::new(vec![vec![1.0, 2.0], vec![2.0, 4.0]], vec![0.0, 0.0]) + .is_err() ); } diff --git a/src/unit_tests/rules/closestvectorproblem_casts.rs b/src/unit_tests/rules/closestvectorproblem_casts.rs deleted file mode 100644 index f26290257..000000000 --- a/src/unit_tests/rules/closestvectorproblem_casts.rs +++ /dev/null @@ -1,40 +0,0 @@ -use super::*; -use crate::rules::{ReduceTo, ReductionError, ReductionGraph, ReductionResult}; -use crate::solvers::SolveOutcome; -use crate::types::MAX_EXACT_F64_INTEGER; - -#[test] -fn test_closestvectorproblem_i64_to_f64_closed_loop() { - let source = ClosestVectorProblem::new(vec![vec![2, 0], vec![1, 2]], vec![3_i64, 2]).unwrap(); - let reduction = ReduceTo::>::reduce_to(&source).unwrap(); - - assert_eq!(reduction.target_problem().basis(), source.basis()); - assert_eq!(reduction.target_problem().target(), &[3.0, 2.0]); - assert_eq!( - reduction - .recover_result( - &source, - SolveOutcome::optimal(reduction.target_problem(), vec![1, 1].clone()).unwrap() - ) - .unwrap() - .into_solution() - .expect("qualifying target result must recover a source solution"), - vec![1, 1] - ); -} - -#[test] -fn test_closestvectorproblem_i64_to_f64_rejects_inexact_target() { - let source = ClosestVectorProblem::new(vec![vec![1]], vec![MAX_EXACT_F64_INTEGER + 1]).unwrap(); - - assert!(matches!( - ReduceTo::>::reduce_to(&source), - Err(ReductionError::InexactFloatConversion { .. }) - )); -} - -#[test] -fn test_closestvectorproblem_numeric_variants_are_connected() { - assert!(ReductionGraph::new() - .has_direct_reduction::, ClosestVectorProblem>()); -} diff --git a/src/unit_tests/rules/closestvectorproblem_qubo.rs b/src/unit_tests/rules/closestvectorproblem_qubo.rs index 54fbfe698..8e421ed51 100644 --- a/src/unit_tests/rules/closestvectorproblem_qubo.rs +++ b/src/unit_tests/rules/closestvectorproblem_qubo.rs @@ -4,7 +4,7 @@ use crate::solvers::SolveOutcome; use crate::traits::Problem; 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, 2]).unwrap() } fn canonical_bits() -> Vec { @@ -45,7 +45,7 @@ fn test_closestvectorproblem_to_qubo_twelve_dimensional_identity() { let basis = (0..size) .map(|column| (0..size).map(|row| i64::from(row == column)).collect()) .collect(); - let source = ClosestVectorProblem::new(basis, vec![1_i64; size]).unwrap(); + let source = ClosestVectorProblem::::new(basis, vec![1_i64; size]).unwrap(); let reduction = ReduceTo::>::reduce_to(&source).unwrap(); let mut bits = vec![false; reduction.target_problem().num_vars()]; for encoding in &reduction.encodings { @@ -67,10 +67,7 @@ fn test_closestvectorproblem_to_qubo_twelve_dimensional_identity() { .into_solution() .expect("qualifying target result must recover a source solution"); assert_eq!(solution, vec![1; size]); - assert_eq!( - source.evaluate(&solution).unwrap().0, - Some(num_rational::BigRational::zero()) - ); + assert_eq!(source.evaluate(&solution).unwrap().0, Some(0)); } #[test] @@ -91,10 +88,7 @@ fn test_closestvectorproblem_to_qubo_closed_loop() { .expect("qualifying target result must recover a source solution"); assert_eq!(source_solution, vec![1, 1]); - assert_eq!( - source.evaluate(&source_solution).unwrap().0, - Some(num_rational::BigRational::zero()) - ); + assert_eq!(source.evaluate(&source_solution).unwrap().0, Some(0)); assert_eq!(reduction.target_problem().num_vars(), 11); } @@ -150,7 +144,7 @@ fn test_closestvectorproblem_to_qubo_exact_range_decoding() { #[test] fn test_closestvectorproblem_to_qubo_preserves_optimum_outside_old_box() { - let source = ClosestVectorProblem::new(vec![vec![1]], vec![20_i64]).unwrap(); + let source = ClosestVectorProblem::::new(vec![vec![1]], vec![20_i64]).unwrap(); let reduction = ReduceTo::>::reduce_to(&source).unwrap(); let target_solution = BruteForce::new() .solve(reduction.target_problem()) @@ -171,13 +165,14 @@ 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 absolute_value = ClosestVectorProblem::::new(vec![vec![1]], vec![i64::MIN]).unwrap(); assert!(matches!( ReduceTo::>::reduce_to(&absolute_value), Err(crate::rules::ReductionError::IntegerOverflow { .. }) )); - let large_exact = ClosestVectorProblem::new(vec![vec![100_000_000]], vec![1_i64]).unwrap(); + let large_exact = + ClosestVectorProblem::::new(vec![vec![100_000_000]], vec![1_i64]).unwrap(); assert!(ReduceTo::>::reduce_to(&large_exact).is_ok()); } @@ -205,7 +200,7 @@ fn test_closestvectorproblem_to_qubo_canonical_example_spec() { #[test] fn qubo_energy_matches_squared_distance_up_to_the_dropped_constant() { - let source = ClosestVectorProblem::new(vec![vec![2]], vec![1_i64]).unwrap(); + let source = ClosestVectorProblem::::new(vec![vec![2]], vec![1_i64]).unwrap(); let reduction = ReduceTo::>::reduce_to(&source).unwrap(); let target = reduction.target_problem(); assert_eq!(target.num_vars(), 3); @@ -221,9 +216,6 @@ fn qubo_energy_matches_squared_distance_up_to_the_dropped_constant() { .into_solution() .expect("qualifying target result must recover a source solution"); let energy = target.evaluate(&bits).unwrap().unwrap(); - assert_eq!( - source.squared_distance(&coefficient).unwrap(), - num_rational::BigRational::from_integer((energy + 25).into()) - ); + assert_eq!(source.squared_distance(&coefficient).unwrap(), energy + 25); } } diff --git a/src/unit_tests/rules/subsetsum_closestvectorproblem.rs b/src/unit_tests/rules/subsetsum_closestvectorproblem.rs index 9106df612..b42192c61 100644 --- a/src/unit_tests/rules/subsetsum_closestvectorproblem.rs +++ b/src/unit_tests/rules/subsetsum_closestvectorproblem.rs @@ -32,7 +32,7 @@ fn test_subsetsum_to_closestvectorproblem_closed_loop() { .evaluate(&target_solution) .unwrap() .0, - Some(BigRational::from_integer(4.into())) + Some(4) ); } @@ -47,7 +47,7 @@ fn test_subsetsum_to_closestvectorproblem_structure() { assert_eq!(&target.inner().target()[..8], &[0, 0, 0, 0, 1, 1, 1, 1]); assert_eq!( ClosestVectorProblem::::variant(), - vec![("target", "i64")] + vec![("coefficient", "i64")] ); } @@ -58,10 +58,7 @@ fn test_subsetsum_to_closestvectorproblem_binary_minimizers() { let target = reduction.target_problem(); for solution in [vec![1, 0, 0, 1, 0, 0, 0], vec![1, 1, 1, 0, 1, 1, 1]] { - assert_eq!( - target.inner().evaluate(&solution).unwrap().0, - Some(BigRational::from_integer(4.into())) - ); + assert_eq!(target.inner().evaluate(&solution).unwrap().0, Some(4)); assert!( source .evaluate( @@ -96,7 +93,7 @@ fn test_subsetsum_to_closestvectorproblem_unsatisfiable_instance() { .evaluate(&solution) .unwrap() .unwrap() - > BigRational::from_integer(source.num_elements().into()) + > i64::try_from(source.num_elements()).unwrap() ); } @@ -110,7 +107,7 @@ fn test_subsetsum_to_closestvectorproblem_binary_carries_preserve_large_inputs() witness[0] = 1; assert_eq!( result.target_problem().inner().evaluate(&witness).unwrap(), - crate::types::Min(Some(BigRational::from_integer(1.into()))) + crate::types::Min(Some(1)) ); assert_eq!( result @@ -182,10 +179,8 @@ fn test_subsetsum_to_closestvectorproblem_all_small_coefficients() { }) .collect(); let value = target.inner().evaluate(&config).unwrap(); - let certificate = value - == crate::types::Min(Some(BigRational::from_integer( - source.num_elements().into(), - ))); + let certificate = + value == crate::types::Min(Some(i64::try_from(source.num_elements()).unwrap())); assert_eq!( crate::types::Or(OptimizationValue::meets_bound( &(value), @@ -243,3 +238,20 @@ fn test_subsetsum_to_closestvectorproblem_dimension_boundaries() { assert!(R::dimensions((1usize << 30) - 1, 1).is_ok()); } } + +#[test] +fn test_subset_sum_to_cvp_recovers_selected_items() { + let source = SubsetSum::new(vec![3u32, 5, 7], 8u32); + let reduction = ReduceTo::>>::reduce_to(&source).unwrap(); + let target = reduction.target_problem(); + let solution = + crate::solvers::customized::closest_vector_problem::solve(target.inner()).unwrap(); + assert_eq!(target.inner().evaluate(&solution).unwrap().0, Some(3)); + let recovered = reduction + .recover_result(&source, SolveOutcome::optimal(target, solution).unwrap()) + .unwrap() + .into_solution() + .unwrap(); + assert_eq!(recovered, vec![true, true, false]); + assert!(source.evaluate(&recovered).unwrap().0); +} diff --git a/src/unit_tests/solvers/customized/closest_vector_problem.rs b/src/unit_tests/solvers/customized/closest_vector_problem.rs index 240a43792..eb7577ee2 100644 --- a/src/unit_tests/solvers/customized/closest_vector_problem.rs +++ b/src/unit_tests/solvers/customized/closest_vector_problem.rs @@ -6,35 +6,35 @@ use std::collections::BTreeMap; #[test] fn test_cvp_solver_handles_integer_and_real_targets() { - let integer = ClosestVectorProblem::new(vec![vec![1]], vec![12_i64]).unwrap(); + let integer = ClosestVectorProblem::::new(vec![vec![1]], vec![12_i64]).unwrap(); assert_eq!(solve(&integer).unwrap(), vec![12]); - let real = ClosestVectorProblem::new(vec![vec![1]], vec![0.6]).unwrap(); - assert_eq!(solve(&real).unwrap(), vec![1]); + let real = ClosestVectorProblem::::new(vec![vec![1.0]], vec![0.6]).unwrap(); + assert_eq!(solve_float(&real).unwrap(), vec![1]); } #[test] fn test_cvp_solver_handles_nonorthogonal_rectangular_and_negative_coefficients() { let problem = - ClosestVectorProblem::new(vec![vec![2, 0, 1], vec![1, 2, 0]], vec![-3_i64, -2, -1]) + ClosestVectorProblem::::new(vec![vec![2, 0, 1], vec![1, 2, 0]], vec![-3_i64, -2, -1]) .unwrap(); assert_eq!(solve(&problem).unwrap(), vec![-1, -1]); } #[test] fn test_cvp_solver_keeps_zero_on_tie_and_handles_empty_basis() { - let tied = ClosestVectorProblem::new(vec![vec![1]], vec![0.5]).unwrap(); - assert_eq!(solve(&tied).unwrap(), vec![0]); + let tied = ClosestVectorProblem::::new(vec![vec![1.0]], vec![0.5]).unwrap(); + assert_eq!(solve_float(&tied).unwrap(), vec![0]); - let empty = ClosestVectorProblem::new(Vec::new(), vec![1_i64, 2]).unwrap(); + let empty = ClosestVectorProblem::::new(Vec::new(), vec![1_i64, 2]).unwrap(); assert!(solve(&empty).unwrap().is_empty()); } #[test] fn test_cvp_solver_reports_search_representation_overflow() { - let out_of_range = ClosestVectorProblem::new(vec![vec![1]], vec![1e20]).unwrap(); + let out_of_range = ClosestVectorProblem::::new(vec![vec![1.0]], vec![1e20]).unwrap(); assert!(matches!( - solve(&out_of_range), + solve_float(&out_of_range), Err(SolveError::IntegerOverflow(_)) )); } @@ -43,7 +43,7 @@ fn test_cvp_solver_reports_search_representation_overflow() { fn test_cvp_solver_is_registered_without_brute_force() { let key = ExactProblemKey::new( ClosestVectorProblem::::NAME, - BTreeMap::from([("target".to_string(), "i64".to_string())]), + BTreeMap::from([("coefficient".to_string(), "i64".to_string())]), ); let capabilities = solver_capabilities(&key).unwrap(); assert_eq!( @@ -56,18 +56,22 @@ fn test_cvp_solver_is_registered_without_brute_force() { #[test] fn test_cvp_solver_handles_large_translated_targets() { for target in [-1_000_000_000_i64, 1_000_000_000] { - let problem = ClosestVectorProblem::new(vec![vec![1]], vec![target]).unwrap(); + let problem = ClosestVectorProblem::::new(vec![vec![1]], vec![target]).unwrap(); assert_eq!(solve(&problem).unwrap(), vec![target]); - let rectangular = - ClosestVectorProblem::new(vec![vec![2, 0], vec![1, 2]], vec![3 * target, 2 * target]) - .unwrap(); + let rectangular = ClosestVectorProblem::::new( + vec![vec![2, 0], vec![1, 2]], + vec![3 * target, 2 * target], + ) + .unwrap(); assert_eq!(solve(&rectangular).unwrap(), vec![target, target]); - let fractional = - ClosestVectorProblem::new(vec![vec![2, 0]], vec![2.0 * target as f64 + 0.6, 3.0]) - .unwrap(); - assert_eq!(solve(&fractional).unwrap(), vec![target]); + let fractional = ClosestVectorProblem::::new( + vec![vec![2.0, 0.0]], + vec![2.0 * target as f64 + 0.6, 3.0], + ) + .unwrap(); + assert_eq!(solve_float(&fractional).unwrap(), vec![target]); } } @@ -79,12 +83,12 @@ fn test_cvp_nearest_first_matches_exhaustive_small_lattices() { for skew in -2..=2_i64 { for tx in -4..=4 { for ty in -4..=4 { - let problem = ClosestVectorProblem::new( - vec![vec![diagonal, 0, 0], vec![skew, 1, 0]], + let problem = ClosestVectorProblem::::new( + vec![vec![diagonal as f64, 0.0, 0.0], vec![skew as f64, 1.0, 0.0]], vec![tx as f64 / 2.0, ty as f64 / 2.0, 1.0], ) .unwrap(); - let actual = solve(&problem).unwrap(); + let actual = solve_float(&problem).unwrap(); let distance = |x: i64, y: i64| { let dx = (diagonal * x + skew * y) as f64 - tx as f64 / 2.0; let dy = y as f64 - ty as f64 / 2.0; @@ -103,9 +107,11 @@ fn test_cvp_nearest_first_matches_exhaustive_small_lattices() { #[test] fn test_cvp_enumeration_improves_the_nearest_plane_candidate() { - let problem = ClosestVectorProblem::new(vec![vec![2, 0], vec![1, 1]], vec![0.9, 0.49]).unwrap(); + let problem = + ClosestVectorProblem::::new(vec![vec![2.0, 0.0], vec![1.0, 1.0]], vec![0.9, 0.49]) + .unwrap(); // Nearest-plane rounding yields [0, 0]; the adjacent branch is closer. - assert_eq!(solve(&problem).unwrap(), vec![0, 1]); + assert_eq!(solve_float(&problem).unwrap(), vec![0, 1]); } #[test] @@ -113,19 +119,24 @@ fn test_cvp_pruning_preserves_exact_large_translation_optimum() { for coefficient in [-100_000_000_000_000_i64, 100_000_000_000_000] { let basis = vec![vec![3, 1], vec![2, 1]]; let target = vec![5 * coefficient, 2 * coefficient]; - let integer = ClosestVectorProblem::new(basis.clone(), target.clone()).unwrap(); - let real = ClosestVectorProblem::new( - basis, + let integer = ClosestVectorProblem::::new(basis.clone(), target.clone()).unwrap(); + let real = ClosestVectorProblem::::new( + basis + .iter() + .map(|col| col.iter().map(|&v| v as f64).collect()) + .collect(), target.into_iter().map(|value| value as f64).collect(), ) .unwrap(); let expected = vec![coefficient, coefficient]; assert_eq!(solve(&integer).unwrap(), expected); - assert_eq!(solve(&real).unwrap(), expected); - assert_eq!( - integer.evaluate(&expected).unwrap().0, - Some(BigRational::zero()) - ); + assert!(real + .evaluate(&solve_float(&real).unwrap()) + .unwrap() + .0 + .unwrap() + .is_finite()); + assert_eq!(integer.evaluate(&expected).unwrap().0, Some(0)); } } @@ -133,7 +144,33 @@ fn test_cvp_pruning_preserves_exact_large_translation_optimum() { fn test_cvp_pruning_handles_nearly_parallel_integer_columns() { let n = 100_000_000_i64; let problem = - ClosestVectorProblem::new(vec![vec![n, n + 1], vec![n + 1, n + 2]], vec![1_i64, 0]) + ClosestVectorProblem::::new(vec![vec![n, n + 1], vec![n + 1, n + 2]], vec![1_i64, 0]) .unwrap(); assert_eq!(solve(&problem).unwrap(), vec![-n - 2, n + 1]); } + +#[test] +fn test_cvp_search_steps_do_not_limit_integer_solutions() { + let problem = + ClosestVectorProblem::::new(vec![vec![2, 0], vec![0, 1]], vec![1, i64::MIN]).unwrap(); + let solution = solve(&problem).unwrap(); + assert_eq!(problem.squared_distance(&solution).unwrap(), 1); + let unrepresentable = + ClosestVectorProblem::::new(vec![vec![1, 0], vec![1, 1]], vec![i64::MIN, i64::MAX]) + .unwrap(); + assert!(matches!( + solve(&unrepresentable), + Err(SolveError::IntegerOverflow(_)) + )); +} + +#[test] +fn test_cvp_numerical_solver_handles_empty_and_reports_breakdown() { + let empty = ClosestVectorProblem::::new(vec![], vec![1.0, 2.0]).unwrap(); + assert_eq!(solve_float(&empty).unwrap(), Vec::::new()); + let problem = ClosestVectorProblem::::new(vec![vec![f64::MAX]], vec![1.0]).unwrap(); + assert!(matches!( + solve_float(&problem), + Err(SolveError::NonFiniteResult(_)) + )); +} diff --git a/src/unit_tests/solvers/customized/solver.rs b/src/unit_tests/solvers/customized/solver.rs index dcc2729c3..d8b0690d9 100644 --- a/src/unit_tests/solvers/customized/solver.rs +++ b/src/unit_tests/solvers/customized/solver.rs @@ -28,7 +28,7 @@ impl CustomizedTestSolver { .unwrap() .lookup(&key) .customized?; - let solution = (registration.solve_fn)(problem).unwrap()?; + let solution = (registration.solve_fn)(problem).unwrap().into_solution()?; Some( serde_json::from_value(solution) .expect("customized solver returned the wrong witness representation"), diff --git a/src/unit_tests/solvers/registry.rs b/src/unit_tests/solvers/registry.rs index a41cd89a0..e27ac76b1 100644 --- a/src/unit_tests/solvers/registry.rs +++ b/src/unit_tests/solvers/registry.rs @@ -206,8 +206,8 @@ fn source_variant() -> Vec<(&'static str, &'static str)> { fn no_solution( _: &dyn std::any::Any, -) -> Result, crate::solvers::SolveError> { - Ok(None) +) -> Result { + Ok(crate::solvers::SolveOutcome::Infeasible) } static CUSTOMIZED_A: CustomizedSolverRegistration = CustomizedSolverRegistration { diff --git a/src/unit_tests/solvers/resolver.rs b/src/unit_tests/solvers/resolver.rs index e94fab704..9cdeb06aa 100644 --- a/src/unit_tests/solvers/resolver.rs +++ b/src/unit_tests/solvers/resolver.rs @@ -628,7 +628,7 @@ fn decision_closest_vector_solver_preserves_bound_after_serialization() { let target = reduction.target_problem(); let loaded = load_dyn( >>::NAME, - &BTreeMap::from([("target".into(), "i64".into())]), + &BTreeMap::from([("coefficient".into(), "i64".into())]), serde_json::to_value(target).unwrap(), ) .unwrap(); @@ -683,7 +683,11 @@ fn customized_dispatch_rejects_a_constraint_violating_candidate() { source_name: "MinimumVertexCover", source_variant_fn: || vec![("graph", "SimpleGraph"), ("weight", "i64")], implementation: "invalid-candidate", - solve_fn: |_| Ok(Some(serde_json::json!([false, false]))), + solve_fn: |any| { + let problem = any.downcast_ref::>().unwrap(); + let outcome = SolveOutcome::optimal(problem, vec![false, false])?; + Ok(crate::solvers::outcome_to_json(&outcome)?) + }, }; let problem = load_dyn( "MinimumVertexCover", From 40ed72f3aa532d3f0355713b826528788e8f5bca Mon Sep 17 00:00:00 2001 From: Xiwei Pan Date: Wed, 16 Sep 2026 16:08:07 +0800 Subject: [PATCH 11/42] fix: align path and ILP solver contracts --- src/models/graph/length_bounded_disjoint_paths.rs | 13 +++++++++++-- src/solvers/ilp/adapter.rs | 6 ------ .../models/graph/length_bounded_disjoint_paths.rs | 11 +++++++++++ 3 files changed, 22 insertions(+), 8 deletions(-) diff --git a/src/models/graph/length_bounded_disjoint_paths.rs b/src/models/graph/length_bounded_disjoint_paths.rs index ae93de0c2..59d2d3405 100644 --- a/src/models/graph/length_bounded_disjoint_paths.rs +++ b/src/models/graph/length_bounded_disjoint_paths.rs @@ -49,6 +49,7 @@ struct LengthBoundedDisjointPathsData { graph: G, source: usize, sink: usize, + max_paths: usize, max_length: usize, } @@ -58,8 +59,16 @@ where { fn deserialize>(deserializer: D) -> Result { let data = LengthBoundedDisjointPathsData::::deserialize(deserializer)?; - Self::try_new(data.graph, data.source, data.sink, data.max_length) - .map_err(serde::de::Error::custom) + let max_paths = data.max_paths; + let instance = Self::try_new(data.graph, data.source, data.sink, data.max_length) + .map_err(serde::de::Error::custom)?; + if max_paths != instance.max_paths { + return Err(serde::de::Error::custom(format!( + "max_paths must equal min(deg(source), deg(sink)): expected {}, got {max_paths}", + instance.max_paths + ))); + } + Ok(instance) } } diff --git a/src/solvers/ilp/adapter.rs b/src/solvers/ilp/adapter.rs index 98930f0f2..6149dea78 100644 --- a/src/solvers/ilp/adapter.rs +++ b/src/solvers/ilp/adapter.rs @@ -176,12 +176,6 @@ impl HighsAdapter { return Err(IlpBackendError::Unbounded); } accept_backend_status(solved.status())?; - let gap = solved.mip_gap(); - if gap.is_finite() && gap > 0.0 { - return Err(IlpBackendError::BackendFailure(format!( - "HiGHS returned a nonzero optimality gap: {gap}" - ))); - } if solved.primal_solution_status() != HighsSolutionStatus::Feasible { return Err(IlpBackendError::BackendFailure( "HiGHS returned no feasible primal solution".into(), diff --git a/src/unit_tests/models/graph/length_bounded_disjoint_paths.rs b/src/unit_tests/models/graph/length_bounded_disjoint_paths.rs index 9e01688e4..b042d2fb7 100644 --- a/src/unit_tests/models/graph/length_bounded_disjoint_paths.rs +++ b/src/unit_tests/models/graph/length_bounded_disjoint_paths.rs @@ -189,6 +189,17 @@ fn test_length_bounded_disjoint_paths_serialization() { assert_eq!(round_trip.max_length(), 3); } +#[test] +fn test_length_bounded_disjoint_paths_rejects_inconsistent_max_paths() { + let mut json = serde_json::to_value(sample_problem()).unwrap(); + json["max_paths"] = serde_json::json!(2); + let error = + serde_json::from_value::>(json).unwrap_err(); + assert!(error + .to_string() + .contains("max_paths must equal min(deg(source), deg(sink)): expected 3, got 2")); +} + #[test] fn test_length_bounded_disjoint_paths_graph_getter() { let problem = sample_problem(); From faaa9a8c70f421d26b232fd362a5378444efbd94 Mon Sep 17 00:00:00 2001 From: Xiwei Pan Date: Wed, 16 Sep 2026 16:42:49 +0800 Subject: [PATCH 12/42] refactor: make closest vector problem integer-only --- docs/src/design.md | 18 +- .../src/commands/create/tests.rs | 27 -- .../algebraic/closest_vector_problem.rs | 259 ++++++------------ src/rules/closestvectorproblem_qubo.rs | 6 +- src/rules/subsetsum_closestvectorproblem.rs | 20 +- .../customized/closest_vector_problem.rs | 168 +----------- src/solvers/customized/solver.rs | 39 +-- .../algebraic/closest_vector_problem.rs | 193 ++----------- .../rules/closestvectorproblem_qubo.rs | 15 +- .../rules/subsetsum_closestvectorproblem.rs | 18 +- .../customized/closest_vector_problem.rs | 119 ++------ src/unit_tests/solvers/resolver.rs | 5 +- 12 files changed, 172 insertions(+), 715 deletions(-) diff --git a/docs/src/design.md b/docs/src/design.md index 53edc6157..76eaa52a4 100644 --- a/docs/src/design.md +++ b/docs/src/design.md @@ -211,19 +211,11 @@ counting preservation. coefficients. Backend feasibility tolerances must not expand the model's feasible set. A declared input convention, such as checking probability sums, is distinct from accepting a solver's returned assignment. -- CVP has separate `coefficient=i64` and `coefficient=f64` variants for both - basis and target. Both return integer coefficient vectors. Integer CVP evaluates - squared distance as `Min` with checked arithmetic; floating CVP evaluates - it as `Min` with ordinary rounding and non-finite-result checks. Decision - bounds use the corresponding squared-distance type. SubsetSum uses the integer - variant with bound equal to its item count. Integer sphere enumeration retains - implementation-local exact arithmetic and returns an optimal solution; numerical - float enumeration returns a feasible candidate without an optimality claim. - A numerical float decision solve returns a satisfying candidate when found; - missing its bound reports insufficient solution quality, not infeasibility. - Customized solver callbacks carry these statuses through dispatch. No exact - CVP integer-to-float edge is registered: coordinate conversion alone does not - establish preservation of rounded objective ordering. +- CVP uses integer basis and target coordinates and returns an integer coefficient + vector. It evaluates squared distance as `Min` with checked arithmetic, and + Decision bounds use the same squared-distance type. SubsetSum uses the CVP bound + equal to its item count. Exact sphere enumeration uses implementation-local exact + arithmetic and returns an optimal solution. - `i64_to_exact_f64()` accepts integers in `[-(2^53-1), 2^53-1]` and rejects everything outside that supported conversion range. This is a conservative interface limit, not the set of all exactly representable f64 integers. Ordinary conversion diff --git a/problemreductions-cli/src/commands/create/tests.rs b/problemreductions-cli/src/commands/create/tests.rs index 05c803d07..6b678d6fd 100644 --- a/problemreductions-cli/src/commands/create/tests.rs +++ b/problemreductions-cli/src/commands/create/tests.rs @@ -359,33 +359,6 @@ fn test_create_schema_driven_builds_integer_target_closest_vector_problem() { assert!(data.get("bounds").is_none()); } -#[test] -fn test_create_schema_driven_builds_real_target_closest_vector_problem() { - let cli = Cli::try_parse_from([ - "pred", - "create", - "CVP", - "--basis", - "1,0;0.5,0.8", - "--target-vec", - "1.6,0.9", - ]) - .expect("create command parses"); - - let Commands::Create(args) = cli.command else { - panic!("expected create command"); - }; - - let resolved_variant = BTreeMap::from([("coefficient".to_string(), "f64".to_string())]); - let (data, variant) = create_schema_driven(&args, "ClosestVectorProblem", &resolved_variant) - .expect("schema-driven create should parse"); - let entry = problemreductions::registry::find_variant_entry("ClosestVectorProblem", &variant) - .expect("variant entry"); - (entry.factory)(data.clone()).expect("factory should deserialize generated JSON"); - assert_eq!(data["basis"], serde_json::json!([[1.0, 0.0], [0.5, 0.8]])); - assert_eq!(data["target"], serde_json::json!([1.6, 0.9])); -} - #[test] fn test_create_schema_driven_builds_cdft() { let cli = Cli::try_parse_from([ diff --git a/src/models/algebraic/closest_vector_problem.rs b/src/models/algebraic/closest_vector_problem.rs index 436b6057a..b62e21989 100644 --- a/src/models/algebraic/closest_vector_problem.rs +++ b/src/models/algebraic/closest_vector_problem.rs @@ -10,54 +10,68 @@ use num_rational::BigRational; use num_traits::Zero; use serde::{Deserialize, Serialize}; -macro_rules! cvp_create_spec { - ($name:ident, $target:ty) => { - #[derive(Debug, Deserialize, crate::CreateSpec)] - struct $name { - /// Basis matrix as semicolon-separated column vectors. - #[create(codec = "semicolon-separated")] - basis: Vec>, - /// Target vector. - #[create(name = "target_vec", codec = "comma-separated")] - target: Vec<$target>, - } +#[derive(Debug, Deserialize, crate::CreateSpec)] +struct ClosestVectorProblemCreateSpec { + /// Basis matrix as semicolon-separated column vectors. + #[create(codec = "semicolon-separated")] + basis: Vec>, + /// Target vector. + #[create(name = "target_vec", codec = "comma-separated")] + target: Vec, +} - impl TryFrom<$name> for ClosestVectorProblem<$target> { - type Error = ConstructionError; +impl TryFrom for ClosestVectorProblem { + type Error = ConstructionError; - fn try_from(spec: $name) -> Result { - ClosestVectorProblem::<$target>::new(spec.basis, spec.target) - } - } - }; + fn try_from(spec: ClosestVectorProblemCreateSpec) -> Result { + ClosestVectorProblem::new(spec.basis, spec.target) + } } -cvp_create_spec!(ClosestVectorProblemI64CreateSpec, i64); -cvp_create_spec!(ClosestVectorProblemF64CreateSpec, f64); - inventory::submit! { ProblemSchemaEntry { name: "ClosestVectorProblem", display_name: "Closest Vector Problem", aliases: &["CVP"], - dimensions: &[VariantDimension::new("coefficient", "i64", &["i64", "f64"])], + dimensions: &[VariantDimension::new("coefficient", "i64", &["i64"])], category: crate::registry::ProblemCategory::Algebraic, module_path: module_path!(), description: "Find the closest point in a lattice to a target vector", - fields: ClosestVectorProblemI64CreateSpec::FIELDS, + fields: ClosestVectorProblemCreateSpec::FIELDS, } } -/// Euclidean Closest Vector Problem with integer or floating-point coordinates. +/// Euclidean Closest Vector Problem with integer coordinates. #[derive(Debug, Clone, Serialize)] -pub struct ClosestVectorProblem { +pub struct ClosestVectorProblem { /// Basis matrix stored as column vectors. - basis: Vec>, + basis: Vec>, /// Target vector in the ambient space. - target: Vec, + target: Vec, } -impl ClosestVectorProblem { +impl ClosestVectorProblem { + /// Construct a CVP instance with a full-column-rank basis. + pub fn new(basis: Vec>, target: Vec) -> Result { + let instance = Self { basis, target }; + instance.validate_dimensions()?; + let matrix = (0..instance.ambient_dimension()) + .map(|row| { + instance + .basis + .iter() + .map(|column| BigRational::from_integer(column[row].into())) + .collect() + }) + .collect(); + if independent_rows(matrix, instance.num_basis_vectors()).is_none() { + return Err(ConstructionError::Conversion( + "closest-vector basis columns must be linearly independent".into(), + )); + } + Ok(instance) + } + /// Number of basis vectors. pub fn num_basis_vectors(&self) -> usize { self.basis.len() @@ -67,11 +81,11 @@ impl ClosestVectorProblem { self.target.len() } /// Basis columns in the variant's numeric domain. - pub fn basis(&self) -> &[Vec] { + pub fn basis(&self) -> &[Vec] { &self.basis } /// Target coordinates. - pub fn target(&self) -> &[T] { + pub fn target(&self) -> &[i64] { &self.target } @@ -103,77 +117,7 @@ impl ClosestVectorProblem { } Ok(()) } -} - -macro_rules! cvp_numeric_impl { - ($numeric:ty, $name:literal, $rational:expr) => { - impl ClosestVectorProblem<$numeric> { - /// Construct a CVP instance with full-column-rank basis. - pub fn new( - basis: Vec>, - target: Vec<$numeric>, - ) -> Result { - use crate::types::WeightElement; - let instance = Self { basis, target }; - instance.validate_dimensions()?; - for value in instance.basis.iter().flatten().chain(&instance.target) { - value.validate_element("CVP coordinate")?; - } - let matrix = (0..instance.ambient_dimension()) - .map(|row| { - instance - .basis - .iter() - .map(|column| ($rational)(column[row])) - .collect() - }) - .collect(); - if independent_rows(matrix, instance.num_basis_vectors()).is_none() { - return Err(ConstructionError::Conversion( - "closest-vector basis columns must be linearly independent".into(), - )); - } - Ok(instance) - } - } - impl<'de> Deserialize<'de> for ClosestVectorProblem<$numeric> { - fn deserialize>(deserializer: D) -> Result { - #[derive(Deserialize)] - struct Raw { - basis: Vec>, - target: Vec<$numeric>, - } - let raw = Raw::deserialize(deserializer)?; - Self::new(raw.basis, raw.target).map_err(serde::de::Error::custom) - } - } - - impl Problem for ClosestVectorProblem<$numeric> { - const NAME: &'static str = "ClosestVectorProblem"; - type Solution = Vec; - type Value = Min<$numeric>; - crate::problem_parameters![ - ("ambient_dimension", ambient_dimension), - ("num_basis_vectors", num_basis_vectors), - ]; - fn evaluate(&self, solution: &Self::Solution) -> Result { - Ok(Min(Some(self.squared_distance(solution)?))) - } - fn variant() -> Vec<(&'static str, &'static str)> { - vec![("coefficient", $name)] - } - } - }; -} - -cvp_numeric_impl!(i64, "i64", |value: i64| BigRational::from_integer( - value.into() -)); -cvp_numeric_impl!(f64, "f64", |value: f64| BigRational::from_float(value) - .expect("validated finite coordinate")); - -impl ClosestVectorProblem { pub(crate) fn independent_rows(&self) -> Vec { let matrix = (0..self.ambient_dimension()) .map(|row| { @@ -207,25 +151,31 @@ impl ClosestVectorProblem { } } -impl ClosestVectorProblem { - /// Squared distance in row/column order with ordinary floating-point rounding. - pub fn squared_distance(&self, solution: &[i64]) -> Result { - self.validate_solution(solution)?; - let finite = |value: f64| { - value.is_finite().then_some(value).ok_or_else(|| { - EvaluationError::NonFiniteResult("computing CVP squared distance".into()) - }) - }; - let mut squared = 0.0; - for (row, &target) in self.target.iter().enumerate() { - let mut coordinate = 0.0; - for (&coefficient, column) in solution.iter().zip(&self.basis) { - coordinate = finite(coordinate + finite(coefficient as f64 * column[row])?)?; - } - let difference = finite(coordinate - target)?; - squared = finite(squared + finite(difference * difference)?)?; +impl<'de> Deserialize<'de> for ClosestVectorProblem { + fn deserialize>(deserializer: D) -> Result { + #[derive(Deserialize)] + struct Raw { + basis: Vec>, + target: Vec, } - Ok(squared) + let raw = Raw::deserialize(deserializer)?; + Self::new(raw.basis, raw.target).map_err(serde::de::Error::custom) + } +} + +impl Problem for ClosestVectorProblem { + const NAME: &'static str = "ClosestVectorProblem"; + type Solution = Vec; + type Value = Min; + crate::problem_parameters![ + ("ambient_dimension", ambient_dimension), + ("num_basis_vectors", num_basis_vectors), + ]; + fn evaluate(&self, solution: &Self::Solution) -> Result { + Ok(Min(Some(self.squared_distance(solution)?))) + } + fn variant() -> Vec<(&'static str, &'static str)> { + vec![("coefficient", "i64")] } } @@ -250,8 +200,7 @@ fn independent_rows(mut matrix: Vec>, n: usize) -> Option => "2^(num_basis_vectors * log(num_basis_vectors))" create ClosestVectorProblemI64CreateSpec, - ClosestVectorProblem => "2^(num_basis_vectors * log(num_basis_vectors))" create ClosestVectorProblemF64CreateSpec, + default ClosestVectorProblem => "2^(num_basis_vectors * log(num_basis_vectors))" create ClosestVectorProblemCreateSpec, } #[cfg(feature = "example-db")] @@ -259,7 +208,7 @@ pub(crate) fn canonical_model_example_specs() -> Vec::new(vec![vec![2, 0], vec![1, 2]], vec![3_i64, 2]) + ClosestVectorProblem::new(vec![vec![2, 0], vec![1, 2]], vec![3_i64, 2]) .expect("canonical closest-vector instance must be valid"), ), optimal_config: serde_json::json!(vec![1, 1]), @@ -271,13 +220,12 @@ pub(crate) fn canonical_model_example_specs() -> Vec, "DecisionClosestVectorProblem"); -crate::decision_problem_meta!(ClosestVectorProblem, "DecisionClosestVectorProblem"); +crate::decision_problem_meta!(ClosestVectorProblem, "DecisionClosestVectorProblem"); inventory::submit! { crate::registry::ProblemSchemaEntry { name: "DecisionClosestVectorProblem", display_name: "Decision ClosestVectorProblem", aliases: &[], - dimensions: &[VariantDimension::new("coefficient", "i64", &["i64", "f64"])], category: crate::registry::ProblemCategory::Algebraic, module_path: module_path!(), + dimensions: &[VariantDimension::new("coefficient", "i64", &["i64"])], category: crate::registry::ProblemCategory::Algebraic, module_path: module_path!(), description: "Does a feasible solution meet the objective bound?", fields: &[ crate::registry::FieldInfo { name: "basis", type_name: "Vec>", description: "Basis matrix as semicolon-separated column vectors." }, @@ -287,54 +235,29 @@ inventory::submit! { } } crate::declare_variants! { - default crate::models::decision::Decision> => "2^(num_basis_vectors * log(num_basis_vectors))" create crate::models::decision::DecisionCreateSpec>, - crate::models::decision::Decision> => "2^(num_basis_vectors * log(num_basis_vectors))" create crate::models::decision::DecisionCreateSpec>, + default crate::models::decision::Decision => "2^(num_basis_vectors * log(num_basis_vectors))" create crate::models::decision::DecisionCreateSpec, } -crate::register_decision_variant!(@edges ClosestVectorProblem, "DecisionClosestVectorProblem"); -crate::register_decision_variant!(@edges ClosestVectorProblem, "DecisionClosestVectorProblem"); +crate::register_decision_variant!(@edges ClosestVectorProblem, "DecisionClosestVectorProblem"); #[cfg(feature = "example-db")] pub(crate) fn decision_canonical_rule_example_specs( ) -> Vec { - vec![ - crate::example_db::specs::RuleExampleSpec { - id: "decision_closest_vector_problem_to_closest_vector_problem", - build: || { - let source = crate::models::decision::Decision::new( - ClosestVectorProblem::::new(vec![vec![2, 0], vec![1, 2]], vec![3_i64, 2]) - .expect("canonical closest-vector instance must be valid"), - 0, - ); - let witness = serde_json::json!(vec![1, 1]); - crate::example_db::specs::rule_example_with_witness::<_, ClosestVectorProblem>( - source, - crate::export::SolutionPair { - source_config: witness.clone(), - target_config: witness, - }, - ) - }, + vec![crate::example_db::specs::RuleExampleSpec { + id: "decision_closest_vector_problem_to_closest_vector_problem", + build: || { + let source = crate::models::decision::Decision::new( + ClosestVectorProblem::new(vec![vec![2, 0], vec![1, 2]], vec![3_i64, 2]) + .expect("canonical closest-vector instance must be valid"), + 0, + ); + let witness = serde_json::json!(vec![1, 1]); + crate::example_db::specs::rule_example_with_witness::<_, ClosestVectorProblem>( + source, + crate::export::SolutionPair { + source_config: witness.clone(), + target_config: witness, + }, + ) }, - crate::example_db::specs::RuleExampleSpec { - id: "decision_closest_vector_problem_float_to_closest_vector_problem", - build: || { - let source = crate::models::decision::Decision::new( - ClosestVectorProblem::::new( - vec![vec![1.0, 0.0], vec![0.5, 0.8]], - vec![1.6, 0.9], - ) - .expect("canonical oblique-grid instance must be valid"), - 0.03, - ); - let witness = serde_json::json!(vec![1, 1]); - crate::example_db::specs::rule_example_with_witness::<_, ClosestVectorProblem>( - source, - crate::export::SolutionPair { - source_config: witness.clone(), - target_config: witness, - }, - ) - }, - }, - ] + }] } diff --git a/src/rules/closestvectorproblem_qubo.rs b/src/rules/closestvectorproblem_qubo.rs index 55a8cb8fd..fa492a88d 100644 --- a/src/rules/closestvectorproblem_qubo.rs +++ b/src/rules/closestvectorproblem_qubo.rs @@ -13,7 +13,7 @@ use crate::solvers::ProblemOutcome; use num_bigint::BigInt; use num_traits::Zero; -type Source = ClosestVectorProblem; +type Source = ClosestVectorProblem; type Target = QUBO; #[derive(Debug, Clone)] @@ -234,7 +234,7 @@ fn dot(left: &[i64], right: &[i64], operation: &str) -> Result> for ClosestVectorProblem { +impl ReduceTo> for ClosestVectorProblem { type Result = ReductionCVPToQUBO; fn reduce_to(&self) -> Result { @@ -333,7 +333,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, 2]) .expect("canonical closest-vector instance must be valid") } diff --git a/src/rules/subsetsum_closestvectorproblem.rs b/src/rules/subsetsum_closestvectorproblem.rs index defef0519..7d0726e92 100644 --- a/src/rules/subsetsum_closestvectorproblem.rs +++ b/src/rules/subsetsum_closestvectorproblem.rs @@ -10,13 +10,13 @@ use crate::solvers::ProblemOutcome; /// Result of reducing SubsetSum to ClosestVectorProblem. #[derive(Debug, Clone)] pub struct ReductionSubsetSumToClosestVectorProblem { - target: Decision>, + target: Decision, num_elements: usize, } impl ReductionResult for ReductionSubsetSumToClosestVectorProblem { type Source = SubsetSum; - type Target = Decision>; + type Target = Decision; fn target_problem(&self) -> &Self::Target { &self.target @@ -54,7 +54,7 @@ impl ReductionSubsetSumToClosestVectorProblem { let overflow = || { crate::rules::ReductionError::integer_overflow::< SubsetSum, - Decision>, + Decision, >("sizing the binary-carry lattice") }; let bits = usize::try_from(bit_width).map_err(|_| overflow())?; @@ -77,7 +77,7 @@ impl ReductionSubsetSumToClosestVectorProblem { num_basis_vectors = "n+b-1 depends on input bit length b, which is not a registered SubsetSum parameter", }, )] -impl ReduceTo>> for SubsetSum { +impl ReduceTo> for SubsetSum { type Result = ReductionSubsetSumToClosestVectorProblem; fn reduce_to(&self) -> Result { @@ -114,13 +114,12 @@ impl ReduceTo>> for SubsetSum { for bit in 0..bits { target[rows - 1 - bit] = i64::from(self.target().bit(bit as u64)); } - let target = ClosestVectorProblem::::new(basis, target).map_err( - >>>::target_construction, - )?; + let target = ClosestVectorProblem::new(basis, target) + .map_err(>>::target_construction)?; Ok(ReductionSubsetSumToClosestVectorProblem { target: Decision::new( target, - >>>::exact_i64( + >>::exact_i64( n, "representing the subset count", )?, @@ -137,10 +136,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec>, - >( + crate::example_db::specs::rule_example_with_witness::<_, Decision>( SubsetSum::new(vec![3u32, 7, 1, 8], 11u32), SolutionPair { source_config: serde_json::json!(vec![true, false, false, true]), diff --git a/src/solvers/customized/closest_vector_problem.rs b/src/solvers/customized/closest_vector_problem.rs index 28770812c..b04025bd0 100644 --- a/src/solvers/customized/closest_vector_problem.rs +++ b/src/solvers/customized/closest_vector_problem.rs @@ -1,4 +1,4 @@ -//! Integer and numerical CVP sphere enumeration in nearest-first (Schnorr--Euchner) order. +//! Exact CVP sphere enumeration in nearest-first (Schnorr--Euchner) order. use crate::models::algebraic::ClosestVectorProblem; use crate::solvers::SolveError; @@ -8,7 +8,7 @@ use num_traits::{Signed, ToPrimitive, Zero}; type GramSchmidtData = (Vec>, Vec, Vec); -pub(crate) fn solve(problem: &ClosestVectorProblem) -> Result, SolveError> { +pub(crate) fn solve(problem: &ClosestVectorProblem) -> Result, SolveError> { let n = problem.num_basis_vectors(); if n == 0 { return Ok(Vec::new()); @@ -148,170 +148,6 @@ fn enumerate( } } -type FloatGramSchmidtData = (Vec>, Vec, Vec); - -pub(crate) fn solve_float(problem: &ClosestVectorProblem) -> Result, SolveError> { - let n = problem.num_basis_vectors(); - if n == 0 { - return Ok(Vec::new()); - } - - let (mu, norms, alpha) = float_gram_schmidt(problem.basis(), problem.target())?; - let mut best_squared = 0.0; - for i in 0..n { - best_squared = finite( - best_squared + norms[i] * alpha[i] * alpha[i], - "computing the initial CVP sphere radius", - )?; - } - - let mut coefficients = vec![0_i64; n]; - let mut best = coefficients.clone(); - enumerate_float( - n - 1, - 0.0, - &mu, - &norms, - &alpha, - &mut coefficients, - &mut best, - &mut best_squared, - )?; - Ok(best) -} - -fn float_gram_schmidt( - basis: &[Vec], - target: &[f64], -) -> Result { - let n = basis.len(); - let mut orthogonal = basis.to_vec(); - let mut mu = vec![vec![0.0; n]; n]; - let mut norms = vec![0.0; n]; - - for i in 0..n { - for j in 0..i { - let dot = basis[i] - .iter() - .zip(&orthogonal[j]) - .try_fold(0.0, |total, (&left, &right)| { - finite(total + left * right, "computing a CVP projection") - })?; - mu[i][j] = finite(dot / norms[j], "computing a CVP projection")?; - for row in 0..orthogonal[i].len() { - orthogonal[i][row] = finite( - orthogonal[i][row] - mu[i][j] * orthogonal[j][row], - "orthogonalizing a CVP basis", - )?; - } - } - norms[i] = orthogonal[i].iter().try_fold(0.0, |total, &value| { - finite(total + value * value, "computing a CVP Gram--Schmidt norm") - })?; - if norms[i] <= 0.0 { - return Err(SolveError::NonFiniteResult( - "the basis is numerically rank deficient".into(), - )); - } - } - - let alpha = orthogonal - .iter() - .zip(&norms) - .map(|(column, &norm)| { - let dot = target - .iter() - .zip(column) - .try_fold(0.0, |total, (&left, &right)| { - finite(total + left * right, "projecting the CVP target") - })?; - finite(dot / norm, "projecting the CVP target") - }) - .collect::, _>>()?; - Ok((mu, norms, alpha)) -} - -#[allow(clippy::too_many_arguments)] -fn enumerate_float( - level: usize, - partial_squared: f64, - mu: &[Vec], - norms: &[f64], - alpha: &[f64], - coefficients: &mut [i64], - best: &mut [i64], - best_squared: &mut f64, -) -> Result<(), SolveError> { - if partial_squared >= *best_squared { - return Ok(()); - } - - let mut center = alpha[level]; - for later in (level + 1)..coefficients.len() { - let coefficient = coefficients[later] as f64; - center = finite( - center - mu[later][level] * coefficient, - "computing a CVP enumeration center", - )?; - } - let mut candidate = center - .round() - .to_i64() - .ok_or_else(|| SolveError::IntegerOverflow("rounding a CVP enumeration center".into()))?; - let nearest = candidate as f64; - let mut step = if center > nearest { 1_i64 } else { -1 }; - - // Visit the nearest integer, then alternate sides in increasing distance. - // The first descent tries the nearest-plane candidate; every subsequent - // branch uses the improved incumbent rather than a fixed initial interval. - loop { - coefficients[level] = candidate; - let delta = candidate as f64 - center; - let next_squared = finite( - partial_squared + norms[level] * delta * delta, - "computing a CVP partial distance", - )?; - if next_squared >= *best_squared { - break; - } - if level == 0 { - *best_squared = next_squared; - best.clone_from_slice(coefficients); - break; - } - enumerate_float( - level - 1, - next_squared, - mu, - norms, - alpha, - coefficients, - best, - best_squared, - )?; - if partial_squared >= *best_squared { - break; - } - // Differences +1,-2,+3,... (or -1,+2,-3,...) alternate around the center. - candidate = candidate.checked_add(step).ok_or_else(|| { - SolveError::IntegerOverflow("advancing a numerical CVP coefficient".into()) - })?; - step = step - .checked_neg() - .and_then(|v| v.checked_sub(step.signum())) - .ok_or_else(|| SolveError::IntegerOverflow("advancing a numerical CVP step".into()))?; - } - Ok(()) -} - -fn finite(value: f64, operation: &str) -> Result { - if value.is_finite() { - Ok(value) - } else { - Err(SolveError::NonFiniteResult(operation.into())) - } -} - #[cfg(test)] #[path = "../../unit_tests/solvers/customized/closest_vector_problem.rs"] mod tests; diff --git a/src/solvers/customized/solver.rs b/src/solvers/customized/solver.rs index 128fef328..ca4ca62b2 100644 --- a/src/solvers/customized/solver.rs +++ b/src/solvers/customized/solver.rs @@ -95,55 +95,22 @@ register_customized_solver!( ); register_customized_solver!( - crate::models::algebraic::ClosestVectorProblem, + crate::models::algebraic::ClosestVectorProblem, "cvp-sphere-enumeration", |problem| super::closest_vector_problem::solve(problem).map(Some) ); -inventory::submit! { - CustomizedSolverRegistration { - source_name: "ClosestVectorProblem", - source_variant_fn: crate::models::algebraic::ClosestVectorProblem::::variant, - implementation: "cvp-numerical-sphere-enumeration", - solve_fn: |any| { - let problem = any.downcast_ref::>() - .expect("registered CVP float variant"); - let solution = super::closest_vector_problem::solve_float(problem)?; - let outcome = crate::solvers::SolveOutcome::feasible(problem, solution)?; - Ok(crate::solvers::outcome_to_json(&outcome)?) - }, - } -} register_customized_solver!( - crate::models::decision::Decision>, + crate::models::decision::Decision, "cvp-sphere-enumeration", |problem: &crate::models::decision::Decision< - crate::models::algebraic::ClosestVectorProblem, + crate::models::algebraic::ClosestVectorProblem, >| { let solution = super::closest_vector_problem::solve(problem.inner())?; Ok(problem.evaluate(&solution)?.0.then_some(solution)) } ); -inventory::submit! { - CustomizedSolverRegistration { - source_name: "DecisionClosestVectorProblem", - source_variant_fn: crate::models::decision::Decision::>::variant, - implementation: "cvp-numerical-sphere-enumeration", - solve_fn: |any| { - let problem = any.downcast_ref::>>() - .expect("registered CVP float decision variant"); - let solution = super::closest_vector_problem::solve_float(problem.inner())?; - let evaluation = problem.evaluate(&solution)?; - if !evaluation.0 { - return Err(crate::rules::ExtractionError::InsufficientSolutionQuality.into()); - } - let outcome = crate::solvers::SolveOutcome::Feasible { solution, evaluation }; - Ok(crate::solvers::outcome_to_json(&outcome)?) - }, - } -} - /// Solve MinimumCardinalityKey: find a minimal key with smallest cardinality. /// /// Uses iterative deepening by cardinality to guarantee the first solution diff --git a/src/unit_tests/models/algebraic/closest_vector_problem.rs b/src/unit_tests/models/algebraic/closest_vector_problem.rs index 4cee65938..fd7065289 100644 --- a/src/unit_tests/models/algebraic/closest_vector_problem.rs +++ b/src/unit_tests/models/algebraic/closest_vector_problem.rs @@ -1,38 +1,24 @@ use super::*; -use crate::solvers::SolveOutcome; use crate::traits::Problem; use crate::types::Min; #[test] -fn test_cvp_constructs_integer_and_real_targets() { +fn test_cvp_constructs_integer_coordinates() { let integer = - ClosestVectorProblem::::new(vec![vec![2, 0, 0], vec![1, 2, 0]], vec![3_i64, 3, 1]) - .unwrap(); + ClosestVectorProblem::new(vec![vec![2, 0, 0], vec![1, 2, 0]], vec![3_i64, 3, 1]).unwrap(); assert_eq!(integer.num_basis_vectors(), 2); assert_eq!(integer.ambient_dimension(), 3); assert_eq!(integer.target(), &[3, 3, 1]); assert_eq!( - ClosestVectorProblem::::variant(), + ClosestVectorProblem::variant(), vec![("coefficient", "i64")] ); - - let real = ClosestVectorProblem::::new( - vec![vec![2.0, 0.0, 0.0], vec![1.0, 2.0, 0.0]], - vec![2.5, 1.25, -0.5], - ) - .unwrap(); - assert_eq!(real.target(), &[2.5, 1.25, -0.5]); - assert_eq!( - ClosestVectorProblem::::variant(), - vec![("coefficient", "f64")] - ); } #[test] fn test_cvp_evaluates_without_coefficient_bounds() { let problem = - ClosestVectorProblem::::new(vec![vec![2, 0, 0], vec![1, 2, 0]], vec![3_i64, 3, 1]) - .unwrap(); + ClosestVectorProblem::new(vec![vec![2, 0, 0], vec![1, 2, 0]], vec![3_i64, 3, 1]).unwrap(); assert_eq!(problem.evaluate(&vec![1, 1]).unwrap(), Min(Some(2))); assert!(problem.evaluate(&vec![11, -12]).unwrap().0.is_some()); assert!(matches!( @@ -43,54 +29,36 @@ fn test_cvp_evaluates_without_coefficient_bounds() { #[test] fn test_cvp_rejects_invalid_basis() { - assert!(ClosestVectorProblem::::new(vec![vec![1_i64]], vec![0_i64, 0]).is_err()); - assert!( - ClosestVectorProblem::::new(vec![vec![1_i64, 0], vec![2_i64, 0]], vec![0_i64, 0],) - .is_err() - ); + assert!(ClosestVectorProblem::new(vec![vec![1_i64]], vec![0_i64, 0]).is_err()); assert!( - ClosestVectorProblem::::new(vec![vec![1_i64], vec![2_i64]], vec![0_i64],).is_err() + ClosestVectorProblem::new(vec![vec![1_i64, 0], vec![2_i64, 0]], vec![0_i64, 0],).is_err() ); + assert!(ClosestVectorProblem::new(vec![vec![1_i64], vec![2_i64]], vec![0_i64],).is_err()); } #[test] fn test_cvp_rank_uses_exact_integer_elimination() { - let problem = ClosestVectorProblem::::new( - vec![vec![i64::MAX, 1], vec![1, i64::MAX]], - vec![0_i64, 0], - ) - .unwrap(); + let problem = + ClosestVectorProblem::new(vec![vec![i64::MAX, 1], vec![1, i64::MAX]], vec![0_i64, 0]) + .unwrap(); assert_eq!(problem.independent_rows(), vec![0, 1]); // Swapped pivots and a redundant ambient row preserve column rank. let rectangular = - ClosestVectorProblem::::new(vec![vec![0, 0, 1], vec![0, 1, 0]], vec![0_i64; 3]) - .unwrap(); + ClosestVectorProblem::new(vec![vec![0, 0, 1], vec![0, 1, 0]], vec![0_i64; 3]).unwrap(); assert_eq!(rectangular.independent_rows(), vec![2, 1]); } -#[test] -fn test_cvp_rejects_non_finite_real_target() { - assert!(matches!( - ClosestVectorProblem::::new(vec![vec![1.0]], vec![f64::NAN]), - Err(ConstructionError::NonFiniteFloat(_)) - )); - assert!(matches!( - ClosestVectorProblem::::new(vec![vec![1.0]], vec![f64::INFINITY]), - Err(ConstructionError::NonFiniteFloat(_)) - )); -} - #[test] fn test_cvp_integer_coordinates_preserve_zero_and_unit_distance() { let target = (1_i64 << 53) + 1; - let problem = ClosestVectorProblem::::new(vec![vec![1]], vec![target]).unwrap(); + let problem = ClosestVectorProblem::new(vec![vec![1]], vec![target]).unwrap(); assert_eq!( crate::solvers::customized::closest_vector_problem::solve(&problem).unwrap(), vec![target] ); assert_eq!(problem.squared_distance(&[target]).unwrap(), 0); assert_eq!(problem.squared_distance(&[target - 1]).unwrap(), 1); - let cancellation = ClosestVectorProblem::::new( + let cancellation = ClosestVectorProblem::new( vec![vec![i64::MAX, 1], vec![i64::MAX - 1, 1]], vec![1_i64, 0], ) @@ -99,161 +67,52 @@ fn test_cvp_integer_coordinates_preserve_zero_and_unit_distance() { } #[test] -fn test_cvp_float_evaluation_and_solve_preserve_numeric_status() { - let problem = ClosestVectorProblem::::new(vec![vec![1.0]], vec![0.25]).unwrap(); - assert_eq!(problem.squared_distance(&[1]).unwrap(), 0.5625); - let value = problem.evaluate(&vec![1]).unwrap(); - let serialized = - crate::registry::DynProblem::evaluate_json(&problem, &serde_json::json!([1])).unwrap(); - assert_eq!( - serde_json::from_value::>(serialized).unwrap(), - value - ); - assert_eq!( - crate::registry::DynProblem::evaluate_dyn(&problem, &serde_json::json!([1])).unwrap(), - ("Min(0.5625)".into(), true) - ); - let loaded = crate::registry::LoadedDynProblem::new(Box::new(problem)); - let outcome = crate::solvers::solve(&loaded, crate::solvers::SolverRequest::Default) - .unwrap() - .outcome; - assert_eq!( - outcome, - SolveOutcome::Feasible { - solution: serde_json::json!([0]), - evaluation: "Min(0.0625)".into(), - } - ); -} - -#[test] -fn test_cvp_serialization_round_trips_both_targets() { - let integer = ClosestVectorProblem::::new(vec![vec![1_i64]], vec![2_i64]).unwrap(); +fn test_cvp_serialization_round_trips() { + let integer = ClosestVectorProblem::new(vec![vec![1_i64]], vec![2_i64]).unwrap(); let json = serde_json::to_string(&integer).unwrap(); assert!(!json.contains("bounds")); - let decoded: ClosestVectorProblem = serde_json::from_str(&json).unwrap(); + let decoded: ClosestVectorProblem = serde_json::from_str(&json).unwrap(); assert_eq!(decoded.basis(), integer.basis()); assert_eq!(decoded.target(), integer.target()); - - let real = ClosestVectorProblem::::new(vec![vec![1.0]], vec![2.5]).unwrap(); - let json = serde_json::to_string(&real).unwrap(); - let decoded: ClosestVectorProblem = serde_json::from_str(&json).unwrap(); - assert_eq!(decoded.target(), real.target()); } #[test] fn test_cvp_create_specs_have_no_bounds() { - let integer = ClosestVectorProblem::::try_from(ClosestVectorProblemI64CreateSpec { + let integer = ClosestVectorProblem::try_from(ClosestVectorProblemCreateSpec { basis: vec![vec![1]], target: vec![2], }) .unwrap(); assert_eq!(integer.target(), &[2]); - - let real = ClosestVectorProblem::::try_from(ClosestVectorProblemF64CreateSpec { - basis: vec![vec![1.0]], - target: vec![2.5], - }) - .unwrap(); - assert_eq!(real.target(), &[2.5]); } #[test] -fn test_cvp_registers_both_target_variants() { - let mut variants = crate::registry::variant_entries() +fn test_cvp_registers_integer_variant() { + let variants = crate::registry::variant_entries() .into_iter() - .filter(|entry| entry.name == ClosestVectorProblem::::NAME) + .filter(|entry| entry.name == ClosestVectorProblem::NAME) .map(|entry| entry.variant_map()) .collect::>(); - variants.sort(); assert_eq!( variants, - vec![ - std::collections::BTreeMap::from([("coefficient".into(), "f64".into())]), - std::collections::BTreeMap::from([("coefficient".into(), "i64".into())]), - ] + vec![std::collections::BTreeMap::from([( + "coefficient".into(), + "i64".into() + )])] ); } #[test] fn test_cvp_empty_basis_is_valid() { - let problem = ClosestVectorProblem::::new(Vec::new(), vec![3_i64, 4]).unwrap(); + let problem = ClosestVectorProblem::new(Vec::new(), vec![3_i64, 4]).unwrap(); assert_eq!(problem.evaluate(&Vec::new()).unwrap(), Min(Some(25))); } -#[test] -fn test_cvp_oblique_grid_quantization() { - use crate::models::decision::Decision; - use crate::rules::{ReduceTo, ReductionResult}; - let problem = - ClosestVectorProblem::::new(vec![vec![1.0, 0.0], vec![0.5, 0.8]], vec![1.6, 0.9]) - .unwrap(); - let solution = - crate::solvers::customized::closest_vector_problem::solve_float(&problem).unwrap(); - assert_eq!(solution, vec![1, 1]); - assert!((problem.squared_distance(&solution).unwrap() - 0.02).abs() < 1e-12); - let decision = Decision::new(problem.clone(), 0.03); - assert!(decision.evaluate(&solution).unwrap().0); - let too_close = Decision::new(problem.clone(), 0.01); - assert!(!too_close.evaluate(&solution).unwrap().0); - let reduction = ReduceTo::>::reduce_to(&too_close).unwrap(); - assert!(matches!( - reduction.recover_result( - &too_close, - SolveOutcome::feasible(&problem, solution).unwrap() - ), - Err(crate::rules::ExtractionError::InsufficientSolutionQuality) - )); - let loaded = crate::registry::LoadedDynProblem::new(Box::new(problem)); - assert!(matches!( - crate::solvers::solve(&loaded, crate::solvers::SolverRequest::Default) - .unwrap() - .outcome, - SolveOutcome::Feasible { .. } - )); - for (bound, accepted) in [(0.03, true), (0.01, false)] { - let decision = Decision::new( - ClosestVectorProblem::::new(vec![vec![1.0, 0.0], vec![0.5, 0.8]], vec![1.6, 0.9]) - .unwrap(), - bound, - ); - let loaded = crate::registry::LoadedDynProblem::new(Box::new(decision)); - let result = crate::solvers::solve(&loaded, crate::solvers::SolverRequest::Default); - if accepted { - assert!(matches!( - result.unwrap().outcome, - SolveOutcome::Feasible { .. } - )); - } else { - assert!(matches!( - result, - Err(crate::solvers::SolveError::Extraction( - crate::rules::ExtractionError::InsufficientSolutionQuality - )) - )); - } - } - let graph = crate::rules::ReductionGraph::new(); - assert!(!graph.has_direct_reduction::, ClosestVectorProblem>()); -} - #[test] fn test_cvp_reports_declared_arithmetic_errors() { - let integer = ClosestVectorProblem::::new(vec![vec![2]], vec![0]).unwrap(); + let integer = ClosestVectorProblem::new(vec![vec![2]], vec![0]).unwrap(); assert!(matches!( integer.evaluate(&vec![i64::MAX]), Err(EvaluationError::IntegerOverflow(_)) )); - let float = ClosestVectorProblem::::new(vec![vec![f64::MAX]], vec![0.0]).unwrap(); - assert!(matches!( - float.evaluate(&vec![2]), - Err(EvaluationError::NonFiniteResult(_)) - )); - assert!(float.evaluate(&vec![]).is_err()); - assert!(ClosestVectorProblem::::new(vec![vec![f64::NAN]], vec![0.0]).is_err()); - assert!(ClosestVectorProblem::::new(vec![vec![1.0], vec![2.0]], vec![0.0]).is_err()); - assert!( - ClosestVectorProblem::::new(vec![vec![1.0, 2.0], vec![2.0, 4.0]], vec![0.0, 0.0]) - .is_err() - ); } diff --git a/src/unit_tests/rules/closestvectorproblem_qubo.rs b/src/unit_tests/rules/closestvectorproblem_qubo.rs index 8e421ed51..e483678cf 100644 --- a/src/unit_tests/rules/closestvectorproblem_qubo.rs +++ b/src/unit_tests/rules/closestvectorproblem_qubo.rs @@ -3,8 +3,8 @@ use crate::solvers::BruteForce; use crate::solvers::SolveOutcome; use crate::traits::Problem; -fn canonical_cvp() -> ClosestVectorProblem { - ClosestVectorProblem::::new(vec![vec![2, 0], vec![1, 2]], vec![3_i64, 2]).unwrap() +fn canonical_cvp() -> ClosestVectorProblem { + ClosestVectorProblem::new(vec![vec![2, 0], vec![1, 2]], vec![3_i64, 2]).unwrap() } fn canonical_bits() -> Vec { @@ -45,7 +45,7 @@ fn test_closestvectorproblem_to_qubo_twelve_dimensional_identity() { let basis = (0..size) .map(|column| (0..size).map(|row| i64::from(row == column)).collect()) .collect(); - let source = ClosestVectorProblem::::new(basis, vec![1_i64; size]).unwrap(); + let source = ClosestVectorProblem::new(basis, vec![1_i64; size]).unwrap(); let reduction = ReduceTo::>::reduce_to(&source).unwrap(); let mut bits = vec![false; reduction.target_problem().num_vars()]; for encoding in &reduction.encodings { @@ -144,7 +144,7 @@ fn test_closestvectorproblem_to_qubo_exact_range_decoding() { #[test] fn test_closestvectorproblem_to_qubo_preserves_optimum_outside_old_box() { - let source = ClosestVectorProblem::::new(vec![vec![1]], vec![20_i64]).unwrap(); + let source = ClosestVectorProblem::new(vec![vec![1]], vec![20_i64]).unwrap(); let reduction = ReduceTo::>::reduce_to(&source).unwrap(); let target_solution = BruteForce::new() .solve(reduction.target_problem()) @@ -165,14 +165,13 @@ 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 absolute_value = ClosestVectorProblem::new(vec![vec![1]], vec![i64::MIN]).unwrap(); assert!(matches!( ReduceTo::>::reduce_to(&absolute_value), Err(crate::rules::ReductionError::IntegerOverflow { .. }) )); - let large_exact = - ClosestVectorProblem::::new(vec![vec![100_000_000]], vec![1_i64]).unwrap(); + let large_exact = ClosestVectorProblem::new(vec![vec![100_000_000]], vec![1_i64]).unwrap(); assert!(ReduceTo::>::reduce_to(&large_exact).is_ok()); } @@ -200,7 +199,7 @@ fn test_closestvectorproblem_to_qubo_canonical_example_spec() { #[test] fn qubo_energy_matches_squared_distance_up_to_the_dropped_constant() { - let source = ClosestVectorProblem::::new(vec![vec![2]], vec![1_i64]).unwrap(); + let source = ClosestVectorProblem::new(vec![vec![2]], vec![1_i64]).unwrap(); let reduction = ReduceTo::>::reduce_to(&source).unwrap(); let target = reduction.target_problem(); assert_eq!(target.num_vars(), 3); diff --git a/src/unit_tests/rules/subsetsum_closestvectorproblem.rs b/src/unit_tests/rules/subsetsum_closestvectorproblem.rs index b42192c61..3e49fe70c 100644 --- a/src/unit_tests/rules/subsetsum_closestvectorproblem.rs +++ b/src/unit_tests/rules/subsetsum_closestvectorproblem.rs @@ -10,7 +10,7 @@ use crate::types::OptimizationValue; #[test] fn test_subsetsum_to_closestvectorproblem_closed_loop() { let source = SubsetSum::new(vec![3u32, 7, 1, 8], 11u32); - let reduction = ReduceTo::>>::reduce_to(&source).unwrap(); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); let target_solution = crate::solvers::customized::closest_vector_problem::solve( reduction.target_problem().inner(), ) @@ -39,14 +39,14 @@ fn test_subsetsum_to_closestvectorproblem_closed_loop() { #[test] fn test_subsetsum_to_closestvectorproblem_structure() { let source = SubsetSum::new(vec![3u32, 7, 1, 8], 11u32); - let reduction = ReduceTo::>>::reduce_to(&source).unwrap(); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); let target = reduction.target_problem(); assert_eq!(target.inner().num_basis_vectors(), 7); assert_eq!(target.inner().ambient_dimension(), 12); assert_eq!(&target.inner().target()[..8], &[0, 0, 0, 0, 1, 1, 1, 1]); assert_eq!( - ClosestVectorProblem::::variant(), + ClosestVectorProblem::variant(), vec![("coefficient", "i64")] ); } @@ -54,7 +54,7 @@ fn test_subsetsum_to_closestvectorproblem_structure() { #[test] fn test_subsetsum_to_closestvectorproblem_binary_minimizers() { let source = SubsetSum::new(vec![3u32, 7, 1, 8], 11u32); - let reduction = ReduceTo::>>::reduce_to(&source).unwrap(); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); let target = reduction.target_problem(); for solution in [vec![1, 0, 0, 1, 0, 0, 0], vec![1, 1, 1, 0, 1, 1, 1]] { @@ -81,7 +81,7 @@ fn test_subsetsum_to_closestvectorproblem_binary_minimizers() { #[test] fn test_subsetsum_to_closestvectorproblem_unsatisfiable_instance() { let source = SubsetSum::new(vec![2u32, 4, 6], 5u32); - let reduction = ReduceTo::>>::reduce_to(&source).unwrap(); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); let solution = crate::solvers::customized::closest_vector_problem::solve( reduction.target_problem().inner(), ) @@ -102,7 +102,7 @@ fn test_subsetsum_to_closestvectorproblem_binary_carries_preserve_large_inputs() use num_bigint::BigUint; let size = BigUint::from(1u32) << 70usize; let source = SubsetSum::new(vec![size.clone()], size); - let result = ReduceTo::>>::reduce_to(&source).unwrap(); + let result = ReduceTo::>::reduce_to(&source).unwrap(); let mut witness = vec![0; result.target_problem().inner().num_basis_vectors()]; witness[0] = 1; assert_eq!( @@ -129,7 +129,7 @@ fn test_subsetsum_to_closestvectorproblem_binary_carries_preserve_large_inputs() .all(|&x| (-2..=1).contains(&x))); let source = SubsetSum::new(vec![1u32; 40], 20u32); - let result = ReduceTo::>>::reduce_to(&source).unwrap(); + let result = ReduceTo::>::reduce_to(&source).unwrap(); let mut witness = vec![0; result.target_problem().inner().num_basis_vectors()]; witness[..20].fill(1); witness[40..].copy_from_slice(&[1, 2, 5, 10]); @@ -161,7 +161,7 @@ fn test_subsetsum_to_closestvectorproblem_all_small_coefficients() { (vec![2, 4], 5), ] { let source = SubsetSum::new(sizes, target_sum); - let result = ReduceTo::>>::reduce_to(&source).unwrap(); + let result = ReduceTo::>::reduce_to(&source).unwrap(); let target = result.target_problem(); assert!(std::ptr::eq( target, @@ -242,7 +242,7 @@ fn test_subsetsum_to_closestvectorproblem_dimension_boundaries() { #[test] fn test_subset_sum_to_cvp_recovers_selected_items() { let source = SubsetSum::new(vec![3u32, 5, 7], 8u32); - let reduction = ReduceTo::>>::reduce_to(&source).unwrap(); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); let target = reduction.target_problem(); let solution = crate::solvers::customized::closest_vector_problem::solve(target.inner()).unwrap(); diff --git a/src/unit_tests/solvers/customized/closest_vector_problem.rs b/src/unit_tests/solvers/customized/closest_vector_problem.rs index eb7577ee2..327485494 100644 --- a/src/unit_tests/solvers/customized/closest_vector_problem.rs +++ b/src/unit_tests/solvers/customized/closest_vector_problem.rs @@ -5,44 +5,29 @@ use crate::traits::Problem; use std::collections::BTreeMap; #[test] -fn test_cvp_solver_handles_integer_and_real_targets() { - let integer = ClosestVectorProblem::::new(vec![vec![1]], vec![12_i64]).unwrap(); +fn test_cvp_solver_handles_integer_targets() { + let integer = ClosestVectorProblem::new(vec![vec![1]], vec![12_i64]).unwrap(); assert_eq!(solve(&integer).unwrap(), vec![12]); - - let real = ClosestVectorProblem::::new(vec![vec![1.0]], vec![0.6]).unwrap(); - assert_eq!(solve_float(&real).unwrap(), vec![1]); } #[test] fn test_cvp_solver_handles_nonorthogonal_rectangular_and_negative_coefficients() { let problem = - ClosestVectorProblem::::new(vec![vec![2, 0, 1], vec![1, 2, 0]], vec![-3_i64, -2, -1]) + ClosestVectorProblem::new(vec![vec![2, 0, 1], vec![1, 2, 0]], vec![-3_i64, -2, -1]) .unwrap(); assert_eq!(solve(&problem).unwrap(), vec![-1, -1]); } #[test] -fn test_cvp_solver_keeps_zero_on_tie_and_handles_empty_basis() { - let tied = ClosestVectorProblem::::new(vec![vec![1.0]], vec![0.5]).unwrap(); - assert_eq!(solve_float(&tied).unwrap(), vec![0]); - - let empty = ClosestVectorProblem::::new(Vec::new(), vec![1_i64, 2]).unwrap(); +fn test_cvp_solver_handles_empty_basis() { + let empty = ClosestVectorProblem::new(Vec::new(), vec![1_i64, 2]).unwrap(); assert!(solve(&empty).unwrap().is_empty()); } -#[test] -fn test_cvp_solver_reports_search_representation_overflow() { - let out_of_range = ClosestVectorProblem::::new(vec![vec![1.0]], vec![1e20]).unwrap(); - assert!(matches!( - solve_float(&out_of_range), - Err(SolveError::IntegerOverflow(_)) - )); -} - #[test] fn test_cvp_solver_is_registered_without_brute_force() { let key = ExactProblemKey::new( - ClosestVectorProblem::::NAME, + ClosestVectorProblem::NAME, BTreeMap::from([("coefficient".to_string(), "i64".to_string())]), ); let capabilities = solver_capabilities(&key).unwrap(); @@ -56,86 +41,26 @@ fn test_cvp_solver_is_registered_without_brute_force() { #[test] fn test_cvp_solver_handles_large_translated_targets() { for target in [-1_000_000_000_i64, 1_000_000_000] { - let problem = ClosestVectorProblem::::new(vec![vec![1]], vec![target]).unwrap(); + let problem = ClosestVectorProblem::new(vec![vec![1]], vec![target]).unwrap(); assert_eq!(solve(&problem).unwrap(), vec![target]); - let rectangular = ClosestVectorProblem::::new( - vec![vec![2, 0], vec![1, 2]], - vec![3 * target, 2 * target], - ) - .unwrap(); + let rectangular = + ClosestVectorProblem::new(vec![vec![2, 0], vec![1, 2]], vec![3 * target, 2 * target]) + .unwrap(); assert_eq!(solve(&rectangular).unwrap(), vec![target, target]); - - let fractional = ClosestVectorProblem::::new( - vec![vec![2.0, 0.0]], - vec![2.0 * target as f64 + 0.6, 3.0], - ) - .unwrap(); - assert_eq!(solve_float(&fractional).unwrap(), vec![target]); - } -} - -#[test] -fn test_cvp_nearest_first_matches_exhaustive_small_lattices() { - // For these triangular bases, the zero witness bounds the projected optimal distance - // by sqrt(8). Thus |y coefficient| <= 4 and |x coefficient| <= 12. - for diagonal in 1..=3_i64 { - for skew in -2..=2_i64 { - for tx in -4..=4 { - for ty in -4..=4 { - let problem = ClosestVectorProblem::::new( - vec![vec![diagonal as f64, 0.0, 0.0], vec![skew as f64, 1.0, 0.0]], - vec![tx as f64 / 2.0, ty as f64 / 2.0, 1.0], - ) - .unwrap(); - let actual = solve_float(&problem).unwrap(); - let distance = |x: i64, y: i64| { - let dx = (diagonal * x + skew * y) as f64 - tx as f64 / 2.0; - let dy = y as f64 - ty as f64 / 2.0; - dx * dx + dy * dy + 1.0 - }; - let expected = (-12..=12) - .flat_map(|x| (-4..=4).map(move |y| distance(x, y))) - .fold(f64::INFINITY, f64::min); - assert!((distance(actual[0], actual[1]) - expected).abs() < 1e-9, - "diagonal={diagonal}, skew={skew}, target=({tx}/2,{ty}/2), solution={actual:?}"); - } - } - } } } -#[test] -fn test_cvp_enumeration_improves_the_nearest_plane_candidate() { - let problem = - ClosestVectorProblem::::new(vec![vec![2.0, 0.0], vec![1.0, 1.0]], vec![0.9, 0.49]) - .unwrap(); - // Nearest-plane rounding yields [0, 0]; the adjacent branch is closer. - assert_eq!(solve_float(&problem).unwrap(), vec![0, 1]); -} - #[test] fn test_cvp_pruning_preserves_exact_large_translation_optimum() { for coefficient in [-100_000_000_000_000_i64, 100_000_000_000_000] { - let basis = vec![vec![3, 1], vec![2, 1]]; - let target = vec![5 * coefficient, 2 * coefficient]; - let integer = ClosestVectorProblem::::new(basis.clone(), target.clone()).unwrap(); - let real = ClosestVectorProblem::::new( - basis - .iter() - .map(|col| col.iter().map(|&v| v as f64).collect()) - .collect(), - target.into_iter().map(|value| value as f64).collect(), + let integer = ClosestVectorProblem::new( + vec![vec![3, 1], vec![2, 1]], + vec![5 * coefficient, 2 * coefficient], ) .unwrap(); let expected = vec![coefficient, coefficient]; assert_eq!(solve(&integer).unwrap(), expected); - assert!(real - .evaluate(&solve_float(&real).unwrap()) - .unwrap() - .0 - .unwrap() - .is_finite()); assert_eq!(integer.evaluate(&expected).unwrap().0, Some(0)); } } @@ -144,7 +69,7 @@ fn test_cvp_pruning_preserves_exact_large_translation_optimum() { fn test_cvp_pruning_handles_nearly_parallel_integer_columns() { let n = 100_000_000_i64; let problem = - ClosestVectorProblem::::new(vec![vec![n, n + 1], vec![n + 1, n + 2]], vec![1_i64, 0]) + ClosestVectorProblem::new(vec![vec![n, n + 1], vec![n + 1, n + 2]], vec![1_i64, 0]) .unwrap(); assert_eq!(solve(&problem).unwrap(), vec![-n - 2, n + 1]); } @@ -152,25 +77,13 @@ fn test_cvp_pruning_handles_nearly_parallel_integer_columns() { #[test] fn test_cvp_search_steps_do_not_limit_integer_solutions() { let problem = - ClosestVectorProblem::::new(vec![vec![2, 0], vec![0, 1]], vec![1, i64::MIN]).unwrap(); + ClosestVectorProblem::new(vec![vec![2, 0], vec![0, 1]], vec![1, i64::MIN]).unwrap(); let solution = solve(&problem).unwrap(); assert_eq!(problem.squared_distance(&solution).unwrap(), 1); let unrepresentable = - ClosestVectorProblem::::new(vec![vec![1, 0], vec![1, 1]], vec![i64::MIN, i64::MAX]) - .unwrap(); + ClosestVectorProblem::new(vec![vec![1, 0], vec![1, 1]], vec![i64::MIN, i64::MAX]).unwrap(); assert!(matches!( solve(&unrepresentable), Err(SolveError::IntegerOverflow(_)) )); } - -#[test] -fn test_cvp_numerical_solver_handles_empty_and_reports_breakdown() { - let empty = ClosestVectorProblem::::new(vec![], vec![1.0, 2.0]).unwrap(); - assert_eq!(solve_float(&empty).unwrap(), Vec::::new()); - let problem = ClosestVectorProblem::::new(vec![vec![f64::MAX]], vec![1.0]).unwrap(); - assert!(matches!( - solve_float(&problem), - Err(SolveError::NonFiniteResult(_)) - )); -} diff --git a/src/unit_tests/solvers/resolver.rs b/src/unit_tests/solvers/resolver.rs index 9cdeb06aa..91253e4f4 100644 --- a/src/unit_tests/solvers/resolver.rs +++ b/src/unit_tests/solvers/resolver.rs @@ -623,11 +623,10 @@ fn decision_closest_vector_solver_preserves_bound_after_serialization() { for (weights, sum, expected) in [(vec![1u32, 2], 3u32, true), (vec![2, 4], 3, false)] { let source = SubsetSum::new(weights, sum); - let reduction = - ReduceTo::>>::reduce_to(&source).unwrap(); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); let target = reduction.target_problem(); let loaded = load_dyn( - >>::NAME, + >::NAME, &BTreeMap::from([("coefficient".into(), "i64".into())]), serde_json::to_value(target).unwrap(), ) From f0c89af94c0dd74c6191292d1cdfd7135807c501 Mon Sep 17 00:00:00 2001 From: Xiwei Pan Date: Wed, 16 Sep 2026 17:32:12 +0800 Subject: [PATCH 13/42] test: remove obsolete floating-point CVP case --- problemreductions-cli/tests/cli_tests.rs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/problemreductions-cli/tests/cli_tests.rs b/problemreductions-cli/tests/cli_tests.rs index 69679a87a..f140ca04b 100644 --- a/problemreductions-cli/tests/cli_tests.rs +++ b/problemreductions-cli/tests/cli_tests.rs @@ -10265,13 +10265,10 @@ fn test_extract_preserves_feasible_status_and_rejects_invalid_witnesses() { } #[test] -fn test_cvp_variants_create_and_solve() { +fn test_cvp_i64_create_and_solve() { use std::io::Write; use std::process::Stdio; - for (variant, basis, target, status, expected) in [ - ("i64", "2,0;1,2", "3,2", "optimal", 0.0), - ("f64", "1,0;0.5,0.8", "1.6,0.9", "feasible", 0.02), - ] { + for (variant, basis, target, status, expected) in [("i64", "2,0;1,2", "3,2", "optimal", 0.0)] { let created = pred() .args([ "create", From 71cbcf792565fa908ce97f2df691730c0613c20f Mon Sep 17 00:00:00 2001 From: Xiwei Pan Date: Wed, 16 Sep 2026 17:32:19 +0800 Subject: [PATCH 14/42] fix: keep only useful KColoring variants --- src/models/graph/kcoloring.rs | 19 +--------- src/solvers/customized/solver.rs | 41 +++++++++++++++++++-- src/unit_tests/graph_models.rs | 10 ++--- src/unit_tests/models/graph/kcoloring.rs | 10 ++--- src/unit_tests/solvers/customized/solver.rs | 15 +++++++- 5 files changed, 64 insertions(+), 31 deletions(-) diff --git a/src/models/graph/kcoloring.rs b/src/models/graph/kcoloring.rs index fa3fe5561..a054c42ea 100644 --- a/src/models/graph/kcoloring.rs +++ b/src/models/graph/kcoloring.rs @@ -6,7 +6,7 @@ use crate::registry::{CreateSpec, ProblemSchemaEntry, VariantDimension}; use crate::topology::{Graph, SimpleGraph}; use crate::traits::Problem; -use crate::variant::{KValue, VariantParam, K1, K2, K3, K4, K5, KN}; +use crate::variant::{KValue, VariantParam, K2, K3, KN}; use serde::{Deserialize, Serialize}; inventory::submit! { @@ -16,7 +16,7 @@ inventory::submit! { aliases: &[], dimensions: &[ VariantDimension::new("graph", "SimpleGraph", &["SimpleGraph"]), - VariantDimension::new("k", "KN", &["KN", "K1", "K2", "K3", "K4", "K5"]), + VariantDimension::new("k", "KN", &["KN", "K2", "K3"]), ], category: crate::registry::ProblemCategory::Graph, module_path: module_path!(), @@ -337,32 +337,17 @@ crate::impl_random_generate!(KColoring, crate::random::Coloring if spec.k.is_some_and(|k| k != 3) { return Err("k must match the selected K3 variant".to_string().into()); } Ok(KColoring::new(spec.graph()?)) }); -crate::impl_random_generate!(KColoring, crate::random::ColoringRandomSpec, |spec| { - if spec.k.is_some_and(|k| k != 4) { return Err("k must match the selected K4 variant".to_string().into()); } - Ok(KColoring::new(spec.graph()?)) -}); -crate::impl_random_generate!(KColoring, crate::random::ColoringRandomSpec, |spec| { - if spec.k.is_some_and(|k| k != 5) { return Err("k must match the selected K5 variant".to_string().into()); } - Ok(KColoring::new(spec.graph()?)) -}); crate::declare_variants! { default KColoring => "2^num_vertices" create RuntimeKColoringCreateSpec random, - KColoring => "num_vertices + num_edges" create FixedKColoringCreateSpec, KColoring => "num_vertices + num_edges" create FixedKColoringCreateSpec random, KColoring => "1.3289^num_vertices" create FixedKColoringCreateSpec random, - KColoring => "1.7159^num_vertices" create FixedKColoringCreateSpec random, - // Best known: O*((2-ε)^n) for some ε > 0 (Zamir 2021), concrete ε unknown - KColoring => "2^num_vertices" create FixedKColoringCreateSpec random, } crate::register_brute_force! { KColoring, - KColoring, KColoring, KColoring, - KColoring, - KColoring, } #[cfg(test)] diff --git a/src/solvers/customized/solver.rs b/src/solvers/customized/solver.rs index ca4ca62b2..6d708082d 100644 --- a/src/solvers/customized/solver.rs +++ b/src/solvers/customized/solver.rs @@ -5,7 +5,7 @@ use super::fd_subset_search::{ is_minimal_key, is_superkey, BranchDecision, }; use crate::models::graph::{ - MinimumCostCirculation, MinimumIntersectionGraphBasis, PartialFeedbackEdgeSet, + KColoring, MinimumCostCirculation, MinimumIntersectionGraphBasis, PartialFeedbackEdgeSet, RootedTreeArrangement, }; use crate::models::misc::{ @@ -14,9 +14,10 @@ use crate::models::misc::{ }; use crate::models::set::{MinimumCardinalityKey, PrimeAttributeName}; use crate::solvers::registry::CustomizedSolverRegistration; -use crate::topology::SimpleGraph; +use crate::topology::{Graph, SimpleGraph}; use crate::traits::Problem; -use std::collections::HashSet; +use crate::variant::K2; +use std::collections::{HashSet, VecDeque}; macro_rules! register_customized_solver { ($problem:ty, $implementation:literal, $solve:expr) => { @@ -94,12 +95,46 @@ register_customized_solver!( |problem| Ok(TimetableDesign::solve_via_required_assignments(problem)) ); +register_customized_solver!(KColoring, "bipartite-coloring", |problem| { + Ok(solve_two_coloring(problem)) +}); + register_customized_solver!( crate::models::algebraic::ClosestVectorProblem, "cvp-sphere-enumeration", |problem| super::closest_vector_problem::solve(problem).map(Some) ); +fn solve_two_coloring(problem: &KColoring) -> Option> { + let mut adjacency = vec![Vec::new(); problem.num_vertices()]; + for (left, right) in problem.graph().edges() { + adjacency[left].push(right); + adjacency[right].push(left); + } + + let mut colors = vec![usize::MAX; problem.num_vertices()]; + let mut queue = VecDeque::new(); + for start in 0..problem.num_vertices() { + if colors[start] != usize::MAX { + continue; + } + colors[start] = 0; + queue.push_back(start); + while let Some(vertex) = queue.pop_front() { + let next_color = 1 - colors[vertex]; + for &neighbor in &adjacency[vertex] { + if colors[neighbor] == usize::MAX { + colors[neighbor] = next_color; + queue.push_back(neighbor); + } else if colors[neighbor] == colors[vertex] { + return None; + } + } + } + } + Some(colors) +} + register_customized_solver!( crate::models::decision::Decision, "cvp-sphere-enumeration", diff --git a/src/unit_tests/graph_models.rs b/src/unit_tests/graph_models.rs index 97ef035a1..190bd9705 100644 --- a/src/unit_tests/graph_models.rs +++ b/src/unit_tests/graph_models.rs @@ -12,7 +12,7 @@ use crate::solvers::BruteForceProblem as _; use crate::topology::{Graph, SimpleGraph}; use crate::traits::Problem; use crate::types::{Max, Min}; -use crate::variant::{K1, K2, K3, K4}; +use crate::variant::{K2, K3, KN}; // ============================================================================= // Independent Set Tests @@ -603,7 +603,7 @@ mod kcoloring { #[test] fn test_empty_graph() { - let problem = KColoring::::new(SimpleGraph::new(3, vec![])); + let problem = KColoring::::with_k(SimpleGraph::new(3, vec![]), 1); let solver = BruteForce::new(); let solutions = solver.find_all_witnesses(&problem).unwrap(); @@ -614,10 +614,10 @@ mod kcoloring { #[test] fn test_complete_graph_k4() { // K4 needs 4 colors - let problem = KColoring::::new(SimpleGraph::new( + let problem = KColoring::::with_k( + SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]), 4, - vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)], - )); + ); let solver = BruteForce::new(); let solutions = solver.find_all_witnesses(&problem).unwrap(); diff --git a/src/unit_tests/models/graph/kcoloring.rs b/src/unit_tests/models/graph/kcoloring.rs index 9c377c0fc..912b6bf3a 100644 --- a/src/unit_tests/models/graph/kcoloring.rs +++ b/src/unit_tests/models/graph/kcoloring.rs @@ -34,7 +34,7 @@ fn fixed_and_runtime_variants_report_num_colors_parameter() { include!("../../jl_helpers.rs"); use crate::solvers::BruteForce; use crate::topology::SimpleGraph; -use crate::variant::{K1, K2, K3, K4}; +use crate::variant::{K2, K3, KN}; #[test] fn test_kcoloring_creation() { @@ -136,7 +136,7 @@ fn test_is_valid_coloring_wrong_len() { fn test_empty_graph() { use crate::traits::Problem; - let problem = KColoring::::new(SimpleGraph::new(3, vec![])); + let problem = KColoring::::with_k(SimpleGraph::new(3, vec![]), 1); let solver = BruteForce::new(); let solutions = solver.find_all_witnesses(&problem).unwrap(); @@ -152,10 +152,10 @@ fn test_complete_graph_k4() { use crate::traits::Problem; // K4 needs 4 colors - let problem = KColoring::::new(SimpleGraph::new( + let problem = KColoring::::with_k( + SimpleGraph::new(4, vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)]), 4, - vec![(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)], - )); + ); let solver = BruteForce::new(); let solutions = solver.find_all_witnesses(&problem).unwrap(); diff --git a/src/unit_tests/solvers/customized/solver.rs b/src/unit_tests/solvers/customized/solver.rs index d8b0690d9..c40daf729 100644 --- a/src/unit_tests/solvers/customized/solver.rs +++ b/src/unit_tests/solvers/customized/solver.rs @@ -1,9 +1,10 @@ -use crate::models::graph::{PartialFeedbackEdgeSet, RootedTreeArrangement}; +use crate::models::graph::{KColoring, PartialFeedbackEdgeSet, RootedTreeArrangement}; use crate::solvers::brute_force::CartesianIndices; use crate::solvers::registry::solver_capability_registry; use crate::solvers::ExactProblemKey; use crate::topology::{Graph, SimpleGraph}; use crate::traits::Problem; +use crate::variant::K2; struct CustomizedTestSolver; @@ -81,6 +82,18 @@ fn test_customized_solver_returns_none_for_unsupported_problem() { assert!(solver.solve_dyn(&problem).is_none()); } +#[test] +fn test_two_coloring_solver_handles_disconnected_and_non_bipartite_graphs() { + let bipartite = KColoring::::new(SimpleGraph::new(6, vec![(0, 1), (1, 2), (3, 4)])); + let coloring = CustomizedTestSolver::new() + .solve_dyn(&bipartite) + .expect("bipartite graph must have a two-coloring"); + assert!(bipartite.evaluate(&coloring).unwrap().0); + + let triangle = KColoring::::new(SimpleGraph::new(3, vec![(0, 1), (1, 2), (2, 0)])); + assert!(super::solve_two_coloring(&triangle).is_none()); +} + // --- FD model parity tests against BruteForce --- #[test] From e6705a287ea060c4acffb121ece2ee799fd9f5e6 Mon Sep 17 00:00:00 2001 From: Xiwei Pan Date: Thu, 17 Sep 2026 03:49:30 +0800 Subject: [PATCH 15/42] fix: preserve reduction correctness across supported inputs Normalize forced vertex-cover choices, long NAE clauses, and repeated set elements. Preserve empty matching instances and target construction errors. Keep Ullman filler layers nonempty and recover satisfiability from the makespan threshold. Add regression tests and matching proof updates. --- docs/paper/reductions.typ | 28 ++-- src/models/set/set_splitting.rs | 5 +- ...onminimumvertexcover_hamiltoniancircuit.rs | 27 ++- .../ksatisfiability_preemptivescheduling.rs | 113 +++++++------ ...imumvertexcover_minimumweightandorgraph.rs | 40 ++--- ...fiability_partitionintoperfectmatchings.rs | 109 ++++++------ src/rules/setsplitting_betweenness.rs | 10 +- ...threedimensionalmatching_threepartition.rs | 29 ++-- ...onminimumvertexcover_hamiltoniancircuit.rs | 35 ++++ .../ksatisfiability_preemptivescheduling.rs | 155 +++++++++++++++++- ...imumvertexcover_minimumweightandorgraph.rs | 18 ++ ...fiability_partitionintoperfectmatchings.rs | 55 ++++++- .../rules/setsplitting_betweenness.rs | 25 +++ ...threedimensionalmatching_threepartition.rs | 42 +++++ 14 files changed, 520 insertions(+), 171 deletions(-) diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index 588450da9..2cc0f27e8 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -11865,7 +11865,7 @@ with the target, rather than stored separately by solution extraction. )[ This reduction encodes vertex cover as a minimum-weight solution subgraph problem on a three-layer AND/OR DAG. The root AND gate requires all edges to be covered; each edge becomes an OR gate selecting which endpoint covers it; and each vertex becomes a sink whose arc weight equals the vertex weight. The minimum-weight solution subgraph selects exactly the arcs corresponding to a minimum vertex cover. ][ - _Construction._ Given a Minimum Vertex Cover instance $(G = (V, E), bold(w))$ with $n = |V|$ vertices and $m = |E|$ edges, build an AND/OR graph $D$ with $1 + m + 2n$ vertices arranged in three layers: + _Construction._ First let $N = {v in V : w_v < 0}$. Every optimum contains $N$: adding an omitted negative-weight vertex preserves coverage and strictly decreases cost. Remove all edges incident to $N$ and set their vertices' residual weights to zero. Keep the original vertex indices. Below, $E$ denotes these residual edges, $m = |E|$, and $w$ denotes the nonnegative residual weights; the original optimum equals the residual optimum plus $sum_(v in N) w_v$ using the original weights. Build an AND/OR graph $D$ with $1 + m + 2n$ vertices, where $n = |V|$: - *Root (AND gate):* A single vertex $r$ (index 0) with gate type AND. - *Edge layer (OR gates):* For each edge $e_i = {u, v}$ ($i = 0, dots, m-1$), create vertex $e_i$ (index $1 + i$) with gate type OR and an arc $(r, e_i)$ of weight 1. @@ -11874,9 +11874,9 @@ with the target, rather than stored separately by solution extraction. Since $r$ is AND, any solution subgraph must include all arcs from $r$ to the edge-layer vertices (cost $m$). Each edge-OR vertex $e_i$ requires at least one of its two outgoing arcs to $c_u$ and $c_v$ (selecting which endpoint covers edge $i$). Each activated cover vertex $c_j$ requires its outgoing arc to $s_j$ (contributing $w_j$). The total weight is $m + |{"activated cover arcs"}| + sum_(j in C) w_j$. - _Correctness._ ($arrow.r.double$) If $C subset.eq V$ is a vertex cover with weight $W$, then for each edge $e_i = {u, v}$, at least one endpoint lies in $C$; select the arc from $e_i$ to that endpoint's cover vertex. Activate all cover-to-sink arcs for vertices in $C$. This satisfies the AND gate at the root (all edge arcs selected), every edge OR gate (at least one child selected), and all activated cover vertices (sink arc selected). The total weight is $m + |{"edge-to-cover arcs"}| + W$. ($arrow.l.double$) In any valid solution subgraph, the AND root forces all $m$ edge arcs. Each edge OR vertex selects at least one arc to a cover vertex, activating that cover vertex and its sink arc. The set of activated cover vertices forms a vertex cover (every edge has at least one endpoint activated). The sink arc weights sum to the cover weight, so any minimum-weight solution subgraph corresponds to a minimum vertex cover. + _Correctness._ ($arrow.r.double$) Given a residual cover $C$, choose exactly one endpoint in $C$ for each edge. Activate only cover vertices reached by these choices, and their sink arcs. This is a valid solution of cost at most $2m + w(C)$, since omitted cover vertices have nonnegative weight. ($arrow.l.double$) Any valid target solution activates a residual cover $C$ and costs at least $2m + w(C)$: the root requires all $m$ arcs and each edge gate requires at least one unit-weight outgoing arc. These bounds imply that the target optimum is exactly $2m$ plus the residual cover optimum, and every target optimum recovers an optimal residual cover. Adding $N$ restores an original optimum, including negative vertices that are isolated or redundant for coverage. - _Solution extraction._ Examine the cover-to-sink arcs (indices $3m, dots, 3m + n - 1$ in the arc list): $c_j = 1$ if arc $(c_j, s_j)$ is selected, $c_j = 0$ otherwise. + _Solution extraction._ Select every vertex in $N$, together with vertices whose cover-to-sink arcs (indices $3m, dots, 3m + n - 1$) are selected. Evaluate this cover using the original weights. ] #reduction-rule("MaximumMatching", "MaximumSetPacking")[ @@ -14364,7 +14364,7 @@ The following reductions to Integer Linear Programming are straightforward formu )[ This $O(n + sum_(S in cal(C)) |S|)$ reduction @garey1979[MS1] first normalizes each large subset to size 2 or 3 with complementarity pairs, then builds a Betweenness instance with one pole element $p$, one element $a_u$ per normalized universe element, and one auxiliary betweenness element for each size-3 subset. If the normalized Set Splitting instance has universe size $n'$ with $s_2$ size-2 subsets and $s_3$ size-3 subsets, the target has $n' + 1 + s_3$ elements and $s_2 + 2 s_3$ triples. ][ - _Construction._ Given Set Splitting instance $(U, cal(C))$, first normalize every subset to size 2 or 3. For a subset $S = {s_1, dots, s_k}$ with $k >= 4$, introduce fresh elements $y^+, y^-$, replace $S$ by the size-3 subset ${s_1, s_2, y^+}$ and the complementarity subset ${y^+, y^-}$, and continue recursively on ${y^-, s_3, dots, s_k}$. Repeating this step yields an equivalent normalized instance $(U', cal(C)')$ in which every subset has size 2 or 3. + _Construction._ Given Set Splitting instance $(U, cal(C))$, discard repeated occurrences within each subset: multiplicity does not affect its colors. A resulting singleton ${u}$ is unsplittable; encode it with a fresh element $d$ and the incompatible triples $(a_u,p,d)$ and $(p,a_u,d)$. Each singleton adds one element and two triples to the counts above. For each remaining subset $S = {s_1, dots, s_k}$ with $k >= 4$, introduce fresh elements $y^+, y^-$, replace $S$ by the size-3 subset ${s_1, s_2, y^+}$ and the complementarity subset ${y^+, y^-}$, and continue recursively on ${y^-, s_3, dots, s_k}$. Repeating this step yields equivalent size-2 and size-3 constraints. 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)$. @@ -17206,11 +17206,11 @@ The following table shows concrete target-variable counts for example instances, )[ Garey and Johnson's Theorem 3.4 replaces each source edge by a 12-vertex cover-testing gadget and uses $k$ selector vertices to choose $k$ source vertices whose incident gadget-paths together cover every gadget @garey1979. The registered source uses the `One` weight variant of Decision Minimum Vertex Cover. The constructed graph is Hamiltonian iff the source graph has a vertex cover of size at most $k$. ][ - _Construction._ Let the source be a unit-weight Decision Minimum Vertex Cover instance $(G = (V, E), k)$ with $G$ simple. For each edge $e = {u, v} in E$, create a gadget with vertices $(u, e, i)$ and $(v, e, i)$ for $1 <= i <= 6$. Add the two 6-chains on the $u$-side and $v$-side together with the four cross edges ${(u, e, 3), (v, e, 1)}$, ${(v, e, 3), (u, e, 1)}$, ${(u, e, 6), (v, e, 4)}$, and ${(v, e, 6), (u, e, 4)}$. For every source vertex $v$, order its incident edges as $e_(v[1]), dots, e_(v[deg(v)])$ and connect ${(v, e_(v[i]), 6), (v, e_(v[i+1]), 1)}$ for $1 <= i < deg(v)$, forming one path that contains exactly the gadget copies labeled by $v$. Finally add selector vertices $a_1, dots, a_k$ and join each selector to both endpoints of every non-isolated vertex-path. Thus the theorem branch has $k + 12|E|$ vertices and $14|E| + sum_(v in V^+) (deg(v)-1) + 2k|V^+|$ edges, where $V^+ = {v in V : deg(v) > 0}$. + _Construction._ Let $L$ be the vertices with self-loops. Every cover contains $L$. Delete their incident edges, deduplicate remaining edges, and reduce the budget by $|L|$; keep original vertex indices. A cover of this residual loopless graph lifts by adjoining $L$, and every original cover restricts to a residual cover. A negative residual budget or zero budget with remaining edges gives a fixed NO path on three vertices. A budget covering all non-isolated residual vertices gives a fixed YES triangle and a stored cover. Below, $(G=(V,E),k)$ denotes the residual instance with $0 0}$. _Correctness._ ($arrow.r.double$) Suppose $C subset.eq V$ is a vertex cover with $|C| <= k$. Because all weights are 1, we may pad $C$ with arbitrary additional non-isolated vertices until it has exactly $k$ elements, say $v_1, dots, v_k$. For every edge gadget $e = {u, v}$, traverse it in one of the three gadget modes from @garey1979: if only $u in C$, follow the unique Hamiltonian path from $(u, e, 1)$ to $(u, e, 6)$ through all 12 gadget vertices; if only $v in C$, use the symmetric path from $(v, e, 1)$ to $(v, e, 6)$ through all 12 vertices; if both endpoints lie in $C$, use the two disjoint side paths from $(u, e, 1)$ to $(u, e, 6)$ and from $(v, e, 1)$ to $(v, e, 6)$. Chaining these gadget traversals along the paths for $v_1, dots, v_k$ and connecting consecutive paths through the selectors yields a Hamiltonian circuit of the target graph. ($arrow.l.double$) Suppose the target graph has a Hamiltonian circuit. Each selector has degree two inside the circuit and therefore cuts the circuit into $k$ selector-to-selector segments. Inside any edge gadget, the circuit can appear only in the three modes above, so each segment must stay on the path corresponding to one source vertex. Mark a source vertex $v$ selected exactly when both endpoints of its path are adjacent to selectors in the Hamiltonian circuit. This selects exactly $k$ source vertices. Every edge gadget must be completely visited, and that is possible only if at least one of its endpoint paths is selected, so every source edge has a selected endpoint. Hence the extracted set is a vertex cover of size at most $k$. - _Solution extraction._ Given a Hamiltonian circuit witness, inspect the two endpoints of each source vertex-path. Set $x_v = 1$ iff both path endpoints are adjacent to selector vertices in the cycle; otherwise set $x_v = 0$. The resulting indicator vector is a valid source-side vertex cover. + _Solution extraction._ Given a Hamiltonian circuit witness, inspect the two endpoints of each residual vertex-path. Select vertices whose two endpoints touch selectors, and add all loop-forced vertices $L$. For a fixed YES instance, return the stored cover. The resulting indicator vector is a valid source-side vertex cover. ] #let ksat_mono = load-example("KSatisfiability", "MonochromaticTriangle") @@ -17583,7 +17583,7 @@ The following table shows concrete target-variable counts for example instances, [ *Step 1 -- Source instance.* The formula is $phi = (x_1 or x_2 or x_3)$ with satisfying assignment $(x_1, x_2, x_3) = (#fmt-values(ksat_ps_sol.source_config))$. - *Step 2 -- Build Ullman's unit-task gadgets.* For $n = #n$, the reduction creates $2 n (n + 1) = #(2 * n * (n + 1))$ chain jobs $x_(i,j), overline(x)_(i,j)$, $2n = #(2 * n)$ forcing jobs $y_i, overline(y)_i$, and $7m = #(7 * m)$ clause jobs $D_(r,s)$. The slot capacities are $(#(n), #(2 * n + 1), #(2 * n + 2), #(2 * n + 2), #(m + n + 1), #(6 * m)) = (3, 7, 8, 8, 5, 6)$. We realize these capacities with $p = max(2n + 2, 6m) = #p$ processors and $F = #filler-jobs$ filler jobs, giving $#num-jobs$ total unit jobs. In this example the filler counts are $(5, 1, 0, 0, 3, 2)$. + *Step 2 -- Build Ullman's unit-task gadgets.* For $n = #n$, the reduction creates $2 n (n + 1) = #(2 * n * (n + 1))$ chain jobs $x_(i,j), overline(x)_(i,j)$, $2n = #(2 * n)$ forcing jobs $y_i, overline(y)_i$, and $7m = #(7 * m)$ clause jobs $D_(r,s)$. The slot capacities are $(#(n), #(2 * n + 1), #(2 * n + 2), #(2 * n + 2), #(m + n + 1), #(6 * m)) = (3, 7, 8, 8, 5, 6)$. We realize these capacities with $p = 1 + max_t c_t = #p$ processors and $F = #filler-jobs$ filler jobs, giving $#num-jobs$ total unit jobs. In this example every filler layer is nonempty; its size is $p-c_t$. *Step 3 -- Verify a schedule.* The witness schedule has exactly $p = #p$ jobs in each of the $T = #t$ slots: $(#fmt-values(slot-counts))$. The positive chain starters $x_(1,0), x_(2,0), x_(3,0)$ are jobs $0, 8, 16$, placed at slots $(#sigma.at(0), #sigma.at(8), #sigma.at(16)) = (1, 1, 0)$, so extraction reads $(0, 0, 1)$ back from slot 0. The clause-pattern jobs are indices $30, dots, 36$; their slots are $(#fmt-values(clause-slots))$, so exactly one clause job is promoted to slot $n + 1 = 4$ and the remaining six sit at slot $n + 2 = 5$. @@ -17594,14 +17594,14 @@ The following table shows concrete target-variable counts for example instances, )[ Ullman's reduction first builds a variable-capacity unit-task scheduling instance for 3-SAT, then pads each time slot with chained filler jobs so a fixed number of processors simulates the desired capacity profile. Because every task has length $1$, preemption is irrelevant: the resulting instance is already a valid preemptive scheduling instance whose optimal makespan is at most $T = n + 3$ iff the formula is satisfiable @ullman1975 @garey1979. ][ - _Construction._ Let $phi$ be a 3-CNF formula with variables $x_1, dots, x_n$ and clauses $C_1, dots, C_m$. Create unit jobs $x_(i,j)$ and $overline(x)_(i,j)$ for $1 <= i <= n$ and $0 <= j <= n$, plus forcing jobs $y_i, overline(y)_i$, and clause jobs $D_(r,s)$ for $1 <= r <= m$, $1 <= s <= 7$. Add chain precedences $x_(i,j) prec x_(i,j+1)$ and $overline(x)_(i,j) prec overline(x)_(i,j+1)$, and branching precedences $x_(i,i-1) prec y_i$, $overline(x)_(i,i-1) prec overline(y)_i$. Set $T = n + 3$ and slot capacities $c_0 = n$, $c_1 = 2n + 1$, $c_t = 2n + 2$ for $2 <= t <= n$, $c_(n+1) = m + n + 1$, and $c_(n+2) = 6m$. + _Construction._ An empty conjunction maps to one unit job with threshold 1; a formula containing an empty clause maps to that job with threshold 0. Otherwise repeat literals cyclically in each short clause until it has three positions, preserving its disjunction. Let $phi$ be the resulting 3-CNF formula with variables $x_1, dots, x_n$ and clauses $C_1, dots, C_m$. Create unit jobs $x_(i,j)$ and $overline(x)_(i,j)$ for $1 <= i <= n$ and $0 <= j <= n$, plus forcing jobs $y_i, overline(y)_i$, and clause jobs $D_(r,s)$ for $1 <= r <= m$, $1 <= s <= 7$. Add chain precedences $x_(i,j) prec x_(i,j+1)$ and $overline(x)_(i,j) prec overline(x)_(i,j+1)$, and branching precedences $x_(i,i-1) prec y_i$, $overline(x)_(i,i-1) prec overline(y)_i$. Set $T = n + 3$ and slot capacities $c_0 = n$, $c_1 = 2n + 1$, $c_t = 2n + 2$ for $2 <= t <= n$, $c_(n+1) = m + n + 1$, and $c_(n+2) = 6m$. For each clause $C_r = (ell_1 or ell_2 or ell_3)$ and each nonzero bit pattern $b in {1, dots, 7}$, create clause job $D_(r,b)$. Its predecessors are the three chain endpoints chosen according to the bits of $b$: for literal position $k$, use the endpoint of $ell_k$ when bit $k$ is 1 and of $not ell_k$ when bit $k$ is 0. This makes exactly one clause job per clause ready one slot earlier when the clause is satisfied. - To convert the variable-capacity instance to fixed processors, let $p = max(2n + 2, 6m)$. For every slot $t$, add $p - c_t$ filler jobs and impose complete-bipartite precedences from every filler at slot $t$ to every filler at slot $t+1$. Keep every task length equal to $1$ and use $p$ processors. The total work is exactly $p T$, so any schedule of makespan at most $T$ must saturate every slot and therefore realizes the intended capacities. + To convert the variable-capacity instance to fixed processors, let $p = 1 + max_t c_t$. For every slot $t$, add $p - c_t >= 1$ filler jobs and impose complete-bipartite precedences from every filler at slot $t$ to every filler at slot $t+1$. Every filler belongs to a chain through all $T$ nonempty layers. Thus, in any schedule finishing by $T$, layer $t$ must occupy slot $t$. The total work is exactly $p T$, so every slot is saturated and precisely $c_t$ positions remain for original jobs. This is the nonempty-layer invariant of Ullman's Lemma 1. _Correctness._ ($arrow.r.double$) Given a satisfying assignment, place exactly one of $x_(i,0), overline(x)_(i,0)$ at slot $0$ for each variable, propagate the two chains forward one step at a time, schedule the forcing jobs immediately after their branch points, and place the unique matching clause job for each clause at slot $n + 1$ (all other clause jobs at slot $n + 2$). The filler jobs occupy the remaining $p - c_t$ processor positions in slot $t$, so the schedule finishes by time $T = n + 3$. ($arrow.l.double$) Conversely, if the constructed instance has makespan at most $T$, then every slot is full and the filler chains force exactly $p - c_t$ filler jobs into slot $t$, leaving precisely $c_t$ non-filler positions. Ullman's capacity argument then applies: at slot $0$ exactly one of $x_(i,0), overline(x)_(i,0)$ is chosen per variable, this choice propagates consistently through the chains, and the availability of one clause job per clause at slot $n + 1$ implies each clause has a satisfied literal. Hence the extracted assignment satisfies $phi$. - _Solution extraction._ In the binary schedule encoding, inspect the row for each starter job $x_(i,0)$. Set $x_i = 1$ iff that row has its single $1$ in column $0$; otherwise set $x_i = 0$. + _Solution extraction._ An optimal makespan greater than $T$ proves source infeasibility; a merely feasible schedule above $T$ is insufficient. For a schedule within $T$, set $x_i = 1$ iff starter job $x_(i,0)$ is in slot 0. The capacity proof guarantees a satisfying assignment; a failed extraction is an error, not a proof of infeasibility. The constant YES case returns any assignment. ] #let ksat_td = load-example("KSatisfiability", "TimetableDesign") @@ -18538,9 +18538,9 @@ The following table shows concrete target-variable counts for example instances, } ], )[ - This $O(n + m)$ reduction @schaefer1978 @garey1979[GT16] normalizes each 2-literal clause $(ell_1, ell_2)$ to $(ell_1, ell_1, ell_2)$, then builds 4-vertex variable gadgets, 2-vertex signal pairs, 4-vertex $K_4$ clause gadgets, and 2-vertex equality-chain links. For $m$ normalized clauses it produces $4n + 16m$ vertices, $3n + 21m$ edges, and fixes $K = 2$. + The ternary gadget reduction @schaefer1978 @garey1979[GT16] builds 4-vertex variable gadgets, 2-vertex signal pairs, 4-vertex $K_4$ clause gadgets, and 2-vertex equality-chain links. After clause normalization, let $n'$ and $m'$ count variables and clauses. The target has $4n' + 16m'$ vertices, $3n' + 21m'$ edges, and $K = 2$. Normalization and construction take linear time in the original variables and literal occurrences. ][ - _Construction._ Let $phi$ be a NAE-SAT instance on variables $x_1, dots, x_n$ whose clauses have size 2 or 3, matching the implemented rule. Replace every 2-literal clause $(ell_1, ell_2)$ by $(ell_1, ell_1, ell_2)$, yielding normalized 3-literal clauses $C_j = (ell_(j,0), ell_(j,1), ell_(j,2))$ for $j = 0, dots, m - 1$. For each variable $x_i$, create vertices $t_i, t'_i, f_i, f'_i$ with edges $(t_i, t'_i)$, $(f_i, f'_i)$, and $(t_i, f_i)$. For each clause position $(j, k)$, create a signal pair $s_(j,k), s'_(j,k)$ with edge $(s_(j,k), s'_(j,k))$. For each clause $C_j$, create vertices $w_(j,0), w_(j,1), w_(j,2), w_(j,3)$ forming a $K_4$, and add connection edges $(s_(j,k), w_(j,k))$ for $k in {0,1,2}$. + _Construction._ For every long clause, repeatedly use $"NAE"(a,b,R) arrow.l.r.double exists z: "NAE"(a,b,z) and "NAE"(not z,R)$ with a fresh variable $z$. If $a=b$, the first constraint forces $z=not a$, and the second is satisfiable exactly when some value in $R$ differs from $a$. If $a!=b$, choose $z$ equal to any value in $R$; both constraints hold. Conversely, an all-equal original clause makes the two constraints incompatible. A length-$k$ clause adds $k-3$ variables and becomes $k-2$ triples. Let $phi$ denote the resulting formula on $n$ variables with clauses of size 2 or 3. Replace every 2-literal clause $(ell_1, ell_2)$ by $(ell_1, ell_1, ell_2)$, yielding normalized 3-literal clauses $C_j = (ell_(j,0), ell_(j,1), ell_(j,2))$ for $j = 0, dots, m - 1$. For each variable $x_i$, create vertices $t_i, t'_i, f_i, f'_i$ with edges $(t_i, t'_i)$, $(f_i, f'_i)$, and $(t_i, f_i)$. For each clause position $(j, k)$, create a signal pair $s_(j,k), s'_(j,k)$ with edge $(s_(j,k), s'_(j,k))$. For each clause $C_j$, create vertices $w_(j,0), w_(j,1), w_(j,2), w_(j,3)$ forming a $K_4$, and add connection edges $(s_(j,k), w_(j,k))$ for $k in {0,1,2}$. For each variable, chain its positive occurrences starting from $t_i$ and its negative occurrences starting from $f_i$. If $(j, k)$ is the next occurrence in the chosen sign-order and $"src"$ is the current chain source, create fresh vertices $mu, mu'$ with edges $(mu, mu')$, $("src", mu)$, and $(s_(j,k), mu)$, then update $"src" := s_(j,k)$. Output the Partition Into Perfect Matchings instance $(G, 2)$. @@ -18548,7 +18548,7 @@ The following table shows concrete target-variable counts for example instances, ($arrow.l.double$) Suppose $(G, 2)$ admits a partition into two perfect matchings. In each variable gadget, the edges $(t_i, t'_i)$ and $(f_i, f'_i)$ force those pairs to share a group, while the edge $(t_i, f_i)$ forces $t_i$ and $f_i$ to lie in opposite groups. Each equality-chain pair forces its signal vertex to share the group of the chain source, so positive signals copy $t_i$ and negative signals copy $f_i$. In a clause gadget, each signal vertex is opposite its corresponding $w_(j,k)$, and the $K_4$ must split $2 + 2$; therefore $w_(j,0), w_(j,1), w_(j,2)$ cannot all share one group, so neither can the three signal vertices. Defining $alpha(x_i) = 1$ iff $t_i$ lies in group 0 makes every normalized clause NAE-satisfied, hence every original clause is NAE-satisfied as well. - _Solution extraction._ Read the variable gadgets: set $alpha(x_i) = 1$ iff $t_i$ lies in group 0. + _Solution extraction._ Read only the original variable gadgets: set $alpha(x_i) = 1$ iff $t_i$ lies in group 0. Discard auxiliary variables. ] // 7. ExactCoverBy3Sets → SubsetProduct (#388) @@ -18953,7 +18953,7 @@ 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. ][ - _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 some coordinate of $W union X union Y$ is absent from all triples, the source instance is trivially NO, so the implementation returns a fixed infeasible 3-Partition instance with sizes $(6, 6, 6, 6, 7, 9)$ and bound $20$. + _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 $ u_l = 10 r^4 - c_l r^3 - b_l r^2 - a_l r, $ diff --git a/src/models/set/set_splitting.rs b/src/models/set/set_splitting.rs index adf234449..7cc984333 100644 --- a/src/models/set/set_splitting.rs +++ b/src/models/set/set_splitting.rs @@ -68,6 +68,9 @@ fn normalize_subsets(universe_size: usize, subsets: &[Vec]) -> (usize, Ve for subset in subsets { let mut remainder = subset.clone(); + // Multiplicity has no meaning in a set, including before decomposition. + remainder.sort_unstable(); + remainder.dedup(); while remainder.len() > 3 { let positive_aux = next_element; let negative_aux = next_element + 1; @@ -154,7 +157,7 @@ impl SetSplitting { (universe_size, size2, size3) } - /// Universe size after decomposing all subsets to size 2 or 3. + /// Universe size after deduplicating subsets and decomposing sizes above 3. pub fn normalized_universe_size(&self) -> usize { self.normalized_stats().0 } diff --git a/src/rules/decisionminimumvertexcover_hamiltoniancircuit.rs b/src/rules/decisionminimumvertexcover_hamiltoniancircuit.rs index b1f931e2a..65e554d68 100644 --- a/src/rules/decisionminimumvertexcover_hamiltoniancircuit.rs +++ b/src/rules/decisionminimumvertexcover_hamiltoniancircuit.rs @@ -20,7 +20,7 @@ enum ConstructionKind { #[derive(Debug, Clone)] struct TheoremConstruction { - num_source_vertices: usize, + forced_cover: Vec, selector_count: usize, edges: Vec<(usize, usize)>, incident_edges: Vec>, @@ -73,7 +73,7 @@ impl TheoremConstruction { #[cfg(any(test, feature = "example-db"))] fn exact_selected_vertices(&self, source_cover: &[bool]) -> Option> { - if source_cover.len() != self.num_source_vertices || !self.covers_all_edges(source_cover) { + if source_cover.len() != self.forced_cover.len() || !self.covers_all_edges(source_cover) { return None; } @@ -187,7 +187,7 @@ impl TheoremConstruction { target_solution: &[usize], ) -> crate::rules::ExtractionResult> { Ok({ - let mut source_cover = vec![false; self.num_source_vertices]; + let mut source_cover = self.forced_cover.clone(); let mut positions = vec![usize::MAX; target_solution.len()]; for (idx, &vertex) in target_solution.iter().enumerate() { positions[vertex] = idx; @@ -276,6 +276,7 @@ fn normalize_edges(edges: Vec<(usize, usize)>) -> Vec<(usize, usize)> { .map(|(u, v)| if u < v { (u, v) } else { (v, u) }) .collect(); normalized.sort_unstable(); + normalized.dedup(); normalized } @@ -295,7 +296,18 @@ impl ReduceTo> for Decision Result { let num_source_vertices = self.inner().graph().num_vertices(); - let raw_bound = *self.bound(); + // Every loop forces its vertex into every cover. Apply the loopless + // theorem only to edges not already covered by these forced vertices. + let mut forced_cover = vec![false; num_source_vertices]; + let mut edges = normalize_edges(self.inner().graph().edges()); + for &(u, v) in &edges { + if u == v { + forced_cover[u] = true; + } + } + let raw_bound = i128::from(*self.bound()) + - forced_cover.iter().filter(|&&selected| selected).count() as i128; + edges.retain(|&(u, v)| !forced_cover[u] && !forced_cover[v]); if raw_bound < 0 { return Ok(ReductionDecisionMinimumVertexCoverToHamiltonianCircuit { target: HamiltonianCircuit::new(SimpleGraph::path(3)), @@ -305,7 +317,6 @@ impl ReduceTo> for Decision> for Decision= active_count as i128 { - let mut source_cover = vec![false; num_source_vertices]; + if raw_bound >= active_count as i128 { + let mut source_cover = forced_cover; for vertex in active_vertices { source_cover[vertex] = true; } @@ -341,7 +352,7 @@ impl ReduceTo> for Decision usize { num_vars + 3 } -fn processor_upper_bound(num_vars: usize, num_clauses: usize) -> usize { - (2 * num_vars + 2).max(6 * num_clauses) -} - fn slot_capacities(num_vars: usize, num_clauses: usize) -> Vec { let mut capacities = vec![0; time_limit(num_vars)]; capacities[0] = num_vars; @@ -75,8 +73,11 @@ fn build_ullman_construction(source: &KSatisfiability) -> UllmanConstruction let num_vars = source.num_vars(); let num_clauses = source.num_clauses(); let time_limit = time_limit(num_vars); - let num_processors = processor_upper_bound(num_vars, num_clauses); let capacities = slot_capacities(num_vars, num_clauses); + // Ullman's clock requires a nonempty filler layer in EVERY slot. + // A chain through all T layers forces layer i into slot i in any + // T-slot schedule, leaving exactly capacities[i] slots for original jobs. + let num_processors = capacities.iter().max().unwrap() + 1; let mut next_job = 0usize; @@ -152,8 +153,8 @@ fn build_ullman_construction(source: &KSatisfiability) -> UllmanConstruction for (clause_index, clause) in source.clauses().iter().enumerate() { for (pattern_index, &clause_job) in clause_jobs[clause_index].iter().enumerate() { let pattern = pattern_index + 1; - for position in 0..3 { - let literal = clause.literals[position]; + // Repeating literals preserves disjunction for short clauses. + for (position, &literal) in clause.literals.iter().cycle().take(3).enumerate() { let bit_is_one = ((pattern >> (2 - position)) & 1) == 1; precedences.push(( literal_endpoint( @@ -196,13 +197,6 @@ fn build_ullman_construction(source: &KSatisfiability) -> UllmanConstruction } } -fn task_slot(config: &[Vec], task: usize, d_max: usize) -> Option { - let task_slice = config.get(task)?; - (task_slice.len() == d_max) - .then(|| task_slice.iter().position(|&value| value)) - .flatten() -} - #[cfg(any(test, feature = "example-db"))] fn set_task_slot(task_slots: &mut [Option], job: usize, slot: usize) { task_slots[job] = Some(slot); @@ -214,9 +208,9 @@ fn clause_pattern_for_assignment( assignment: &[bool], ) -> usize { let mut pattern = 0usize; - for (position, &literal) in clause.literals.iter().enumerate() { + for (position, &literal) in clause.literals.iter().cycle().take(3).enumerate() { let variable = literal.unsigned_abs() as usize - 1; - let value = assignment.get(variable).copied().unwrap_or(false); + let value = assignment[variable]; let literal_true = if literal > 0 { value } else { !value }; if literal_true { pattern |= 1 << (2 - position); @@ -231,6 +225,13 @@ fn construct_schedule_from_assignment( assignment: &[bool], source: &KSatisfiability, ) -> Option>> { + if source.num_clauses() == 0 || source.clauses().iter().any(|c| c.literals.is_empty()) { + return source + .evaluate(&assignment.to_vec()) + .unwrap() + .0 + .then(|| vec![vec![true]]); + } let construction = build_ullman_construction(source); if assignment.len() != source.num_vars() || target.num_tasks() != construction.num_jobs { return None; @@ -345,56 +346,47 @@ impl ReductionResult for Reduction3SATToPreemptiveScheduling { ) -> crate::rules::ExtractionResult> { match target { SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - let evaluation = source.evaluate(&solution)?; - if evaluation.0 { - Ok(SolveOutcome::Optimal { - solution, - evaluation, - }) - } else { - Ok(SolveOutcome::Infeasible) + SolveOutcome::Optimal { + solution, + evaluation, + } => { + if evaluation.0.expect("valid schedule has a makespan") > self.threshold as i64 { + return Ok(SolveOutcome::Infeasible); } + // A threshold schedule must decode to a satisfying assignment. + // A mapping failure is an error, never evidence of infeasibility. + Ok(SolveOutcome::optimal(source, self.map_solution(&solution))?) } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - let evaluation = source.evaluate(&solution)?; - if evaluation.0 { - Ok(SolveOutcome::Feasible { - solution, - evaluation, - }) - } else { - Err(crate::rules::ExtractionError::InsufficientSolutionQuality) + SolveOutcome::Feasible { + solution, + evaluation, + } => { + if evaluation.0.expect("valid schedule has a makespan") > self.threshold as i64 { + return Err(crate::rules::ExtractionError::InsufficientSolutionQuality); } + Ok(SolveOutcome::feasible( + source, + self.map_solution(&solution), + )?) } } } } impl Reduction3SATToPreemptiveScheduling { - fn map_solution( - &self, - target_solution: &<::Target as crate::traits::Problem>::Solution, - ) -> crate::rules::ExtractionResult< - <::Source as crate::traits::Problem>::Solution, - > { - Ok({ - let d_max = self.target.d_max(); - self.positive_start_jobs - .iter() - .map(|&job| task_slot(target_solution, job, d_max) == Some(0)) - .collect() - }) + fn map_solution(&self, target_solution: &[Vec]) -> Vec { + self.positive_start_jobs + .iter() + .map(|&job| target_solution[job][0]) + .collect() } } #[reduction( transform = upper_bound { - num_tasks = "(2 * num_vars + 2 + 6 * num_clauses) * (num_vars + 3)", - num_processors = "2 * num_vars + 2 + 6 * num_clauses", - d_max = "(2 * num_vars + 2 + 6 * num_clauses) * (num_vars + 3)", + 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", @@ -404,6 +396,19 @@ impl ReduceTo for KSatisfiability { type Result = Reduction3SATToPreemptiveScheduling; fn reduce_to(&self) -> Result { + let has_empty_clause = self + .clauses() + .iter() + .any(|clause| clause.literals.is_empty()); + if self.num_clauses() == 0 || has_empty_clause { + // The empty conjunction is true; any empty disjunction is false. + return Ok(Reduction3SATToPreemptiveScheduling { + target: PreemptiveScheduling::new(vec![1], 1, vec![]) + .map_err(>::target_construction)?, + positive_start_jobs: vec![0; self.num_vars()], + threshold: usize::from(!has_empty_clause), + }); + } let construction = build_ullman_construction(self); let target = PreemptiveScheduling::new( vec![1_i64; construction.num_jobs], diff --git a/src/rules/minimumvertexcover_minimumweightandorgraph.rs b/src/rules/minimumvertexcover_minimumweightandorgraph.rs index ff779e51e..4dd28ebe3 100644 --- a/src/rules/minimumvertexcover_minimumweightandorgraph.rs +++ b/src/rules/minimumvertexcover_minimumweightandorgraph.rs @@ -13,7 +13,7 @@ use crate::topology::SimpleGraph; pub struct ReductionVCToAndOrGraph { target: MinimumWeightAndOrGraph, sink_arc_start: usize, - num_source_vertices: usize, + forced_cover: Vec, } impl ReductionResult for ReductionVCToAndOrGraph { @@ -29,27 +29,19 @@ impl ReductionResult for ReductionVCToAndOrGraph { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - recover_preserving_status(source, target, |solution| self.map_solution(solution)) - } -} - -impl ReductionVCToAndOrGraph { - fn map_solution( - &self, - target_solution: &<::Target as crate::traits::Problem>::Solution, - ) -> crate::rules::ExtractionResult< - <::Source as crate::traits::Problem>::Solution, - > { - Ok({ - (0..self.num_source_vertices) - .map(|j| target_solution[self.sink_arc_start + j]) - .collect() + recover_preserving_status(source, target, |solution| { + Ok(self + .forced_cover + .iter() + .enumerate() + .map(|(j, &forced)| forced || solution[self.sink_arc_start + j]) + .collect()) }) } } #[reduction( - transform = exact { + transform = upper_bound { num_vertices = "1 + num_edges + 2 * num_vertices", num_arcs = "3 * num_edges + num_vertices", } @@ -59,7 +51,15 @@ impl ReduceTo for MinimumVertexCover fn reduce_to(&self) -> Result { let n = self.graph().num_vertices(); - let edges = self.graph().edges(); + // Adding a negative-weight vertex preserves coverage and strictly + // lowers cost, so every optimum contains all such vertices. + let forced_cover: Vec<_> = self.weights().iter().map(|&w| w < 0).collect(); + let edges: Vec<_> = self + .graph() + .edges() + .into_iter() + .filter(|&(u, v)| !forced_cover[u] && !forced_cover[v]) + .collect(); let m = edges.len(); let num_target_vertices = 1 + m + (2 * n); @@ -91,7 +91,7 @@ impl ReduceTo for MinimumVertexCover let sink_arc_start = arcs.len(); for (j, &weight) in self.weights().iter().enumerate() { arcs.push((cover_vertex(j), sink_vertex(j))); - arc_weights.push(weight); + arc_weights.push(weight.max(0)); } let target = @@ -100,7 +100,7 @@ impl ReduceTo for MinimumVertexCover Ok(ReductionVCToAndOrGraph { target, sink_arc_start, - num_source_vertices: n, + forced_cover, }) } } diff --git a/src/rules/naesatisfiability_partitionintoperfectmatchings.rs b/src/rules/naesatisfiability_partitionintoperfectmatchings.rs index 92a44b545..968f47b14 100644 --- a/src/rules/naesatisfiability_partitionintoperfectmatchings.rs +++ b/src/rules/naesatisfiability_partitionintoperfectmatchings.rs @@ -2,7 +2,8 @@ //! //! This implements the Schaefer-style reduction for the `K = 2` case. //! Clauses with two literals are normalized to three literals by duplicating -//! the first literal, and clauses with more than three literals are rejected. +//! the first literal. Longer clauses are split using +//! NAE(a,b,R) iff there exists z: NAE(a,b,z) and NAE(-z,R). use crate::models::formula::NAESatisfiability; use crate::models::graph::PartitionIntoPerfectMatchings; @@ -40,6 +41,9 @@ struct ChainPairVertices { #[derive(Debug, Clone)] struct ReductionLayout { + source_num_vars: usize, + #[cfg(any(test, feature = "example-db"))] + auxiliary_inputs: Vec<[i64; 3]>, variables: Vec, #[cfg(any(test, feature = "example-db"))] clauses: Vec, @@ -71,23 +75,14 @@ impl ReductionResult for ReductionNAESATToPartitionIntoPerfectMatchings { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - recover_preserving_status(source, target, |solution| self.map_solution(solution)) - } -} - -impl ReductionNAESATToPartitionIntoPerfectMatchings { - fn map_solution( - &self, - target_solution: &<::Target as crate::traits::Problem>::Solution, - ) -> crate::rules::ExtractionResult< - <::Source as crate::traits::Problem>::Solution, - > { - Ok({ - self.layout + recover_preserving_status(source, target, |solution| { + Ok(self + .layout .variables .iter() - .map(|variable| target_solution[variable.t] == 0) - .collect() + .take(self.layout.source_num_vars) + .map(|variable| solution[variable.t] == 0) + .collect()) }) } } @@ -97,12 +92,25 @@ impl ReductionNAESATToPartitionIntoPerfectMatchings { fn construct_target_solution(&self, source_solution: &[bool]) -> Vec { assert_eq!( source_solution.len(), - self.layout.variables.len(), + self.layout.source_num_vars, "source solution has {} variables but reduction expects {}", source_solution.len(), - self.layout.variables.len() + self.layout.source_num_vars ); + let mut source_solution = source_solution.to_vec(); + for &[a, b, last] in &self.layout.auxiliary_inputs { + let value = |literal: i64| { + source_solution[literal.unsigned_abs() as usize - 1] == (literal > 0) + }; + let z = if value(a) == value(b) { + !value(a) + } else { + value(last) + }; + source_solution.push(z); + } + let mut target_solution = vec![usize::MAX; self.layout.num_vertices]; let mut true_groups = Vec::with_capacity(self.layout.variables.len()); let mut false_groups = Vec::with_capacity(self.layout.variables.len()); @@ -175,29 +183,36 @@ impl ReductionNAESATToPartitionIntoPerfectMatchings { } } -fn normalize_clauses( - problem: &NAESatisfiability, -) -> Result, crate::registry::ConstructionError> { - problem - .clauses() - .iter() - .map(|clause| match clause.literals.as_slice() { - [a, b] => Ok([*a, *a, *b]), - [a, b, c] => Ok([*a, *b, *c]), - literals => Err(format!( - "the construction expects clauses of size 2 or 3, got {}", - literals.len() - ) - .into()), - }) - .collect() -} - fn build_layout( problem: &NAESatisfiability, ) -> Result { - let num_vars = problem.num_vars(); - let clauses = normalize_clauses(problem)?; + let mut num_vars = problem.num_vars(); + let mut clauses = Vec::new(); + #[cfg(any(test, feature = "example-db"))] + let mut auxiliary_inputs = Vec::new(); + for clause in problem.clauses() { + let literals = &clause.literals; + if literals.len() == 2 { + clauses.push([literals[0], literals[0], literals[1]]); + continue; + } + let mut first = literals[0]; + for &middle in &literals[1..literals.len() - 2] { + num_vars += 1; + let auxiliary = i64::try_from(num_vars).map_err(|_| { + crate::registry::ConstructionError::from("auxiliary literal index exceeds i64") + })?; + #[cfg(any(test, feature = "example-db"))] + auxiliary_inputs.push([first, middle, literals[literals.len() - 1]]); + clauses.push([first, middle, auxiliary]); + first = -auxiliary; + } + clauses.push([ + first, + literals[literals.len() - 2], + literals[literals.len() - 1], + ]); + } let num_clauses = clauses.len(); let mut next_vertex = 0usize; @@ -310,6 +325,9 @@ fn build_layout( } Ok(ReductionLayout { + source_num_vars: problem.num_vars(), + #[cfg(any(test, feature = "example-db"))] + auxiliary_inputs, variables, #[cfg(any(test, feature = "example-db"))] clauses: clause_layouts, @@ -323,9 +341,9 @@ fn build_layout( } #[reduction( - transform = exact { - num_vertices = "4 * num_vars + 16 * num_clauses", - num_edges = "3 * num_vars + 21 * num_clauses", + transform = upper_bound { + num_vertices = "4 * num_vars + 20 * num_literals - 24 * num_clauses", + num_edges = "3 * num_vars + 24 * num_literals - 27 * num_clauses", num_matchings = "2", } )] @@ -333,12 +351,9 @@ impl ReduceTo> for NAESatisfiability type Result = ReductionNAESATToPartitionIntoPerfectMatchings; fn reduce_to(&self) -> Result { - let layout = build_layout(self).map_err(|message| { - crate::rules::ReductionError::invalid_target::< - NAESatisfiability, - PartitionIntoPerfectMatchings, - >(message.to_string()) - })?; + let layout = build_layout(self).map_err( + >>::target_construction, + )?; let target = PartitionIntoPerfectMatchings::new( SimpleGraph::new(layout.num_vertices, layout.edges.clone()), 2, diff --git a/src/rules/setsplitting_betweenness.rs b/src/rules/setsplitting_betweenness.rs index 67241143e..953377261 100644 --- a/src/rules/setsplitting_betweenness.rs +++ b/src/rules/setsplitting_betweenness.rs @@ -74,6 +74,14 @@ impl ReduceTo for SetSplitting { for subset in normalized_subsets { match subset.as_slice() { + [u] => { + // A singleton cannot contain both colors. These two + // incompatible orders encode that same impossibility. + let auxiliary = num_elements; + num_elements += 1; + triples.push((*u, pole, auxiliary)); + triples.push((pole, *u, auxiliary)); + } [u, v] => triples.push((*u, pole, *v)), [u, v, w] => { let auxiliary = num_elements; @@ -86,7 +94,7 @@ impl ReduceTo for SetSplitting { SetSplitting, Betweenness, >( - "normalized subset must contain two or three elements" + "normalized subset must contain one, two or three elements", )); } } diff --git a/src/rules/threedimensionalmatching_threepartition.rs b/src/rules/threedimensionalmatching_threepartition.rs index de788656d..937291d57 100644 --- a/src/rules/threedimensionalmatching_threepartition.rs +++ b/src/rules/threedimensionalmatching_threepartition.rs @@ -279,6 +279,9 @@ impl ReductionThreeDimensionalMatchingToThreePartition { ) -> crate::rules::ExtractionResult< <::Source as crate::traits::Problem>::Solution, > { + if self.num_source_triples == 0 { + return Ok(Vec::new()); + } let mut groups = vec![Vec::with_capacity(3); self.target.num_groups()]; let mut positions = Vec::with_capacity(target_solution.len()); for (element, &group) in target_solution.iter().enumerate() { @@ -344,9 +347,9 @@ fn enumerate_pair_keys(num_regulars: usize) -> Option> { } #[reduction( - transform = exact { - num_elements = "24 * num_triples * num_triples - 3 * num_triples", - num_groups = "8 * num_triples * num_triples - num_triples", + transform = upper_bound { + num_elements = "24 * num_triples * num_triples - 3 * num_triples + 6", + num_groups = "8 * num_triples * num_triples - num_triples + 2", })] impl ReduceTo for ThreeDimensionalMatching { type Result = ReductionThreeDimensionalMatchingToThreePartition; @@ -356,16 +359,13 @@ impl ReduceTo for ThreeDimensionalMatching { let t = self.num_triples(); if q == 0 { - return Err(crate::rules::ReductionError::invalid_target::< - ThreeDimensionalMatching, - ThreePartition, - >("source universe must be nonempty")); - } - if t == 0 { - return Err(crate::rules::ReductionError::invalid_target::< - ThreeDimensionalMatching, - ThreePartition, - >("source must contain at least one triple")); + // The empty matching covers the empty universe. + return Ok(ReductionThreeDimensionalMatchingToThreePartition { + target: ThreePartition::new(vec![1, 1, 1], 3), + step2_items: Vec::new(), + pair_keys: Vec::new(), + num_source_triples: 0, + }); } let mut covered_w = vec![false; q]; @@ -576,7 +576,8 @@ impl ReduceTo for ThreeDimensionalMatching { .ok_or_else(|| arithmetic_overflow("computing the 3-Partition bound"))?; Ok(ReductionThreeDimensionalMatchingToThreePartition { - target: ThreePartition::new(sizes, bound), + target: ThreePartition::try_new(sizes, bound) + .map_err(>::target_construction)?, step2_items, pair_keys, num_source_triples: t, diff --git a/src/unit_tests/rules/decisionminimumvertexcover_hamiltoniancircuit.rs b/src/unit_tests/rules/decisionminimumvertexcover_hamiltoniancircuit.rs index e128dadc6..6ffe789e7 100644 --- a/src/unit_tests/rules/decisionminimumvertexcover_hamiltoniancircuit.rs +++ b/src/unit_tests/rules/decisionminimumvertexcover_hamiltoniancircuit.rs @@ -163,3 +163,38 @@ fn unit_cover_bound_handles_negative_and_empty_graphs() { } } } + +#[test] +fn loops_force_vertices_before_the_loopless_construction() { + for (n, edges, bound, cover) in [ + (2, vec![(0, 0), (0, 1)], 1, vec![true, false]), + ( + 4, + vec![(0, 0), (0, 1), (1, 2), (2, 3)], + 2, + vec![true, false, true, false], + ), + ] { + let source = decision_mvc(n, &edges, bound); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + let witness = reduction.build_target_witness(&cover); + let recovered = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), witness).unwrap(), + ) + .unwrap() + .into_solution() + .unwrap(); + assert!(recovered[0]); + assert!(source.evaluate(&recovered).unwrap().0); + } + for (edges, bound) in [(vec![(0, 0), (1, 1)], 1), (vec![(0, 0), (1, 2)], 1)] { + let source = decision_mvc(3, &edges, bound); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + assert!(BruteForce::new() + .solve(reduction.target_problem()) + .unwrap() + .is_none()); + } +} diff --git a/src/unit_tests/rules/ksatisfiability_preemptivescheduling.rs b/src/unit_tests/rules/ksatisfiability_preemptivescheduling.rs index b34178728..89b53f601 100644 --- a/src/unit_tests/rules/ksatisfiability_preemptivescheduling.rs +++ b/src/unit_tests/rules/ksatisfiability_preemptivescheduling.rs @@ -62,10 +62,10 @@ fn test_ksatisfiability_to_preemptivescheduling_structure() { let target = reduction.target_problem(); assert_eq!(reduction.threshold(), 4); - assert_eq!(target.num_processors(), 6); - assert_eq!(target.num_tasks(), 24); - assert_eq!(target.d_max(), 24); - assert_eq!(target.num_precedences(), 49); + assert_eq!(target.num_processors(), 7); + assert_eq!(target.num_tasks(), 28); + assert_eq!(target.d_max(), 28); + assert_eq!(target.num_precedences(), 69); assert!(target.lengths().iter().all(|&length| length == 1)); } @@ -165,3 +165,150 @@ fn test_ksatisfiability_to_preemptivescheduling_unsatisfiable_threshold_gap() { "unsatisfiable instance should not admit a schedule by the threshold" ); } + +#[test] +fn filler_clock_excludes_every_unsatisfying_slot_zero_assignment() { + use crate::models::algebraic::{LinearConstraint, ObjectiveSense}; + let source = KSatisfiability::::new(3, vec![CNFClause::new(vec![1, 2, 3])]); + let reduction = ReduceTo::::reduce_to(&source).unwrap(); + let target = reduction.target_problem(); + let construction = build_ullman_construction(&source); + assert!(construction + .filler_jobs_by_slot + .iter() + .all(|layer| !layer.is_empty())); + assert_eq!( + target.num_tasks(), + target.num_processors() * reduction.threshold() + ); + let pcs = PrecedenceConstrainedScheduling::new( + target.num_tasks(), + target.num_processors(), + reduction.threshold() as i64, + target.precedences().to_vec(), + ); + let pcs_to_ilp = ReduceTo::>::reduce_to(&pcs).unwrap(); + let base = pcs_to_ilp.target_problem(); + // Fix the extracted assignment, not one particular schedule. This searches + // ALL threshold schedules with that assignment, including noncanonical ones. + for bits in 0..8 { + let assignment: Vec<_> = (0..3).map(|i| bits & (1 << i) != 0).collect(); + let mut constraints = base.constraints().to_vec(); + for (&job, &value) in reduction.positive_start_jobs.iter().zip(&assignment) { + constraints.push(LinearConstraint::eq( + vec![(job * reduction.threshold(), 1)], + i64::from(value), + )); + } + let constrained = ILP::::new( + base.num_vars(), + constraints, + vec![], + ObjectiveSense::Minimize, + ) + .unwrap(); + match ILPSolver::new().solve(&constrained) { + Ok(witness) => { + assert!(source.evaluate(&assignment).unwrap().0); + let slots = pcs_to_ilp + .recover_result(&pcs, SolveOutcome::optimal(base, witness).unwrap()) + .unwrap() + .into_solution() + .unwrap(); + let mut schedule = vec![vec![false; target.d_max()]; target.num_tasks()]; + for (job, slot) in slots.into_iter().enumerate() { + schedule[job][slot] = true; + } + for (slot, layer) in construction.filler_jobs_by_slot.iter().enumerate() { + assert!(layer.iter().all(|&job| schedule[job][slot])); + } + let recovered = reduction + .recover_result( + &source, + SolveOutcome::optimal(target, schedule.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .unwrap(); + assert_eq!(recovered, assignment); + assert!(reduction + .recover_result(&source, SolveOutcome::feasible(target, schedule).unwrap()) + .unwrap() + .into_solution() + .is_some()); + } + Err(crate::solvers::ILPSolveError::Infeasible) => { + assert!(!source.evaluate(&assignment).unwrap().0) + } + Err(error) => panic!("ILP execution failed: {error}"), + } + } +} + +#[test] +fn short_and_empty_clauses_preserve_truth_and_recovery_status() { + for (n, clauses) in [ + (0, vec![]), + (2, vec![]), + (0, vec![vec![]]), + (2, vec![vec![]]), + (1, vec![vec![1]]), + (1, vec![vec![1], vec![-1]]), + (2, vec![vec![1, -2]]), + ] { + let source = KSatisfiability::::new_allow_less( + n, + clauses.into_iter().map(CNFClause::new).collect(), + ); + let reduction = ReduceTo::::reduce_to(&source).unwrap(); + let expected = crate::solvers::BruteForce::new().solve(&source).unwrap(); + let schedule = if reduction.threshold() == 0 { + None + } else { + solve_threshold_schedule_via_ilp(reduction.target_problem(), reduction.threshold()) + }; + assert_eq!(schedule.is_some(), expected.is_some()); + if let Some(schedule) = schedule { + assert!(reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), schedule).unwrap() + ) + .unwrap() + .into_solution() + .is_some()); + let witness = construct_schedule_from_assignment( + reduction.target_problem(), + &expected.unwrap(), + &source, + ) + .unwrap(); + assert!(reduction + .target_problem() + .evaluate(&witness) + .unwrap() + .0 + .is_some()); + } + if reduction.threshold() == 0 { + let schedule = vec![vec![true]]; + assert!(matches!( + reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), schedule.clone()) + .unwrap() + ) + .unwrap(), + SolveOutcome::Infeasible + )); + assert!(matches!( + reduction.recover_result( + &source, + SolveOutcome::feasible(reduction.target_problem(), schedule).unwrap() + ), + Err(crate::rules::ExtractionError::InsufficientSolutionQuality) + )); + } + } +} diff --git a/src/unit_tests/rules/minimumvertexcover_minimumweightandorgraph.rs b/src/unit_tests/rules/minimumvertexcover_minimumweightandorgraph.rs index a06e1c9a7..76a59d017 100644 --- a/src/unit_tests/rules/minimumvertexcover_minimumweightandorgraph.rs +++ b/src/unit_tests/rules/minimumvertexcover_minimumweightandorgraph.rs @@ -30,6 +30,24 @@ fn test_minimumvertexcover_to_minimumweightandorgraph_closed_loop() { ); } +#[test] +fn negative_vertices_are_selected_even_when_isolated_or_redundant() { + for (n, edges, weights) in [ + (1, vec![], vec![-1]), + (2, vec![(0, 1)], vec![-1, -1]), + (3, vec![(0, 1), (1, 2)], vec![-2, 3, 1]), + (2, vec![(0, 0), (0, 1)], vec![-1, 2]), + ] { + let source = MinimumVertexCover::new(SimpleGraph::new(n, edges), weights); + let reduction = ReduceTo::::reduce_to(&source).unwrap(); + assert_optimization_round_trip_from_optimization_target( + &source, + &reduction, + "signed vertex cover", + ); + } +} + #[test] fn test_reduction_structure() { let source = issue_example_source(); diff --git a/src/unit_tests/rules/naesatisfiability_partitionintoperfectmatchings.rs b/src/unit_tests/rules/naesatisfiability_partitionintoperfectmatchings.rs index 5b1c965e7..f0430ce53 100644 --- a/src/unit_tests/rules/naesatisfiability_partitionintoperfectmatchings.rs +++ b/src/unit_tests/rules/naesatisfiability_partitionintoperfectmatchings.rs @@ -295,14 +295,53 @@ fn test_naesatisfiability_to_partitionintoperfectmatchings_two_literal_clause_no } #[test] -fn test_naesatisfiability_to_partitionintoperfectmatchings_rejects_long_clauses() { - let source = NAESatisfiability::new(4, vec![CNFClause::new(vec![1, 2, 3, 4])]); - let error = - ReduceTo::>::reduce_to(&source).unwrap_err(); - assert!(matches!( - error, - crate::rules::ReductionError::InvalidTarget { .. } - )); +fn long_nae_clauses_preserve_assignments_with_auxiliary_variables() { + let entry = inventory::iter:: + .into_iter() + .find(|e| { + e.source_name == NAESatisfiability::NAME + && e.target_name == PartitionIntoPerfectMatchings::::NAME + }) + .unwrap(); + let contract = entry.parameter_contract().unwrap(); + let transform = contract.transform().unwrap(); + for literals in [vec![1, 2, 3, 4], vec![1, -2, 1, 3, -4, 2], vec![1, 1, 1, 1]] { + let source = NAESatisfiability::new(4, vec![CNFClause::new(literals)]); + let reduction = + ReduceTo::>::reduce_to(&source).unwrap(); + let layout = &reduction.layout; + let bound = transform.evaluate(&source.parameters()).unwrap(); + assert!(layout.num_vertices as u64 <= bound.get("num_vertices").unwrap()); + assert!(layout.edges.len() as u64 <= bound.get("num_edges").unwrap()); + for bits in 0..16 { + let assignment: Vec<_> = (0..4).map(|i| bits & (1 << i) != 0).collect(); + let mut extendible = false; + for aux in 0..(1 << (layout.variables.len() - 4)) { + let mut extended = assignment.clone(); + extended.extend((0..layout.variables.len() - 4).map(|i| aux & (1 << i) != 0)); + let satisfies = layout.clauses.iter().all(|clause| { + let values = clause + .literals + .map(|l| extended[l.unsigned_abs() as usize - 1] == (l > 0)); + values.iter().any(|&v| v) && values.iter().any(|&v| !v) + }); + extendible |= satisfies; + } + assert_eq!(extendible, source.evaluate(&assignment).unwrap().0); + if extendible { + let target = reduction.construct_target_solution(&assignment); + let recovered = reduction + .recover_result( + &source, + SolveOutcome::optimal(reduction.target_problem(), target).unwrap(), + ) + .unwrap() + .into_solution() + .unwrap(); + assert_eq!(recovered, assignment); + } + } + } } #[cfg(feature = "example-db")] diff --git a/src/unit_tests/rules/setsplitting_betweenness.rs b/src/unit_tests/rules/setsplitting_betweenness.rs index 74e3952da..9d9b23030 100644 --- a/src/unit_tests/rules/setsplitting_betweenness.rs +++ b/src/unit_tests/rules/setsplitting_betweenness.rs @@ -32,6 +32,31 @@ fn test_setsplitting_to_betweenness_closed_loop() { ); } +#[test] +fn repeated_elements_obey_set_semantics_including_singletons() { + for subset in [ + vec![0, 0, 1], + vec![1, 0, 1, 0, 1], + vec![0, 0], + vec![1, 1, 1, 1], + ] { + let source = SetSplitting::new(2, vec![subset]); + let reduction = ReduceTo::::reduce_to(&source).unwrap(); + if BruteForce::new().solve(&source).unwrap().is_some() { + assert_satisfaction_round_trip_from_satisfaction_target( + &source, + &reduction, + "set multiplicity", + ); + } else { + assert!(BruteForce::new() + .solve(reduction.target_problem()) + .unwrap() + .is_none()); + } + } +} + #[test] fn test_setsplitting_to_betweenness_issue_yes_instance_structure() { let source = issue_yes_instance(); diff --git a/src/unit_tests/rules/threedimensionalmatching_threepartition.rs b/src/unit_tests/rules/threedimensionalmatching_threepartition.rs index 99902e069..e254be13a 100644 --- a/src/unit_tests/rules/threedimensionalmatching_threepartition.rs +++ b/src/unit_tests/rules/threedimensionalmatching_threepartition.rs @@ -39,6 +39,48 @@ fn test_threedimensionalmatching_to_threepartition_q1_overhead_and_bounds() { .all(|&size| 4 * i128::from(size) > bound && 2 * i128::from(size) < bound)); } +#[test] +fn empty_triple_sets_preserve_empty_and_nonempty_universe_truth() { + let entry = inventory::iter:: + .into_iter() + .find(|e| { + e.source_name == ThreeDimensionalMatching::NAME && e.target_name == ThreePartition::NAME + }) + .unwrap(); + let contract = entry.parameter_contract().unwrap(); + let transform = contract.transform().unwrap(); + for q in [0, 1] { + let (source, reduction) = reduce(q, &[]); + let bound = transform.evaluate(&source.parameters()).unwrap(); + assert!( + reduction.target_problem().num_elements() as u64 <= bound.get("num_elements").unwrap() + ); + assert!(reduction.target_problem().num_groups() as u64 <= bound.get("num_groups").unwrap()); + if q == 0 { + crate::rules::test_helpers::assert_satisfaction_round_trip_from_satisfaction_target( + &source, + &reduction, + "empty matching domain", + ); + } else { + assert!(BruteForce::new().solve(&source).unwrap().is_none()); + assert!(BruteForce::new() + .solve(reduction.target_problem()) + .unwrap() + .is_none()); + } + } +} + +#[test] +fn target_sum_overflow_is_a_construction_error() { + let source = ThreeDimensionalMatching::new(20, (0..20).map(|i| (i, i, i)).collect()); + assert!(matches!( + ReduceTo::::reduce_to(&source), + Err(crate::rules::ReductionError::Construction { .. }) + )); +} + #[test] fn test_threedimensionalmatching_to_threepartition_q2_overhead_matches_vector() { let (_source, reduction) = reduce(2, &[(0, 0, 0), (1, 1, 1)]); From 385170d0d2339488719348bfc93accc01f41e2f6 Mon Sep 17 00:00:00 2001 From: Xiwei Pan Date: Thu, 17 Sep 2026 13:59:26 +0800 Subject: [PATCH 16/42] Validate optional evaluation values in external solve results --- docs/src/cli-commands.md | 8 +- docs/src/design.md | 8 +- problemreductions-cli/src/commands/extract.rs | 4 +- problemreductions-cli/src/dispatch.rs | 14 +--- problemreductions-cli/tests/cli_tests.rs | 77 +++++++++++++------ src/rules/graph.rs | 4 +- src/rules/traits.rs | 46 +++++++---- src/solvers/mod.rs | 4 +- src/solvers/outcome.rs | 59 ++++++++++++++ src/unit_tests/example_db.rs | 21 +++-- src/unit_tests/rules/graph.rs | 5 +- src/unit_tests/rules/maximumsetpacking_ilp.rs | 8 +- ...mumdiscreteplanarinversekinematics_qubo.rs | 5 +- ...inimumvertexcover_minimumfeedbackarcset.rs | 5 +- .../rules/satisfiability_naesatisfiability.rs | 5 +- src/unit_tests/rules/traits.rs | 76 +++++++++++++++--- .../rules/travelingsalesman_qubo.rs | 5 +- src/unit_tests/solvers/outcome.rs | 38 +++++++++ 18 files changed, 284 insertions(+), 108 deletions(-) diff --git a/docs/src/cli-commands.md b/docs/src/cli-commands.md index 3775afda9..bb21b52da 100644 --- a/docs/src/cli-commands.md +++ b/docs/src/cli-commands.md @@ -105,7 +105,13 @@ The bundle contains the source instance, the target instance, and the variant-le The example shape assumes a Boolean target solution; use the actual target's solution representation. Use `feasible` when optimality is not established, or `{"status":"infeasible"}` when the target solver proves infeasibility. -The command checks the target witness and recomputes its evaluation. Insufficient +The command checks the target witness and computes its evaluation. `evaluation` +may be omitted; if provided, it must be a correctly formatted string matching the +computed value. Integer and Boolean values are compared exactly. Floating-point +values match when `abs(reported - computed) <= 1e-9 * max(1, abs(reported), abs(computed))`. +Malformed or mismatched values (including `null`) are errors. An `infeasible` +result must not include `evaluation`. Results with a solution always include the +model-computed evaluation in the output. This check does not prove optimality. Insufficient candidate quality is an error, not a source NO answer. ## Solve diff --git a/docs/src/design.md b/docs/src/design.md index 76eaa52a4..ea9e3aa27 100644 --- a/docs/src/design.md +++ b/docs/src/design.md @@ -144,8 +144,12 @@ becomes a mathematical result at the terminal boundary. Only the typed that method's solution-returning contract. `ReductionChain::recover_result_json()` returns `(source_result, target_result)`. -It decodes and evaluates the external target once, then retains the model-computed -target evaluation for output while recovering the source. CLI callers use both +It accepts target-result JSON with an optional `evaluation`, decodes and evaluates +the target once, and rejects any supplied evaluation that does not match the model. +Integer and Boolean comparisons are exact; floating-point comparisons use +`abs(reported - computed) <= 1e-9 * max(1, abs(reported), abs(computed))`. +An `infeasible` result must not contain `evaluation`. +It retains the model-computed target evaluation for output while recovering the source. CLI callers use both returned results; they do not independently evaluate the external candidate. For example, an independent set of size 2 in a four-vertex graph maps to a diff --git a/problemreductions-cli/src/commands/extract.rs b/problemreductions-cli/src/commands/extract.rs index 223c06950..18f094c42 100644 --- a/problemreductions-cli/src/commands/extract.rs +++ b/problemreductions-cli/src/commands/extract.rs @@ -1,14 +1,14 @@ use crate::dispatch::{read_input, BundleReplay, ReductionBundle}; use crate::output::OutputConfig; use anyhow::{Context, Result}; -use problemreductions::solvers::{SolveOutcome, SolverExecution}; +use problemreductions::solvers::SolverExecution; use std::path::Path; /// Recover the source result from an external solver's explicit target result. pub fn extract(input: &Path, result_path: &Path, out: &OutputConfig) -> Result<()> { let bundle: ReductionBundle = serde_json::from_str(&read_input(input)?) .context("pred extract requires a reduction bundle produced by pred reduce")?; - let target: SolveOutcome = serde_json::from_str(&read_input(result_path)?) + let target: serde_json::Value = serde_json::from_str(&read_input(result_path)?) .context("Target result must declare optimal, feasible, or infeasible status")?; let replay = BundleReplay::prepare(&bundle)?; let result = replay.recover_result(target, SolverExecution::External)?; diff --git a/problemreductions-cli/src/dispatch.rs b/problemreductions-cli/src/dispatch.rs index f2e9b51f6..3a1386655 100644 --- a/problemreductions-cli/src/dispatch.rs +++ b/problemreductions-cli/src/dispatch.rs @@ -295,7 +295,7 @@ impl BundleReplay { /// Recover an externally supplied or internally solved target result. pub(crate) fn recover_result( &self, - target_outcome: SolveOutcome, + target_outcome: serde_json::Value, solver: SolverExecution, ) -> Result { let (source_outcome, target_outcome) = self @@ -312,7 +312,7 @@ impl BundleReplay { pub(crate) fn solve(&self, request: SolverRequest) -> Result { let result = self.target.solve(request)?; - self.recover_result(result.outcome, result.solver) + self.recover_result(serde_json::to_value(result.outcome)?, result.solver) } } @@ -444,10 +444,7 @@ mod tests { assert_eq!( replay .recover_result( - SolveOutcome::Optimal { - solution: target, - evaluation: String::new() - }, + json!({"status": "optimal", "solution": target}), SolverExecution::External ) .unwrap() @@ -536,10 +533,7 @@ mod tests { assert_eq!( replay .recover_result( - SolveOutcome::Optimal { - solution: json!([true, false]), - evaluation: String::new() - }, + json!({"status": "optimal", "solution": [true, false]}), SolverExecution::External ) .unwrap() diff --git a/problemreductions-cli/tests/cli_tests.rs b/problemreductions-cli/tests/cli_tests.rs index f140ca04b..b981a6031 100644 --- a/problemreductions-cli/tests/cli_tests.rs +++ b/problemreductions-cli/tests/cli_tests.rs @@ -9602,10 +9602,7 @@ fn test_extract_roundtrip_mis_to_qubo() { std::env::temp_dir().join("test_extract_roundtrip_mis_to_qubo_target_result.json"); std::fs::write( &result_file, - format!( - r#"{{"status":"optimal","solution":{},"evaluation":""}}"#, - target_cfg - ), + format!(r#"{{"status":"optimal","solution":{}}}"#, target_cfg), ) .unwrap(); let extract_out = pred() @@ -9637,6 +9634,48 @@ fn test_extract_roundtrip_mis_to_qubo() { let expected_target: serde_json::Value = serde_json::from_str(&target_cfg).unwrap(); assert_eq!(json["intermediate"]["solution"], expected_target); + // Our own result JSON is valid input, including its evaluation and metadata. + std::fs::write(&result_file, json["intermediate"].to_string()).unwrap(); + let roundtrip = pred() + .args([ + "--json", + "extract", + bundle_file.to_str().unwrap(), + "--result", + result_file.to_str().unwrap(), + ]) + .output() + .unwrap(); + assert!( + roundtrip.status.success(), + "{}", + String::from_utf8_lossy(&roundtrip.stderr) + ); + let roundtrip: serde_json::Value = serde_json::from_slice(&roundtrip.stdout).unwrap(); + assert_eq!(roundtrip["evaluation"], json["evaluation"]); + + for evaluation in [ + serde_json::json!("Min(999)"), + serde_json::json!("garbage"), + serde_json::Value::Null, + ] { + let mut input = json["intermediate"].clone(); + input["evaluation"] = evaluation; + std::fs::write(&result_file, input.to_string()).unwrap(); + let rejected = pred() + .args([ + "--json", + "extract", + bundle_file.to_str().unwrap(), + "--result", + result_file.to_str().unwrap(), + ]) + .output() + .unwrap(); + assert!(!rejected.status.success()); + assert!(String::from_utf8_lossy(&rejected.stderr).contains("evaluation")); + } + // Source config is over 4 MIS variables and must describe an independent set // whose size matches `expected_source_eval` (e.g. "Max(2)" -> 2 ones). let source_sol: Vec = json["solution"] @@ -9700,7 +9739,7 @@ fn test_extract_decodes_a_qualifying_tour() { std::fs::write( &result_file, format!( - r#"{{"status":"optimal","solution":{},"evaluation":""}}"#, + r#"{{"status":"optimal","solution":{}}}"#, "[true,false,false,false,true,false,false,false,true]" ), ) @@ -9751,7 +9790,7 @@ fn test_extract_rejects_plain_problem_file() { std::fs::write( &result_file, format!( - r#"{{"status":"optimal","solution":{},"evaluation":""}}"#, + r#"{{"status":"optimal","solution":{}}}"#, "[false,true,false]" ), ) @@ -9810,10 +9849,7 @@ fn test_extract_rejects_wrong_config_length() { std::env::temp_dir().join("test_extract_rejects_wrong_config_length_target_result.json"); std::fs::write( &result_file, - format!( - r#"{{"status":"optimal","solution":{},"evaluation":""}}"#, - "[false,true]" - ), + format!(r#"{{"status":"optimal","solution":{}}}"#, "[false,true]"), ) .unwrap(); let extract_out = pred() @@ -9878,10 +9914,7 @@ fn test_extract_rejects_non_boolean_solution_value() { .join("test_extract_rejects_non_boolean_solution_value_target_result.json"); std::fs::write( &result_file, - format!( - r#"{{"status":"optimal","solution":{},"evaluation":""}}"#, - bad_cfg - ), + format!(r#"{{"status":"optimal","solution":{}}}"#, bad_cfg), ) .unwrap(); let extract_out = pred() @@ -9896,7 +9929,7 @@ fn test_extract_rejects_non_boolean_solution_value() { assert!(!extract_out.status.success()); let stderr = String::from_utf8(extract_out.stderr).unwrap(); assert!( - stderr.contains("invalid solution JSON"), + stderr.contains("invalid target result JSON"), "unexpected stderr: {stderr}" ); @@ -9950,7 +9983,7 @@ fn test_extract_rejects_malformed_bundle_path_source_mismatch() { std::fs::write( &result_file, format!( - r#"{{"status":"optimal","solution":{},"evaluation":""}}"#, + r#"{{"status":"optimal","solution":{}}}"#, "[false,true,false]" ), ) @@ -10030,10 +10063,7 @@ fn test_extract_rejects_tampered_target_data() { std::env::temp_dir().join("test_extract_rejects_tampered_target_data_target_result.json"); std::fs::write( &result_file, - format!( - r#"{{"status":"optimal","solution":{},"evaluation":""}}"#, - target_cfg - ), + format!(r#"{{"status":"optimal","solution":{}}}"#, target_cfg), ) .unwrap(); let extract_out = pred() @@ -10119,10 +10149,7 @@ fn test_extract_reads_bundle_from_stdin() { std::env::temp_dir().join("test_extract_reads_bundle_from_stdin_target_result.json"); std::fs::write( &result_file, - format!( - r#"{{"status":"optimal","solution":{},"evaluation":""}}"#, - target_cfg - ), + format!(r#"{{"status":"optimal","solution":{}}}"#, target_cfg), ) .unwrap(); let mut child = pred() @@ -10228,7 +10255,7 @@ fn test_extract_preserves_feasible_status_and_rejects_invalid_witnesses() { std::fs::write( &result_file, serde_json::json!({ - "status":"feasible", "solution":solution, "evaluation":"untrusted external evaluation", + "status":"feasible", "solution":solution, }) .to_string(), ) diff --git a/src/rules/graph.rs b/src/rules/graph.rs index ad79ea1fc..d838a0543 100644 --- a/src/rules/graph.rs +++ b/src/rules/graph.rs @@ -1612,11 +1612,11 @@ impl ReductionChain { /// Recover the source result and return it with the validated target result. /// The returned pair is `(source, target)`; target evaluation is computed once - /// from the model, never trusted from the incoming display string. + /// from the model. An optional incoming evaluation must match that value. pub fn recover_result_json( &self, source: &dyn Any, - target: crate::solvers::SolveOutcome, + target: serde_json::Value, ) -> crate::rules::ExtractionResult<(crate::solvers::SolveOutcome, crate::solvers::SolveOutcome)> { let last = self.steps.last().expect("ReductionChain has no steps"); diff --git a/src/rules/traits.rs b/src/rules/traits.rs index 65e773528..74ab2e3f1 100644 --- a/src/rules/traits.rs +++ b/src/rules/traits.rs @@ -312,7 +312,7 @@ pub trait DynReductionResult { /// Decode and evaluate once, returning the typed result and its canonical JSON. fn target_result_from_json( &self, - target: crate::solvers::SolveOutcome, + target: serde_json::Value, ) -> ExtractionResult<(crate::solvers::ErasedOutcome, crate::solvers::SolveOutcome)>; fn source_result_json( &self, @@ -349,24 +349,42 @@ where fn target_result_from_json( &self, - target: crate::solvers::SolveOutcome, + target: serde_json::Value, ) -> ExtractionResult<(crate::solvers::ErasedOutcome, crate::solvers::SolveOutcome)> { use crate::solvers::SolveOutcome; - // Numeric evaluation is model-owned, not parsed from a display string. - let decode = |solution| { - serde_json::from_value(solution).map_err(|error| { - ExtractionError::invalid(format!("invalid solution JSON: {error}")) - }) - }; - let target = match target { - SolveOutcome::Optimal { solution, .. } => { - SolveOutcome::optimal(self.target_problem(), decode(solution)?)? + #[derive(serde::Deserialize)] + #[serde(tag = "status", rename_all = "snake_case")] + enum Candidate { + Optimal { solution: S }, + Feasible { solution: S }, + Infeasible, + } + let reported = target.get("evaluation").cloned(); + let candidate = serde_json::from_value(target).map_err(|error| { + ExtractionError::invalid(format!("invalid target result JSON: {error}")) + })?; + let target = match candidate { + Candidate::Optimal { solution } => { + SolveOutcome::optimal(self.target_problem(), solution)? } - SolveOutcome::Feasible { solution, .. } => { - SolveOutcome::feasible(self.target_problem(), decode(solution)?)? + Candidate::Feasible { solution } => { + SolveOutcome::feasible(self.target_problem(), solution)? } - SolveOutcome::Infeasible => SolveOutcome::Infeasible, + Candidate::Infeasible => SolveOutcome::Infeasible, }; + if let Some(reported) = reported { + match &target { + SolveOutcome::Optimal { evaluation, .. } + | SolveOutcome::Feasible { evaluation, .. } => { + crate::solvers::check_reported_evaluation(&reported, evaluation)?; + } + SolveOutcome::Infeasible => { + return Err(ExtractionError::invalid( + "infeasible results must not contain evaluation", + )); + } + } + } let target_json = crate::solvers::outcome_to_json(&target)?; Ok((crate::solvers::erase_outcome(target), target_json)) } diff --git a/src/solvers/mod.rs b/src/solvers/mod.rs index 496b7b575..ad6cddf8a 100644 --- a/src/solvers/mod.rs +++ b/src/solvers/mod.rs @@ -7,7 +7,9 @@ mod outcome; mod pipelines; mod registry; mod resolver; -pub(crate) use outcome::{downcast_outcome, erase_outcome, outcome_to_json, ErasedOutcome}; +pub(crate) use outcome::{ + check_reported_evaluation, downcast_outcome, erase_outcome, outcome_to_json, ErasedOutcome, +}; pub use outcome::{ProblemOutcome, SolveOutcome}; pub mod ilp; diff --git a/src/solvers/outcome.rs b/src/solvers/outcome.rs index c0c1677ff..7bcb096c6 100644 --- a/src/solvers/outcome.rs +++ b/src/solvers/outcome.rs @@ -17,6 +17,65 @@ pub enum SolveOutcome { /// A result retaining the model's concrete solution and value types. pub type ProblemOutcome

= SolveOutcome<

::Solution,

::Value>; +/// Check the optional display-form evaluation at the external JSON boundary. +pub(crate) fn check_reported_evaluation( + reported: &serde_json::Value, + actual: &V, +) -> crate::rules::ExtractionResult<()> { + use crate::types::{Extremum, Max, Min}; + use std::any::TypeId; + + let expected = actual.to_string(); + let matches = reported.as_str().is_some_and(|reported| { + if reported == expected { + return true; + } + let Some((wrapper, expected_value)) = expected.split_once('(') else { + return false; + }; + let Some(value) = reported + .strip_prefix(wrapper) + .and_then(|s| s.strip_prefix('(')) + .and_then(|s| s.strip_suffix(')')) + else { + return false; + }; + let expected_value = expected_value.trim_end_matches(')'); + if [ + TypeId::of::>(), + TypeId::of::>(), + TypeId::of::>(), + ] + .contains(&TypeId::of::()) + { + let (Ok(value), Ok(expected_value)) = + (value.parse::(), expected_value.parse::()) + else { + return false; + }; + // Absolute and relative tolerances apply only to floating-point evaluations. + value.is_finite() + && expected_value.is_finite() + && (value - expected_value).abs() + <= 1e-9 * value.abs().max(expected_value.abs()).max(1.0) + } else { + match ( + value.parse::(), + expected_value.parse::(), + ) { + (Ok(value), Ok(expected_value)) => value == expected_value, + _ => false, + } + } + }); + if !matches { + return Err(crate::rules::ExtractionError::invalid(format!( + "invalid or mismatched evaluation: received {reported}, expected {expected}" + ))); + } + Ok(()) +} + impl SolveOutcome { /// Evaluate and validate a candidate whose optimality is established by the caller. /// diff --git a/src/unit_tests/example_db.rs b/src/unit_tests/example_db.rs index 8b077d7d1..a908e2ef7 100644 --- a/src/unit_tests/example_db.rs +++ b/src/unit_tests/example_db.rs @@ -710,10 +710,7 @@ fn rule_specs_solution_pairs_are_consistent() { let extracted = chain .recover_result_json( source.as_any(), - SolveOutcome::Optimal { - solution: pair.target_config.clone(), - evaluation: target_eval.0.clone(), - }, + serde_json::json!({"status": "optimal", "solution": pair.target_config, "evaluation": target_eval.0}), ) .map(|(outcome, _)| outcome.into_solution().unwrap()) .unwrap(); @@ -730,15 +727,18 @@ fn rule_specs_solution_pairs_are_consistent() { assert_eq!( chain - .recover_result_json(source.as_any(), SolveOutcome::Infeasible) + .recover_result_json( + source.as_any(), + serde_json::json!({"status": "infeasible"}) + ) .unwrap() .0, SolveOutcome::Infeasible, "Rule {label}: target infeasibility must propagate" ); - match chain.recover_result_json(source.as_any(), SolveOutcome::Feasible { - solution: pair.target_config.clone(), evaluation: target_eval.0.clone(), - }) { + match chain.recover_result_json(source.as_any(), serde_json::json!({ + "status": "feasible", "solution": pair.target_config, "evaluation": target_eval.0, + })) { Ok((SolveOutcome::Feasible { solution, evaluation }, _)) => { let (actual, valid) = source.evaluate_dyn(&solution).unwrap(); assert!(valid, "Rule {label}: feasible recovery returned an invalid source witness"); @@ -753,10 +753,7 @@ fn rule_specs_solution_pairs_are_consistent() { chain .recover_result_json( source.as_any(), - SolveOutcome::Optimal { - solution: malformed, - evaluation: String::new() - } + serde_json::json!({"status": "optimal", "solution": malformed}) ) .is_err(), "Rule {label}: extraction accepted malformed target-solution JSON" diff --git a/src/unit_tests/rules/graph.rs b/src/unit_tests/rules/graph.rs index 5f768be7e..12d0d7831 100644 --- a/src/unit_tests/rules/graph.rs +++ b/src/unit_tests/rules/graph.rs @@ -2202,10 +2202,7 @@ fn composed_witness_agrees_across_direct_chain_path_and_json() { chain .recover_result_json( &source, - SolveOutcome::Optimal { - solution: json!(target_solution), - evaluation: String::new(), - } + json!({"status": "optimal", "solution": target_solution}) ) .unwrap() .0, diff --git a/src/unit_tests/rules/maximumsetpacking_ilp.rs b/src/unit_tests/rules/maximumsetpacking_ilp.rs index 63d5f351e..0ed721d9f 100644 --- a/src/unit_tests/rules/maximumsetpacking_ilp.rs +++ b/src/unit_tests/rules/maximumsetpacking_ilp.rs @@ -231,13 +231,7 @@ fn extraction_maps_feasible_witnesses_through_typed_and_dynamic_paths() { ); assert_eq!( chain - .recover_result_json( - &source, - SolveOutcome::Feasible { - solution: json!([0]), - evaluation: String::new(), - } - ) + .recover_result_json(&source, json!({"status": "feasible", "solution": [0]})) .unwrap() .0, SolveOutcome::Feasible { diff --git a/src/unit_tests/rules/minimumdiscreteplanarinversekinematics_qubo.rs b/src/unit_tests/rules/minimumdiscreteplanarinversekinematics_qubo.rs index ef4b4fd82..d8eac3741 100644 --- a/src/unit_tests/rules/minimumdiscreteplanarinversekinematics_qubo.rs +++ b/src/unit_tests/rules/minimumdiscreteplanarinversekinematics_qubo.rs @@ -181,10 +181,7 @@ fn optimum_energy_recovers_distance_and_infeasibility() { let completed = chain .recover_result_json( &source, - SolveOutcome::Optimal { - solution: serde_json::to_value(&solution).unwrap(), - evaluation: String::new(), - }, + serde_json::json!({"status": "optimal", "solution": solution}), ) .unwrap() .0; diff --git a/src/unit_tests/rules/minimumvertexcover_minimumfeedbackarcset.rs b/src/unit_tests/rules/minimumvertexcover_minimumfeedbackarcset.rs index b1e44c93a..391f3ac36 100644 --- a/src/unit_tests/rules/minimumvertexcover_minimumfeedbackarcset.rs +++ b/src/unit_tests/rules/minimumvertexcover_minimumfeedbackarcset.rs @@ -200,10 +200,7 @@ fn feasible_feedback_arc_set_does_not_establish_a_vertex_cover() { )); // The external JSON boundary must report the same rule-level failure. let target = reduction - .target_result_from_json(SolveOutcome::Feasible { - solution: serde_json::json!(candidate), - evaluation: String::new(), - }) + .target_result_from_json(serde_json::json!({"status": "feasible", "solution": candidate})) .unwrap() .0; assert!(matches!( diff --git a/src/unit_tests/rules/satisfiability_naesatisfiability.rs b/src/unit_tests/rules/satisfiability_naesatisfiability.rs index 30d384507..efd72e406 100644 --- a/src/unit_tests/rules/satisfiability_naesatisfiability.rs +++ b/src/unit_tests/rules/satisfiability_naesatisfiability.rs @@ -117,10 +117,7 @@ fn test_solution_extraction_distinguishes_zero_assignment_from_malformed_input() )); assert!(crate::rules::DynReductionResult::target_result_from_json( &reduction, - SolveOutcome::Optimal { - solution: serde_json::json!([false, 2, false]), - evaluation: String::new() - } + serde_json::json!({"status": "optimal", "solution": [false, 2, false]}) ) .is_err()); } diff --git a/src/unit_tests/rules/traits.rs b/src/unit_tests/rules/traits.rs index 71ebfa6f0..afe31fd6c 100644 --- a/src/unit_tests/rules/traits.rs +++ b/src/unit_tests/rules/traits.rs @@ -231,10 +231,7 @@ fn aggregate_value_from_solution_keeps_evaluation_errors_distinct_from_false() { let step = (edge.reduce_fn.unwrap())(&source).unwrap(); let target = step .witness - .target_result_from_json(SolveOutcome::Optimal { - solution: json!([true, false]), - evaluation: String::new(), - }) + .target_result_from_json(json!({"status": "optimal", "solution": [true, false]})) .unwrap() .0; assert!(matches!( @@ -242,10 +239,8 @@ fn aggregate_value_from_solution_keeps_evaluation_errors_distinct_from_false() { SolveOutcome::Infeasible )); assert!(matches!( - step.witness.target_result_from_json(SolveOutcome::Optimal { - solution: json!([true]), - evaluation: String::new(), - }), + step.witness + .target_result_from_json(json!({"status": "optimal", "solution": [true]})), Err(ExtractionError::Evaluation(_)) )); assert!(matches!( @@ -398,10 +393,9 @@ fn test_dyn_aggregate_reduction_result_extracts_value() { .is_some()); TARGET_EVALUATIONS.with(|count| count.set(0)); let (target, target_json) = dyn_result - .target_result_from_json(SolveOutcome::Optimal { - solution: json!([7]), - evaluation: "untrusted external evaluation".into(), - }) + .target_result_from_json( + json!({"status": "optimal", "solution": [7], "evaluation": "Min(7)"}), + ) .unwrap(); assert_eq!( target_json, @@ -423,3 +417,61 @@ fn test_dyn_aggregate_reduction_result_extracts_value() { } ); } + +#[test] +fn external_evaluation_is_optional_but_must_match_when_present() { + let result = TestAggregateReduction { + target: AggregateTargetProblem, + offset: 2, + }; + for status in ["optimal", "feasible"] { + let input = json!({"status": status, "solution": [7]}); + for evaluation in [None, Some(json!("Min(7)")), Some(json!("Min(+007)"))] { + let mut input = input.clone(); + if let Some(evaluation) = evaluation { + input["evaluation"] = evaluation; + } + TARGET_EVALUATIONS.with(|count| count.set(0)); + let (_, output) = result.target_result_from_json(input).unwrap(); + assert_eq!(TARGET_EVALUATIONS.with(|count| count.get()), 1); + assert_eq!( + serde_json::to_value(output).unwrap(), + json!({"status": status, "solution": [7], "evaluation": "Min(7)"}) + ); + } + for evaluation in [ + json!("Min(8)"), + json!("Max(7)"), + json!("garbage"), + json!(""), + json!(null), + json!(7), + json!({"Min": 7}), + ] { + let mut input = input.clone(); + input["evaluation"] = evaluation; + let error = result + .target_result_from_json(input) + .err() + .unwrap() + .to_string(); + assert!(error.contains("evaluation"), "{error}"); + assert!(error.contains("Min(7)"), "{error}"); + } + } + assert!(matches!( + result + .target_result_from_json(json!({"status": "infeasible"})) + .unwrap() + .1, + SolveOutcome::Infeasible + )); + for evaluation in [json!(null), json!("Min(7)")] { + let error = result + .target_result_from_json(json!({"status": "infeasible", "evaluation": evaluation})) + .err() + .unwrap() + .to_string(); + assert!(error.contains("must not contain evaluation"), "{error}"); + } +} diff --git a/src/unit_tests/rules/travelingsalesman_qubo.rs b/src/unit_tests/rules/travelingsalesman_qubo.rs index 685911a25..eeafdede2 100644 --- a/src/unit_tests/rules/travelingsalesman_qubo.rs +++ b/src/unit_tests/rules/travelingsalesman_qubo.rs @@ -150,10 +150,7 @@ fn signed_and_small_tours_recover_all_optima_or_infeasibility() { let completed = chain .recover_result_json( &source, - SolveOutcome::Optimal { - solution: serde_json::to_value(&solution).unwrap(), - evaluation: String::new(), - }, + serde_json::json!({"status": "optimal", "solution": solution}), ) .unwrap() .0; diff --git a/src/unit_tests/solvers/outcome.rs b/src/unit_tests/solvers/outcome.rs index 5bae0e1ca..be105d9f9 100644 --- a/src/unit_tests/solvers/outcome.rs +++ b/src/unit_tests/solvers/outcome.rs @@ -3,6 +3,44 @@ use crate::traits::{EvaluationError, EvaluationValue, Problem}; use crate::types::{Extremum, Max, Min, Or, ProblemParameters}; use std::cell::Cell; +#[test] +fn reported_evaluations_compare_numeric_values_without_losing_integer_precision() { + use super::check_reported_evaluation; + use serde_json::json; + + for reported in ["Min(2)", "Min(2.0)", "Min(2e0)", "Min(2.000000001)"] { + assert!(check_reported_evaluation(&json!(reported), &Min(Some(2.0f64))).is_ok()); + } + for reported in [ + "Min(2.001)", + "Max(2)", + "Min(NaN)", + "Min(inf)", + "Min(abc)", + "Min(2", + "Min2)", + ] { + assert!(check_reported_evaluation(&json!(reported), &Min(Some(2.0f64))).is_err()); + } + assert!(check_reported_evaluation(&json!("Min(0.0000000005)"), &Min(Some(0.0f64))).is_ok()); + assert!(check_reported_evaluation(&json!("Max(2e0)"), &Max(Some(2.0f64))).is_ok()); + assert!( + check_reported_evaluation(&json!("Min(2e0)"), &Extremum::minimize(Some(2.0f64))).is_ok() + ); + assert!( + check_reported_evaluation(&json!("Max(2e0)"), &Extremum::maximize(Some(2.0f64))).is_ok() + ); + assert!(check_reported_evaluation(&json!("Min(+002)"), &Min(Some(2i64))).is_ok()); + assert!(check_reported_evaluation(&json!("Min(3)"), &Min(Some(2i64))).is_err()); + assert!(check_reported_evaluation( + &json!("Min(9007199254740992)"), + &Min(Some(9007199254740993i64)) + ) + .is_err()); + assert!(check_reported_evaluation(&json!("Or(true)"), &Or(true)).is_ok()); + assert!(check_reported_evaluation(&json!("Or(false)"), &Or(true)).is_err()); +} + #[derive(Clone)] struct Evaluated { value: Result, From ae2c0b030d9fe33184d3fa71707fa5631f4ad936 Mon Sep 17 00:00:00 2001 From: Xiwei Pan Date: Thu, 17 Sep 2026 15:51:41 +0800 Subject: [PATCH 17/42] Keep Turing reductions as theory-only graph relations --- .claude/CLAUDE.md | 2 +- docs/paper/reductions.typ | 2 +- docs/src/cli-commands.md | 3 + docs/src/design.md | 7 +- src/models/decision.rs | 3 +- src/rules/graph.rs | 1 + src/rules/registry.rs | 3 +- src/solvers/decision_search.rs | 140 ---------------------- src/solvers/mod.rs | 1 - src/unit_tests/rules/graph.rs | 6 +- src/unit_tests/solvers/decision_search.rs | 78 ------------ 11 files changed, 17 insertions(+), 229 deletions(-) delete mode 100644 src/solvers/decision_search.rs delete mode 100644 src/unit_tests/solvers/decision_search.rs diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 8628e8f35..fc5bda591 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -107,7 +107,7 @@ make papers-pull # Pull PDFs from shared remote - Run `pred list` for the full catalog of problems, variants, and reductions; `pred show ` for details on a specific problem - `src/rules/` - Reduction rules + inventory registration - `src/models/decision.rs` - Generic `Decision

` wrapper converting optimization problems to decision problems -- `src/solvers/` - BruteForce reference solver returning problem solutions, ILP solver, decision search (binary search via Decision queries), and the exact-variant solver capability registry. Solver dispatch uses only registered customized implementations and fixed ILP pipelines; reduction-graph reachability does not imply solver availability. Run `pred inspect ` to see the registered capabilities for that instance. +- `src/solvers/` - BruteForce reference solver returning problem solutions, ILP solver, and the exact-variant solver capability registry. Solver dispatch uses only registered customized implementations and fixed ILP pipelines; reduction-graph reachability does not imply solver availability. Turing edges are theoretical graph metadata, not executable reductions. Run `pred inspect ` to see the registered capabilities for that instance. - `src/traits.rs` - `Problem` trait - `src/rules/traits.rs` - `ReduceTo` and mandatory `ReductionResult::recover_result` - `src/registry/` - Compile-time reduction metadata collection diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index 2cc0f27e8..ec9854756 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -742,7 +742,7 @@ = Introduction -A _reduction_ from problem $A$ to problem $B$, denoted $A arrow.long B$, is a polynomial-time transformation of $A$-instances into $B$-instances such that: (1) the transformation runs in polynomial time, (2) solutions to $B$ can be efficiently mapped back to solutions of $A$, and (3) optimal solutions are preserved. The library implements #graph-data.edges.len() catalogued edges connecting #graph-data.nodes.len() problem types; most are solver-executable witness, aggregate, or Turing reductions, while a few are proof-only NP-hardness embeddings that are excluded from runtime path search. +A _reduction_ from problem $A$ to problem $B$, denoted $A arrow.long B$, is a polynomial-time transformation of $A$-instances into $B$-instances such that: (1) the transformation runs in polynomial time, (2) solutions to $B$ can be efficiently mapped back to solutions of $A$, and (3) optimal solutions are preserved. The library catalogues #graph-data.edges.len() edges connecting #graph-data.nodes.len() problem types. Executable reductions construct a target instance and recover a source result. The graph also retains proof-only NP-hardness embeddings and theoretical Turing relations; both are excluded from runtime path search. Turing relations require multiple adaptive queries and have no executor in the library. == Notation diff --git a/docs/src/cli-commands.md b/docs/src/cli-commands.md index bb21b52da..7134eda0a 100644 --- a/docs/src/cli-commands.md +++ b/docs/src/cli-commands.md @@ -89,6 +89,9 @@ For a problem file, JSON inspection includes `parameter_values`, the model's act ## Reduce +`pred path` searches only executable reductions. The graph also records theoretical +Turing relations requiring multiple queries; these are not executable paths. + ```bash pred path MIS QUBO --json -o paths.json python3 -c 'import json; print(json.dumps(json.load(open("paths.json"))["paths"][0]))' > path.json diff --git a/docs/src/design.md b/docs/src/design.md index ea9e3aa27..3b7dada76 100644 --- a/docs/src/design.md +++ b/docs/src/design.md @@ -200,9 +200,10 @@ Guarantees must cover every qualifying witness, including tied optima. `SolutionAggregate` remains a brute-force solver capability for selecting from an enumeration. Mathematical wrappers such as `Min`, `Max`, `Or`, and `Sum` remain model values. They do not require separate reduction traits or graph -modes. Turing reductions describe adaptive queries and remain a separate -execution capability; exact recovery alone does not imply approximation or -counting preservation. +modes. Turing edges describe multiple adaptive queries and are retained only as +theoretical graph relationships, not executable reductions. The library does not +provide a Turing reduction solver. Default path search and execution exclude these +edges. Exact recovery alone does not imply approximation or counting preservation. ### Arithmetic diff --git a/src/models/decision.rs b/src/models/decision.rs index ba9e3255a..b732d4ec8 100644 --- a/src/models/decision.rs +++ b/src/models/decision.rs @@ -105,7 +105,8 @@ macro_rules! register_decision_variant { } } - // Reverse edge: P → Decision

(Turing/multi-query reduction via binary search) + // Theory-only reverse edge: P → Decision

requires multiple queries. + // Retained for graph display; there is no Turing reduction executor. $crate::inventory::submit! { $crate::rules::ReductionEntry { source_name: <$inner as $crate::traits::Problem>::NAME, diff --git a/src/rules/graph.rs b/src/rules/graph.rs index d838a0543..fe00ebeb5 100644 --- a/src/rules/graph.rs +++ b/src/rules/graph.rs @@ -327,6 +327,7 @@ pub enum ReductionMode { Witness, /// Multi-query (Turing) reductions: solving the source requires multiple /// adaptive queries to the target (e.g., binary search over a bound). + /// Queries theoretical graph relationships only; these paths are not executable. Turing, } diff --git a/src/rules/registry.rs b/src/rules/registry.rs index 4e5bd9240..af9008057 100644 --- a/src/rules/registry.rs +++ b/src/rules/registry.rs @@ -141,12 +141,13 @@ pub struct ExecutedStep { /// Witness/config reduction executor stored in the inventory. pub type ReduceFn = fn(&dyn Any) -> Result; -/// Execution capabilities carried by a reduction edge. +/// Executability and theoretical multi-query relations carried by a reduction edge. #[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub struct EdgeCapabilities { pub witness: bool, /// Turing (multi-query) reduction: solving the source requires multiple /// adaptive queries to the target (e.g., binary search over a decision bound). + /// This is graph metadata only; the library does not execute Turing reductions. #[serde(default)] pub turing: bool, } diff --git a/src/solvers/decision_search.rs b/src/solvers/decision_search.rs deleted file mode 100644 index 7c38fe6ec..000000000 --- a/src/solvers/decision_search.rs +++ /dev/null @@ -1,140 +0,0 @@ -//! Decision-guided binary search for optimization via decision queries. - -use crate::models::decision::{Decision, DecisionProblemMeta}; -use crate::solvers::BruteForce; -use crate::traits::Problem; -use crate::types::{Max, Min, OptimizationValue, Or}; -use serde::de::DeserializeOwned; -use serde::Serialize; -use std::fmt; - -/// Whether a decision problem has at least one satisfying configuration. -fn is_satisfiable

(problem: &P) -> Result -where - P: Problem + 'static, - P::Solution: 'static, -{ - Ok(BruteForce::new().solve(problem)?.is_some()) -} - -fn solve_via_decision_min

( - problem: &P, - lower: i64, - upper: i64, -) -> Result, crate::solvers::SolveError> -where - P: DecisionProblemMeta + Problem> + Clone + 'static, - P::Solution: 'static, -{ - if lower > upper { - return Ok(None); - } - - if !is_satisfiable(&Decision::new(problem.clone(), upper))? { - return Ok(None); - } - - let mut lo = lower; - let mut hi = upper; - while lo < hi { - let mid = lo + (hi - lo) / 2; - if is_satisfiable(&Decision::new(problem.clone(), mid))? { - hi = mid; - } else { - lo = mid + 1; - } - } - - Ok(Some(lo)) -} - -fn solve_via_decision_max

( - problem: &P, - lower: i64, - upper: i64, -) -> Result, crate::solvers::SolveError> -where - P: DecisionProblemMeta + Problem> + Clone + 'static, - P::Solution: 'static, -{ - if lower > upper { - return Ok(None); - } - - if !is_satisfiable(&Decision::new(problem.clone(), lower))? { - return Ok(None); - } - - let mut lo = lower; - let mut hi = upper; - while lo < hi { - let mid = lo + (hi - lo + 1) / 2; - if is_satisfiable(&Decision::new(problem.clone(), mid))? { - lo = mid; - } else { - hi = mid - 1; - } - } - - Ok(Some(lo)) -} - -#[doc(hidden)] -pub trait DecisionSearchValue: - OptimizationValue + Clone + fmt::Debug + Serialize + DeserializeOwned -{ - fn solve_problem

( - problem: &P, - lower: i64, - upper: i64, - ) -> Result, crate::solvers::SolveError> - where - P: DecisionProblemMeta + Problem + Clone + 'static, - P::Solution: 'static; -} - -impl DecisionSearchValue for Min { - fn solve_problem

( - problem: &P, - lower: i64, - upper: i64, - ) -> Result, crate::solvers::SolveError> - where - P: DecisionProblemMeta + Problem + Clone + 'static, - P::Solution: 'static, - { - solve_via_decision_min(problem, lower, upper) - } -} - -impl DecisionSearchValue for Max { - fn solve_problem

( - problem: &P, - lower: i64, - upper: i64, - ) -> Result, crate::solvers::SolveError> - where - P: DecisionProblemMeta + Problem + Clone + 'static, - P::Solution: 'static, - { - solve_via_decision_max(problem, lower, upper) - } -} - -/// Recover an optimization value by querying the problem's decision wrapper. -pub fn solve_via_decision

( - problem: &P, - lower: i64, - upper: i64, -) -> Result, crate::solvers::SolveError> -where - P: DecisionProblemMeta + Clone + 'static, - P::Solution: 'static, - P::Value: DecisionSearchValue, -{ - ::solve_problem(problem, lower, upper) -} - -#[cfg(test)] -#[path = "../unit_tests/solvers/decision_search.rs"] -mod tests; diff --git a/src/solvers/mod.rs b/src/solvers/mod.rs index ad6cddf8a..a280d790b 100644 --- a/src/solvers/mod.rs +++ b/src/solvers/mod.rs @@ -2,7 +2,6 @@ mod brute_force; pub(crate) mod customized; -pub mod decision_search; mod outcome; mod pipelines; mod registry; diff --git a/src/unit_tests/rules/graph.rs b/src/unit_tests/rules/graph.rs index 12d0d7831..7d105b259 100644 --- a/src/unit_tests/rules/graph.rs +++ b/src/unit_tests/rules/graph.rs @@ -859,7 +859,7 @@ fn test_aggregate_reduction_chain_extracts_value_backwards() { } #[test] -fn witness_path_search_rejects_turing_only_edge() { +fn default_path_search_rejects_turing_only_edge() { let source_variant = BTreeMap::new(); let target_variant = BTreeMap::new(); let graph = build_two_node_graph( @@ -875,12 +875,12 @@ fn witness_path_search_rejects_turing_only_edge() { ); assert!(graph - .find_all_paths_mode( + .find_paths_up_to( AggregateChainSource::NAME, &source_variant, AggregateChainMiddle::NAME, &target_variant, - ReductionMode::Witness + 1, ) .is_empty()); assert!(!graph diff --git a/src/unit_tests/solvers/decision_search.rs b/src/unit_tests/solvers/decision_search.rs deleted file mode 100644 index 1a56f00de..000000000 --- a/src/unit_tests/solvers/decision_search.rs +++ /dev/null @@ -1,78 +0,0 @@ -use super::*; -use crate::models::graph::{MaximumIndependentSet, MinimumVertexCover}; -use crate::solvers::BruteForce; -use crate::topology::SimpleGraph; -use crate::types::{Max, Min}; - -#[test] -fn test_decision_search_min() { - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); - let problem = MinimumVertexCover::new(graph, vec![1i64; 3]); - - assert_eq!(solve_via_decision(&problem, 0, 3).unwrap(), Some(1)); -} - -#[test] -fn test_decision_search_max() { - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); - let problem = MaximumIndependentSet::new(graph, vec![1i64; 3]); - - assert_eq!(solve_via_decision(&problem, 0, 3).unwrap(), Some(2)); -} - -#[test] -fn test_decision_search_matches_brute_force() { - let graph = SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4), (4, 0)]); - let problem = MinimumVertexCover::new(graph, vec![1i64; 5]); - - let solution = BruteForce::new().solve(&problem).unwrap().unwrap(); - let brute_force_value = problem.evaluate(&solution).unwrap(); - - assert_eq!( - solve_via_decision(&problem, 0, 5).unwrap(), - brute_force_value.size().copied() - ); -} - -#[test] -fn test_decision_search_min_returns_none_when_upper_bound_is_too_small() { - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); - let problem = MinimumVertexCover::new(graph, vec![1i64; 3]); - - assert_eq!(solve_via_decision(&problem, 0, 0).unwrap(), None); -} - -#[test] -fn test_decision_search_max_returns_none_when_interval_is_above_optimum() { - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); - let problem = MaximumIndependentSet::new(graph, vec![1i64; 3]); - - assert_eq!(solve_via_decision(&problem, 3, 4).unwrap(), None); -} - -#[test] -fn test_decision_search_invalid_interval_returns_none() { - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); - let min_problem = MinimumVertexCover::new(graph.clone(), vec![1i64; 3]); - let max_problem = MaximumIndependentSet::new(graph, vec![1i64; 3]); - - assert_eq!(solve_via_decision(&min_problem, 2, 1).unwrap(), None); - assert_eq!(solve_via_decision(&max_problem, 2, 1).unwrap(), None); -} - -#[test] -fn test_decision_search_preserves_value_direction() { - let graph = SimpleGraph::new(3, vec![(0, 1), (1, 2)]); - let min_problem = MinimumVertexCover::new(graph.clone(), vec![1i64; 3]); - let max_problem = MaximumIndependentSet::new(graph, vec![1i64; 3]); - - let min_solution = BruteForce::new().solve(&min_problem).unwrap().unwrap(); - let max_solution = BruteForce::new().solve(&max_problem).unwrap().unwrap(); - let min_value = min_problem.evaluate(&min_solution).unwrap(); - let max_value = max_problem.evaluate(&max_solution).unwrap(); - - assert_eq!(min_value, Min(Some(1))); - assert_eq!(max_value, Max(Some(2))); - assert_eq!(solve_via_decision(&min_problem, 0, 3).unwrap(), Some(1)); - assert_eq!(solve_via_decision(&max_problem, 0, 3).unwrap(), Some(2)); -} From aaef6359b1619ed848476bf732bb4ad83089dad8 Mon Sep 17 00:00:00 2001 From: Xiwei Pan Date: Thu, 17 Sep 2026 20:25:35 +0800 Subject: [PATCH 18/42] Update dependencies and migrate to rmcp 3 and syn 3 --- Cargo.toml | 25 ++++----- problemreductions-cli/Cargo.toml | 25 ++++----- problemreductions-cli/src/mcp/prompts.rs | 9 +-- problemreductions-cli/src/mcp/tools.rs | 14 +++-- .../tests/mcp_integration.rs | 56 ++++++++++++++++++- problemreductions-expr/Cargo.toml | 12 ++-- problemreductions-macros/Cargo.toml | 8 +-- problemreductions-macros/src/lib.rs | 2 +- 8 files changed, 100 insertions(+), 51 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 6f5d69279..78f32a08b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,26 +21,25 @@ example-db = [] benchmarks = ["dep:criterion"] [dependencies] -petgraph = { version = "0.8", features = ["serde-1"] } -bitvec = "1.0" -serde = { version = "1.0", features = ["derive"] } +petgraph = { version = "0.8.3", features = ["serde-1"] } +bitvec = "1.1.1" +serde = { version = "1.0.229", features = ["derive"] } # Persisted problem and reduction data must preserve every finite f64 on replay. -serde_json = { version = "1.0", features = ["float_roundtrip"] } -thiserror = "2.0" -num-bigint = { version = "0.4", features = ["serde"] } -num-rational = { version = "0.4", features = ["serde"] } -num-traits = "0.2" +serde_json = { version = "1.0.151", features = ["float_roundtrip"] } +thiserror = "2.0.20" +num-bigint = { version = "0.4.8", features = ["serde"] } +num-rational = { version = "0.4.2", features = ["serde"] } +num-traits = "0.2.19" sprs = { version = "0.11.5", default-features = false, features = ["serde"] } highs = "=2.4.0" -inventory = "0.3" -ordered-float = "5.0" -rand = "0.10" -criterion = { version = "0.8", optional = true } +inventory = "0.3.24" +rand = "0.10.2" +criterion = { version = "0.8.2", optional = true } problemreductions-macros = { version = "0.6.0", path = "problemreductions-macros" } problemreductions-expr = { version = "0.6.0", path = "problemreductions-expr" } [dev-dependencies] -proptest = "1.0" +proptest = "1.11.0" [[bench]] name = "solver_benchmarks" diff --git a/problemreductions-cli/Cargo.toml b/problemreductions-cli/Cargo.toml index be37a9085..76cf6c772 100644 --- a/problemreductions-cli/Cargo.toml +++ b/problemreductions-cli/Cargo.toml @@ -17,19 +17,18 @@ path = "src/bin/pred_sym.rs" [features] all = ["mcp"] -mcp = ["dep:rmcp", "dep:tokio", "dep:schemars", "dep:tracing", "dep:tracing-subscriber"] +mcp = ["dep:rmcp", "dep:tokio", "dep:schemars", "dep:tracing-subscriber"] [dependencies] problemreductions = { version = "0.6.0", path = "..", features = ["example-db"] } -clap = { version = "4", features = ["derive", "string"] } -anyhow = "1" -serde = { version = "1", features = ["derive"] } -serde_json = "1" -num-bigint = "0.4" -clap_complete = "4" -owo-colors = { version = "4", features = ["supports-colors"] } -rmcp = { version = "1.2", features = ["server", "macros", "transport-io"], optional = true } -tokio = { version = "1", features = ["full"], optional = true } -schemars = { version = "1.0", optional = true } -tracing = { version = "0.1", optional = true } -tracing-subscriber = { version = "0.3", optional = true } +clap = { version = "4.6.7", features = ["derive", "string"] } +anyhow = "1.0.104" +serde = { version = "1.0.229", features = ["derive"] } +serde_json = "1.0.151" +num-bigint = "0.4.8" +clap_complete = "4.6.11" +owo-colors = { version = "4.4.0", features = ["supports-colors"] } +rmcp = { version = "3.4.0", features = ["server", "macros", "transport-io"], optional = true } +tokio = { version = "1.53.1", features = ["rt-multi-thread"], optional = true } +schemars = { version = "1.2.2", optional = true } +tracing-subscriber = { version = "0.3.23", optional = true } diff --git a/problemreductions-cli/src/mcp/prompts.rs b/problemreductions-cli/src/mcp/prompts.rs index 61ac602df..82fbd18b1 100644 --- a/problemreductions-cli/src/mcp/prompts.rs +++ b/problemreductions-cli/src/mcp/prompts.rs @@ -1,4 +1,4 @@ -use rmcp::model::{GetPromptResult, Prompt, PromptArgument, PromptMessage, PromptMessageRole}; +use rmcp::model::{GetPromptResult, Prompt, PromptArgument, PromptMessage, Role}; /// Return the list of available MCP prompt templates. pub fn list_prompts() -> Vec { @@ -88,11 +88,8 @@ pub fn list_prompts() -> Vec { } fn prompt_result(description: &str, user_message: &str) -> GetPromptResult { - GetPromptResult::new(vec![PromptMessage::new_text( - PromptMessageRole::User, - user_message, - )]) - .with_description(description) + GetPromptResult::new(vec![PromptMessage::new_text(Role::User, user_message)]) + .with_description(description) } /// Return the content for the named prompt, or `None` if the name is unknown. diff --git a/problemreductions-cli/src/mcp/tools.rs b/problemreductions-cli/src/mcp/tools.rs index 149d7398c..e1f7d4749 100644 --- a/problemreductions-cli/src/mcp/tools.rs +++ b/problemreductions-cli/src/mcp/tools.rs @@ -615,14 +615,14 @@ impl McpServer { #[rmcp::tool_handler] impl rmcp::ServerHandler for McpServer { - fn get_info(&self) -> rmcp::model::ServerInfo { + fn get_info(&self) -> rmcp::model::ServerConfig { let capabilities = rmcp::model::ServerCapabilities::builder() .enable_tools() .enable_prompts() .build(); let server_info = rmcp::model::Implementation::new("problemreductions", env!("CARGO_PKG_VERSION")); - rmcp::model::ServerInfo::new(capabilities) + rmcp::model::ServerConfig::new(capabilities) .with_server_info(server_info) .with_instructions( "MCP server for NP-hard problem reductions. \ @@ -647,11 +647,13 @@ impl rmcp::ServerHandler for McpServer { &self, request: rmcp::model::GetPromptRequestParams, _context: rmcp::service::RequestContext, - ) -> Result { + ) -> Result { let args = request.arguments.unwrap_or_default(); - super::prompts::get_prompt(&request.name, &args).ok_or_else(|| { - rmcp::ErrorData::invalid_params(format!("Unknown prompt: {}", request.name), None) - }) + super::prompts::get_prompt(&request.name, &args) + .map(Into::into) + .ok_or_else(|| { + rmcp::ErrorData::invalid_params(format!("Unknown prompt: {}", request.name), None) + }) } } diff --git a/problemreductions-cli/tests/mcp_integration.rs b/problemreductions-cli/tests/mcp_integration.rs index 5542dd1f9..b233f1391 100644 --- a/problemreductions-cli/tests/mcp_integration.rs +++ b/problemreductions-cli/tests/mcp_integration.rs @@ -115,7 +115,7 @@ mod mcp_tests { } #[test] - fn test_mcp_server_initialize_and_list_tools() { + fn test_mcp_server_initialize_list_and_call_tools() { let (mut stdin, mut reader, child) = spawn_mcp(); initialize(&mut stdin, &mut reader); @@ -192,11 +192,30 @@ mod mcp_tests { ); } + send( + &mut stdin, + &serde_json::json!({ + "jsonrpc": "2.0", + "id": 3, + "method": "tools/call", + "params": {"name": "list_problems", "arguments": {}} + }), + ); + let call_resp = read_response(&mut reader); + assert_eq!(call_resp["id"], 3); + assert!(call_resp.get("error").is_none(), "{call_resp}"); + assert_ne!(call_resp["result"]["isError"], true); + assert!(call_resp["result"]["content"][0]["text"] + .as_str() + .unwrap() + .contains("MaximumIndependentSet")); + assert!(call_resp["result"].get("resultType").is_none()); + shutdown(stdin, child); } #[test] - fn test_mcp_server_prompts_list() { + fn test_mcp_server_prompts_list_and_get() { let (mut stdin, mut reader, child) = spawn_mcp(); initialize(&mut stdin, &mut reader); @@ -244,6 +263,39 @@ mod mcp_tests { assert!(prompt_names.contains(&"find_reduction")); assert!(prompt_names.contains(&"overview")); + send( + &mut stdin, + &serde_json::json!({ + "jsonrpc": "2.0", + "id": 3, + "method": "prompts/get", + "params": {"name": "what_is", "arguments": {"problem": "QUBO"}} + }), + ); + let prompt_resp = read_response(&mut reader); + assert_eq!(prompt_resp["id"], 3); + let message = &prompt_resp["result"]["messages"][0]; + assert_eq!(message["role"], "user"); + assert_eq!(message["content"]["type"], "text"); + assert!(message["content"]["text"] + .as_str() + .unwrap() + .contains("QUBO")); + + send( + &mut stdin, + &serde_json::json!({ + "jsonrpc": "2.0", + "id": 4, + "method": "prompts/get", + "params": {"name": "unknown"} + }), + ); + let error_resp = read_response(&mut reader); + assert_eq!(error_resp["id"], 4); + assert_eq!(error_resp["error"]["code"], -32602); + assert_eq!(error_resp["error"]["message"], "Unknown prompt: unknown"); + shutdown(stdin, child); } } diff --git a/problemreductions-expr/Cargo.toml b/problemreductions-expr/Cargo.toml index d19a1b770..09304c30f 100644 --- a/problemreductions-expr/Cargo.toml +++ b/problemreductions-expr/Cargo.toml @@ -7,11 +7,11 @@ license = "MIT" repository = "https://github.com/CodingThrust/problem-reductions" [dependencies] -num-bigint = { version = "0.4", features = ["serde"] } -num-rational = { version = "0.4", features = ["serde"] } -num-traits = "0.2" -serde = { version = "1.0", features = ["derive"] } -thiserror = "2.0" +num-bigint = { version = "0.4.8", features = ["serde"] } +num-rational = { version = "0.4.2", features = ["serde"] } +num-traits = "0.2.19" +serde = { version = "1.0.229", features = ["derive"] } +thiserror = "2.0.20" [dev-dependencies] -serde_json = "1.0" +serde_json = "1.0.151" diff --git a/problemreductions-macros/Cargo.toml b/problemreductions-macros/Cargo.toml index 16b94ead5..d20910c69 100644 --- a/problemreductions-macros/Cargo.toml +++ b/problemreductions-macros/Cargo.toml @@ -10,8 +10,8 @@ repository = "https://github.com/CodingThrust/problem-reductions" proc-macro = true [dependencies] -syn = { version = "2.0", features = ["full", "parsing"] } -quote = "1.0" -proc-macro2 = "1.0" +syn = { version = "3.0.6", features = ["full", "parsing"] } +quote = "1.0.47" +proc-macro2 = "1.0.107" problemreductions-expr = { version = "0.6.0", path = "../problemreductions-expr" } -num-traits = "0.2" +num-traits = "0.2.19" diff --git a/problemreductions-macros/src/lib.rs b/problemreductions-macros/src/lib.rs index ce150f673..28c094f09 100644 --- a/problemreductions-macros/src/lib.rs +++ b/problemreductions-macros/src/lib.rs @@ -489,7 +489,7 @@ fn generate_reduction_entry( let trait_path = impl_block .trait_ .as_ref() - .map(|(_, path, _)| path) + .map(|(path, _)| path) .ok_or_else(|| syn::Error::new_spanned(impl_block, "Expected impl ReduceTo for S"))?; // Extract target type from ReduceTo From c8e25e41e6546566e227ebb321ca22450168407f Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Thu, 17 Sep 2026 23:13:06 +0800 Subject: [PATCH 19/42] Unroll single-element loop in CVP CLI test Co-Authored-By: Claude Fable 5.1 --- problemreductions-cli/tests/cli_tests.rs | 100 +++++++++++------------ 1 file changed, 49 insertions(+), 51 deletions(-) diff --git a/problemreductions-cli/tests/cli_tests.rs b/problemreductions-cli/tests/cli_tests.rs index b981a6031..6a85d4d45 100644 --- a/problemreductions-cli/tests/cli_tests.rs +++ b/problemreductions-cli/tests/cli_tests.rs @@ -10295,55 +10295,53 @@ fn test_extract_preserves_feasible_status_and_rejects_invalid_witnesses() { fn test_cvp_i64_create_and_solve() { use std::io::Write; use std::process::Stdio; - for (variant, basis, target, status, expected) in [("i64", "2,0;1,2", "3,2", "optimal", 0.0)] { - let created = pred() - .args([ - "create", - &format!("CVP/{variant}"), - "--basis", - basis, - "--target-vec", - target, - ]) - .output() - .unwrap(); - assert!( - created.status.success(), - "{}", - String::from_utf8_lossy(&created.stderr) - ); - let instance: serde_json::Value = serde_json::from_slice(&created.stdout).unwrap(); - assert_eq!(instance["variant"]["coefficient"], variant); - let mut child = pred() - .args(["solve", "-"]) - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .spawn() - .unwrap(); - child - .stdin - .take() - .unwrap() - .write_all(&created.stdout) - .unwrap(); - let solved = child.wait_with_output().unwrap(); - assert!( - solved.status.success(), - "{}", - String::from_utf8_lossy(&solved.stderr) - ); - let result: serde_json::Value = serde_json::from_slice(&solved.stdout).unwrap(); - assert_eq!(result["status"], status); - assert_eq!(result["solution"], serde_json::json!([1, 1])); - let display = result["evaluation"].as_str().unwrap(); - let value: f64 = display - .strip_prefix("Min(") - .unwrap() - .strip_suffix(')') - .unwrap() - .parse() - .unwrap(); - assert!((value - expected).abs() < 1e-12); - } + let created = pred() + .args([ + "create", + "CVP/i64", + "--basis", + "2,0;1,2", + "--target-vec", + "3,2", + ]) + .output() + .unwrap(); + assert!( + created.status.success(), + "{}", + String::from_utf8_lossy(&created.stderr) + ); + let instance: serde_json::Value = serde_json::from_slice(&created.stdout).unwrap(); + assert_eq!(instance["variant"]["coefficient"], "i64"); + let mut child = pred() + .args(["solve", "-"]) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .unwrap(); + child + .stdin + .take() + .unwrap() + .write_all(&created.stdout) + .unwrap(); + let solved = child.wait_with_output().unwrap(); + assert!( + solved.status.success(), + "{}", + String::from_utf8_lossy(&solved.stderr) + ); + let result: serde_json::Value = serde_json::from_slice(&solved.stdout).unwrap(); + assert_eq!(result["status"], "optimal"); + assert_eq!(result["solution"], serde_json::json!([1, 1])); + let display = result["evaluation"].as_str().unwrap(); + let value: f64 = display + .strip_prefix("Min(") + .unwrap() + .strip_suffix(')') + .unwrap() + .parse() + .unwrap(); + assert!(value.abs() < 1e-12); } From 9ce1b1552a7d8bcc9df635dd42c5e752e32f47aa Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Thu, 17 Sep 2026 23:13:29 +0800 Subject: [PATCH 20/42] Restore the variant-level 3-SAT chain through unit-weight decision vertex cover Retarget KSatisfiability/K3 to the unit-weight decision cover it constructs, add the explicit One -> i64 decision cast that keeps ComparativeContainment reachable, and let #[reduction] name macro-forwarded Decision inner types. Co-Authored-By: Claude Fable 5.1 --- problemreductions-macros/src/lib.rs | 17 ++++++ src/rules/decisionminimumvertexcover_casts.rs | 23 ++++++++ ...tisfiability_decisionminimumvertexcover.rs | 13 +++-- src/rules/mod.rs | 1 + src/unit_tests/reduction_graph.rs | 31 +++++++++- .../rules/decisionminimumvertexcover_casts.rs | 58 +++++++++++++++++++ ...tisfiability_decisionminimumvertexcover.rs | 10 ++-- 7 files changed, 139 insertions(+), 14 deletions(-) create mode 100644 src/rules/decisionminimumvertexcover_casts.rs create mode 100644 src/unit_tests/rules/decisionminimumvertexcover_casts.rs diff --git a/problemreductions-macros/src/lib.rs b/problemreductions-macros/src/lib.rs index 28c094f09..1b3610241 100644 --- a/problemreductions-macros/src/lib.rs +++ b/problemreductions-macros/src/lib.rs @@ -387,6 +387,8 @@ fn extract_type_name(ty: &Type) -> Option { Some(ident) } + // A `$ty:ty` fragment forwarded by `macro_rules!` arrives in an invisible group. + Type::Group(group) => extract_type_name(&group.elem), _ => None, } } @@ -996,6 +998,21 @@ mod tests { ); } + #[test] + fn extract_type_name_unwraps_macro_forwarded_decision_inner_type() { + let inner: Type = parse_str("MinimumVertexCover").unwrap(); + let group = Type::Group(syn::TypeGroup { + attrs: Vec::new(), + group_token: Default::default(), + elem: Box::new(inner), + }); + let ty: Type = syn::parse_quote!(Decision<#group>); + assert_eq!( + extract_type_name(&ty).as_deref(), + Some("DecisionMinimumVertexCover") + ); + } + #[test] fn declare_variants_accepts_single_default() { let input: DeclareVariantsInput = syn::parse_quote! { diff --git a/src/rules/decisionminimumvertexcover_casts.rs b/src/rules/decisionminimumvertexcover_casts.rs new file mode 100644 index 000000000..41fb04710 --- /dev/null +++ b/src/rules/decisionminimumvertexcover_casts.rs @@ -0,0 +1,23 @@ +//! Variant reductions for Decision. + +use crate::impl_variant_reduction; +use crate::models::decision::Decision; +use crate::models::graph::MinimumVertexCover; +use crate::topology::SimpleGraph; +use crate::types::One; + +// Unit-to-integer weight reduction; the cover-cost bound is unchanged. +impl_variant_reduction!( + Decision, + > => >, + fields: [num_vertices, num_edges], + + |src| Decision::new( + MinimumVertexCover::new( + src.inner().graph().clone(), vec![1_i64; src.num_vertices()]), + *src.bound()) +); + +#[cfg(test)] +#[path = "../unit_tests/rules/decisionminimumvertexcover_casts.rs"] +mod tests; diff --git a/src/rules/ksatisfiability_decisionminimumvertexcover.rs b/src/rules/ksatisfiability_decisionminimumvertexcover.rs index eef000b06..2943ce778 100644 --- a/src/rules/ksatisfiability_decisionminimumvertexcover.rs +++ b/src/rules/ksatisfiability_decisionminimumvertexcover.rs @@ -18,18 +18,19 @@ use crate::reduction; use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; use crate::topology::SimpleGraph; +use crate::types::One; use crate::variant::K3; /// Result of reducing KSatisfiability to Decision. #[derive(Debug, Clone)] pub struct Reduction3SATToDecisionMVC { - target: Decision>, + target: Decision>, source_num_vars: usize, } impl ReductionResult for Reduction3SATToDecisionMVC { type Source = KSatisfiability; - type Target = Decision>; + type Target = Decision>; fn target_problem(&self) -> &Self::Target { &self.target @@ -59,7 +60,7 @@ impl ReductionResult for Reduction3SATToDecisionMVC { num_edges = "num_vars + 6 * num_clauses", } )] -impl ReduceTo>> for KSatisfiability { +impl ReduceTo>> for KSatisfiability { type Result = Reduction3SATToDecisionMVC; fn reduce_to(&self) -> Result { @@ -72,7 +73,7 @@ impl ReduceTo>> for KSatisfiabilit .ok_or_else(|| { crate::rules::ReductionError::integer_overflow::< KSatisfiability, - Decision>, + Decision>, >("computing the target cover bound") })?; let total_vertices = 2 * n + 3 * m; @@ -107,7 +108,7 @@ impl ReduceTo>> for KSatisfiabilit } let graph = SimpleGraph::new(total_vertices, edges); - let weights = vec![1i64; total_vertices]; + let weights = vec![One; total_vertices]; let target = MinimumVertexCover::new(graph, weights); Ok(Reduction3SATToDecisionMVC { @@ -134,7 +135,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec>, + Decision>, >( source, SolutionPair { diff --git a/src/rules/mod.rs b/src/rules/mod.rs index 6cc013f6f..fcac87bdb 100644 --- a/src/rules/mod.rs +++ b/src/rules/mod.rs @@ -16,6 +16,7 @@ pub(crate) mod coloring_qubo; pub(crate) mod decisionmaximumindependentset_integralflowbundles; pub(crate) mod decisionminimumdominatingset_minimumsummulticenter; pub(crate) mod decisionminimumdominatingset_minmaxmulticenter; +mod decisionminimumvertexcover_casts; pub(crate) mod decisionminimumvertexcover_hamiltoniancircuit; pub(crate) mod ensemblecomputation_ilp; pub(crate) mod exactcoverby3sets_algebraicequationsovergf2; diff --git a/src/unit_tests/reduction_graph.rs b/src/unit_tests/reduction_graph.rs index cb8b6a28a..76231f82e 100644 --- a/src/unit_tests/reduction_graph.rs +++ b/src/unit_tests/reduction_graph.rs @@ -1007,18 +1007,43 @@ fn test_ksatisfiability_k3_to_decision_minimum_vertex_cover_direct_witness_edge( assert!(graph.has_direct_reduction_mode::< KSatisfiability, - Decision>, + Decision>, >(ReductionMode::Witness)); assert!(graph.has_direct_reduction_mode::< KSatisfiability, - Decision>, + Decision>, >(ReductionMode::Witness)); assert!(!graph.has_direct_reduction_mode::< KSatisfiability, - Decision>, + Decision>, >(ReductionMode::Turing)); } +#[test] +fn test_ksatisfiability_k3_reaches_unit_and_integer_decision_vertex_cover_targets() { + // Variant-level NP-hardness chains: HamiltonianCircuit consumes the unit-weight + // decision cover, ComparativeContainment the integer-weight one. + let graph = ReductionGraph::new(); + let src = ReductionGraph::variant_to_map(&KSatisfiability::::variant()); + let targets = [ + ( + "HamiltonianCircuit", + HamiltonianCircuit::::variant(), + ), + ( + "ComparativeContainment", + ComparativeContainment::::variant(), + ), + ]; + + for (name, variant) in targets { + let dst = ReductionGraph::variant_to_map(&variant); + let paths = graph.find_paths_up_to("KSatisfiability", &src, name, &dst, 1); + assert_eq!(paths.len(), 1, "no witness path from 3-SAT to {name}"); + assert_eq!(paths[0].steps.last().unwrap().variant, dst); + } +} + #[test] fn test_find_paths_bounded_limits_depth() { let graph = ReductionGraph::new(); diff --git a/src/unit_tests/rules/decisionminimumvertexcover_casts.rs b/src/unit_tests/rules/decisionminimumvertexcover_casts.rs new file mode 100644 index 000000000..db8823e65 --- /dev/null +++ b/src/unit_tests/rules/decisionminimumvertexcover_casts.rs @@ -0,0 +1,58 @@ +use super::*; +use crate::rules::traits::ReductionResult; +use crate::rules::ReduceTo; +use crate::solvers::{BruteForce, SolveOutcome}; +use crate::traits::Problem; +use crate::types::Or; + +fn unit_cycle_cover(bound: i64) -> Decision> { + Decision::new( + MinimumVertexCover::new(SimpleGraph::cycle(5), vec![One; 5]), + bound, + ) +} + +#[test] +fn test_decisionminimumvertexcover_one_to_i64_cast_closed_loop() { + // C5 has minimum vertex cover size 3. + let source = unit_cycle_cover(3); + let reduction = ReduceTo::>>::reduce_to(&source) + .expect("reduction should succeed"); + let target = reduction.target_problem(); + + assert_eq!(target.inner().graph(), source.inner().graph()); + assert_eq!(target.inner().weights(), &[1_i64; 5]); + assert_eq!(target.bound(), &3); + + let target_solution = BruteForce::new().solve(target).unwrap().unwrap(); + let source_solution = reduction + .recover_result( + &source, + SolveOutcome::optimal(target, target_solution.clone()).unwrap(), + ) + .unwrap() + .into_solution() + .expect("qualifying target result must recover a source solution"); + + assert_eq!(source_solution, target_solution); + assert_eq!(source.evaluate(&source_solution).unwrap(), Or(true)); +} + +#[test] +fn test_decisionminimumvertexcover_one_to_i64_cast_preserves_infeasibility() { + let source = unit_cycle_cover(2); + let reduction = ReduceTo::>>::reduce_to(&source) + .expect("reduction should succeed"); + + assert_eq!(BruteForce::new().solve(&source).unwrap(), None); + assert_eq!( + BruteForce::new().solve(reduction.target_problem()).unwrap(), + None + ); + assert!(matches!( + reduction + .recover_result(&source, SolveOutcome::Infeasible) + .unwrap(), + SolveOutcome::Infeasible + )); +} diff --git a/src/unit_tests/rules/ksatisfiability_decisionminimumvertexcover.rs b/src/unit_tests/rules/ksatisfiability_decisionminimumvertexcover.rs index 1fab78ca7..0635eed9d 100644 --- a/src/unit_tests/rules/ksatisfiability_decisionminimumvertexcover.rs +++ b/src/unit_tests/rules/ksatisfiability_decisionminimumvertexcover.rs @@ -18,7 +18,7 @@ fn test_ksatisfiability_to_decisionminimumvertexcover_closed_loop() { CNFClause::new(vec![-1, -2, 3]), ], ); - let reduction = ReduceTo::>>::reduce_to(&source) + let reduction = ReduceTo::>>::reduce_to(&source) .expect("reduction should succeed"); let target = reduction.target_problem(); @@ -43,7 +43,7 @@ fn test_ksatisfiability_to_decisionminimumvertexcover_unsatisfiable() { CNFClause::new(vec![1, 1, 1]), ], ); - let reduction = ReduceTo::>>::reduce_to(&source) + let reduction = ReduceTo::>>::reduce_to(&source) .expect("reduction should succeed"); let target = reduction.target_problem(); @@ -54,7 +54,7 @@ fn test_ksatisfiability_to_decisionminimumvertexcover_unsatisfiable() { #[test] fn test_ksatisfiability_to_decisionminimumvertexcover_structure_and_bound() { let source = KSatisfiability::::new(2, vec![CNFClause::new(vec![1, -1, 2])]); - let reduction = ReduceTo::>>::reduce_to(&source) + let reduction = ReduceTo::>>::reduce_to(&source) .expect("reduction should succeed"); let target = reduction.target_problem(); @@ -72,7 +72,7 @@ fn test_ksatisfiability_to_decisionminimumvertexcover_extract_solution() { CNFClause::new(vec![-1, -2, 3]), ], ); - let reduction = ReduceTo::>>::reduce_to(&source) + let reduction = ReduceTo::>>::reduce_to(&source) .expect("reduction should succeed"); let cover = vec![ false, true, false, true, true, false, true, true, false, true, true, false, @@ -99,7 +99,7 @@ fn test_ksatisfiability_to_decisionminimumvertexcover_extract_solution() { fn test_ksatisfiability_to_decisionminimumvertexcover_all_negated() { // (~x1 v ~x2 v ~x3) — 7 satisfying assignments let ksat = KSatisfiability::::new(3, vec![CNFClause::new(vec![-1, -2, -3])]); - let reduction = ReduceTo::>>::reduce_to(&ksat) + let reduction = ReduceTo::>>::reduce_to(&ksat) .expect("reduction should succeed"); assert_satisfaction_round_trip_from_satisfaction_target( From c096a003bdf0d3e2016e42148058cfd6c20e6174 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Thu, 17 Sep 2026 23:14:44 +0800 Subject: [PATCH 21/42] Describe the --result file in pred extract help Co-Authored-By: Claude Fable 5.1 --- problemreductions-cli/src/cli.rs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/problemreductions-cli/src/cli.rs b/problemreductions-cli/src/cli.rs index 0fc09ef13..a8dfb08de 100644 --- a/problemreductions-cli/src/cli.rs +++ b/problemreductions-cli/src/cli.rs @@ -222,7 +222,16 @@ the corresponding solution in the original source problem space without having to shell back into `pred solve`. Input: a reduction bundle JSON (from `pred reduce`). Use - to read from stdin. ---config is the target problem's solution encoded as JSON (e.g. '[1,0,1,0]').")] +--result is the target solver's result as JSON (use - for stdin), e.g. + {\"status\":\"feasible\",\"solution\":[true,false,true,false]} +`solution` uses the target problem's solution encoding; `evaluation` is optional +and is checked against the computed value when present. + +Status: + optimal the solver PROVED optimality; recovery may conclude the source is infeasible + feasible a valid solution without an optimality proof (samplers, QAOA, annealers) + infeasible the solver PROVED the target has no solution; takes no `solution` +Use feasible unless optimality is proven: a false optimal claim can yield a wrong source answer.")] Extract(ExtractArgs), /// Start MCP (Model Context Protocol) server for AI assistant integration #[cfg(feature = "mcp")] @@ -335,9 +344,9 @@ pub struct ReduceArgs { #[derive(clap::Args)] pub struct ExtractArgs { - /// Reduction bundle JSON (from pred reduce). + /// Reduction bundle JSON (from pred reduce). Use - for stdin. pub input: PathBuf, - /// JSON result file from the target solver, with an explicit solve status. + /// Target solver result JSON with an explicit solve status. Use - for stdin. #[arg(long)] pub result: PathBuf, } From 191d4dfb3792fdd3a196b5d41a082c14328f791e Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Thu, 17 Sep 2026 23:14:44 +0800 Subject: [PATCH 22/42] Report the expected shape for malformed extract results Co-Authored-By: Claude Fable 5.1 --- problemreductions-cli/src/commands/extract.rs | 18 ++- problemreductions-cli/tests/cli_tests.rs | 129 ++++++++++++++++++ 2 files changed, 145 insertions(+), 2 deletions(-) diff --git a/problemreductions-cli/src/commands/extract.rs b/problemreductions-cli/src/commands/extract.rs index 18f094c42..35cd02933 100644 --- a/problemreductions-cli/src/commands/extract.rs +++ b/problemreductions-cli/src/commands/extract.rs @@ -2,14 +2,28 @@ use crate::dispatch::{read_input, BundleReplay, ReductionBundle}; use crate::output::OutputConfig; use anyhow::{Context, Result}; use problemreductions::solvers::SolverExecution; +use serde::Deserialize; use std::path::Path; +const RESULT_SHAPE: &str = r#"Target result must be {"status": "optimal"|"feasible", "solution": [...]} or {"status": "infeasible"}; a bare configuration is no longer accepted, wrap it as {"status": "feasible", "solution": [...]}"#; + +/// Status envelope of a target result; rules validate the typed solution. +#[derive(Deserialize)] +#[serde(tag = "status", rename_all = "snake_case")] +#[allow(dead_code)] +enum ResultEnvelope { + Optimal { solution: serde::de::IgnoredAny }, + Feasible { solution: serde::de::IgnoredAny }, + Infeasible, +} + /// Recover the source result from an external solver's explicit target result. pub fn extract(input: &Path, result_path: &Path, out: &OutputConfig) -> Result<()> { let bundle: ReductionBundle = serde_json::from_str(&read_input(input)?) .context("pred extract requires a reduction bundle produced by pred reduce")?; - let target: serde_json::Value = serde_json::from_str(&read_input(result_path)?) - .context("Target result must declare optimal, feasible, or infeasible status")?; + let target: serde_json::Value = + serde_json::from_str(&read_input(result_path)?).context("Target result is not JSON")?; + ResultEnvelope::deserialize(&target).context(RESULT_SHAPE)?; let replay = BundleReplay::prepare(&bundle)?; let result = replay.recover_result(target, SolverExecution::External)?; out.emit( diff --git a/problemreductions-cli/tests/cli_tests.rs b/problemreductions-cli/tests/cli_tests.rs index 6a85d4d45..8981857e2 100644 --- a/problemreductions-cli/tests/cli_tests.rs +++ b/problemreductions-cli/tests/cli_tests.rs @@ -10291,6 +10291,135 @@ fn test_extract_preserves_feasible_status_and_rejects_invalid_witnesses() { std::fs::remove_dir_all(directory).unwrap(); } +/// Reduce a 4-vertex path MIS to QUBO in a fresh directory; the final +/// MaximumSetPacking -> QUBO step cannot recover from unproven candidates. +fn extract_test_mis_qubo_bundle(name: &str) -> (std::path::PathBuf, std::path::PathBuf) { + let directory = std::env::temp_dir().join(name); + std::fs::create_dir_all(&directory).unwrap(); + let problem_file = directory.join("source.json"); + let bundle_file = directory.join("bundle.json"); + let created = pred() + .args(["-o", problem_file.to_str().unwrap()]) + .args(["create", "MIS", "--graph", "0-1,1-2,2-3"]) + .output() + .unwrap(); + assert!(created.status.success()); + let reduced = reduce_named_to_file( + &problem_file, + "MIS/SimpleGraph/One", + "QUBO/f64", + &[ + "MaximumIndependentSet", + "MaximumIndependentSet", + "MaximumSetPacking", + "MaximumSetPacking", + "QUBO", + ], + &bundle_file, + ); + assert!( + reduced.status.success(), + "{}", + String::from_utf8_lossy(&reduced.stderr) + ); + (directory, bundle_file) +} + +fn extract_test_run( + directory: &std::path::Path, + bundle_file: &std::path::Path, + result: &str, +) -> std::process::Output { + let result_file = directory.join("result.json"); + std::fs::write(&result_file, result).unwrap(); + pred() + .args(["--json", "extract", bundle_file.to_str().unwrap()]) + .args(["--result", result_file.to_str().unwrap()]) + .output() + .unwrap() +} + +#[test] +fn test_extract_rejects_bare_configuration_with_expected_shape() { + let (directory, bundle_file) = extract_test_mis_qubo_bundle("pred_extract_bare_configuration"); + for result in [ + "[0,1,0,1]", + r#"{"solution":[false,true,false,true]}"#, + r#"{"status":"optimal"}"#, + r#"{"status":"sampled","solution":[false,true,false,true]}"#, + ] { + let output = extract_test_run(&directory, &bundle_file, result); + assert!(!output.status.success(), "accepted {result}"); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + stderr.contains(r#"wrap it as {"status": "feasible", "solution": [...]}"#), + "{result}: {stderr}" + ); + } + std::fs::remove_dir_all(directory).unwrap(); +} + +#[test] +fn test_extract_infeasible_target_result() { + let (directory, bundle_file) = extract_test_mis_qubo_bundle("pred_extract_infeasible_result"); + let output = extract_test_run(&directory, &bundle_file, r#"{"status":"infeasible"}"#); + assert!( + output.status.success(), + "{}", + String::from_utf8_lossy(&output.stderr) + ); + let result: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap(); + assert_eq!( + result, + serde_json::json!({ + "problem": "MaximumIndependentSet", + "status": "infeasible", + "solver": {"kind": "external"}, + "intermediate": {"problem": "QUBO", "status": "infeasible"}, + }) + ); + + let output = extract_test_run( + &directory, + &bundle_file, + r#"{"status":"infeasible","evaluation":"Min(0)"}"#, + ); + assert!(!output.status.success()); + assert!(String::from_utf8_lossy(&output.stderr) + .contains("infeasible results must not contain evaluation")); + std::fs::remove_dir_all(directory).unwrap(); +} + +#[test] +fn test_extract_rejects_feasible_result_the_rule_cannot_use() { + let (directory, bundle_file) = + extract_test_mis_qubo_bundle("pred_extract_insufficient_quality"); + let solution = "[false,true,false,true]"; + let output = extract_test_run( + &directory, + &bundle_file, + &format!(r#"{{"status":"feasible","solution":{solution}}}"#), + ); + assert_eq!(output.status.code(), Some(1)); + assert!(output.stdout.is_empty()); + assert!(String::from_utf8_lossy(&output.stderr).contains( + "the target result does not establish the conditions required for source recovery" + )); + + // The same witness is usable once the solver claims optimality. + let output = extract_test_run( + &directory, + &bundle_file, + &format!(r#"{{"status":"optimal","solution":{solution}}}"#), + ); + assert!( + output.status.success(), + "{}", + String::from_utf8_lossy(&output.stderr) + ); + std::fs::remove_dir_all(directory).unwrap(); +} + #[test] fn test_cvp_i64_create_and_solve() { use std::io::Write; From 116a2a30fd4eec17b9851b6873c4d19e60a38220 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Thu, 17 Sep 2026 23:15:21 +0800 Subject: [PATCH 23/42] Reject negative UndirectedFlowLowerBounds requirements on every construction path The create spec now delegates to try_new so the sign check lives in one place. Co-Authored-By: Claude Fable 5.1 --- .../graph/undirected_flow_lower_bounds.rs | 38 +------------- .../graph/undirected_flow_lower_bounds.rs | 50 +++++++++++++++++++ 2 files changed, 51 insertions(+), 37 deletions(-) diff --git a/src/models/graph/undirected_flow_lower_bounds.rs b/src/models/graph/undirected_flow_lower_bounds.rs index 1144eb34f..ea6785896 100644 --- a/src/models/graph/undirected_flow_lower_bounds.rs +++ b/src/models/graph/undirected_flow_lower_bounds.rs @@ -61,42 +61,6 @@ struct UndirectedFlowLowerBoundsCreateSpec { impl TryFrom for UndirectedFlowLowerBounds { type Error = crate::registry::ConstructionError; fn try_from(spec: UndirectedFlowLowerBoundsCreateSpec) -> Result { - let edges = spec.graph.num_edges(); - if spec.capacities.len() != edges { - return Err(format!( - "capacities has {} entries, expected {edges}", - spec.capacities.len() - ) - .into()); - } - if spec.lower_bounds.len() != edges { - return Err(format!( - "lower_bounds has {} entries, expected {edges}", - spec.lower_bounds.len() - ) - .into()); - } - let vertices = spec.graph.num_vertices(); - if spec.source >= vertices || spec.sink >= vertices { - return Err("source and sink must be valid graph vertices" - .to_string() - .into()); - } - if spec.source == spec.sink { - return Err("source and sink must be distinct".to_string().into()); - } - if spec.requirement == 0 { - return Err("requirement must be at least 1".to_string().into()); - } - if let Some((index, _)) = spec - .lower_bounds - .iter() - .zip(&spec.capacities) - .enumerate() - .find(|(_, (&lower, &upper))| lower > upper) - { - return Err(format!("lower bound at edge {index} exceeds its capacity").into()); - } Self::try_new( spec.graph, spec.capacities, @@ -146,7 +110,7 @@ impl UndirectedFlowLowerBounds { if source == sink { return Err("source and sink must be distinct".into()); } - if requirement == 0 { + if requirement < 1 { return Err("requirement must be at least 1".into()); } 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 c46460a96..80e0eaca1 100644 --- a/src/unit_tests/models/graph/undirected_flow_lower_bounds.rs +++ b/src/unit_tests/models/graph/undirected_flow_lower_bounds.rs @@ -128,3 +128,53 @@ fn test_undirected_flow_lower_bounds_paper_example() { let all = BruteForce::new().find_all_witnesses(&problem).unwrap(); assert!(all.contains(&config)); } + +#[test] +fn test_undirected_flow_lower_bounds_rejects_nonpositive_requirement_on_every_path() { + let expected = "problem construction failed: requirement must be at least 1"; + for requirement in [0, -5, i64::MIN] { + let graph = || SimpleGraph::new(2, vec![(0, 1)]); + + let error = + UndirectedFlowLowerBounds::try_new(graph(), vec![1], vec![0], 0, 1, requirement) + .unwrap_err(); + assert_eq!(error.to_string(), expected); + + let error = UndirectedFlowLowerBounds::try_from(UndirectedFlowLowerBoundsCreateSpec { + graph: graph(), + capacities: vec![1], + lower_bounds: vec![0], + source: 0, + sink: 1, + requirement, + }) + .unwrap_err(); + assert_eq!(error.to_string(), expected); + + let mut json = serde_json::to_value(UndirectedFlowLowerBounds::new( + graph(), + vec![1], + vec![0], + 0, + 1, + 1, + )) + .unwrap(); + json["requirement"] = serde_json::json!(requirement); + let error = serde_json::from_value::(json).unwrap_err(); + assert_eq!(error.to_string(), expected); + } +} + +#[test] +#[should_panic(expected = "requirement must be at least 1")] +fn test_undirected_flow_lower_bounds_new_panics_on_negative_requirement() { + UndirectedFlowLowerBounds::new( + SimpleGraph::new(2, vec![(0, 1)]), + vec![1], + vec![0], + 0, + 1, + -5, + ); +} From e1367c8979d2be0148c5c4093e7f94814491e7fe Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Thu, 17 Sep 2026 23:15:30 +0800 Subject: [PATCH 24/42] Name the registry build failure in solver capability errors Co-Authored-By: Claude Fable 5.1 --- problemreductions-cli/src/dispatch.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/problemreductions-cli/src/dispatch.rs b/problemreductions-cli/src/dispatch.rs index 3a1386655..37b975170 100644 --- a/problemreductions-cli/src/dispatch.rs +++ b/problemreductions-cli/src/dispatch.rs @@ -77,7 +77,7 @@ pub struct SolverCapabilitiesView { pub fn solver_capabilities_view(problem: &LoadedProblem) -> Result { let key = ExactProblemKey::new(problem.problem_name(), problem.variant_map()); let registered = solver_capabilities(&key) - .map_err(|error| anyhow::anyhow!("cannot inspect brute-force coordinates: {error}"))?; + .map_err(|error| anyhow::anyhow!("cannot build the solver capability registry: {error}"))?; let customized = registered .customized .map(|entry| CustomizedSolverCapabilityView { From 7f5a309034bb6bbc9fc886a798ef0a752a553385 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Thu, 17 Sep 2026 23:17:41 +0800 Subject: [PATCH 25/42] Update extract docs to the --result and recover_result workflow Co-Authored-By: Claude Fable 5.1 --- docs/paper/reductions.typ | 3 +- docs/src/static/reduction-workflow-dark.svg | 431 +------------------- docs/src/static/reduction-workflow.svg | 431 +------------------- docs/src/static/reduction-workflow.typ | 6 +- 4 files changed, 7 insertions(+), 864 deletions(-) diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index ec9854756..c9f742a4c 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -17831,7 +17831,8 @@ The following table shows concrete target-variable counts for example instances, "pred create --example DecisionMaximumIndependentSet/One -o independent-set.json", "pred reduce independent-set.json --via route.json -o bundle.json", "pred solve bundle.json", - "pred extract bundle.json --config " + cli-config(mis_ifb_sol.target_config), + "echo '" + json.encode((status: "feasible", solution: mis_ifb_sol.target_config), pretty: false) + "' > result.json", + "pred extract bundle.json --result result.json", ) Source bound: #mis_ifb.source.instance.bound; selected vertices: #fmt-values(mis_ifb_sol.source_config) \ Target: #mis_ifb.target.instance.graph.num_vertices vertices, #mis_ifb.target.instance.graph.arcs.len() arcs, #mis_ifb.target.instance.bundles.len() bundles; requirement #mis_ifb.target.instance.requirement \ diff --git a/docs/src/static/reduction-workflow-dark.svg b/docs/src/static/reduction-workflow-dark.svg index 8e97a4ad2..b621fce67 100644 --- a/docs/src/static/reduction-workflow-dark.svg +++ b/docs/src/static/reduction-workflow-dark.svg @@ -1,430 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/docs/src/static/reduction-workflow.svg b/docs/src/static/reduction-workflow.svg index 334c2c182..4e3ed112d 100644 --- a/docs/src/static/reduction-workflow.svg +++ b/docs/src/static/reduction-workflow.svg @@ -1,430 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/docs/src/static/reduction-workflow.typ b/docs/src/static/reduction-workflow.typ index df911fb8b..dc3cf4703 100644 --- a/docs/src/static/reduction-workflow.typ +++ b/docs/src/static/reduction-workflow.typ @@ -23,12 +23,12 @@ node((0, 0), box(width: 28mm, align(center)[*Problem A*\ #text(size: 8pt)[source problem]]), fill: box-fill, corner-radius: 6pt, inset: 10pt, name: ), node((1, 0), box(width: 28mm, align(center)[*Problem B*\ #text(size: 8pt)[target problem]]), fill: box-fill, corner-radius: 6pt, inset: 10pt, name: ), node((2, 0), box(width: 28mm, align(center)[*Solution B*\ #text(size: 8pt)[solver output]]), fill: box-fill, corner-radius: 6pt, inset: 10pt, name: ), - node((1, 1), box(width: 28mm, align(center)[*Solution A*\ #text(size: 8pt)[extracted result]]), fill: success-fill, stroke: 1.5pt + success, corner-radius: 6pt, inset: 10pt, name: ), + node((1, 1), box(width: 28mm, align(center)[*Solution A*\ #text(size: 8pt)[recovered result]]), fill: success-fill, stroke: 1.5pt + success, corner-radius: 6pt, inset: 10pt, name: ), // Edges with labels edge(, , "->", stroke: 1.5pt + accent, label: text(size: 9pt)[`reduce_to`], label-sep: 5pt, label-pos: 0.5, label-side: left), - edge(, , "->", stroke: 1.5pt + accent, label: text(size: 9pt)[`find_witness`], label-sep: 5pt, label-pos: 0.5, label-side: left), - edge(, , "->", stroke: 1.5pt + success, label: text(size: 9pt)[`extract_solution`], label-sep: 2pt, label-pos: 0.5, label-side: left), + edge(, , "->", stroke: 1.5pt + accent, label: text(size: 9pt)[`solve`], label-sep: 5pt, label-pos: 0.5, label-side: left), + edge(, , "->", stroke: 1.5pt + success, label: text(size: 9pt)[`recover_result`], label-sep: 2pt, label-pos: 0.5, label-side: left), ) } From a1ee27d2fe5c954f9123956fae03f0fa833f8014 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Thu, 17 Sep 2026 23:18:39 +0800 Subject: [PATCH 26/42] Point skills at recover_result and the recovery contract Co-Authored-By: Claude Fable 5.1 --- .claude/skills/add-rule/SKILL.md | 2 +- .claude/skills/review-pipeline/SKILL.md | 2 +- .claude/skills/review-structural/SKILL.md | 6 +++--- .claude/skills/verify-reduction/SKILL.md | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.claude/skills/add-rule/SKILL.md b/.claude/skills/add-rule/SKILL.md index 24fc87579..3620c2f84 100644 --- a/.claude/skills/add-rule/SKILL.md +++ b/.claude/skills/add-rule/SKILL.md @@ -80,7 +80,7 @@ All verification artifacts are ephemeral — they exist only in conversation con **Proceed to implementation only when verification reports VERIFIED. For FAILED or INCOMPLETE, report the concrete defect or missing evidence and resolve it before implementing.** -If verification passes, the verified Python `reduce()` and `extract_solution()` functions, along with the YES/NO instances, carry forward in conversation context to inform Steps 2-5. Use them as the canonical spec for the Rust implementation. +If verification passes, the checked construction, recovery mapping, and YES/NO instances carry forward in conversation context to inform Steps 2-5. Use them as the canonical spec for the Rust `reduce_to()` and `recover_result()` implementation. ## Step 2: Implement the reduction diff --git a/.claude/skills/review-pipeline/SKILL.md b/.claude/skills/review-pipeline/SKILL.md index 51930ca79..15644c153 100644 --- a/.claude/skills/review-pipeline/SKILL.md +++ b/.claude/skills/review-pipeline/SKILL.md @@ -152,7 +152,7 @@ Run three independent sub-reviews. All three are **read-only** — they evaluate Invoke `/review-structural` (file: `.claude/skills/review-structural/SKILL.md`) with the pre-generated `IMPL_REPORT`. This runs the model/rule checklists, build checks, semantic review, and issue compliance checks. **Mathematical correctness is critical.** In addition to the standard structural checks, verify: -- **For rules**: Is the reduction mathematically correct? Trace through the `reduce_to()` logic with a small example and confirm the target instance encodes the same problem. Check that `extract_solution` correctly inverts the mapping. Verify the paper proof sketch is sound — not just present, but logically valid. +- **For rules**: Is the reduction mathematically correct? Trace through the `reduce_to()` logic with a small example and confirm the target instance encodes the same problem. Check that `recover_result` correctly inverts the mapping for each target status. Verify the paper proof sketch is sound — not just present, but logically valid. - **For models**: Does `evaluate()` correctly compute the objective for the mathematical definition? Are edge cases handled (empty graph, zero weights, infeasible configs)? - **Overhead expressions**: Manually count the sizes in `reduce_to()` output and verify they match the `overhead = { ... }` formulas. diff --git a/.claude/skills/review-structural/SKILL.md b/.claude/skills/review-structural/SKILL.md index bf6609374..ed7a0365d 100644 --- a/.claude/skills/review-structural/SKILL.md +++ b/.claude/skills/review-structural/SKILL.md @@ -85,8 +85,8 @@ Only run if review type includes "rule". Given: source `S`, target `T`, rule fil | 9 | Canonical rule example registered | `Grep("canonical_rule_example_specs", rule file)` and verify it is included by `src/rules/mod.rs` | | 10 | Example-db lookup tests exist | `Grep("find_rule_example|build_rule_db", "src/unit_tests/example_db.rs")` | | 11 | Paper `reduction-rule` entry | `Grep('reduction-rule.*"{S}".*"{T}"', "docs/paper/reductions.typ")` | -| 12 | Extraction contract | Follow the canonical responsibility boundaries. Both external extraction and internal rule mappings rely on documented premises; parsing and type conversion stay at the transport boundary. Reject repeated feasibility checks and error branches excluded by construction. Solver orchestration interprets aggregate thresholds to determine source answers. No independent optimality certification is required. | -| 13 | Numeric and error contracts | Check the [witness/aggregate contract](../../../docs/src/design.md#witness-and-aggregate-reductions) and actual construction arithmetic under the canonical policy. Do not reject different objective directions/value types or demand backend precision tests for every rule. Verify public reduction paths return `ReductionError`, preserve target `ConstructionError` as its construction cause, and never stringify or silently handle either failure. | +| 12 | Extraction contract | Follow the canonical responsibility boundaries. Both external extraction and internal rule mappings rely on documented premises; parsing and type conversion stay at the transport boundary. Reject repeated feasibility checks and error branches excluded by construction. Rules own the mathematical interpretation of optimum values, source infeasibility, and insufficient feasible candidates; solver and CLI callers invoke the same `recover_result` and add no interpretation branches. No independent optimality certification is required. | +| 13 | Numeric and error contracts | Check the [complete-result recovery contract](../../../docs/src/design.md#complete-result-recovery) and actual construction arithmetic under the canonical policy. Do not reject different objective directions/value types or demand backend precision tests for every rule. Verify public reduction paths return `ReductionError`, preserve target `ConstructionError` as its construction cause, and never stringify or silently handle either failure. | ## Step 2b: Blacklisted File Check @@ -116,7 +116,7 @@ Report pass/fail. If tests fail, identify which tests. **Do NOT fix anything** 5. **Numeric safety** — Are element and total types distinct where required, do serde and constructors enforce the same range, and are overflow and non-finite values rejected explicitly? ### For Rules: -1. **`extract_solution` correctness** — Does it implement the mathematical inverse? Is every branch either a defined mathematical case or an `ExtractionError`, with no defaulting, truncation, clamping, panic, or recovery? +1. **`recover_result` correctness** — Does it implement the mathematical inverse and handle `Optimal`, `Feasible`, and `Infeasible` explicitly? Is every branch either a defined mathematical case or an `ExtractionError`, with no defaulting, truncation, clamping, panic, or recovery? 2. **Overhead accuracy** — Does `overhead = { field = "expr" }` reflect the actual size relationship? 3. **Example quality** — Is it tutorial-style? Does the JSON export include both source and target data? 4. **Paper quality** — Is the reduction-rule statement precise? Is the proof sketch sound? diff --git a/.claude/skills/verify-reduction/SKILL.md b/.claude/skills/verify-reduction/SKILL.md index d0e42c3c9..b328e7d8b 100644 --- a/.claude/skills/verify-reduction/SKILL.md +++ b/.claude/skills/verify-reduction/SKILL.md @@ -25,7 +25,7 @@ Extract the construction, mathematical domain, correctness argument, witness mapping, parameter formulas, worked example, and references. Consult the cited literature when needed to resolve a mathematical claim. -Read the canonical [witness/aggregate contract](../../../docs/src/design.md#witness-and-aggregate-reductions), +Read the canonical [complete-result recovery contract](../../../docs/src/design.md#complete-result-recovery), [arithmetic policy](../../../docs/src/design.md#arithmetic), and [validation policy](../../../docs/src/design.md#validation-evidence). From dcff030f7e7c05d2d6cf13204bc5531eb4f69e08 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Thu, 17 Sep 2026 23:22:47 +0800 Subject: [PATCH 27/42] Rebuild MonochromaticTriangle derived data on deserialization Persisted triangle and edge lists were trusted verbatim, so malformed JSON panicked in evaluate and graph-only construction input was rejected. Co-Authored-By: Claude Fable 5.1 --- src/models/graph/monochromatic_triangle.rs | 20 ++++++++++++-- .../models/graph/monochromatic_triangle.rs | 27 +++++++++++++++++++ 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/src/models/graph/monochromatic_triangle.rs b/src/models/graph/monochromatic_triangle.rs index a483c8175..0ce5a7581 100644 --- a/src/models/graph/monochromatic_triangle.rs +++ b/src/models/graph/monochromatic_triangle.rs @@ -57,8 +57,7 @@ inventory::submit! { /// let solution = solver.solve(&problem).unwrap(); /// assert!(solution.is_some()); /// ``` -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(bound(deserialize = "G: serde::Deserialize<'de>"))] +#[derive(Debug, Clone, Serialize)] pub struct MonochromaticTriangle { /// The underlying graph. graph: G, @@ -68,6 +67,23 @@ pub struct MonochromaticTriangle { edge_list: Vec<(usize, usize)>, } +// The persisted triangle and edge lists are derived data; loading rebuilds them. +#[derive(Deserialize)] +#[serde(bound(deserialize = "G: Graph + Deserialize<'de>"))] +struct MonochromaticTriangleData { + graph: G, +} + +impl<'de, G> Deserialize<'de> for MonochromaticTriangle +where + G: Graph + Deserialize<'de>, +{ + fn deserialize>(deserializer: D) -> Result { + let data = MonochromaticTriangleData::::deserialize(deserializer)?; + Ok(Self::new(data.graph)) + } +} + impl MonochromaticTriangle { /// Create a new Monochromatic Triangle instance. pub fn new(graph: G) -> Self { diff --git a/src/unit_tests/models/graph/monochromatic_triangle.rs b/src/unit_tests/models/graph/monochromatic_triangle.rs index 6dcb462b7..cd74129d3 100644 --- a/src/unit_tests/models/graph/monochromatic_triangle.rs +++ b/src/unit_tests/models/graph/monochromatic_triangle.rs @@ -125,3 +125,30 @@ fn test_monochromatic_triangle_serialization() { assert_eq!(deserialized.num_edges(), 6); assert_eq!(deserialized.triangles().len(), 4); } + +#[test] +fn test_monochromatic_triangle_deserialization_rebuilds_derived_triangles() { + // Triangle 0-1-2 with a pendant edge 2-3. + let problem = + MonochromaticTriangle::new(SimpleGraph::new(4, vec![(0, 1), (0, 2), (1, 2), (2, 3)])); + let valid = serde_json::to_value(&problem).unwrap(); + + let mut corrupted = valid.clone(); + corrupted["triangles"] = serde_json::json!([[99, 0, 1]]); + corrupted["edge_list"] = serde_json::json!([]); + let graph_only = serde_json::json!({ "graph": valid["graph"] }); + + for json in [valid.clone(), corrupted, graph_only] { + let restored: MonochromaticTriangle = serde_json::from_value(json).unwrap(); + assert_eq!(serde_json::to_value(&restored).unwrap(), valid); + assert_eq!(restored.triangles(), &[[0, 1, 2]]); + assert_eq!( + restored.evaluate(&vec![true, true, true, false]).unwrap(), + crate::types::Or(false) + ); + assert_eq!( + restored.evaluate(&vec![true, false, true, true]).unwrap(), + crate::types::Or(true) + ); + } +} From 4b021e5b7ca239acb2ada487c6fc6858f903f3d2 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Thu, 17 Sep 2026 23:22:47 +0800 Subject: [PATCH 28/42] Validate four remaining models on deserialization PathConstrainedNetworkFlow, MixedChinesePostman, ConsecutiveOnesMatrixAugmentation, and MaximumContactMapOverlap now load through their fallible constructors; MaximumContactMapOverlap gains try_new. Co-Authored-By: Claude Fable 5.1 --- .../consecutive_ones_matrix_augmentation.rs | 1 + .../graph/maximum_contact_map_overlap.rs | 81 ++++++++++++++----- src/models/graph/mixed_chinese_postman.rs | 21 ++++- .../graph/path_constrained_network_flow.rs | 25 ++++++ .../consecutive_ones_matrix_augmentation.rs | 28 +++++++ .../graph/maximum_contact_map_overlap.rs | 64 +++++++++++++++ .../models/graph/mixed_chinese_postman.rs | 38 +++++++++ .../graph/path_constrained_network_flow.rs | 51 ++++++++++++ 8 files changed, 288 insertions(+), 21 deletions(-) diff --git a/src/models/algebraic/consecutive_ones_matrix_augmentation.rs b/src/models/algebraic/consecutive_ones_matrix_augmentation.rs index 46031689b..a4a186092 100644 --- a/src/models/algebraic/consecutive_ones_matrix_augmentation.rs +++ b/src/models/algebraic/consecutive_ones_matrix_augmentation.rs @@ -22,6 +22,7 @@ inventory::submit! { } #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "ConsecutiveOnesMatrixAugmentationCreateSpec")] pub struct ConsecutiveOnesMatrixAugmentation { matrix: Vec>, bound: i64, diff --git a/src/models/graph/maximum_contact_map_overlap.rs b/src/models/graph/maximum_contact_map_overlap.rs index 5eea5ba5d..29189ff11 100644 --- a/src/models/graph/maximum_contact_map_overlap.rs +++ b/src/models/graph/maximum_contact_map_overlap.rs @@ -70,6 +70,7 @@ inventory::submit! { /// entries are pairwise distinct (injectivity) and strictly increasing along /// the index order of `V_1` (order-preserving). #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(try_from = "MaximumContactMapOverlapData")] pub struct MaximumContactMapOverlap { num_vertices_1: usize, contacts_1: Vec<(usize, usize)>, @@ -77,51 +78,91 @@ pub struct MaximumContactMapOverlap { contacts_2: Vec<(usize, usize)>, } -/// Canonicalize a contact set: each pair is normalized to `(min, max)`, no -/// self-loops are allowed, all endpoints must be in range, and duplicates -/// (after normalization) cause a panic. +#[derive(Deserialize)] +struct MaximumContactMapOverlapData { + num_vertices_1: usize, + contacts_1: Vec<(usize, usize)>, + num_vertices_2: usize, + contacts_2: Vec<(usize, usize)>, +} + +impl TryFrom for MaximumContactMapOverlap { + type Error = crate::registry::ConstructionError; + fn try_from(data: MaximumContactMapOverlapData) -> Result { + Self::try_new( + data.num_vertices_1, + data.contacts_1, + data.num_vertices_2, + data.contacts_2, + ) + } +} + +/// Canonicalize a contact set: each pair is normalized to `(min, max)`. +/// Self-loops, out-of-range endpoints, and duplicates (after normalization) +/// are rejected. fn canonicalize_contacts( raw: Vec<(usize, usize)>, num_vertices: usize, side: &str, -) -> Vec<(usize, usize)> { +) -> Result, crate::registry::ConstructionError> { let mut seen: HashSet<(usize, usize)> = HashSet::new(); let mut out = Vec::with_capacity(raw.len()); for (u, v) in raw { - assert!( - u < num_vertices && v < num_vertices, - "{side} contact endpoint out of range for num_vertices = {num_vertices}: ({u}, {v})" - ); - assert!(u != v, "{side} contact has self-loop: ({u}, {v})"); + if u >= num_vertices || v >= num_vertices { + return Err(format!( + "{side} contact endpoint out of range for num_vertices = {num_vertices}: ({u}, {v})" + ) + .into()); + } + if u == v { + return Err(format!("{side} contact has self-loop: ({u}, {v})").into()); + } let (a, b) = if u < v { (u, v) } else { (v, u) }; - assert!( - seen.insert((a, b)), - "{side} has duplicate contact after normalization: ({a}, {b})" - ); + if !seen.insert((a, b)) { + return Err( + format!("{side} has duplicate contact after normalization: ({a}, {b})").into(), + ); + } out.push((a, b)); } - out + Ok(out) } impl MaximumContactMapOverlap { /// Construct a new instance from two ordered contact maps. /// - /// Contacts are canonicalized to `(min, max)` pairs. Self-loops, duplicate - /// contacts (after normalization), and out-of-range endpoints panic. + /// Contacts are canonicalized to `(min, max)` pairs. + /// + /// # Panics + /// + /// Panics on self-loops, duplicate contacts (after normalization), and + /// out-of-range endpoints. pub fn new( num_vertices_1: usize, contacts_1: Vec<(usize, usize)>, num_vertices_2: usize, contacts_2: Vec<(usize, usize)>, ) -> Self { - let contacts_1 = canonicalize_contacts(contacts_1, num_vertices_1, "G_1"); - let contacts_2 = canonicalize_contacts(contacts_2, num_vertices_2, "G_2"); - Self { + Self::try_new(num_vertices_1, contacts_1, num_vertices_2, contacts_2) + .unwrap_or_else(|error| panic!("{error}")) + } + + /// Create an instance, returning validation errors instead of panicking. + pub fn try_new( + num_vertices_1: usize, + contacts_1: Vec<(usize, usize)>, + num_vertices_2: usize, + contacts_2: Vec<(usize, usize)>, + ) -> Result { + let contacts_1 = canonicalize_contacts(contacts_1, num_vertices_1, "G_1")?; + let contacts_2 = canonicalize_contacts(contacts_2, num_vertices_2, "G_2")?; + Ok(Self { num_vertices_1, contacts_1, num_vertices_2, contacts_2, - } + }) } /// Number of ordered residues/vertices in `G_1`. diff --git a/src/models/graph/mixed_chinese_postman.rs b/src/models/graph/mixed_chinese_postman.rs index 450959b53..847ca70f4 100644 --- a/src/models/graph/mixed_chinese_postman.rs +++ b/src/models/graph/mixed_chinese_postman.rs @@ -35,13 +35,32 @@ inventory::submit! { /// edge. The minimum-cost closed walk is then computed via the directed Chinese /// Postman subproblem, using all available arcs (including both directions of /// every undirected edge) for degree-balancing detours. -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize)] pub struct MixedChinesePostman> { graph: MixedGraph, arc_weights: Vec, edge_weights: Vec, } +#[derive(Deserialize)] +#[serde(bound(deserialize = "W: WeightElement + Deserialize<'de>"))] +struct MixedChinesePostmanData> { + graph: MixedGraph, + arc_weights: Vec, + edge_weights: Vec, +} + +impl<'de, W> Deserialize<'de> for MixedChinesePostman +where + W: WeightElement + Deserialize<'de>, +{ + fn deserialize>(deserializer: D) -> Result { + let data = MixedChinesePostmanData::::deserialize(deserializer)?; + Self::try_new(data.graph, data.arc_weights, data.edge_weights) + .map_err(serde::de::Error::custom) + } +} + macro_rules! mixed_chinese_postman_create_spec { ($name:ident, $weight:ty, $one:expr $(, $arc_weights:ident, $edge_weights:ident)?) => { #[derive(Debug, Deserialize, crate::CreateSpec)] diff --git a/src/models/graph/path_constrained_network_flow.rs b/src/models/graph/path_constrained_network_flow.rs index d88aa7ceb..e4db720fe 100644 --- a/src/models/graph/path_constrained_network_flow.rs +++ b/src/models/graph/path_constrained_network_flow.rs @@ -34,6 +34,7 @@ inventory::submit! { /// - the induced arc loads do not exceed the arc capacities /// - the total delivered flow reaches the requirement #[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(try_from = "PathConstrainedNetworkFlowData")] pub struct PathConstrainedNetworkFlow { graph: DirectedGraph, capacities: Vec, @@ -43,6 +44,30 @@ pub struct PathConstrainedNetworkFlow { requirement: i64, } +#[derive(Deserialize)] +struct PathConstrainedNetworkFlowData { + graph: DirectedGraph, + capacities: Vec, + source: usize, + sink: usize, + paths: Vec>, + requirement: i64, +} + +impl TryFrom for PathConstrainedNetworkFlow { + type Error = crate::registry::ConstructionError; + fn try_from(data: PathConstrainedNetworkFlowData) -> Result { + Self::try_new( + data.graph, + data.capacities, + data.source, + data.sink, + data.paths, + data.requirement, + ) + } +} + #[derive(Debug, Deserialize, crate::CreateSpec)] struct PathConstrainedNetworkFlowCreateSpec { /// Directed graph arcs. diff --git a/src/unit_tests/models/algebraic/consecutive_ones_matrix_augmentation.rs b/src/unit_tests/models/algebraic/consecutive_ones_matrix_augmentation.rs index dc1f27d42..b1c5bd6fc 100644 --- a/src/unit_tests/models/algebraic/consecutive_ones_matrix_augmentation.rs +++ b/src/unit_tests/models/algebraic/consecutive_ones_matrix_augmentation.rs @@ -162,3 +162,31 @@ fn test_consecutive_ones_matrix_augmentation_rejects_ragged_matrix() { fn test_consecutive_ones_matrix_augmentation_rejects_negative_bound() { ConsecutiveOnesMatrixAugmentation::new(issue_yes_matrix(), -1); } + +#[test] +fn test_consecutive_ones_matrix_augmentation_deserialization_rejects_invalid_instances() { + let valid = serde_json::json!({ + "matrix": [[true, false, true], [false, true, true], [true, true, false]], + "bound": 1, + }); + let restored: ConsecutiveOnesMatrixAugmentation = + serde_json::from_value(valid.clone()).unwrap(); + assert_eq!(serde_json::to_value(&restored).unwrap(), valid); + + let cases = [ + ( + "matrix", + serde_json::json!([[true, false, true], [false, true], [true, true, false]]), + "all matrix rows must have the same length", + ), + ("bound", serde_json::json!(-1), "bound must be nonnegative"), + ]; + for (field, value, expected) in cases { + let mut json = valid.clone(); + json[field] = value; + let error = serde_json::from_value::(json) + .unwrap_err() + .to_string(); + assert_eq!(error, format!("problem construction failed: {expected}")); + } +} diff --git a/src/unit_tests/models/graph/maximum_contact_map_overlap.rs b/src/unit_tests/models/graph/maximum_contact_map_overlap.rs index 2976a7617..d255687c4 100644 --- a/src/unit_tests/models/graph/maximum_contact_map_overlap.rs +++ b/src/unit_tests/models/graph/maximum_contact_map_overlap.rs @@ -198,3 +198,67 @@ fn test_maximum_contact_map_overlap_panics_on_duplicate_contact() { fn test_maximum_contact_map_overlap_panics_on_endpoint_out_of_range() { let _ = MaximumContactMapOverlap::new(3, vec![(0, 3)], 2, vec![]); } + +#[test] +fn test_maximum_contact_map_overlap_try_new_and_deserialization_reject_invalid_contacts() { + let valid = serde_json::json!({ + "num_vertices_1": 4, + "contacts_1": [[0, 1], [1, 2], [2, 3]], + "num_vertices_2": 3, + "contacts_2": [[0, 1], [0, 2]], + }); + let restored: MaximumContactMapOverlap = serde_json::from_value(valid.clone()).unwrap(); + assert_eq!(serde_json::to_value(&restored).unwrap(), valid); + + let cases = [ + ( + "contacts_1", + vec![(0, 1), (2, 2)], + "G_1 contact has self-loop: (2, 2)", + ), + ( + "contacts_2", + vec![(0, 1), (1, 0)], + "G_2 has duplicate contact after normalization: (0, 1)", + ), + ( + "contacts_1", + vec![(0, 4)], + "G_1 contact endpoint out of range for num_vertices = 4: (0, 4)", + ), + ( + "contacts_2", + vec![(3, 0)], + "G_2 contact endpoint out of range for num_vertices = 3: (3, 0)", + ), + ]; + for (field, contacts, expected) in cases { + let expected = format!("problem construction failed: {expected}"); + let mut json = valid.clone(); + json[field] = serde_json::json!(contacts); + let error = serde_json::from_value::(json).unwrap_err(); + assert_eq!(error.to_string(), expected); + + let (contacts_1, contacts_2) = match field { + "contacts_1" => (contacts, vec![(0, 1), (0, 2)]), + _ => (vec![(0, 1), (1, 2), (2, 3)], contacts), + }; + let error = MaximumContactMapOverlap::try_new(4, contacts_1, 3, contacts_2).unwrap_err(); + assert_eq!(error.to_string(), expected); + } +} + +#[test] +fn test_maximum_contact_map_overlap_deserialization_canonicalizes_contacts() { + let restored: MaximumContactMapOverlap = serde_json::from_value(serde_json::json!({ + "num_vertices_1": 3, + "contacts_1": [[2, 0]], + "num_vertices_2": 3, + "contacts_2": [[1, 0]], + })) + .unwrap(); + assert_eq!( + restored, + MaximumContactMapOverlap::new(3, vec![(0, 2)], 3, vec![(0, 1)]) + ); +} diff --git a/src/unit_tests/models/graph/mixed_chinese_postman.rs b/src/unit_tests/models/graph/mixed_chinese_postman.rs index e5c1528f3..fa082bc93 100644 --- a/src/unit_tests/models/graph/mixed_chinese_postman.rs +++ b/src/unit_tests/models/graph/mixed_chinese_postman.rs @@ -181,3 +181,41 @@ fn test_mixed_chinese_postman_ignores_isolated_vertices() { Min(Some(69)) ); } + +#[test] +fn test_mixed_chinese_postman_deserialization_rejects_invalid_weights() { + let valid = serde_json::to_value(sample_instance()).unwrap(); + let restored: MixedChinesePostman = serde_json::from_value(valid.clone()).unwrap(); + assert_eq!(serde_json::to_value(&restored).unwrap(), valid); + + let cases = [ + ( + "arc_weights", + serde_json::json!([2, 3, 1]), + "arc_weights length must match num_arcs", + ), + ( + "edge_weights", + serde_json::json!([2, 3, 1, 2, 7]), + "edge_weights length must match num_edges", + ), + ( + "arc_weights", + serde_json::json!([2, 3, -1, 4]), + "arc weight at index 2 must be nonnegative", + ), + ( + "edge_weights", + serde_json::json!([2, -3, 1, 2]), + "edge weight at index 1 must be nonnegative", + ), + ]; + for (field, value, expected) in cases { + let mut json = valid.clone(); + json[field] = value; + let error = serde_json::from_value::>(json) + .unwrap_err() + .to_string(); + assert_eq!(error, format!("problem construction failed: {expected}")); + } +} 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 4f78768d6..b045b99a5 100644 --- a/src/unit_tests/models/graph/path_constrained_network_flow.rs +++ b/src/unit_tests/models/graph/path_constrained_network_flow.rs @@ -175,3 +175,54 @@ fn test_path_constrained_network_flow_paper_example() { assert_eq!(all.len(), 2); assert!(all.contains(&config)); } + +#[test] +fn test_path_constrained_network_flow_deserialization_rejects_invalid_instances() { + let valid = serde_json::to_value(yes_instance()).unwrap(); + let restored: PathConstrainedNetworkFlow = serde_json::from_value(valid.clone()).unwrap(); + assert_eq!(serde_json::to_value(&restored).unwrap(), valid); + + let cases = [ + ( + "paths", + serde_json::json!([[99]]), + "arc index 99 out of bounds", + ), + ("paths", serde_json::json!([[]]), "must be non-empty"), + ("paths", serde_json::json!([[0, 5]]), "not contiguous"), + ( + "paths", + serde_json::json!([[0, 2, 5, 8], [0, 2]]), + "path 1: ", + ), + ( + "paths", + serde_json::json!([[0, 2]]), + "must end at sink 7, ended at 3", + ), + ( + "capacities", + serde_json::json!([1, 1]), + "capacities length must match graph num_arcs", + ), + ( + "source", + serde_json::json!(8), + "source (8) >= num_vertices (8)", + ), + ("sink", serde_json::json!(8), "sink (8) >= num_vertices (8)"), + ( + "sink", + serde_json::json!(0), + "source and sink must be distinct", + ), + ]; + for (field, value, expected) in cases { + let mut json = valid.clone(); + json[field] = value; + let error = serde_json::from_value::(json) + .unwrap_err() + .to_string(); + assert!(error.contains(expected), "{field}: {error}"); + } +} From ce5a8eda74467444a928cf19b0517e7c78069c0e Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Thu, 17 Sep 2026 23:28:46 +0800 Subject: [PATCH 29/42] Make default-feature builds warning-free Import SolveOutcome inside the example-db builders that are its only users, gate the example-only cover check, and drop the unread normalized_n field. Co-Authored-By: Claude Fable 5.1 --- src/rules/acyclicpartition_ilp.rs | 2 +- src/rules/biconnectivityaugmentation_ilp.rs | 2 +- src/rules/boundedcomponentspanningforest_ilp.rs | 2 +- src/rules/circuit_sat.rs | 2 +- src/rules/consecutiveonesmatrixaugmentation_ilp.rs | 2 +- src/rules/consecutiveonessubmatrix_ilp.rs | 2 +- ...ecisionmaximumindependentset_integralflowbundles.rs | 2 +- .../decisionminimumvertexcover_hamiltoniancircuit.rs | 1 + src/rules/ksatisfiability_bicliquecover.rs | 8 ++------ .../ksatisfiability_feasibleregisterassignment.rs | 2 +- src/rules/ksatisfiability_monochromatictriangle.rs | 2 +- src/rules/ksatisfiability_registersufficiency.rs | 2 +- src/rules/minimumexternalmacrodatacompression_ilp.rs | 2 +- ...ertexset_minimumcodegenerationunlimitedregisters.rs | 2 +- src/rules/minimuminternalmacrodatacompression_ilp.rs | 2 +- ...ement_sequencingtominimizeweightedcompletiontime.rs | 2 +- src/rules/paintshop_ilp.rs | 2 +- src/rules/rootedtreestorageassignment_ilp.rs | 2 +- src/rules/sequencingwithinintervals_ilp.rs | 2 +- src/rules/shortestcommonsupersequence_ilp.rs | 2 +- src/rules/sparsematrixcompression_ilp.rs | 2 +- src/rules/stringtostringcorrection_ilp.rs | 2 +- src/rules/strongconnectivityaugmentation_ilp.rs | 2 +- src/rules/undirectedtwocommodityintegralflow_ilp.rs | 2 +- src/unit_tests/rules/ksatisfiability_bicliquecover.rs | 10 ++++++++-- 25 files changed, 33 insertions(+), 30 deletions(-) diff --git a/src/rules/acyclicpartition_ilp.rs b/src/rules/acyclicpartition_ilp.rs index 004983192..91f96e346 100644 --- a/src/rules/acyclicpartition_ilp.rs +++ b/src/rules/acyclicpartition_ilp.rs @@ -10,7 +10,6 @@ use crate::reduction; use crate::rules::ilp_helpers::mccormick_product; use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; #[derive(Debug, Clone)] pub struct ReductionAcyclicPartitionToILP { @@ -146,6 +145,7 @@ impl ReduceTo> for AcyclicPartition { #[cfg(feature = "example-db")] pub(crate) fn canonical_rule_example_specs() -> Vec { use crate::export::SolutionPair; + use crate::solvers::SolveOutcome; use crate::topology::DirectedGraph; vec![crate::example_db::specs::RuleExampleSpec { id: "acyclicpartition_to_ilp", diff --git a/src/rules/biconnectivityaugmentation_ilp.rs b/src/rules/biconnectivityaugmentation_ilp.rs index 165add188..c00cdd8d3 100644 --- a/src/rules/biconnectivityaugmentation_ilp.rs +++ b/src/rules/biconnectivityaugmentation_ilp.rs @@ -9,7 +9,6 @@ use crate::models::graph::BiconnectivityAugmentation; use crate::reduction; use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; #[derive(Debug, Clone)] @@ -240,6 +239,7 @@ impl ReduceTo> for BiconnectivityAugmentation { #[cfg(feature = "example-db")] pub(crate) fn canonical_rule_example_specs() -> Vec { use crate::export::SolutionPair; + use crate::solvers::SolveOutcome; vec![crate::example_db::specs::RuleExampleSpec { id: "biconnectivityaugmentation_to_ilp", build: || { diff --git a/src/rules/boundedcomponentspanningforest_ilp.rs b/src/rules/boundedcomponentspanningforest_ilp.rs index e6389c000..6844ba24c 100644 --- a/src/rules/boundedcomponentspanningforest_ilp.rs +++ b/src/rules/boundedcomponentspanningforest_ilp.rs @@ -10,7 +10,6 @@ use crate::reduction; use crate::rules::ilp_helpers::one_hot_decode_rows; use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; #[derive(Debug, Clone)] @@ -192,6 +191,7 @@ impl ReduceTo> for BoundedComponentSpanningForest { #[cfg(feature = "example-db")] pub(crate) fn canonical_rule_example_specs() -> Vec { use crate::export::SolutionPair; + use crate::solvers::SolveOutcome; vec![crate::example_db::specs::RuleExampleSpec { id: "boundedcomponentspanningforest_to_ilp", build: || { diff --git a/src/rules/circuit_sat.rs b/src/rules/circuit_sat.rs index b7e18850f..e7d5a7350 100644 --- a/src/rules/circuit_sat.rs +++ b/src/rules/circuit_sat.rs @@ -7,7 +7,6 @@ use crate::reduction; use crate::rules::sat_helpers::SatVariableAllocator; use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use std::collections::HashMap; #[derive(Debug, Clone, PartialEq, Eq)] @@ -341,6 +340,7 @@ fn issue_example_source() -> CircuitSAT { pub(crate) fn canonical_rule_example_specs() -> Vec { use crate::export::SolutionPair; use crate::solvers::BruteForce; + use crate::solvers::SolveOutcome; vec![crate::example_db::specs::RuleExampleSpec { id: "circuitsat_to_satisfiability", diff --git a/src/rules/consecutiveonesmatrixaugmentation_ilp.rs b/src/rules/consecutiveonesmatrixaugmentation_ilp.rs index 145284717..cbf507470 100644 --- a/src/rules/consecutiveonesmatrixaugmentation_ilp.rs +++ b/src/rules/consecutiveonesmatrixaugmentation_ilp.rs @@ -11,7 +11,6 @@ use crate::reduction; use crate::rules::ilp_helpers::{one_hot_assignment_constraints, one_hot_decode}; use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; #[derive(Debug, Clone)] pub struct ReductionCOMAToILP { @@ -204,6 +203,7 @@ impl ReduceTo> for ConsecutiveOnesMatrixAugmentation { #[cfg(feature = "example-db")] pub(crate) fn canonical_rule_example_specs() -> Vec { use crate::export::SolutionPair; + use crate::solvers::SolveOutcome; vec![crate::example_db::specs::RuleExampleSpec { id: "consecutiveonesmatrixaugmentation_to_ilp", build: || { diff --git a/src/rules/consecutiveonessubmatrix_ilp.rs b/src/rules/consecutiveonessubmatrix_ilp.rs index 15f47c45b..bc41cdea7 100644 --- a/src/rules/consecutiveonessubmatrix_ilp.rs +++ b/src/rules/consecutiveonessubmatrix_ilp.rs @@ -8,7 +8,6 @@ use crate::models::algebraic::{ConsecutiveOnesSubmatrix, LinearConstraint, Objec use crate::reduction; use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; #[derive(Debug, Clone)] pub struct ReductionCOSToILP { @@ -218,6 +217,7 @@ impl ReduceTo> for ConsecutiveOnesSubmatrix { #[cfg(feature = "example-db")] pub(crate) fn canonical_rule_example_specs() -> Vec { use crate::export::SolutionPair; + use crate::solvers::SolveOutcome; vec![crate::example_db::specs::RuleExampleSpec { id: "consecutiveonessubmatrix_to_ilp", build: || { diff --git a/src/rules/decisionmaximumindependentset_integralflowbundles.rs b/src/rules/decisionmaximumindependentset_integralflowbundles.rs index 7e8ad154a..87511ec93 100644 --- a/src/rules/decisionmaximumindependentset_integralflowbundles.rs +++ b/src/rules/decisionmaximumindependentset_integralflowbundles.rs @@ -10,7 +10,6 @@ use crate::models::graph::{IntegralFlowBundles, MaximumIndependentSet}; use crate::reduction; use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; use crate::types::One; @@ -127,6 +126,7 @@ impl ReduceTo for Decision Vec { use crate::export::SolutionPair; use crate::solvers::BruteForce; + use crate::solvers::SolveOutcome; vec![crate::example_db::specs::RuleExampleSpec { id: "decisionmaximumindependentset_to_integralflowbundles", build: || { diff --git a/src/rules/decisionminimumvertexcover_hamiltoniancircuit.rs b/src/rules/decisionminimumvertexcover_hamiltoniancircuit.rs index 65e554d68..2468d23e3 100644 --- a/src/rules/decisionminimumvertexcover_hamiltoniancircuit.rs +++ b/src/rules/decisionminimumvertexcover_hamiltoniancircuit.rs @@ -65,6 +65,7 @@ impl TheoremConstruction { )) } + #[cfg(any(test, feature = "example-db"))] fn covers_all_edges(&self, selected: &[bool]) -> bool { self.edges .iter() diff --git a/src/rules/ksatisfiability_bicliquecover.rs b/src/rules/ksatisfiability_bicliquecover.rs index 9dadcd3d7..514a8ebf7 100644 --- a/src/rules/ksatisfiability_bicliquecover.rs +++ b/src/rules/ksatisfiability_bicliquecover.rs @@ -64,9 +64,6 @@ pub struct ReductionKSatisfiabilityToBicliqueCover { target: BicliqueCover, /// Number of variables in the source 3-CNF formula. source_num_vars: usize, - /// Number of normalized variables `n = 2^ell` (a power of two and - /// at least twice the number of appearing variables). Zero for sentinels. - normalized_n: usize, /// Bipartite-local offset of the `S_1` block on the left side. /// Used to locate vertex `s_11^u` for B_1 identification. s1_left_offset: usize, @@ -268,7 +265,6 @@ impl ReduceTo for KSatisfiability { return Ok(ReductionKSatisfiabilityToBicliqueCover { target: BicliqueCover::new(BipartiteGraph::new(size, size, edges), 0), source_num_vars, - normalized_n: 0, s1_left_offset: 0, s1_right_offset: 0, source_variables: vec![], @@ -505,7 +501,6 @@ impl ReduceTo for KSatisfiability { Ok(ReductionKSatisfiabilityToBicliqueCover { target, source_num_vars, - normalized_n: n, s1_left_offset: s_offset, s1_right_offset: s_offset, source_variables, @@ -765,7 +760,8 @@ fn forward_witness_single_variable_single_clause(source: &KSatisfiability) - let mut config = vec![vec![false; num_vertices]; k]; // Bipartite-local helpers, mirroring `reduce_to`. - let n = reduction.normalized_n; + let (n, _) = normalize(source, &reduction.source_variables) + .expect("canonical normalization must succeed"); let ell = ceil_log2(n).max(1); let m = 3usize; // hard-coded for the canonical case let k_f = free_edge_budget(ell, m).expect("canonical free-edge budget must fit usize"); diff --git a/src/rules/ksatisfiability_feasibleregisterassignment.rs b/src/rules/ksatisfiability_feasibleregisterassignment.rs index 55ba3cd7a..cb5ea2228 100644 --- a/src/rules/ksatisfiability_feasibleregisterassignment.rs +++ b/src/rules/ksatisfiability_feasibleregisterassignment.rs @@ -17,7 +17,6 @@ use crate::models::misc::FeasibleRegisterAssignment; use crate::reduction; use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::variant::K3; use std::collections::BTreeSet; @@ -239,6 +238,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec Vec for KSatisfiability { pub(crate) fn canonical_rule_example_specs() -> Vec { use crate::export::SolutionPair; use crate::models::formula::CNFClause; + use crate::solvers::SolveOutcome; vec![crate::example_db::specs::RuleExampleSpec { id: "ksatisfiability_to_registersufficiency", diff --git a/src/rules/minimumexternalmacrodatacompression_ilp.rs b/src/rules/minimumexternalmacrodatacompression_ilp.rs index dded452a9..3141fd882 100644 --- a/src/rules/minimumexternalmacrodatacompression_ilp.rs +++ b/src/rules/minimumexternalmacrodatacompression_ilp.rs @@ -18,7 +18,6 @@ use crate::models::misc::MinimumExternalMacroDataCompression; use crate::reduction; use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; /// Index layout for ILP variables. #[derive(Debug, Clone)] @@ -371,6 +370,7 @@ impl ReduceTo> for MinimumExternalMacroDataCompression { #[cfg(feature = "example-db")] pub(crate) fn canonical_rule_example_specs() -> Vec { use crate::export::SolutionPair; + use crate::solvers::SolveOutcome; // s = "ab" (len 2), alphabet {a,b} (size 2), h=2 // Optimal: uncompressed, D="" C="ab", cost = 0+2+0 = 2 diff --git a/src/rules/minimumfeedbackvertexset_minimumcodegenerationunlimitedregisters.rs b/src/rules/minimumfeedbackvertexset_minimumcodegenerationunlimitedregisters.rs index a79ad763d..ee8756d6a 100644 --- a/src/rules/minimumfeedbackvertexset_minimumcodegenerationunlimitedregisters.rs +++ b/src/rules/minimumfeedbackvertexset_minimumcodegenerationunlimitedregisters.rs @@ -11,7 +11,6 @@ use crate::models::misc::MinimumCodeGenerationUnlimitedRegisters; use crate::reduction; use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::types::One; /// Result of the unit-weight FVS to code-generation reduction. @@ -134,6 +133,7 @@ fn issue_example_source() -> MinimumFeedbackVertexSet { pub(crate) fn canonical_rule_example_specs() -> Vec { use crate::export::SolutionPair; use crate::solvers::BruteForce; + use crate::solvers::SolveOutcome; vec![crate::example_db::specs::RuleExampleSpec { id: "minimumfeedbackvertexset_to_minimumcodegenerationunlimitedregisters", diff --git a/src/rules/minimuminternalmacrodatacompression_ilp.rs b/src/rules/minimuminternalmacrodatacompression_ilp.rs index ae953dcc7..18f13aa76 100644 --- a/src/rules/minimuminternalmacrodatacompression_ilp.rs +++ b/src/rules/minimuminternalmacrodatacompression_ilp.rs @@ -20,7 +20,6 @@ use crate::models::misc::MinimumInternalMacroDataCompression; use crate::reduction; use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; /// Index layout for ILP variables. #[derive(Debug, Clone)] @@ -296,6 +295,7 @@ impl ReduceTo> for MinimumInternalMacroDataCompression { #[cfg(feature = "example-db")] pub(crate) fn canonical_rule_example_specs() -> Vec { use crate::export::SolutionPair; + use crate::solvers::SolveOutcome; // s = "ab" (len 2), alphabet {a,b} (size 2), h=2 // Optimal: uncompressed C="ab", cost = 2 diff --git a/src/rules/optimallineararrangement_sequencingtominimizeweightedcompletiontime.rs b/src/rules/optimallineararrangement_sequencingtominimizeweightedcompletiontime.rs index 49b8654b8..16cff5e91 100644 --- a/src/rules/optimallineararrangement_sequencingtominimizeweightedcompletiontime.rs +++ b/src/rules/optimallineararrangement_sequencingtominimizeweightedcompletiontime.rs @@ -16,7 +16,6 @@ use crate::models::misc::SequencingToMinimizeWeightedCompletionTime; use crate::reduction; use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; /// Result of reducing OptimalLinearArrangement to SequencingToMinimizeWeightedCompletionTime. @@ -129,6 +128,7 @@ pub(crate) fn canonical_rule_example_specs() -> Vec> for PaintShop { #[cfg(feature = "example-db")] pub(crate) fn canonical_rule_example_specs() -> Vec { use crate::export::SolutionPair; + use crate::solvers::SolveOutcome; vec![crate::example_db::specs::RuleExampleSpec { id: "paintshop_to_ilp", build: || { diff --git a/src/rules/rootedtreestorageassignment_ilp.rs b/src/rules/rootedtreestorageassignment_ilp.rs index 4f6123bb1..c91c4ee04 100644 --- a/src/rules/rootedtreestorageassignment_ilp.rs +++ b/src/rules/rootedtreestorageassignment_ilp.rs @@ -10,7 +10,6 @@ use crate::reduction; use crate::rules::ilp_helpers::{mccormick_product, one_hot_decode_rows}; use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; // Index helpers @@ -397,6 +396,7 @@ impl ReduceTo> for RootedTreeStorageAssignment { #[cfg(feature = "example-db")] pub(crate) fn canonical_rule_example_specs() -> Vec { use crate::export::SolutionPair; + use crate::solvers::SolveOutcome; vec![crate::example_db::specs::RuleExampleSpec { id: "rootedtreestorageassignment_to_ilp", build: || { diff --git a/src/rules/sequencingwithinintervals_ilp.rs b/src/rules/sequencingwithinintervals_ilp.rs index d64c88756..446d9eb1a 100644 --- a/src/rules/sequencingwithinintervals_ilp.rs +++ b/src/rules/sequencingwithinintervals_ilp.rs @@ -20,7 +20,6 @@ use crate::models::misc::SequencingWithinIntervals; use crate::reduction; use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; /// Result of reducing SequencingWithinIntervals to `ILP`. /// @@ -164,6 +163,7 @@ impl ReduceTo> for SequencingWithinIntervals { #[cfg(feature = "example-db")] pub(crate) fn canonical_rule_example_specs() -> Vec { use crate::export::SolutionPair; + use crate::solvers::SolveOutcome; vec![crate::example_db::specs::RuleExampleSpec { id: "sequencingwithinintervals_to_ilp", diff --git a/src/rules/shortestcommonsupersequence_ilp.rs b/src/rules/shortestcommonsupersequence_ilp.rs index 23a7940a0..a72ff2286 100644 --- a/src/rules/shortestcommonsupersequence_ilp.rs +++ b/src/rules/shortestcommonsupersequence_ilp.rs @@ -10,7 +10,6 @@ use crate::models::misc::ShortestCommonSupersequence; use crate::reduction; use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; #[derive(Debug, Clone)] pub struct ReductionSCSToILP { @@ -164,6 +163,7 @@ impl ReduceTo> for ShortestCommonSupersequence { #[cfg(feature = "example-db")] pub(crate) fn canonical_rule_example_specs() -> Vec { use crate::export::SolutionPair; + use crate::solvers::SolveOutcome; vec![crate::example_db::specs::RuleExampleSpec { id: "shortestcommonsupersequence_to_ilp", build: || { diff --git a/src/rules/sparsematrixcompression_ilp.rs b/src/rules/sparsematrixcompression_ilp.rs index eb0b01063..6b2a8793d 100644 --- a/src/rules/sparsematrixcompression_ilp.rs +++ b/src/rules/sparsematrixcompression_ilp.rs @@ -7,7 +7,6 @@ use crate::models::algebraic::{LinearConstraint, ObjectiveSense, SparseMatrixCom use crate::reduction; use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; #[derive(Debug, Clone)] pub struct ReductionSMCToILP { @@ -125,6 +124,7 @@ impl ReduceTo> for SparseMatrixCompression { #[cfg(feature = "example-db")] pub(crate) fn canonical_rule_example_specs() -> Vec { use crate::export::SolutionPair; + use crate::solvers::SolveOutcome; vec![crate::example_db::specs::RuleExampleSpec { id: "sparsematrixcompression_to_ilp", build: || { diff --git a/src/rules/stringtostringcorrection_ilp.rs b/src/rules/stringtostringcorrection_ilp.rs index 58e1eccb9..0d5cdd125 100644 --- a/src/rules/stringtostringcorrection_ilp.rs +++ b/src/rules/stringtostringcorrection_ilp.rs @@ -9,7 +9,6 @@ use crate::models::misc::StringToStringCorrection; use crate::reduction; use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; /// Result of reducing StringToStringCorrection to ILP. #[derive(Debug, Clone)] @@ -391,6 +390,7 @@ impl ReduceTo> for StringToStringCorrection { #[cfg(feature = "example-db")] pub(crate) fn canonical_rule_example_specs() -> Vec { use crate::export::SolutionPair; + use crate::solvers::SolveOutcome; vec![crate::example_db::specs::RuleExampleSpec { id: "stringtostringcorrection_to_ilp", build: || { diff --git a/src/rules/strongconnectivityaugmentation_ilp.rs b/src/rules/strongconnectivityaugmentation_ilp.rs index 7f63a8b55..c8d528657 100644 --- a/src/rules/strongconnectivityaugmentation_ilp.rs +++ b/src/rules/strongconnectivityaugmentation_ilp.rs @@ -9,7 +9,6 @@ use crate::models::graph::StrongConnectivityAugmentation; use crate::reduction; use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; #[derive(Debug, Clone)] pub struct ReductionSCAToILP { @@ -197,6 +196,7 @@ impl ReduceTo> for StrongConnectivityAugmentation { #[cfg(feature = "example-db")] pub(crate) fn canonical_rule_example_specs() -> Vec { use crate::export::SolutionPair; + use crate::solvers::SolveOutcome; use crate::topology::DirectedGraph; vec![crate::example_db::specs::RuleExampleSpec { id: "strongconnectivityaugmentation_to_ilp", diff --git a/src/rules/undirectedtwocommodityintegralflow_ilp.rs b/src/rules/undirectedtwocommodityintegralflow_ilp.rs index 0b2126d36..b53c7608f 100644 --- a/src/rules/undirectedtwocommodityintegralflow_ilp.rs +++ b/src/rules/undirectedtwocommodityintegralflow_ilp.rs @@ -29,7 +29,6 @@ use crate::models::graph::UndirectedTwoCommodityIntegralFlow; use crate::reduction; use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::topology::Graph; /// Result of reducing UndirectedTwoCommodityIntegralFlow to `ILP`. @@ -212,6 +211,7 @@ impl ReduceTo> for UndirectedTwoCommodityIntegralFlow { #[cfg(feature = "example-db")] pub(crate) fn canonical_rule_example_specs() -> Vec { use crate::export::SolutionPair; + use crate::solvers::SolveOutcome; use crate::topology::SimpleGraph; vec![crate::example_db::specs::RuleExampleSpec { diff --git a/src/unit_tests/rules/ksatisfiability_bicliquecover.rs b/src/unit_tests/rules/ksatisfiability_bicliquecover.rs index 0a77590a6..fb9ca9a43 100644 --- a/src/unit_tests/rules/ksatisfiability_bicliquecover.rs +++ b/src/unit_tests/rules/ksatisfiability_bicliquecover.rs @@ -221,7 +221,10 @@ fn test_ksatisfiability_to_bicliquecover_construct_two_vars_no_panic() { // m_normalized = 2 source + 2 * 2 = 6. // k_f = 4*2 + 2*ceil(log2 6) + 6 = 8 + 6 + 6 = 20. // rank = 20 + 4 + 2 = 26. - assert_eq!(reduction.normalized_n, 4); + assert_eq!( + normalize(&source, &reduction.source_variables).unwrap().0, + 4 + ); assert_eq!(target.k(), 26); } @@ -232,7 +235,10 @@ fn test_ksatisfiability_to_bicliquecover_sparse_variable_inverse() { let source = KSatisfiability::::new_allow_less(7, vec![CNFClause::new(vec![7])]); let reduction = ReduceTo::::reduce_to(&source).unwrap(); assert_eq!(reduction.source_variables, vec![6]); - assert_eq!(reduction.normalized_n, 2); + assert_eq!( + normalize(&source, &reduction.source_variables).unwrap().0, + 2 + ); let cover = super::forward_witness_single_variable_single_clause(&source); let assignment = reduction .recover_result( From 675711d812309b0444b00b02d7da2e0badf98db4 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Thu, 17 Sep 2026 23:34:30 +0800 Subject: [PATCH 30/42] Remove the unused Sum and And value wrappers Problem::Value requires EvaluationValue, which only Max, Min, Or, and Extremum implement, so Sum and And could no longer be model values. Delete the types, the Sum-only AggregationError::ArithmeticOverflow variant, their tests, and the docs and skills that advertised aggregate-only models. Rename the test fixtures that still described the removed aggregate reduction path. Co-Authored-By: Claude Fable 5.1 --- .claude/CLAUDE.md | 4 +- .claude/skills/add-model/SKILL.md | 13 +- .claude/skills/fix-issue/SKILL.md | 2 +- .claude/skills/review-structural/SKILL.md | 4 +- docs/src/design.md | 4 +- docs/src/static/trait-hierarchy-dark.svg | 2 +- docs/src/static/trait-hierarchy.svg | 2 +- docs/src/static/trait-hierarchy.typ | 5 +- src/lib.rs | 7 +- src/types.rs | 49 ----- src/unit_tests/registry/dispatch.rs | 3 +- src/unit_tests/rules/graph.rs | 252 +++++++++++----------- src/unit_tests/rules/traits.rs | 56 ++--- src/unit_tests/solvers/brute_force.rs | 25 +-- src/unit_tests/types.rs | 46 ---- 15 files changed, 173 insertions(+), 301 deletions(-) diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index fc5bda591..0b563b63d 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -149,7 +149,7 @@ solves. Common aggregate wrappers live in `src/types.rs`: ```rust -Max, Min, Sum, Or, And, Extremum, ExtremumSense +Max, Min, Or, Extremum, ExtremumSense ``` `OptimizationValue` trait (in `src/types.rs`) enables generic Decision conversion: @@ -171,7 +171,7 @@ Max, Min, Sum, Or, And, Extremum, ExtremumSense - `ReductionResult` provides `target_problem()` and mandatory `recover_result(source, target_outcome)`. Recovery returns typed `Optimal`, `Feasible`, or `Infeasible` outcomes, including solution and evaluation. Each rule handles all statuses explicitly; no optional completion callback or separate value-only path exists. - `pred solve bundle.json` and `pred extract bundle.json --result target-result.json` use the same complete recovery. External results declare their status; the transport boundary validates target feasibility, while the external solver supplies the optimality claim. Insufficient witness quality is an error, never evidence of source infeasibility. - Decode only the reduction's defined mathematical mapping. Preserve reachable mathematical and representation errors; do not add fallback values or recovery branches for violations already excluded by the calling contract. Explicit mathematical alternatives and sentinels are allowed. -- CLI-facing dynamic formatting uses aggregate wrapper names directly (for example `Max(2)`, `Min(None)`, `Or(true)`, or `Sum(56)`) +- CLI-facing dynamic formatting uses aggregate wrapper names directly (for example `Max(2)`, `Min(None)`, or `Or(true)`) - Graph types: SimpleGraph, PlanarGraph, BipartiteGraph, UnitDiskGraph, KingsSubgraph, TriangularSubgraph - Weight types: `One` (unit weight marker), `i64`, `f64` — all implement `WeightElement` trait - `WeightElement` trait: `type Sum: NumericSize` + `fn to_sum(&self)` — converts weight to a summable numeric type diff --git a/.claude/skills/add-model/SKILL.md b/.claude/skills/add-model/SKILL.md index 4df33fdb6..926eb901b 100644 --- a/.claude/skills/add-model/SKILL.md +++ b/.claude/skills/add-model/SKILL.md @@ -17,7 +17,7 @@ Before any implementation, collect all required information. If called from `iss |---|------|-------------|---------| | 1 | **Problem name** | Struct name with optimization prefix | `MaximumClique`, `MinimumDominatingSet` | | 2 | **Mathematical definition** | Formal definition with objective/constraints | "Given graph G=(V,E), find max-weight subset S where all pairs in S are adjacent" | -| 3 | **Problem type** | Objective (`Max`/`Min`), witness (`bool`), or aggregate-only (`Sum`/`And`/custom `Aggregate`) | Objective (Maximize) | +| 3 | **Problem type** | Objective (`Max`/`Min`/`Extremum`) or witness (`Or`) | Objective (Maximize) | | 4 | **Type parameters** | Graph type `G`, weight type `W`, or other | `G: Graph`, `W: WeightElement` | | 5 | **Struct fields** | What the struct holds | `graph: G`, `weights: Vec` | | 6 | **Configuration space** | Mathematical solution representation and domain | One Boolean selection per vertex | @@ -26,7 +26,7 @@ Before any implementation, collect all required information. If called from `iss | 9 | **Best known exact algorithm** | Complexity with variable definitions | "O(1.1996^n) by Xiao & Nagamochi (2017), where n = \|V\|" | | 10 | **Solving strategy** | How it can be solved | "BruteForce works; ILP reduction available" | | 11 | **Category** | Which sub-module under `src/models/` | `graph`, `formula`, `set`, `algebraic`, `misc` | -| 12 | **Expected outcome from the issue** | Concrete outcome for the issue's example instance | Objective: one optimal solution + optimal value. Witness: one valid/satisfying solution + why it is valid. Aggregate-only: the final aggregate value and how it is derived | +| 12 | **Expected outcome from the issue** | Concrete outcome for the issue's example instance | Objective: one optimal solution + optimal value. Witness: one valid/satisfying solution + why it is valid | If any item is missing, ask the user to provide it. Do NOT proceed until the checklist is complete. @@ -66,7 +66,7 @@ Read these first to understand the patterns: - **Optimization problem:** `src/models/graph/maximum_independent_set.rs` - **Satisfaction problem:** `src/models/formula/sat.rs` - **Model tests:** `src/unit_tests/models/graph/maximum_independent_set.rs` -- **Trait definitions / aggregate types:** `src/traits.rs` (`Problem`), `src/types.rs` (`Aggregate`, `Max`, `Min`, `Sum`, `Or`, `And`, `Extremum`) +- **Trait definitions / aggregate types:** `src/traits.rs` (`Problem`), `src/types.rs` (`Aggregate`, `Max`, `Min`, `Or`, `Extremum`) - **Registry dispatch boundary:** `src/registry/mod.rs`, `src/registry/variant.rs` - **CLI and MCP construction:** discovered from the model's registry entry; no frontend model-name dispatch - **Canonical model examples:** `src/example_db/model_builders.rs` @@ -129,7 +129,7 @@ Key decisions: - **Schema metadata:** `ProblemSchemaEntry` must include the explicit structural `category` and reflect the construction interface through `display_name`, `aliases`, `dimensions`, and `fields` - **Objective problems:** use `type Value = Max<_>`, `Min<_>`, or `Extremum<_>` when the model should expose optimization-style witness helpers - **Witness problems:** use `type Value = Or` for existential feasibility problems -- **Aggregate-only problems:** use a value-only aggregate such as `Sum<_>`, `And`, or a custom `Aggregate` when witnesses are not meaningful +- **No value-only problems:** `Problem::Value` must implement `EvaluationValue` (`Max`, `Min`, `Extremum`, `Or`); global counts or statistics without a representative solution are not modeled as `Problem` - **Weight management:** use inherent methods (`weights()`, `set_weights()`, `is_weighted()`), NOT traits - **`dims()`:** returns the configuration space dimensions (e.g., `vec![2; n]` for binary variables) - **`evaluate()`:** must return `Result`. Invalid configurations remain the aggregate's invalid/false contribution; arithmetic overflow and non-finite computed values are errors. @@ -155,7 +155,7 @@ crate::declare_variants! { - A compiled `complexity_eval_fn` plus registry-backed load/serialize/solve dispatch metadata are auto-generated alongside the symbolic expression - See `src/models/graph/maximum_independent_set.rs` for the reference pattern -`declare_variants!` now handles objective, witness-capable, and aggregate-only models uniformly. Use manual `VariantEntry` wiring only for unusual dynamic-registration work, not for ordinary models. +`declare_variants!` handles objective and witness models uniformly. Use manual `VariantEntry` wiring only for unusual dynamic-registration work, not for ordinary models. ## Step 3: Register the model @@ -320,10 +320,9 @@ Structural and quality review is handled by the `review-pipeline` stage, not her | Omitting or inferring the model category | Set the required `ProblemSchemaEntry.category` explicitly to one of `Algebraic`, `Formula`, `Graph`, `Misc`, or `Set`; never parse `module_path!()`. | | Missing `#[path]` test link | Add `#[cfg(test)] #[path = "..."] mod tests;` at file bottom | | Wrong `dims()` | Must match the actual configuration space (e.g., `vec![2; n]` for binary) | -| Using the wrong aggregate wrapper | Objective models use `Max` / `Min` / `Extremum`, witness models use `bool`, aggregate-only models use a fold value like `Sum` / `And` | +| Using the wrong aggregate wrapper | Objective models use `Max` / `Min` / `Extremum`, witness models use `Or` | | Not registering in `mod.rs` | Must update both `/mod.rs` and `models/mod.rs` | | Forgetting `declare_variants!` | Required for variant complexity metadata and registry-backed load/serialize/solve dispatch | -| Wrong aggregate wrapper | Use `Max` / `Min` / `Extremum` for objective problems, `Or` for existential witness problems, and `Sum` / `And` (or a custom aggregate) for value-only folds | | Wrong `declare_variants!` syntax | Entries no longer use `opt` / `sat`; one entry per problem may be marked `default` | | Adding aliases in CLI code | Declare problem aliases in `ProblemSchemaEntry.aliases` and variant aliases in `declare_variants!` | | Adding a hand-written decision model | Use `Decision

` wrapper instead — see `decision_problem_meta!` + `register_decision_variant!` in `src/models/graph/minimum_vertex_cover.rs` for the pattern | diff --git a/.claude/skills/fix-issue/SKILL.md b/.claude/skills/fix-issue/SKILL.md index fe1d1d777..b8cfff097 100644 --- a/.claude/skills/fix-issue/SKILL.md +++ b/.claude/skills/fix-issue/SKILL.md @@ -196,7 +196,7 @@ Tag each issue as: | Incorrect mathematical claims | Domain expertise needed | | Incomplete reduction algorithm | Core technical content | | Incomplete or trivial example | Present **3 concrete example options** with pros/cons (use `AskUserQuestion` with previews showing vertex/edge counts, optimal values, and suboptimal cases). Prefer examples that match the model issue's example when a companion model exists. | -| Decision vs optimization framing | **Default to objective-style models** unless evidence points otherwise. In the current aggregate-value architecture, that usually means `type Value = Max<_>`, `Min<_>`, or `Extremum<_>` when the sense is runtime data. Check associated `[Rule]` issues (`gh issue list --search " in:title label:rule"`) to see how rules use this model — if rules only need the decision version (e.g., reducing to SAT with a bound), an objective model still works because the bound can be read from the optimal aggregate value. Use `Or` for inherently existential feasibility problems (SAT, KColoring) where there is no natural objective. Use aggregate-only values such as `Sum<_>` or `And` only when the answer is genuinely a fold over all configurations and there is no representative witness. If switching to an objective model, add the appropriate `Minimum`/`Maximum` prefix per codebase conventions. | +| Decision vs optimization framing | **Default to objective-style models** unless evidence points otherwise. In the current aggregate-value architecture, that usually means `type Value = Max<_>`, `Min<_>`, or `Extremum<_>` when the sense is runtime data. Check associated `[Rule]` issues (`gh issue list --search " in:title label:rule"`) to see how rules use this model — if rules only need the decision version (e.g., reducing to SAT with a bound), an objective model still works because the bound can be read from the optimal aggregate value. Use `Or` for inherently existential feasibility problems (SAT, KColoring) where there is no natural objective. A quantity that is a fold over all configurations with no representative witness (counting, global statistics) is not a `Problem` model. If switching to an objective model, add the appropriate `Minimum`/`Maximum` prefix per codebase conventions. | | Ambiguous overhead expressions | Requires understanding the reduction | --- diff --git a/.claude/skills/review-structural/SKILL.md b/.claude/skills/review-structural/SKILL.md index ed7a0365d..0dfe94a3e 100644 --- a/.claude/skills/review-structural/SKILL.md +++ b/.claude/skills/review-structural/SKILL.md @@ -109,7 +109,7 @@ Report pass/fail. If tests fail, identify which tests. **Do NOT fix anything** ## Step 4: Semantic Review ### For Models: -1. **`evaluate()` correctness** — Does it check feasibility before computing the objective when the model has invalid configurations? Objective models should return `Max/Min/Extremum(None)` for infeasible configs, witness problems should return `false`, and aggregate-only models should return the per-configuration contribution that matches the intended fold semantics. +1. **`evaluate()` correctness** — Does it check feasibility before computing the objective when the model has invalid configurations? Objective models should return `Max/Min/Extremum(None)` for infeasible configs, and witness problems should return `Or(false)`. 2. **`dims()` correctness** — Does it return the actual configuration space? (e.g., `vec![2; n]` for binary) 3. **Size getter consistency** — Do inherent getter methods (e.g., `num_vertices()`, `num_edges()`) match names used in overhead expressions? 4. **Weight handling** — Are weights managed via inherent methods, not traits? @@ -131,7 +131,7 @@ Only if a linked issue was provided. |---|-------| | 1 | Problem name matches issue | | 2 | Mathematical definition matches | -| 3 | Problem framing (objective / witness / aggregate-only) matches | +| 3 | Problem framing (objective / witness) matches | | 4 | Type parameters match | | 5 | Configuration space matches | | 6 | Feasibility check matches | diff --git a/docs/src/design.md b/docs/src/design.md index 3b7dada76..0f8b735b0 100644 --- a/docs/src/design.md +++ b/docs/src/design.md @@ -42,7 +42,7 @@ trait Problem: Clone { - **Objective problems** — typically use `Max`, `Min`, or `Extremum` as `Value`. - **Feasibility problems** — typically use `Or`. - **Solve contract** — a successful solve always returns the problem's `Solution`; a global count or statistic without a representative solution is not a `Problem` solve. -- **Common aggregate wrappers** — `Max`, `Min`, `Sum`, `Or`, `And`, `Extremum`, `ExtremumSense`. +- **Common aggregate wrappers** — `Max`, `Min`, `Or`, `Extremum`, `ExtremumSense`. ## Construction inputs @@ -198,7 +198,7 @@ its source-result relation, including thresholds and sentinel constructions. Guarantees must cover every qualifying witness, including tied optima. `SolutionAggregate` remains a brute-force solver capability for selecting from -an enumeration. Mathematical wrappers such as `Min`, `Max`, `Or`, and `Sum` +an enumeration. Mathematical wrappers such as `Min`, `Max`, `Or`, and `Extremum` remain model values. They do not require separate reduction traits or graph modes. Turing edges describe multiple adaptive queries and are retained only as theoretical graph relationships, not executable reductions. The library does not diff --git a/docs/src/static/trait-hierarchy-dark.svg b/docs/src/static/trait-hierarchy-dark.svg index 3e35702f4..8393552e9 100644 --- a/docs/src/static/trait-hierarchy-dark.svg +++ b/docs/src/static/trait-hierarchy-dark.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/docs/src/static/trait-hierarchy.svg b/docs/src/static/trait-hierarchy.svg index a1b82bfb0..de23ebdc2 100644 --- a/docs/src/static/trait-hierarchy.svg +++ b/docs/src/static/trait-hierarchy.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/docs/src/static/trait-hierarchy.typ b/docs/src/static/trait-hierarchy.typ index a9ce6343d..f1d9d6b94 100644 --- a/docs/src/static/trait-hierarchy.typ +++ b/docs/src/static/trait-hierarchy.typ @@ -66,9 +66,8 @@ node((0, 2), box(width: 48mm, align(left)[ #strong[Common Value Types]\ #text(size: 8pt, fill: secondary)[ - `Max | Min | Extremum`\ - `Or | Sum | And`\ - #text(style: "italic")[only selecting values implement `SolutionAggregate`] + `Max | Min | Extremum | Or`\ + #text(style: "italic")[all implement `SolutionAggregate`] ] ]), fill: type-fill, corner-radius: 6pt, inset: 10pt, name: ), diff --git a/src/lib.rs b/src/lib.rs index e84718db2..8b1b2a1cd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -106,9 +106,7 @@ pub mod prelude { // Types pub use crate::error::{ProblemError, Result}; - pub use crate::types::{ - And, Extremum, ExtremumSense, Max, Min, One, Or, ProblemParameters, Sum, - }; + pub use crate::types::{Extremum, ExtremumSense, Max, Min, One, Or, ProblemParameters}; } // Re-export commonly used items at crate root @@ -122,8 +120,7 @@ pub use registry::{ComplexityClass, ProblemInfo}; pub use solvers::BruteForce; pub use traits::{EvaluationValue, Problem}; pub use types::{ - And, Extremum, ExtremumSense, Max, Min, NumericSize, One, Or, ProblemParameters, Sum, - WeightElement, + Extremum, ExtremumSense, Max, Min, NumericSize, One, Or, ProblemParameters, WeightElement, }; // Re-export proc macros for reduction registration and variant declaration diff --git a/src/types.rs b/src/types.rs index 7b19d3e4e..1fba6f5b9 100644 --- a/src/types.rs +++ b/src/types.rs @@ -284,8 +284,6 @@ impl std::fmt::Display for One { /// Failure while combining configuration values during a solve. #[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] pub enum AggregationError { - #[error("aggregate arithmetic overflow or non-finite result")] - ArithmeticOverflow, #[error("aggregate values are not comparable")] UnorderedComparison, #[error("cannot combine extrema with different optimization senses")] @@ -449,29 +447,6 @@ impl Optimiza } } -/// Additive fold value. -#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] -pub struct Sum(pub W); - -impl Aggregate for Sum { - fn identity() -> Self { - Sum(W::zero()) - } - - fn combine(self, other: Self) -> Result { - self.0 - .checked_add_value(other.0) - .map(Sum) - .map_err(|_| AggregationError::ArithmeticOverflow) - } -} - -impl fmt::Display for Sum { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "Sum({})", self.0) - } -} - /// Disjunction aggregate for existential satisfaction. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] pub struct Or(pub bool); @@ -532,30 +507,6 @@ impl PartialEq for bool { } } -/// Conjunction aggregate for universal satisfaction. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] -pub struct And(pub bool); - -impl Aggregate for And { - fn identity() -> Self { - And(true) - } - - fn combine(self, other: Self) -> Result { - Ok(And(self.0 && other.0)) - } - - fn is_absorbing(&self) -> bool { - !self.0 - } -} - -impl fmt::Display for And { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "And({})", self.0) - } -} - #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum ExtremumSense { Maximize, diff --git a/src/unit_tests/registry/dispatch.rs b/src/unit_tests/registry/dispatch.rs index 9452c9cd6..8b665788e 100644 --- a/src/unit_tests/registry/dispatch.rs +++ b/src/unit_tests/registry/dispatch.rs @@ -5,7 +5,7 @@ use crate::registry::variant::find_variant_entry; use crate::registry::{load_dyn, serialize_any, DynProblem, LoadedDynProblem}; use crate::solvers::{brute_force_dimensions, solve, SolveOutcome, SolverRequest}; use crate::topology::SimpleGraph; -use crate::types::{Max, Sum}; +use crate::types::Max; use crate::Problem; use std::any::Any; use std::collections::BTreeMap; @@ -375,7 +375,6 @@ fn test_format_metric_uses_display() { assert_eq!(format_metric(&Max::(None)), "Max(None)"); assert_eq!(format_metric(&Min(Some(7))), "Min(7)"); assert_eq!(format_metric(&Or(true)), "Or(true)"); - assert_eq!(format_metric(&Sum(99u64)), "Sum(99)"); } #[test] diff --git a/src/unit_tests/rules/graph.rs b/src/unit_tests/rules/graph.rs index 7d105b259..8c6260f39 100644 --- a/src/unit_tests/rules/graph.rs +++ b/src/unit_tests/rules/graph.rs @@ -68,19 +68,19 @@ fn named_path(names: &[&str]) -> ReductionPath { } #[derive(Clone)] -struct AggregateChainSource; +struct OffsetChainSource; #[derive(Clone)] -struct AggregateChainMiddle; +struct OffsetChainMiddle; #[derive(Clone)] -struct AggregateChainTarget; +struct OffsetChainTarget; #[derive(Clone)] struct NaturalVariantProblem; -impl Problem for AggregateChainSource { - const NAME: &'static str = "AggregateChainSource"; +impl Problem for OffsetChainSource { + const NAME: &'static str = "OffsetChainSource"; type Solution = Vec; type Value = Min; @@ -103,7 +103,7 @@ impl Problem for AggregateChainSource { } } -impl crate::solvers::BruteForceProblem for AggregateChainSource { +impl crate::solvers::BruteForceProblem for OffsetChainSource { fn num_variables(&self) -> Result { Ok(1usize) } @@ -113,8 +113,8 @@ impl crate::solvers::BruteForceProblem for AggregateChainSource { } } -impl Problem for AggregateChainMiddle { - const NAME: &'static str = "AggregateChainMiddle"; +impl Problem for OffsetChainMiddle { + const NAME: &'static str = "OffsetChainMiddle"; type Solution = Vec; type Value = Min; @@ -137,7 +137,7 @@ impl Problem for AggregateChainMiddle { } } -impl crate::solvers::BruteForceProblem for AggregateChainMiddle { +impl crate::solvers::BruteForceProblem for OffsetChainMiddle { fn num_variables(&self) -> Result { Ok(1usize) } @@ -147,8 +147,8 @@ impl crate::solvers::BruteForceProblem for AggregateChainMiddle { } } -impl Problem for AggregateChainTarget { - const NAME: &'static str = "AggregateChainTarget"; +impl Problem for OffsetChainTarget { + const NAME: &'static str = "OffsetChainTarget"; type Solution = Vec; type Value = Min; @@ -171,7 +171,7 @@ impl Problem for AggregateChainTarget { } } -impl crate::solvers::BruteForceProblem for AggregateChainTarget { +impl crate::solvers::BruteForceProblem for OffsetChainTarget { fn num_variables(&self) -> Result { Ok(1usize) } @@ -215,13 +215,13 @@ impl crate::solvers::BruteForceProblem for NaturalVariantProblem { } } -struct SourceToMiddleAggregateResult { - target: AggregateChainMiddle, +struct SourceToMiddleOffsetResult { + target: OffsetChainMiddle, } -impl ReductionResult for SourceToMiddleAggregateResult { - type Source = AggregateChainSource; - type Target = AggregateChainMiddle; +impl ReductionResult for SourceToMiddleOffsetResult { + type Source = OffsetChainSource; + type Target = OffsetChainMiddle; fn target_problem(&self) -> &Self::Target { &self.target @@ -246,13 +246,13 @@ impl ReductionResult for SourceToMiddleAggregateResult { } } -struct MiddleToTargetAggregateResult { - target: AggregateChainTarget, +struct MiddleToTargetOffsetResult { + target: OffsetChainTarget, } -impl ReductionResult for MiddleToTargetAggregateResult { - type Source = AggregateChainMiddle; - type Target = AggregateChainTarget; +impl ReductionResult for MiddleToTargetOffsetResult { + type Source = OffsetChainMiddle; + type Target = OffsetChainTarget; fn target_problem(&self) -> &Self::Target { &self.target @@ -277,47 +277,47 @@ impl ReductionResult for MiddleToTargetAggregateResult { } } -fn reduce_source_to_middle_aggregate( +fn reduce_source_to_middle_offset( any: &dyn Any, ) -> Result { - any.downcast_ref::().ok_or( + any.downcast_ref::().ok_or( crate::rules::ReductionError::SourceTypeMismatch { - source_problem: AggregateChainSource::NAME, - target_problem: AggregateChainMiddle::NAME, - expected: std::any::type_name::(), + source_problem: OffsetChainSource::NAME, + target_problem: OffsetChainMiddle::NAME, + expected: std::any::type_name::(), }, )?; Ok(crate::rules::registry::ExecutedStep { - witness: std::rc::Rc::new(SourceToMiddleAggregateResult { - target: AggregateChainMiddle, + witness: std::rc::Rc::new(SourceToMiddleOffsetResult { + target: OffsetChainMiddle, }), }) } -fn reduce_middle_to_target_aggregate( +fn reduce_middle_to_target_offset( any: &dyn Any, ) -> Result { - any.downcast_ref::().ok_or( + any.downcast_ref::().ok_or( crate::rules::ReductionError::SourceTypeMismatch { - source_problem: AggregateChainMiddle::NAME, - target_problem: AggregateChainTarget::NAME, - expected: std::any::type_name::(), + source_problem: OffsetChainMiddle::NAME, + target_problem: OffsetChainTarget::NAME, + expected: std::any::type_name::(), }, )?; Ok(crate::rules::registry::ExecutedStep { - witness: std::rc::Rc::new(MiddleToTargetAggregateResult { - target: AggregateChainTarget, + witness: std::rc::Rc::new(MiddleToTargetOffsetResult { + target: OffsetChainTarget, }), }) } struct SourceToMiddleWitnessResult { - target: AggregateChainMiddle, + target: OffsetChainMiddle, } impl ReductionResult for SourceToMiddleWitnessResult { - type Source = AggregateChainSource; - type Target = AggregateChainMiddle; + type Source = OffsetChainSource; + type Target = OffsetChainMiddle; fn target_problem(&self) -> &Self::Target { &self.target @@ -358,16 +358,16 @@ impl SourceToMiddleWitnessResult { fn reduce_source_to_middle_witness( any: &dyn Any, ) -> Result { - any.downcast_ref::().ok_or( + any.downcast_ref::().ok_or( crate::rules::ReductionError::SourceTypeMismatch { - source_problem: AggregateChainSource::NAME, - target_problem: AggregateChainMiddle::NAME, - expected: std::any::type_name::(), + source_problem: OffsetChainSource::NAME, + target_problem: OffsetChainMiddle::NAME, + expected: std::any::type_name::(), }, )?; Ok(crate::rules::registry::ExecutedStep { witness: std::rc::Rc::new(SourceToMiddleWitnessResult { - target: AggregateChainMiddle, + target: OffsetChainMiddle, }), }) } @@ -376,8 +376,8 @@ fn fail_source_to_middle_witness( _any: &dyn Any, ) -> Result { Err(crate::rules::ReductionError::InvalidTarget { - source_problem: AggregateChainSource::NAME, - target_problem: AggregateChainMiddle::NAME, + source_problem: OffsetChainSource::NAME, + target_problem: OffsetChainMiddle::NAME, message: "synthetic target construction failure".to_string(), }) } @@ -392,12 +392,12 @@ fn reduce_counted_source_to_middle_witness( } struct MiddleToTargetWitnessResult { - target: AggregateChainTarget, + target: OffsetChainTarget, } impl ReductionResult for MiddleToTargetWitnessResult { - type Source = AggregateChainMiddle; - type Target = AggregateChainTarget; + type Source = OffsetChainMiddle; + type Target = OffsetChainTarget; fn target_problem(&self) -> &Self::Target { &self.target @@ -438,16 +438,16 @@ impl MiddleToTargetWitnessResult { fn reduce_middle_to_target_witness( any: &dyn Any, ) -> Result { - any.downcast_ref::().ok_or( + any.downcast_ref::().ok_or( crate::rules::ReductionError::SourceTypeMismatch { - source_problem: AggregateChainMiddle::NAME, - target_problem: AggregateChainTarget::NAME, - expected: std::any::type_name::(), + source_problem: OffsetChainMiddle::NAME, + target_problem: OffsetChainTarget::NAME, + expected: std::any::type_name::(), }, )?; Ok(crate::rules::registry::ExecutedStep { witness: std::rc::Rc::new(MiddleToTargetWitnessResult { - target: AggregateChainTarget, + target: OffsetChainTarget, }), }) } @@ -521,29 +521,29 @@ fn execute_paths_executes_a_shared_prefix_once() { }; let graph = ReductionGraph::from_test_edges( &[ - AggregateChainSource::NAME, - AggregateChainMiddle::NAME, - AggregateChainTarget::NAME, + OffsetChainSource::NAME, + OffsetChainMiddle::NAME, + OffsetChainTarget::NAME, ], &[ ( - AggregateChainSource::NAME, - AggregateChainMiddle::NAME, + OffsetChainSource::NAME, + OffsetChainMiddle::NAME, witness_edge(reduce_counted_source_to_middle_witness), ), ( - AggregateChainMiddle::NAME, - AggregateChainTarget::NAME, + OffsetChainMiddle::NAME, + OffsetChainTarget::NAME, witness_edge(reduce_middle_to_target_witness), ), ], ); let mut paths = vec![ - named_path(&[AggregateChainSource::NAME, AggregateChainMiddle::NAME]), + named_path(&[OffsetChainSource::NAME, OffsetChainMiddle::NAME]), named_path(&[ - AggregateChainSource::NAME, - AggregateChainMiddle::NAME, - AggregateChainTarget::NAME, + OffsetChainSource::NAME, + OffsetChainMiddle::NAME, + OffsetChainTarget::NAME, ]), ]; @@ -551,7 +551,7 @@ fn execute_paths_executes_a_shared_prefix_once() { paths.push(paths[1].clone()); let executed = graph - .execute_paths(&paths, &AggregateChainSource) + .execute_paths(&paths, &OffsetChainSource) .expect("both paths are executable"); assert_eq!(executed.len(), 4); @@ -559,8 +559,8 @@ fn execute_paths_executes_a_shared_prefix_once() { assert_eq!(execution.steps.len(), path.len()); assert_eq!( execution - .recover_result::( - &AggregateChainSource, + .recover_result::( + &OffsetChainSource, SolveOutcome::Optimal { solution: vec![1usize], evaluation: Min(Some(1)) @@ -759,24 +759,24 @@ fn test_find_direct_path() { } #[test] -fn test_aggregate_reduction_chain_extracts_value_backwards() { +fn test_reduction_chain_recovers_result_backwards() { let source_variant = BTreeMap::new(); let middle_variant = BTreeMap::new(); let target_variant = BTreeMap::new(); let nodes = vec![ VariantNode { - name: AggregateChainSource::NAME, + name: OffsetChainSource::NAME, variant: source_variant.clone(), complexity: "", }, VariantNode { - name: AggregateChainMiddle::NAME, + name: OffsetChainMiddle::NAME, variant: middle_variant.clone(), complexity: "", }, VariantNode { - name: AggregateChainTarget::NAME, + name: OffsetChainTarget::NAME, variant: target_variant.clone(), complexity: "", }, @@ -792,7 +792,7 @@ fn test_aggregate_reduction_chain_extracts_value_backwards() { middle_idx, ReductionEdgeData { parameter_contract: empty_parameter_contract(), - reduce_fn: Some(reduce_source_to_middle_aggregate), + reduce_fn: Some(reduce_source_to_middle_offset), turing: false, }, ); @@ -801,7 +801,7 @@ fn test_aggregate_reduction_chain_extracts_value_backwards() { target_idx, ReductionEdgeData { parameter_contract: empty_parameter_contract(), - reduce_fn: Some(reduce_middle_to_target_aggregate), + reduce_fn: Some(reduce_middle_to_target_offset), turing: false, }, ); @@ -810,44 +810,43 @@ fn test_aggregate_reduction_chain_extracts_value_backwards() { graph, nodes, name_to_nodes: HashMap::from([ - (AggregateChainSource::NAME, vec![source_idx]), - (AggregateChainMiddle::NAME, vec![middle_idx]), - (AggregateChainTarget::NAME, vec![target_idx]), + (OffsetChainSource::NAME, vec![source_idx]), + (OffsetChainMiddle::NAME, vec![middle_idx]), + (OffsetChainTarget::NAME, vec![target_idx]), ]), default_variants: HashMap::new(), }; let path = ReductionPath { steps: vec![ ReductionStep { - name: AggregateChainSource::NAME.to_string(), + name: OffsetChainSource::NAME.to_string(), variant: source_variant, }, ReductionStep { - name: AggregateChainMiddle::NAME.to_string(), + name: OffsetChainMiddle::NAME.to_string(), variant: middle_variant, }, ReductionStep { - name: AggregateChainTarget::NAME.to_string(), + name: OffsetChainTarget::NAME.to_string(), variant: target_variant, }, ], }; let chain = reduction_graph - .reduce_along_path(&path, &AggregateChainSource as &dyn Any) - .expect("aggregate reduction should not fail") - .expect("expected aggregate reduction chain"); + .reduce_along_path(&path, &OffsetChainSource as &dyn Any) + .expect("offset reduction should not fail") + .expect("expected offset reduction chain"); assert_eq!( - crate::solvers::cartesian_dimensions(chain.target_problem::()) - .unwrap(), + crate::solvers::cartesian_dimensions(chain.target_problem::()).unwrap(), vec![1] ); assert_eq!( chain - .recover_result::( - &AggregateChainSource, - SolveOutcome::optimal(chain.target_problem::(), vec![7]) + .recover_result::( + &OffsetChainSource, + SolveOutcome::optimal(chain.target_problem::(), vec![7]) .unwrap() ) .unwrap(), @@ -863,9 +862,9 @@ fn default_path_search_rejects_turing_only_edge() { let source_variant = BTreeMap::new(); let target_variant = BTreeMap::new(); let graph = build_two_node_graph( - AggregateChainSource::NAME, + OffsetChainSource::NAME, source_variant.clone(), - AggregateChainMiddle::NAME, + OffsetChainMiddle::NAME, target_variant.clone(), ReductionEdgeData { parameter_contract: empty_parameter_contract(), @@ -876,18 +875,18 @@ fn default_path_search_rejects_turing_only_edge() { assert!(graph .find_paths_up_to( - AggregateChainSource::NAME, + OffsetChainSource::NAME, &source_variant, - AggregateChainMiddle::NAME, + OffsetChainMiddle::NAME, &target_variant, 1, ) .is_empty()); assert!(!graph .find_all_paths_mode( - AggregateChainSource::NAME, + OffsetChainSource::NAME, &source_variant, - AggregateChainMiddle::NAME, + OffsetChainMiddle::NAME, &target_variant, ReductionMode::Turing ) @@ -899,9 +898,9 @@ fn turing_path_search_rejects_witness_only_edge() { let source_variant = BTreeMap::new(); let target_variant = BTreeMap::new(); let graph = build_two_node_graph( - AggregateChainSource::NAME, + OffsetChainSource::NAME, source_variant.clone(), - AggregateChainMiddle::NAME, + OffsetChainMiddle::NAME, target_variant.clone(), ReductionEdgeData { parameter_contract: empty_parameter_contract(), @@ -913,18 +912,18 @@ fn turing_path_search_rejects_witness_only_edge() { assert!(graph .find_all_paths_mode( - AggregateChainSource::NAME, + OffsetChainSource::NAME, &source_variant, - AggregateChainMiddle::NAME, + OffsetChainMiddle::NAME, &target_variant, ReductionMode::Turing ) .is_empty()); assert!(!graph .find_all_paths_mode( - AggregateChainSource::NAME, + OffsetChainSource::NAME, &source_variant, - AggregateChainMiddle::NAME, + OffsetChainMiddle::NAME, &target_variant, ReductionMode::Witness ) @@ -972,24 +971,24 @@ fn witness_executor_does_not_imply_turing_capability() { fn reduce_result_along_path_rejects_single_step_path() { let source_variant = BTreeMap::new(); let graph = build_two_node_graph( - AggregateChainSource::NAME, + OffsetChainSource::NAME, source_variant.clone(), - AggregateChainMiddle::NAME, + OffsetChainMiddle::NAME, BTreeMap::new(), ReductionEdgeData { parameter_contract: empty_parameter_contract(), - reduce_fn: Some(reduce_source_to_middle_aggregate), + reduce_fn: Some(reduce_source_to_middle_offset), turing: false, }, ); let single_step_path = ReductionPath { steps: vec![ReductionStep { - name: AggregateChainSource::NAME.to_string(), + name: OffsetChainSource::NAME.to_string(), variant: source_variant, }], }; assert!(graph - .reduce_along_path(&single_step_path, &AggregateChainSource as &dyn Any) + .reduce_along_path(&single_step_path, &OffsetChainSource as &dyn Any) .expect("single-step path lookup should not fail") .is_none()); } @@ -999,9 +998,9 @@ fn reduce_result_returns_none_for_turing_only_edge() { let source_variant = BTreeMap::new(); let target_variant = BTreeMap::new(); let graph = build_two_node_graph( - AggregateChainSource::NAME, + OffsetChainSource::NAME, source_variant.clone(), - AggregateChainMiddle::NAME, + OffsetChainMiddle::NAME, target_variant.clone(), ReductionEdgeData { parameter_contract: empty_parameter_contract(), @@ -1013,17 +1012,17 @@ fn reduce_result_returns_none_for_turing_only_edge() { let path = ReductionPath { steps: vec![ ReductionStep { - name: AggregateChainSource::NAME.to_string(), + name: OffsetChainSource::NAME.to_string(), variant: source_variant, }, ReductionStep { - name: AggregateChainMiddle::NAME.to_string(), + name: OffsetChainMiddle::NAME.to_string(), variant: target_variant, }, ], }; assert!(graph - .reduce_along_path(&path, &AggregateChainSource as &dyn Any) + .reduce_along_path(&path, &OffsetChainSource as &dyn Any) .expect("Turing-only edge lookup should not fail") .is_none()); } @@ -1033,9 +1032,9 @@ fn reduce_along_path_preserves_edge_failure() { let source_variant = BTreeMap::new(); let target_variant = BTreeMap::new(); let graph = build_two_node_graph( - AggregateChainSource::NAME, + OffsetChainSource::NAME, source_variant.clone(), - AggregateChainMiddle::NAME, + OffsetChainMiddle::NAME, target_variant.clone(), ReductionEdgeData { parameter_contract: empty_parameter_contract(), @@ -1047,25 +1046,25 @@ fn reduce_along_path_preserves_edge_failure() { let path = ReductionPath { steps: vec![ ReductionStep { - name: AggregateChainSource::NAME.to_string(), + name: OffsetChainSource::NAME.to_string(), variant: source_variant, }, ReductionStep { - name: AggregateChainMiddle::NAME.to_string(), + name: OffsetChainMiddle::NAME.to_string(), variant: target_variant, }, ], }; - let error = match graph.reduce_along_path(&path, &AggregateChainSource as &dyn Any) { + let error = match graph.reduce_along_path(&path, &OffsetChainSource as &dyn Any) { Err(error) => error, Ok(_) => panic!("registered edge failure must be returned"), }; assert_eq!( error, crate::rules::ReductionError::InvalidTarget { - source_problem: AggregateChainSource::NAME, - target_problem: AggregateChainMiddle::NAME, + source_problem: OffsetChainSource::NAME, + target_problem: OffsetChainMiddle::NAME, message: "synthetic target construction failure".to_string(), } ); @@ -2095,11 +2094,11 @@ fn witness_and_value_mapping_share_one_executed_construction() { static CONSTRUCTIONS: AtomicUsize = AtomicUsize::new(0); let chain = crate::rules::ReductionChain::execute( - &AggregateChainSource, + &OffsetChainSource, &[|_| { CONSTRUCTIONS.fetch_add(1, Ordering::SeqCst); let result = Rc::new(SourceToMiddleWitnessResult { - target: AggregateChainMiddle, + target: OffsetChainMiddle, }); Ok(ExecutedStep { witness: result }) }], @@ -2109,20 +2108,17 @@ fn witness_and_value_mapping_share_one_executed_construction() { assert!(std::ptr::eq( step.witness .target_problem_any() - .downcast_ref::() + .downcast_ref::() .unwrap(), - chain.target_problem::(), + chain.target_problem::(), )); let witness = vec![7usize]; assert_eq!( chain - .recover_result::( - &AggregateChainSource, - SolveOutcome::optimal( - chain.target_problem::(), - witness.clone() - ) - .unwrap(), + .recover_result::( + &OffsetChainSource, + SolveOutcome::optimal(chain.target_problem::(), witness.clone()) + .unwrap(), ) .unwrap(), SolveOutcome::Optimal { diff --git a/src/unit_tests/rules/traits.rs b/src/unit_tests/rules/traits.rs index afe31fd6c..f9e304a2b 100644 --- a/src/unit_tests/rules/traits.rs +++ b/src/unit_tests/rules/traits.rs @@ -209,7 +209,7 @@ fn test_reduction() { } #[test] -fn aggregate_value_from_solution_keeps_evaluation_errors_distinct_from_false() { +fn decision_recovery_keeps_evaluation_errors_distinct_from_infeasible() { use crate::models::decision::Decision; use crate::models::graph::MinimumVertexCover; use crate::rules::ExtractionError; @@ -256,29 +256,29 @@ fn aggregate_value_from_solution_keeps_evaluation_errors_distinct_from_false() { } #[derive(Clone)] -struct AggregateSourceProblem; +struct OffsetSourceProblem; #[derive(Clone)] -struct AggregateTargetProblem; +struct OffsetTargetProblem; thread_local! { static TARGET_EVALUATIONS: std::cell::Cell = const { std::cell::Cell::new(0) }; } -impl AggregateSourceProblem { +impl OffsetSourceProblem { fn num_variables(&self) -> usize { 1 } } -impl AggregateTargetProblem { +impl OffsetTargetProblem { fn num_variables(&self) -> usize { 1 } } -impl Problem for AggregateSourceProblem { - const NAME: &'static str = "AggregateSource"; +impl Problem for OffsetSourceProblem { + const NAME: &'static str = "OffsetSource"; type Solution = Vec; type Value = Min; @@ -296,8 +296,8 @@ impl Problem for AggregateSourceProblem { } } -impl Problem for AggregateTargetProblem { - const NAME: &'static str = "AggregateTarget"; +impl Problem for OffsetTargetProblem { + const NAME: &'static str = "OffsetTarget"; type Solution = Vec; type Value = Min; @@ -316,14 +316,14 @@ impl Problem for AggregateTargetProblem { } } -struct TestAggregateReduction { - target: AggregateTargetProblem, +struct TestOffsetReduction { + target: OffsetTargetProblem, offset: u64, } -impl ReductionResult for TestAggregateReduction { - type Source = AggregateSourceProblem; - type Target = AggregateTargetProblem; +impl ReductionResult for TestOffsetReduction { + type Source = OffsetSourceProblem; + type Target = OffsetTargetProblem; fn target_problem(&self) -> &Self::Target { &self.target @@ -348,21 +348,21 @@ impl ReductionResult for TestAggregateReduction { } } -impl ReduceTo for AggregateSourceProblem { - type Result = TestAggregateReduction; +impl ReduceTo for OffsetSourceProblem { + type Result = TestOffsetReduction; fn reduce_to(&self) -> Result { - Ok(TestAggregateReduction { - target: AggregateTargetProblem, + Ok(TestOffsetReduction { + target: OffsetTargetProblem, offset: 3, }) } } #[test] -fn test_aggregate_reduction_extracts_value() { - let source = AggregateSourceProblem; - let result = >::reduce_to(&source) +fn test_offset_reduction_recovers_shifted_result() { + let source = OffsetSourceProblem; + let result = >::reduce_to(&source) .expect("reduction should succeed"); assert_eq!( @@ -380,16 +380,16 @@ fn test_aggregate_reduction_extracts_value() { } #[test] -fn test_dyn_aggregate_reduction_result_extracts_value() { - let result = TestAggregateReduction { - target: AggregateTargetProblem, +fn test_dyn_reduction_result_recovers_shifted_result() { + let result = TestOffsetReduction { + target: OffsetTargetProblem, offset: 2, }; let dyn_result: &dyn DynReductionResult = &result; assert!(dyn_result .target_problem_any() - .downcast_ref::() + .downcast_ref::() .is_some()); TARGET_EVALUATIONS.with(|count| count.set(0)); let (target, target_json) = dyn_result @@ -406,7 +406,7 @@ fn test_dyn_aggregate_reduction_result_extracts_value() { ); assert_eq!(TARGET_EVALUATIONS.with(|count| count.get()), 1); let recovered = dyn_result - .recover_result_dyn(&AggregateSourceProblem, target) + .recover_result_dyn(&OffsetSourceProblem, target) .unwrap(); assert_eq!(TARGET_EVALUATIONS.with(|count| count.get()), 1); assert_eq!( @@ -420,8 +420,8 @@ fn test_dyn_aggregate_reduction_result_extracts_value() { #[test] fn external_evaluation_is_optional_but_must_match_when_present() { - let result = TestAggregateReduction { - target: AggregateTargetProblem, + let result = TestOffsetReduction { + target: OffsetTargetProblem, offset: 2, }; for status in ["optimal", "feasible"] { diff --git a/src/unit_tests/solvers/brute_force.rs b/src/unit_tests/solvers/brute_force.rs index 8c653314c..32a7c5c41 100644 --- a/src/unit_tests/solvers/brute_force.rs +++ b/src/unit_tests/solvers/brute_force.rs @@ -1,6 +1,6 @@ use super::*; use crate::traits::Problem; -use crate::types::{AggregationError, Max, Min, Or, Sum}; +use crate::types::{AggregationError, Max, Min, Or}; use std::cell::Cell; use std::rc::Rc; @@ -442,15 +442,6 @@ fn test_solver_solve_stops_after_first_optimal_configuration() { assert_eq!(evaluations.get(), 2); } -#[test] -fn test_sum_fold_combines_values_without_problem_solving() { - let total = [Sum(1_u64), Sum(2), Sum(3)] - .into_iter() - .try_fold(Sum::identity(), Aggregate::combine) - .unwrap(); - assert_eq!(total, Sum(6)); -} - #[test] fn test_solver_find_all_witnesses() { let problem = SatProblem { @@ -465,15 +456,6 @@ fn test_solver_find_all_witnesses() { assert!(witnesses.contains(&vec![0, 1])); } -#[test] -fn test_sum_fold_uses_every_input_value() { - let total = [Sum(0_u64), Sum(2), Sum(1), Sum(3)] - .into_iter() - .try_fold(Sum::identity(), Aggregate::combine) - .unwrap(); - assert_eq!(total, Sum(6)); -} - #[test] fn test_solver_with_real_mis() { use crate::models::graph::MaximumIndependentSet; @@ -524,11 +506,6 @@ fn test_solve_with_witnesses_max() { assert_eq!(witnesses, vec![vec![1, 1, 1]]); } -#[test] -fn test_sum_fold_preserves_zero_identity() { - assert_eq!(Sum::::identity().combine(Sum(6)).unwrap(), Sum(6)); -} - #[test] fn solve_with_witnesses_enumerates_only_aggregate_and_witness_passes() { let evaluations = Rc::new(Cell::new(0)); diff --git a/src/unit_tests/types.rs b/src/unit_tests/types.rs index de88e9934..243a77a21 100644 --- a/src/unit_tests/types.rs +++ b/src/unit_tests/types.rs @@ -36,20 +36,6 @@ fn test_max_and_min_report_unordered_comparisons() { ); } -#[test] -fn test_sum_identity_and_combine() { - assert_eq!(Sum::::identity(), Sum(0)); - assert_eq!(Sum(4_u64).combine(Sum(3_u64)).unwrap(), Sum(7)); -} - -#[test] -fn test_sum_combine_reports_overflow() { - assert_eq!( - Sum(u64::MAX).combine(Sum(1)), - Err(AggregationError::ArithmeticOverflow) - ); -} - #[test] fn test_weight_multiplication_reports_integer_overflow() { assert!(matches!( @@ -75,27 +61,6 @@ fn test_or_identity_and_combine() { assert!(Or(true).is_absorbing()); } -#[test] -fn test_and_identity_and_combine() { - assert_eq!(And::identity(), And(true)); - assert_eq!(And(true).combine(And(false)).unwrap(), And(false)); - assert_eq!(And(true).combine(And(true)).unwrap(), And(true)); - assert!(!And(true).is_absorbing()); - assert!(And(false).is_absorbing()); -} - -#[test] -fn test_sum_has_no_absorbing_value() { - assert!(!Sum(0_u64).is_absorbing()); - assert!(!Sum(u64::MAX).is_absorbing()); -} - -#[test] -fn test_and_absorbing_value_is_false() { - assert!(!And(true).is_absorbing()); - assert!(And(false).is_absorbing()); -} - #[test] fn test_max_helpers() { let size = Max(Some(42)); @@ -337,23 +302,12 @@ fn test_min_display() { assert_eq!(format!("{}", Min::(None)), "Min(None)"); } -#[test] -fn test_sum_display() { - assert_eq!(format!("{}", Sum(56_u64)), "Sum(56)"); -} - #[test] fn test_or_display() { assert_eq!(format!("{}", Or(true)), "Or(true)"); assert_eq!(format!("{}", Or(false)), "Or(false)"); } -#[test] -fn test_and_display() { - assert_eq!(format!("{}", And(true)), "And(true)"); - assert_eq!(format!("{}", And(false)), "And(false)"); -} - #[test] fn exact_i64_to_f64_accepts_range_endpoints() { assert_eq!( From 0e7e9721e1a5f359d6d064123bfde027bc574701 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Thu, 17 Sep 2026 23:37:07 +0800 Subject: [PATCH 31/42] Test feasible-incumbent recovery for rules that guard solution quality Cover InsufficientSolutionQuality in each rule's own test file, the zero-penalty Feasible mapping of the penalty rules, and infeasible sources recovered through recover_result. Co-Authored-By: Claude Fable 5.1 --- src/rules/test_helpers.rs | 25 +++++ .../rules/graphpartitioning_maxcut.rs | 19 ++++ .../rules/graphpartitioning_qubo.rs | 18 ++++ ...niancircuit_bottlenecktravelingsalesman.rs | 46 +++++++++ .../hamiltoniancircuit_travelingsalesman.rs | 46 +++++++++ src/unit_tests/rules/ilp_qubo.rs | 87 +++++++++++++++++ src/unit_tests/rules/knapsack_qubo.rs | 18 ++++ .../rules/maximumindependentset_gridgraph.rs | 20 ++++ .../rules/maximumindependentset_triangular.rs | 26 ++++++ .../rules/maximumsetpacking_qubo.rs | 18 ++++ ...mumdiscreteplanarinversekinematics_qubo.rs | 93 +++++++++++++++++++ .../rules/minimummultiwaycut_qubo.rs | 21 +++++ .../rules/partition_sumofsquarespartition.rs | 26 ++++++ .../rules/travelingsalesman_qubo.rs | 92 ++++++++++++++++++ 14 files changed, 555 insertions(+) diff --git a/src/rules/test_helpers.rs b/src/rules/test_helpers.rs index e3abf7857..6f895a677 100644 --- a/src/rules/test_helpers.rs +++ b/src/rules/test_helpers.rs @@ -292,6 +292,31 @@ where assert_eq!(source.evaluate(&extracted).unwrap(), bf_value); } +/// Assert that a rule without an incumbent guarantee refuses a valid target +/// candidate that the supplied target optimum strictly improves on. +pub(crate) fn assert_suboptimal_feasible_target_is_insufficient( + source: &R::Source, + reduction: &R, + candidate: ::Solution, + optimum: &::Solution, +) where + R: ReductionResult, + ::Value: crate::traits::EvaluationValue + std::fmt::Debug + PartialEq, +{ + let target = reduction.target_problem(); + assert_ne!( + target.evaluate(&candidate).unwrap(), + target.evaluate(optimum).unwrap(), + "candidate must be strictly suboptimal" + ); + let candidate = SolveOutcome::feasible(target, candidate) + .expect("candidate must be a valid target solution"); + assert!(matches!( + reduction.recover_result(source, candidate), + Err(crate::rules::ExtractionError::InsufficientSolutionQuality) + )); +} + #[cfg(test)] mod tests { use super::{ diff --git a/src/unit_tests/rules/graphpartitioning_maxcut.rs b/src/unit_tests/rules/graphpartitioning_maxcut.rs index 386ee8dff..c9bdd3ce9 100644 --- a/src/unit_tests/rules/graphpartitioning_maxcut.rs +++ b/src/unit_tests/rules/graphpartitioning_maxcut.rs @@ -97,3 +97,22 @@ fn odd_partition_recovers_infeasibility_from_every_maxcut_optimum() { ); } } + +#[test] +fn test_graphpartitioning_to_maxcut_rejects_feasible_target_incumbent() { + let source = issue_example(); + let reduction = + ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + let optimum = crate::solvers::BruteForce::new() + .solve(reduction.target_problem()) + .unwrap() + .unwrap(); + + // Balanced but suboptimal: {0, 2, 4} | {1, 3, 5} cuts 5 source edges; the optimum cuts 3. + crate::rules::test_helpers::assert_suboptimal_feasible_target_is_insufficient( + &source, + &reduction, + vec![true, false, true, false, true, false], + &optimum, + ); +} diff --git a/src/unit_tests/rules/graphpartitioning_qubo.rs b/src/unit_tests/rules/graphpartitioning_qubo.rs index 1c6ac8fbc..fb2d39259 100644 --- a/src/unit_tests/rules/graphpartitioning_qubo.rs +++ b/src/unit_tests/rules/graphpartitioning_qubo.rs @@ -103,3 +103,21 @@ fn odd_partition_recovers_infeasibility_from_every_qubo_optimum() { ); } } + +#[test] +fn test_graphpartitioning_to_qubo_rejects_feasible_target_incumbent() { + let source = example_problem(); + let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + let optimum = crate::solvers::BruteForce::new() + .solve(reduction.target_problem()) + .unwrap() + .unwrap(); + + // Balanced but suboptimal: {0, 2, 4} | {1, 3, 5} cuts 5 edges; the optimum cuts 3. + crate::rules::test_helpers::assert_suboptimal_feasible_target_is_insufficient( + &source, + &reduction, + vec![true, false, true, false, true, false], + &optimum, + ); +} diff --git a/src/unit_tests/rules/hamiltoniancircuit_bottlenecktravelingsalesman.rs b/src/unit_tests/rules/hamiltoniancircuit_bottlenecktravelingsalesman.rs index 8a6e966c9..e2b7f6c4a 100644 --- a/src/unit_tests/rules/hamiltoniancircuit_bottlenecktravelingsalesman.rs +++ b/src/unit_tests/rules/hamiltoniancircuit_bottlenecktravelingsalesman.rs @@ -93,3 +93,49 @@ fn test_hamiltoniancircuit_to_bottlenecktravelingsalesman_extract_solution_cycle assert_eq!(extracted.len(), 5); assert!(source.evaluate(&extracted).unwrap().is_valid()); } + +/// Edge selection of the closed tour visiting `order` in the complete target graph. +fn tour_edges(graph: &SimpleGraph, order: &[usize]) -> Vec { + graph + .edges() + .into_iter() + .map(|(u, v)| { + (0..order.len()).any(|i| { + let (a, b) = (order[i], order[(i + 1) % order.len()]); + (a, b) == (u, v) || (a, b) == (v, u) + }) + }) + .collect() +} + +#[test] +fn test_hamiltoniancircuit_to_bottlenecktravelingsalesman_feasible_target_incumbents() { + let source = cycle5_hc(); + let reduction = ReduceTo::::reduce_to(&source) + .expect("reduction should succeed"); + let target = reduction.target_problem(); + + // 0-1-2-3-4 uses only source edges (bottleneck 1): the incumbent is a Hamiltonian circuit. + let circuit = tour_edges(target.graph(), &[0, 1, 2, 3, 4]); + assert_eq!(target.evaluate(&circuit).unwrap(), Min(Some(1))); + let recovered = reduction + .recover_result(&source, SolveOutcome::feasible(target, circuit).unwrap()) + .unwrap(); + let SolveOutcome::Feasible { + solution, + evaluation, + } = recovered + else { + panic!("a Hamiltonian incumbent must stay feasible, got {recovered:?}"); + }; + assert_eq!(evaluation, crate::types::Or(true)); + assert_eq!(source.evaluate(&solution).unwrap(), crate::types::Or(true)); + + // 0-2-4-1-3 is the pentagram (bottleneck 2): a valid tour that proves nothing about the source. + let pentagram = tour_edges(target.graph(), &[0, 2, 4, 1, 3]); + assert_eq!(target.evaluate(&pentagram).unwrap(), Min(Some(2))); + assert_eq!( + reduction.recover_result(&source, SolveOutcome::feasible(target, pentagram).unwrap()), + Err(crate::rules::ExtractionError::InsufficientSolutionQuality) + ); +} diff --git a/src/unit_tests/rules/hamiltoniancircuit_travelingsalesman.rs b/src/unit_tests/rules/hamiltoniancircuit_travelingsalesman.rs index 606ccbc87..24968b101 100644 --- a/src/unit_tests/rules/hamiltoniancircuit_travelingsalesman.rs +++ b/src/unit_tests/rules/hamiltoniancircuit_travelingsalesman.rs @@ -84,3 +84,49 @@ fn test_hamiltoniancircuit_to_travelingsalesman_extract_solution_cycle() { assert_eq!(extracted.len(), 4); assert!(source.evaluate(&extracted).unwrap()); } + +/// Edge selection of the closed tour visiting `order` in the complete target graph. +fn tour_edges(graph: &SimpleGraph, order: &[usize]) -> Vec { + graph + .edges() + .into_iter() + .map(|(u, v)| { + (0..order.len()).any(|i| { + let (a, b) = (order[i], order[(i + 1) % order.len()]); + (a, b) == (u, v) || (a, b) == (v, u) + }) + }) + .collect() +} + +#[test] +fn test_hamiltoniancircuit_to_travelingsalesman_feasible_target_incumbents() { + let source = cycle4_hc(); + let reduction = ReduceTo::>::reduce_to(&source) + .expect("reduction should succeed"); + let target = reduction.target_problem(); + + // 0-1-2-3 uses only source edges (cost 4): the incumbent already is a Hamiltonian circuit. + let circuit = tour_edges(target.graph(), &[0, 1, 2, 3]); + assert_eq!(target.evaluate(&circuit).unwrap(), Min(Some(4))); + let recovered = reduction + .recover_result(&source, SolveOutcome::feasible(target, circuit).unwrap()) + .unwrap(); + let SolveOutcome::Feasible { + solution, + evaluation, + } = recovered + else { + panic!("a Hamiltonian incumbent must stay feasible, got {recovered:?}"); + }; + assert_eq!(evaluation, crate::types::Or(true)); + assert_eq!(source.evaluate(&solution).unwrap(), crate::types::Or(true)); + + // 0-1-3-2 uses the two diagonals (cost 6): a valid tour that proves nothing about the source. + let detour = tour_edges(target.graph(), &[0, 1, 3, 2]); + assert_eq!(target.evaluate(&detour).unwrap(), Min(Some(6))); + assert_eq!( + reduction.recover_result(&source, SolveOutcome::feasible(target, detour).unwrap()), + Err(crate::rules::ExtractionError::InsufficientSolutionQuality) + ); +} diff --git a/src/unit_tests/rules/ilp_qubo.rs b/src/unit_tests/rules/ilp_qubo.rs index 8fe3f5c6d..9d33b708d 100644 --- a/src/unit_tests/rules/ilp_qubo.rs +++ b/src/unit_tests/rules/ilp_qubo.rs @@ -392,3 +392,90 @@ fn test_ilp_qubo_checked_dimensions_and_energy_interval() { )); } } + +#[test] +fn test_ilp_to_qubo_feasible_target_incumbents() { + use crate::Problem; + // maximize x0 + 2*x1 + 3*x2 s.t. x0 + x1 <= 1, x1 + x2 <= 1 + let ilp = ILP::::new( + 3, + vec![ + LinearConstraint::le(vec![(0, 1), (1, 1)], 1), + LinearConstraint::le(vec![(1, 1), (2, 1)], 1), + ], + vec![(0, 1), (1, 2), (2, 3)], + ObjectiveSense::Maximize, + ) + .unwrap(); + let reduction = ReduceTo::>::reduce_to(&ilp).unwrap(); + let qubo = reduction.target_problem(); + let num_target_vars = qubo.num_vars(); + + // Every target assignment is a valid QUBO incumbent. It may be mapped back only + // when its penalty vanishes, and then it must be the source-feasible prefix. + let mut recovered_sources = std::collections::BTreeSet::new(); + let mut rejected = 0; + for bits in 0..1usize << num_target_vars { + let candidate: Vec = (0..num_target_vars).map(|i| bits & (1 << i) != 0).collect(); + let prefix: Vec = candidate[..3].iter().map(|&bit| i64::from(bit)).collect(); + match reduction.recover_result(&ilp, SolveOutcome::feasible(qubo, candidate).unwrap()) { + Ok(SolveOutcome::Feasible { + solution, + evaluation, + }) => { + assert!(ilp.is_feasible(&prefix).unwrap()); + assert_eq!(solution, prefix); + assert_eq!(evaluation, ilp.evaluate(&prefix).unwrap()); + recovered_sources.insert(prefix); + } + Err(crate::rules::ExtractionError::InsufficientSolutionQuality) => rejected += 1, + other => panic!("unexpected incumbent recovery: {other:?}"), + } + } + + // Exactly the five source-feasible assignments are reachable, including the + // suboptimal ones such as x = (1, 0, 0) with objective 1 < 4. + let feasible_sources: std::collections::BTreeSet> = (0..8) + .map(|bits| (0..3).map(|i| i64::from(bits & (1 << i) != 0)).collect()) + .filter(|x: &Vec| ilp.is_feasible(x).unwrap()) + .collect(); + assert_eq!(feasible_sources.len(), 5); + assert_eq!(recovered_sources, feasible_sources); + assert!(rejected > 0); +} + +#[test] +fn test_ilp_to_qubo_infeasible_source_recovers_infeasible() { + for sense in [ObjectiveSense::Minimize, ObjectiveSense::Maximize] { + let source = ILP::::new( + 3, + vec![ + LinearConstraint::eq(vec![(0, 1)], 0), + LinearConstraint::eq(vec![(0, 1)], 1), + ], + vec![(1, 2), (2, -1)], + sense, + ) + .unwrap(); + assert!((0..8).all(|bits| { + let x: Vec = (0..3).map(|i| i64::from(bits & (1 << i) != 0)).collect(); + !source.is_feasible(&x).unwrap() + })); + + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + let qubo = reduction.target_problem(); + let optimum = BruteForce::new().solve(qubo).unwrap().unwrap(); + assert_eq!( + reduction.recover_result( + &source, + SolveOutcome::optimal(qubo, optimum.clone()).unwrap() + ), + Ok(SolveOutcome::Infeasible) + ); + // The same assignment without an optimality proof establishes nothing. + assert_eq!( + reduction.recover_result(&source, SolveOutcome::feasible(qubo, optimum).unwrap()), + Err(crate::rules::ExtractionError::InsufficientSolutionQuality) + ); + } +} diff --git a/src/unit_tests/rules/knapsack_qubo.rs b/src/unit_tests/rules/knapsack_qubo.rs index 8d9e763b3..f84844571 100644 --- a/src/unit_tests/rules/knapsack_qubo.rs +++ b/src/unit_tests/rules/knapsack_qubo.rs @@ -102,3 +102,21 @@ fn test_knapsack_to_qubo_canonical_example_spec() { assert_eq!(example.target.instance["matrix"]["nrows"], 7); assert!(!example.solutions.is_empty()); } + +#[test] +fn test_knapsack_to_qubo_rejects_feasible_target_incumbent() { + let knapsack = Knapsack::new(vec![2, 3, 4, 5], vec![3, 4, 5, 7], 7); + let reduction = ReduceTo::>::reduce_to(&knapsack).expect("reduction should succeed"); + let optimum = BruteForce::new() + .solve(reduction.target_problem()) + .unwrap() + .unwrap(); + + // A QUBO incumbent carries no optimality proof, so the slack penalty may be active. + crate::rules::test_helpers::assert_suboptimal_feasible_target_is_insufficient( + &knapsack, + &reduction, + vec![false; 7], + &optimum, + ); +} diff --git a/src/unit_tests/rules/maximumindependentset_gridgraph.rs b/src/unit_tests/rules/maximumindependentset_gridgraph.rs index c62eab53b..02b99728c 100644 --- a/src/unit_tests/rules/maximumindependentset_gridgraph.rs +++ b/src/unit_tests/rules/maximumindependentset_gridgraph.rs @@ -148,3 +148,23 @@ fn test_mis_simple_one_to_kings_one_all_four_vertex_graphs() { ); } } + +#[test] +fn test_mis_simple_one_to_kings_one_rejects_feasible_target_incumbent() { + let problem = MaximumIndependentSet::new( + SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4)]), + vec![One; 5], + ); + let result = ReduceTo::>::reduce_to(&problem) + .expect("reduction should succeed"); + let target = result.target_problem(); + let optimum = BruteForce::new().solve(target).unwrap().unwrap(); + + // The empty set is independent in the grid graph but says nothing about the source optimum. + crate::rules::test_helpers::assert_suboptimal_feasible_target_is_insufficient( + &problem, + &result, + vec![false; target.graph().num_vertices()], + &optimum, + ); +} diff --git a/src/unit_tests/rules/maximumindependentset_triangular.rs b/src/unit_tests/rules/maximumindependentset_triangular.rs index 167ab6124..f72325b6c 100644 --- a/src/unit_tests/rules/maximumindependentset_triangular.rs +++ b/src/unit_tests/rules/maximumindependentset_triangular.rs @@ -188,3 +188,29 @@ fn test_mis_simple_one_to_triangular_graph_methods() { assert_eq!(positions.len(), n); assert_eq!(graph.num_positions(), n); } + +#[test] +fn test_mis_simple_one_to_triangular_rejects_feasible_target_incumbent() { + use crate::test_unitdiskmapping_algorithms::common::solve_weighted_mis_config; + + let source = MaximumIndependentSet::new( + SimpleGraph::new(4, vec![(0, 1), (1, 2), (2, 3)]), + vec![One; 4], + ); + let reduction = + ReduceTo::>::reduce_to(&source).unwrap(); + let target = reduction.target_problem(); + let optimum = crate::config::config_to_bits(&solve_weighted_mis_config( + target.graph().num_vertices(), + &target.graph().edges(), + target.weights(), + )); + + // The empty set is independent in the triangular graph but says nothing about the source optimum. + crate::rules::test_helpers::assert_suboptimal_feasible_target_is_insufficient( + &source, + &reduction, + vec![false; target.graph().num_vertices()], + &optimum, + ); +} diff --git a/src/unit_tests/rules/maximumsetpacking_qubo.rs b/src/unit_tests/rules/maximumsetpacking_qubo.rs index c74206f44..9de1e7858 100644 --- a/src/unit_tests/rules/maximumsetpacking_qubo.rs +++ b/src/unit_tests/rules/maximumsetpacking_qubo.rs @@ -156,3 +156,21 @@ fn test_setpacking_to_qubo_non_finite_penalty_is_typed_error() { Err(crate::rules::ReductionError::NonFiniteResult { .. }) )); } + +#[test] +fn test_setpacking_to_qubo_rejects_feasible_target_incumbent() { + let sp = MaximumSetPacking::::new(vec![vec![0, 2], vec![1, 2], vec![0, 3]]); + let reduction = ReduceTo::>::reduce_to(&sp).expect("reduction should succeed"); + let optimum = BruteForce::new() + .solve(reduction.target_problem()) + .unwrap() + .unwrap(); + + // Packing only {0,2} is valid, but {1,2} and {0,3} together pack two sets. + crate::rules::test_helpers::assert_suboptimal_feasible_target_is_insufficient( + &sp, + &reduction, + vec![true, false, false], + &optimum, + ); +} diff --git a/src/unit_tests/rules/minimumdiscreteplanarinversekinematics_qubo.rs b/src/unit_tests/rules/minimumdiscreteplanarinversekinematics_qubo.rs index d8eac3741..5071c076c 100644 --- a/src/unit_tests/rules/minimumdiscreteplanarinversekinematics_qubo.rs +++ b/src/unit_tests/rules/minimumdiscreteplanarinversekinematics_qubo.rs @@ -238,3 +238,96 @@ fn nonfinite_energy_relation_is_a_construction_error() { Err(crate::rules::ReductionError::NonFiniteResult { .. }) )); } + +/// Decode three two-sample one-hot blocks, or `None` when a block is not one-hot. +fn decode_one_hot_pairs(assignment: &[bool]) -> Option> { + assignment + .chunks(2) + .map(|block| match block { + [true, false] => Some(0), + [false, true] => Some(1), + _ => None, + }) + .collect() +} + +#[test] +fn test_minimumdiscreteplanarinversekinematics_to_qubo_feasible_target_incumbents() { + // Three unit links pointing right or left; the second joint forbids (left, right). + let source = MinimumDiscretePlanarInverseKinematics::new( + vec![1.0, 1.0, 1.0], + (1.0, 0.0), + vec![vec![0.0, PI]; 3], + vec![ + vec![(0, 0), (0, 1), (1, 0), (1, 1)], + vec![(0, 0), (0, 1), (1, 1)], + ], + ) + .unwrap(); + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + let qubo = reduction.target_problem(); + assert_eq!(qubo.num_vars(), 6); + + let mut suboptimal = 0; + let mut rejected = 0; + for bits in 0..1usize << 6 { + let candidate: Vec = (0..6).map(|i| bits & (1 << i) != 0).collect(); + let expected = decode_one_hot_pairs(&candidate) + .map(|config| (source.evaluate(&config).unwrap(), config)) + .filter(|(evaluation, _)| evaluation.0.is_some()); + let recovered = + reduction.recover_result(&source, SolveOutcome::feasible(qubo, candidate).unwrap()); + match expected { + Some((evaluation, solution)) => { + // (right, right, right) ends at (3, 0), squared distance 4 from the goal. + suboptimal += usize::from(matches!(evaluation, Min(Some(v)) if v > 1.0)); + assert_eq!( + recovered, + Ok(SolveOutcome::Feasible { + solution, + evaluation, + }) + ); + } + None => { + rejected += 1; + assert_eq!( + recovered, + Err(crate::rules::ExtractionError::InsufficientSolutionQuality) + ); + } + } + } + // Six of the eight one-hot assignments satisfy the transition constraints. + assert_eq!(rejected, 64 - 6); + assert!(suboptimal > 0); +} + +#[test] +fn test_minimumdiscreteplanarinversekinematics_to_qubo_infeasible_source_recovers_infeasible() { + // The middle link would need sample 0 and sample 1 at once. + let source = MinimumDiscretePlanarInverseKinematics::new( + vec![1.0, 1.0, 1.0], + (0.0, 0.0), + vec![vec![0.0, PI]; 3], + vec![vec![(0, 0)], vec![(1, 0)]], + ) + .unwrap(); + assert_eq!(BruteForce::new().solve(&source).unwrap(), None); + + let reduction = ReduceTo::>::reduce_to(&source).unwrap(); + let qubo = reduction.target_problem(); + let optimum = BruteForce::new().solve(qubo).unwrap().unwrap(); + assert_eq!( + reduction.recover_result( + &source, + SolveOutcome::optimal(qubo, optimum.clone()).unwrap() + ), + Ok(SolveOutcome::Infeasible) + ); + // The same assignment without an optimality proof establishes nothing. + assert_eq!( + reduction.recover_result(&source, SolveOutcome::feasible(qubo, optimum).unwrap()), + Err(crate::rules::ExtractionError::InsufficientSolutionQuality) + ); +} diff --git a/src/unit_tests/rules/minimummultiwaycut_qubo.rs b/src/unit_tests/rules/minimummultiwaycut_qubo.rs index cb615fe65..ca70e62ea 100644 --- a/src/unit_tests/rules/minimummultiwaycut_qubo.rs +++ b/src/unit_tests/rules/minimummultiwaycut_qubo.rs @@ -161,3 +161,24 @@ fn test_minimummultiwaycut_to_qubo_terminal_pinning() { } } } + +#[test] +fn test_minimummultiwaycut_to_qubo_rejects_feasible_target_incumbent() { + let graph = SimpleGraph::new(5, vec![(0, 1), (1, 2), (2, 3), (3, 4), (0, 4), (1, 3)]); + let source = MinimumMultiwayCut::new(graph, vec![0, 2, 4], vec![2, 3, 1, 2, 4, 5]); + let reduction = ReduceTo::>::reduce_to(&source).expect("reduction should succeed"); + let optimum = BruteForce::new() + .solve(reduction.target_problem()) + .unwrap() + .unwrap(); + + // One-hot assignment 0,1 -> terminal 0; 2 -> terminal 2; 3,4 -> terminal 4. + // It cuts edges (1,2), (2,3), (0,4), (1,3) for cost 13; the optimum is 8. + let mut candidate = vec![false; 15]; + for (vertex, component) in [0, 0, 1, 2, 2].into_iter().enumerate() { + candidate[vertex * 3 + component] = true; + } + crate::rules::test_helpers::assert_suboptimal_feasible_target_is_insufficient( + &source, &reduction, candidate, &optimum, + ); +} diff --git a/src/unit_tests/rules/partition_sumofsquarespartition.rs b/src/unit_tests/rules/partition_sumofsquarespartition.rs index 41feb96ac..2ee928494 100644 --- a/src/unit_tests/rules/partition_sumofsquarespartition.rs +++ b/src/unit_tests/rules/partition_sumofsquarespartition.rs @@ -177,3 +177,29 @@ fn test_partition_to_sumofsquarespartition_solution_extraction_identity() { Err(InvalidConfiguration(_)) )); } + +#[test] +fn test_partition_to_sumofsquarespartition_feasible_target_incumbents() { + // sizes [3, 1, 1, 2, 2, 1], S = 10. + let (source, reduction) = reduce_partition(&[3, 1, 1, 2, 2, 1]); + let target = reduction.target_problem(); + + // {3, 2} | {1, 1, 2, 1}: 5^2 + 5^2 = 50 = S^2 / 2, already a balanced partition. + let balanced = vec![0, 1, 1, 0, 1, 1]; + assert_eq!(target.evaluate(&balanced).unwrap(), Min(Some(50))); + assert_eq!( + reduction.recover_result(&source, SolveOutcome::feasible(target, balanced).unwrap()), + Ok(SolveOutcome::Feasible { + solution: vec![false, true, true, false, true, true], + evaluation: crate::types::Or(true), + }) + ); + + // {3, 1, 1, 2} | {2, 1}: 7^2 + 3^2 = 58 > 50 proves nothing about the source. + let unbalanced = vec![0, 0, 0, 0, 1, 1]; + assert_eq!(target.evaluate(&unbalanced).unwrap(), Min(Some(58))); + assert_eq!( + reduction.recover_result(&source, SolveOutcome::feasible(target, unbalanced).unwrap()), + Err(crate::rules::ExtractionError::InsufficientSolutionQuality) + ); +} diff --git a/src/unit_tests/rules/travelingsalesman_qubo.rs b/src/unit_tests/rules/travelingsalesman_qubo.rs index eeafdede2..f7e21b5fe 100644 --- a/src/unit_tests/rules/travelingsalesman_qubo.rs +++ b/src/unit_tests/rules/travelingsalesman_qubo.rs @@ -188,3 +188,95 @@ fn signed_and_small_tours_recover_all_optima_or_infeasibility() { assert_eq!(reduction.map_value(Min(None)), Min(None)); } } + +/// Decode a position-encoded assignment (`x[v * n + p]`: vertex `v` at position `p`) +/// into the source edge selection, or `None` when it is not a permutation matrix. +fn decode_tour( + source: &TravelingSalesman, + assignment: &[bool], +) -> Option> { + let n = source.num_vertices(); + let order: Vec = (0..n) + .map(|p| { + let mut at_position = (0..n).filter(|&v| assignment[v * n + p]); + at_position.next().filter(|_| at_position.next().is_none()) + }) + .collect::>()?; + if assignment.iter().filter(|&&bit| bit).count() != n || (0..n).any(|v| !order.contains(&v)) { + return None; + } + Some( + source + .edges() + .into_iter() + .map(|(u, v, _)| { + (0..n).any(|i| { + let (a, b) = (order[i], order[(i + 1) % n]); + (a, b) == (u, v) || (a, b) == (v, u) + }) + }) + .collect(), + ) +} + +#[test] +fn test_travelingsalesman_to_qubo_feasible_target_incumbents() { + // K4 with tour costs 22, 22 and 10, so most valid tours are suboptimal. + let tsp = TravelingSalesman::new(SimpleGraph::complete(4), vec![9i64, 1, 2, 3, 4, 8]); + let optimum = BruteForce::new().solve(&tsp).unwrap().unwrap(); + assert_eq!(tsp.evaluate(&optimum).unwrap(), Min(Some(10))); + + let reduction = ReduceTo::>::reduce_to(&tsp).unwrap(); + let qubo = reduction.target_problem(); + let mut suboptimal_tours = 0; + let mut rejected = 0; + for bits in 0..1usize << 16 { + let candidate: Vec = (0..16).map(|i| bits & (1 << i) != 0).collect(); + let expected = decode_tour(&tsp, &candidate); + let recovered = + reduction.recover_result(&tsp, SolveOutcome::feasible(qubo, candidate).unwrap()); + match expected { + Some(tour) => { + let evaluation = tsp.evaluate(&tour).unwrap(); + suboptimal_tours += usize::from(evaluation == Min(Some(22))); + assert_eq!( + recovered, + Ok(SolveOutcome::Feasible { + solution: tour, + evaluation, + }) + ); + } + None => { + rejected += 1; + assert_eq!( + recovered, + Err(crate::rules::ExtractionError::InsufficientSolutionQuality) + ); + } + } + } + // 4! position assignments, two thirds of which encode a cost-22 tour. + assert_eq!(suboptimal_tours, 16); + assert_eq!(rejected, (1 << 16) - 24); +} + +#[test] +fn test_travelingsalesman_to_qubo_infeasible_source_recovers_infeasible() { + // A path on four vertices has no Hamiltonian cycle. + let tsp = TravelingSalesman::new(SimpleGraph::path(4), vec![1i64, 2, 3]); + assert_eq!(BruteForce::new().solve(&tsp).unwrap(), None); + + let reduction = ReduceTo::>::reduce_to(&tsp).unwrap(); + let qubo = reduction.target_problem(); + let optimum = BruteForce::new().solve(qubo).unwrap().unwrap(); + assert_eq!( + reduction.recover_result(&tsp, SolveOutcome::optimal(qubo, optimum.clone()).unwrap()), + Ok(SolveOutcome::Infeasible) + ); + // The same assignment without an optimality proof establishes nothing. + assert_eq!( + reduction.recover_result(&tsp, SolveOutcome::feasible(qubo, optimum).unwrap()), + Err(crate::rules::ExtractionError::InsufficientSolutionQuality) + ); +} From 299609486147a84db77ab7199226ae16dac106d6 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Thu, 17 Sep 2026 23:43:29 +0800 Subject: [PATCH 32/42] Point the 3-SAT decision vertex cover example lookup at the unit-weight variant Co-Authored-By: Claude Fable 5.1 --- src/unit_tests/example_db.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/unit_tests/example_db.rs b/src/unit_tests/example_db.rs index a908e2ef7..e8f85b52b 100644 --- a/src/unit_tests/example_db.rs +++ b/src/unit_tests/example_db.rs @@ -899,12 +899,13 @@ fn test_find_rule_example_ksatisfiability_to_minimumvertexcover() { name: "DecisionMinimumVertexCover".to_string(), variant: BTreeMap::from([ ("graph".to_string(), "SimpleGraph".to_string()), - ("weight".to_string(), "i64".to_string()), + ("weight".to_string(), "One".to_string()), ]), }; let example = find_rule_example(&source, &target).unwrap(); assert_eq!(example.source.problem, "KSatisfiability"); assert_eq!(example.target.problem, "DecisionMinimumVertexCover"); + assert_eq!(example.target.variant, target.variant); } #[test] From 1838a7bc458d8dfad30b2d19a1461db7d4964417 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Thu, 17 Sep 2026 23:47:27 +0800 Subject: [PATCH 33/42] Route the decision bundle test and the paper through the unit-weight vertex cover cast Co-Authored-By: Claude Fable 5.1 --- docs/paper/reductions.typ | 10 ++++++++++ problemreductions-cli/src/dispatch.rs | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index ec9854756..82d3bd625 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -16548,6 +16548,16 @@ Problems parameterized by graph type, weight type, target type, or clause width _Solution extraction._ Return the target configuration unchanged. ] +#reduction-rule("DecisionMinimumVertexCover", "DecisionMinimumVertexCover")[ + A unit-weight Decision Minimum Vertex Cover instance converts to the integer-weight variant by mapping every unit weight to $1_ZZ$ and keeping the graph and the bound. +][ + _Construction._ Given $(G, k)$ with unit weights, construct the integer-weight instance $(G, w, k)$ with $w(v) = 1$ for every vertex $v$. + + _Correctness._ For every vertex set $C$, $sum_(v in C) w(v) = |C|$, so $C$ is a vertex cover of cost at most $k$ in the target if and only if it is a vertex cover of size at most $k$ in the source. The size map is the exact identity. + + _Solution extraction._ Return the target configuration unchanged. +] + #reduction-rule("KSatisfiability", "KSatisfiability")[ A $k$-SAT instance with fixed clause width ($k = 2$ or $k = 3$) converts to generic $k$-SAT by constructing the registered $K_N$ variant. The clauses and variables are preserved verbatim; the target uses `new_allow_less` to accept clauses with fewer than $k$ literals. ][ diff --git a/problemreductions-cli/src/dispatch.rs b/problemreductions-cli/src/dispatch.rs index 3a1386655..dd7fbf376 100644 --- a/problemreductions-cli/src/dispatch.rs +++ b/problemreductions-cli/src/dispatch.rs @@ -487,7 +487,7 @@ mod tests { let route = crate::commands::reduce::parse_path_json( r#"{"path":[{ "from":{"name":"KSatisfiability","variant":{"k":"K3"}}, - "to":{"name":"DecisionMinimumVertexCover","variant":{"graph":"SimpleGraph","weight":"i64"}} + "to":{"name":"DecisionMinimumVertexCover","variant":{"graph":"SimpleGraph","weight":"One"}} }]}"#, ).unwrap(); let bundle = crate::commands::reduce::execute_route(source, route).unwrap(); From a94c93c80b9eeeaf4f32bf18b15bdfe3d646e45a Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Thu, 17 Sep 2026 23:51:06 +0800 Subject: [PATCH 34/42] Persist QUBO as project-owned sparse entries and read legacy dense JSON QUBO JSON is now {num_vars, entries: [[row, column, value], ...]} in row-major order instead of the sprs CSR serde layout. Loading feeds entries through the from_sparse validation path and the legacy {num_vars, matrix} shape through from_matrix, with typed errors for missing or conflicting fields, out-of-range indices, duplicates, ragged rows, and a num_vars mismatch. Co-Authored-By: Claude Fable 5.1 --- docs/paper/reductions.typ | 30 ++- docs/src/design.md | 9 +- problemreductions-cli/tests/cli_tests.rs | 69 +++++- src/models/algebraic/qubo.rs | 102 ++++++++- src/unit_tests/models/algebraic/qubo.rs | 200 +++++++++++++++++- .../rules/closestvectorproblem_qubo.rs | 2 +- .../rules/graphpartitioning_qubo.rs | 2 +- src/unit_tests/rules/knapsack_qubo.rs | 2 +- ...mumdiscreteplanarinversekinematics_qubo.rs | 2 +- src/unit_tests/rules/paintshop_qubo.rs | 2 +- tests/data/qubo_legacy_dense.json | 1 + 11 files changed, 391 insertions(+), 30 deletions(-) create mode 100644 tests/data/qubo_legacy_dense.json diff --git a/docs/paper/reductions.typ b/docs/paper/reductions.typ index c9f742a4c..5f45d20a2 100644 --- a/docs/paper/reductions.typ +++ b/docs/paper/reductions.typ @@ -4895,18 +4895,16 @@ In all graph problems below, $G = (V, E)$ denotes an undirected graph with $|V| // Expand small sparse QUBO examples only for typesetting their matrices. #let qubo-matrix(instance) = { - let m = instance.matrix - range(m.nrows).map(i => { - let row = range(m.ncols).map(_ => 0) - for k in range(m.indptr.at(i), m.indptr.at(i + 1)) { - row.at(m.indices.at(k)) = m.data.at(k) - } - row - }) + let n = instance.num_vars + let Q = range(n).map(_ => range(n).map(_ => 0)) + for (i, j, value) in instance.entries { + Q.at(i).at(j) = value + } + Q } #{ let x = load-model-example("QUBO") - let n = x.instance.matrix.nrows + let n = x.instance.num_vars let Q = qubo-matrix(x.instance) let sol = (config: x.optimal_config, metric: x.optimal_value) let xstar = sol.config @@ -11970,7 +11968,7 @@ with the target, rather than stored separately by solution extraction. *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 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.matrix.nrows 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.* 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 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. @@ -12406,7 +12404,7 @@ where $P$ is a penalty weight large enough that any constraint violation costs m #let ks_qubo = load-example("Knapsack", "QUBO") #let ks_qubo_sol = ks_qubo.solutions.at(0) #let ks_qubo_num_items = ks_qubo.source.instance.weights.len() -#let ks_qubo_num_slack = ks_qubo.target.instance.matrix.nrows - ks_qubo_num_items +#let ks_qubo_num_slack = ks_qubo.target.instance.num_vars - ks_qubo_num_items #let ks_qubo_penalty = 1 + ks_qubo.source.instance.values.fold(0, (a, b) => a + b) #let ks_qubo_selected = ks_qubo_sol.source_config.enumerate().filter(((i, x)) => x).map(((i, x)) => i) #let ks_qubo_sel_weight = ks_qubo_selected.fold(0, (a, i) => a + ks_qubo.source.instance.weights.at(i)) @@ -12425,7 +12423,7 @@ where $P$ is a penalty weight large enough that any constraint violation costs m *Step 2 -- Introduce slack variables.* The inequality $sum_i w_i x_i lt.eq C$ becomes an equality by adding $B = #ks_qubo_num_slack$ binary slack bits that encode unused capacity: $ #ks_qubo.source.instance.weights.enumerate().map(((i, w)) => $#w x_#i$).join($+$) + #range(ks_qubo_num_slack).map(j => $#calc.pow(2, j) s_#j$).join($+$) = #ks_qubo.source.instance.capacity $ - This gives $n + B = #ks_qubo_num_items + #ks_qubo_num_slack = #ks_qubo.target.instance.matrix.nrows$ QUBO variables. + This gives $n + B = #ks_qubo_num_items + #ks_qubo_num_slack = #ks_qubo.target.instance.num_vars$ QUBO variables. *Step 3 -- Add the penalty objective.* With penalty $P = 1 + sum_i v_i = #ks_qubo_penalty$, the QUBO minimizes $ H = -(#ks_qubo.source.instance.values.enumerate().map(((i, v)) => $#v x_#i$).join($+$)) + #ks_qubo_penalty (#ks_qubo.source.instance.weights.enumerate().map(((i, w)) => $#w x_#i$).join($+$) + #range(ks_qubo_num_slack).map(j => $#calc.pow(2, j) s_#j$).join($+$) - #ks_qubo.source.instance.capacity)^2 $ @@ -12466,7 +12464,7 @@ where $P$ is a penalty weight large enough that any constraint violation costs m *Step 2 -- One-hot variables.* Introduce one binary selector per sampled orientation: $ underbrace(y_(1,0) y_(1,1), "link 1") #h(6pt) underbrace(y_(2,0) y_(2,1), "link 2") $ - The QUBO therefore has $2 + 2 = #mdpik_qubo.target.instance.matrix.nrows$ variables. + The QUBO therefore has $2 + 2 = #mdpik_qubo.target.instance.num_vars$ variables. *Step 3 -- Quadratic energy.* The geometric coefficients are $c = (2, 0, 1, 0)$ for the $x$-coordinate and $s = (0, 2, 0, 1)$ for the $y$-coordinate, so the position term is $ (2 y_(1,0) + y_(2,0) - 2)^2 + (2 y_(1,1) + y_(2,1) - 1)^2. $ @@ -12580,8 +12578,8 @@ where $P$ is a penalty weight large enough that any constraint violation costs m "pred solve bundle.json", "pred evaluate qubo.json --config " + cli-config(qubo_ilp_sol.source_config), ) - Source: $n = #qubo_ilp.source.instance.matrix.nrows$ binary variables, 3 off-diagonal terms \ - Target: #qubo_ilp.target.instance.variables.len() ILP variables ($#qubo_ilp.source.instance.matrix.nrows$ original $+ #(qubo_ilp.target.instance.variables.len() - qubo_ilp.source.instance.matrix.nrows)$ auxiliary), #qubo_ilp.target.instance.constraints.len() McCormick constraints \ + Source: $n = #qubo_ilp.source.instance.num_vars$ binary variables, 3 off-diagonal terms \ + Target: #qubo_ilp.target.instance.variables.len() ILP variables ($#qubo_ilp.source.instance.num_vars$ original $+ #(qubo_ilp.target.instance.variables.len() - qubo_ilp.source.instance.num_vars)$ auxiliary), #qubo_ilp.target.instance.constraints.len() McCormick constraints \ Canonical optimal witness: $bold(x) = (#fmt-values(qubo_ilp_sol.source_config))$ #sym.checkmark ], )[ @@ -13969,7 +13967,7 @@ The following reductions to Integer Linear Programming are straightforward formu "pred solve bundle.json", "pred evaluate tsp.json --config " + cli-config(tsp_qubo_sol.source_config), ) - *Step 1 -- Encode each tour position as a binary variable.* A tour is a permutation of $n$ vertices. Introduce $n^2 = #tsp_qubo.target.instance.matrix.nrows$ binary variables $x_(v,p)$: vertex $v$ is at position $p$. + *Step 1 -- Encode each tour position as a binary variable.* A tour is a permutation of $n$ vertices. Introduce $n^2 = #tsp_qubo.target.instance.num_vars$ binary variables $x_(v,p)$: vertex $v$ is at position $p$. $ underbrace(x_(0,0) x_(0,1) x_(0,2), "vertex 0") #h(4pt) underbrace(x_(1,0) x_(1,1) x_(1,2), "vertex 1") #h(4pt) underbrace(x_(2,0) x_(2,1) x_(2,2), "vertex 2") $ *Step 2 -- Penalize invalid permutations.* The penalty $A = 1 + |w_(01)| + |w_(02)| + |w_(12)| = 1 + 1 + 2 + 3 = 7$ ensures any row/column constraint violation outweighs any tour cost. Row constraints (each vertex at exactly one position) and column constraints (each position has one vertex) contribute diagonal $-7$ and off-diagonal $+14$ within each group.\ diff --git a/docs/src/design.md b/docs/src/design.md index 0f8b735b0..61c0f455f 100644 --- a/docs/src/design.md +++ b/docs/src/design.md @@ -615,6 +615,11 @@ order, retaining checked integer addition and floating-point summation order. `QUBO::from_sparse` accepts a square CSR or CSC matrix; `matrix()` returns the CSR matrix and `get(i, j)` returns an owned coefficient, including zero for an unstored in-bounds entry. `from_matrix` and CLI `--matrix` accept dense input. -Persisted QUBO JSON stores the `sprs` matrix object (`storage`, `nrows`, `ncols`, -`indptr`, `indices`, `data`); variable count comes from the matrix dimensions. +Persisted QUBO JSON is a project-owned sparse shape, independent of `sprs` +internals: `{"num_vars": n, "entries": [[row, column, value], ...]}` lists every +stored coefficient in row-major order. Loading accepts entries in any order and +stores exactly what is listed, like `from_sparse`; it rejects an index outside +`0..num_vars` and a repeated `(row, column)`. Loading also accepts the legacy +dense shape `{"num_vars": n, "matrix": [[...], ...]}` and reads it like +`from_matrix`. A file must contain exactly one of `entries` and `matrix`. Rules, numeric casts, and solver reductions consume sparse coefficients directly. diff --git a/problemreductions-cli/tests/cli_tests.rs b/problemreductions-cli/tests/cli_tests.rs index 8981857e2..2abbf2bce 100644 --- a/problemreductions-cli/tests/cli_tests.rs +++ b/problemreductions-cli/tests/cli_tests.rs @@ -761,6 +761,73 @@ fn test_evaluate() { std::fs::remove_file(&tmp).ok(); } +#[test] +fn test_legacy_dense_qubo_json_evaluates_and_solves() { + // QUBO files written before sparse storage persist `{num_vars, matrix: [[...]]}`. + let tmp = std::env::temp_dir().join("pred_test_legacy_dense_qubo.json"); + std::fs::write( + &tmp, + format!( + r#"{{"type":"QUBO","variant":{{"weight":"i64"}},"data":{}}}"#, + include_str!("../../tests/data/qubo_legacy_dense.json") + ), + ) + .unwrap(); + + for (args, expected) in [ + ( + vec!["evaluate", "--config", "[true,false,true]"], + serde_json::json!({"problem": "QUBO", "config": [true, false, true], "result": "Min(-3)"}), + ), + ( + vec!["solve", "--solver", "brute-force"], + serde_json::json!({ + "problem": "QUBO", + "status": "optimal", + "solution": [false, false, true], + "evaluation": "Min(-6)", + "solver": {"kind": "brute-force"}, + }), + ), + ] { + let output = pred() + .args(["--json", args[0], tmp.to_str().unwrap()]) + .args(&args[1..]) + .output() + .unwrap(); + assert!( + output.status.success(), + "stderr: {}", + String::from_utf8_lossy(&output.stderr) + ); + let json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap(); + assert_eq!(json, expected); + } + std::fs::remove_file(&tmp).ok(); +} + +#[test] +fn test_create_qubo_writes_sparse_entries() { + let output = pred() + .args(["create", "QUBO", "--matrix", "3,-5,0;0,0,7;0,0,-6"]) + .output() + .unwrap(); + assert!( + output.status.success(), + "stderr: {}", + String::from_utf8_lossy(&output.stderr) + ); + let json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap(); + assert_eq!( + json, + serde_json::json!({ + "type": "QUBO", + "variant": {"weight": "i64"}, + "data": {"num_vars": 3, "entries": [[0, 0, 3], [0, 1, -5], [1, 2, 7], [2, 2, -6]]}, + }) + ); +} + #[test] fn test_evaluate_sat() { let problem_json = r#"{ @@ -10052,7 +10119,7 @@ fn test_extract_rejects_tampered_target_data() { // what the reduction chain actually produces. let bundle_text = std::fs::read_to_string(&bundle_file).unwrap(); let mut bundle: serde_json::Value = serde_json::from_str(&bundle_text).unwrap(); - bundle["target"]["data"]["matrix"]["data"][0] = serde_json::json!(999.0); + bundle["target"]["data"]["entries"][0][2] = serde_json::json!(999.0); let mut f = std::fs::File::create(&tampered_file).unwrap(); f.write_all(bundle.to_string().as_bytes()).unwrap(); diff --git a/src/models/algebraic/qubo.rs b/src/models/algebraic/qubo.rs index 7ba5d10fd..c687e773f 100644 --- a/src/models/algebraic/qubo.rs +++ b/src/models/algebraic/qubo.rs @@ -38,6 +38,10 @@ inventory::submit! { /// finite floating-point coefficients. An explicit variant reduction converts /// exactly representable integer coefficients to `f64`. /// +/// Persisted JSON is `{"num_vars": n, "entries": [[row, column, value], ...]}`, listing the +/// stored coefficients in row-major order. Loading also accepts the legacy dense +/// `{"num_vars": n, "matrix": [[...], ...]}` shape, which is read like [`QUBO::from_matrix`]. +/// /// # Example /// /// ``` @@ -57,7 +61,7 @@ inventory::submit! { /// // Optimal is x = [0, 1] with value -2 /// assert!(solutions.contains(&vec![false, true])); /// ``` -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Deserialize)] #[serde( try_from = "QuboData", bound(deserialize = "W: WeightElement + Deserialize<'de>") @@ -66,15 +70,51 @@ pub struct QUBO { matrix: CsMat, } +/// Persisted sparse shape: every stored coefficient as `[row, column, value]` in row-major order. +#[derive(Serialize)] +struct QuboEntries<'a, W> { + num_vars: usize, + entries: Vec<(usize, usize, &'a W)>, +} + +impl Serialize for QUBO { + fn serialize(&self, serializer: S) -> Result { + QuboEntries { + num_vars: self.num_vars(), + entries: self + .matrix + .iter() + .map(|(value, (row, column))| (row, column, value)) + .collect(), + } + .serialize(serializer) + } +} + +/// Accepted JSON: sparse `entries`, or the legacy dense `matrix` written before sparse storage. #[derive(Deserialize)] struct QuboData { - matrix: CsMat, + num_vars: usize, + entries: Option>, + matrix: Option>>, } impl TryFrom> for QUBO { type Error = ConstructionError; fn try_from(data: QuboData) -> Result { - Self::from_sparse(data.matrix) + match (data.entries, data.matrix) { + (Some(entries), None) => Self::from_entries(data.num_vars, entries), + (None, Some(matrix)) if matrix.len() == data.num_vars => Self::from_matrix(matrix), + (None, Some(matrix)) => Err(ConstructionError::Conversion(format!( + "QUBO num_vars is {}, but the dense matrix has {} rows", + data.num_vars, + matrix.len() + ))), + _ => Err(ConstructionError::Conversion( + "QUBO JSON must contain exactly one of `entries` (sparse) or `matrix` (legacy dense)" + .into(), + )), + } } } @@ -161,6 +201,62 @@ impl QUBO { } offsets.push(values.len()); } + Self::from_csr_parts(n, offsets, indices, values) + } + + // Stores exactly the listed coefficients, like `from_sparse`; explicit zeros and + // lower-triangle entries are kept. + fn from_entries( + num_vars: usize, + mut entries: Vec<(usize, usize, W)>, + ) -> Result { + if let Some(&(row, column, _)) = entries + .iter() + .find(|&&(row, column, _)| row >= num_vars || column >= num_vars) + { + return Err(ConstructionError::Conversion(format!( + "QUBO entry index ({row}, {column}) is outside 0..{num_vars}" + ))); + } + entries.sort_by_key(|&(row, column, _)| (row, column)); + if let Some(pair) = entries + .windows(2) + .find(|pair| (pair[0].0, pair[0].1) == (pair[1].0, pair[1].1)) + { + return Err(ConstructionError::Conversion(format!( + "QUBO entry ({}, {}) is listed more than once", + pair[0].0, pair[0].1 + ))); + } + let mut offsets = Vec::new(); + num_vars + .checked_add(1) + .ok_or_else(|| ConstructionError::IntegerOverflow("counting QUBO rows".into())) + .and_then(|len| { + offsets.try_reserve_exact(len).map_err(|error| { + ConstructionError::Conversion(format!("allocating QUBO rows: {error}")) + }) + })?; + offsets.resize(num_vars + 1, 0); + for &(row, _, _) in &entries { + offsets[row + 1] += 1; + } + for row in 0..num_vars { + offsets[row + 1] += offsets[row]; + } + let (indices, values) = entries + .into_iter() + .map(|(_, column, value)| (column, value)) + .unzip(); + Self::from_csr_parts(num_vars, offsets, indices, values) + } + + fn from_csr_parts( + n: usize, + offsets: Vec, + indices: Vec, + values: Vec, + ) -> Result { let matrix = CsMat::try_new((n, n), offsets, indices, values) .map_err(|(_, _, _, error)| ConstructionError::Conversion(error.to_string()))?; Self::from_sparse(matrix) diff --git a/src/unit_tests/models/algebraic/qubo.rs b/src/unit_tests/models/algebraic/qubo.rs index 97eb6ed27..8b3968291 100644 --- a/src/unit_tests/models/algebraic/qubo.rs +++ b/src/unit_tests/models/algebraic/qubo.rs @@ -276,7 +276,201 @@ fn sparse_qubo_validates_shape_values_and_serialized_structure() { let problem = QUBO::from_sparse(column_matrix).unwrap(); assert!(problem.matrix().is_csr()); assert_eq!(problem.evaluate(&vec![true, true]).unwrap(), Min(Some(5))); - let mut json = serde_json::to_value(&problem).unwrap(); - json["matrix"]["indptr"] = serde_json::json!([0, 3, 2]); - assert!(serde_json::from_value::>(json).is_err()); + assert_eq!( + serde_json::to_string(&problem).unwrap(), + r#"{"num_vars":2,"entries":[[0,0,2],[0,1,3]]}"# + ); +} + +fn assignments(n: usize) -> impl Iterator> { + (0..1usize << n).map(move |mask| (0..n).map(|i| mask & (1 << i) != 0).collect()) +} + +fn load_error(json: &str) -> String +where + W: WeightElement + serde::de::DeserializeOwned, +{ + serde_json::from_str::>(json) + .err() + .expect("QUBO JSON should be rejected") + .to_string() +} + +#[test] +fn qubo_json_writes_row_major_entries() { + // The lower-triangle 99 is stored by from_matrix and therefore persisted, though never evaluated. + let problem = QUBO::from_matrix(vec![vec![3, -5, 0], vec![99, 0, 7], vec![0, 0, -6]]).unwrap(); + assert_eq!( + serde_json::to_string(&problem).unwrap(), + r#"{"num_vars":3,"entries":[[0,0,3],[0,1,-5],[1,0,99],[1,2,7],[2,2,-6]]}"# + ); + let floating = QUBO::new(vec![0.5, 0.0, -2.0], vec![((0, 2), 1e16)]).unwrap(); + assert_eq!( + serde_json::to_string(&floating).unwrap(), + r#"{"num_vars":3,"entries":[[0,0,0.5],[0,2,1e+16],[2,2,-2.0]]}"# + ); + assert_eq!( + serde_json::to_string(&QUBO::::from_matrix(vec![]).unwrap()).unwrap(), + r#"{"num_vars":0,"entries":[]}"# + ); +} + +#[test] +fn qubo_json_round_trip_preserves_storage_and_every_evaluation() { + let integer = QUBO::from_matrix(vec![vec![3, -5, 0], vec![99, 0, 7], vec![0, 0, -6]]).unwrap(); + let restored: QUBO = + serde_json::from_str(&serde_json::to_string(&integer).unwrap()).unwrap(); + assert_eq!(restored.matrix(), integer.matrix()); + for solution in assignments(3) { + assert_eq!( + restored.evaluate(&solution).unwrap(), + integer.evaluate(&solution).unwrap() + ); + } + + let floating = QUBO::from_matrix(vec![ + vec![1e16, 1.0, -1e16, 0.0], + vec![99.0, 0.5, 0.0, -0.25], + vec![0.0, 0.0, -2.0, 0.0], + vec![0.0, 0.0, 0.0, 1.0], + ]) + .unwrap(); + let restored: QUBO = + serde_json::from_str(&serde_json::to_string(&floating).unwrap()).unwrap(); + assert_eq!(restored.matrix(), floating.matrix()); + for solution in assignments(4) { + assert_eq!( + restored.evaluate(&solution).unwrap().unwrap().to_bits(), + floating.evaluate(&solution).unwrap().unwrap().to_bits() + ); + } +} + +#[test] +fn qubo_json_entries_behave_like_from_sparse() { + // Any entry order is accepted; explicit zeros and lower-triangle entries stay stored. + let restored: QUBO = serde_json::from_str( + r#"{"num_vars":3,"entries":[[2,2,-6],[1,0,99],[0,1,-5],[1,1,0],[0,0,3]]}"#, + ) + .unwrap(); + let expected = QUBO::from_sparse(CsMat::new( + (3, 3), + vec![0, 2, 4, 5], + vec![0, 1, 0, 1, 2], + vec![3i64, -5, 99, 0, -6], + )) + .unwrap(); + assert_eq!(restored.matrix(), expected.matrix()); + assert_eq!( + serde_json::to_string(&restored).unwrap(), + r#"{"num_vars":3,"entries":[[0,0,3],[0,1,-5],[1,0,99],[1,1,0],[2,2,-6]]}"# + ); + assert_eq!( + restored.evaluate(&vec![true, true, false]).unwrap(), + Min(Some(-2)) + ); +} + +#[test] +fn qubo_json_legacy_dense_matrix_loads_like_from_matrix() { + let legacy = include_str!("../../../../tests/data/qubo_legacy_dense.json"); + let restored: QUBO = serde_json::from_str(legacy).unwrap(); + let expected = QUBO::from_matrix(vec![vec![3, -5, 0], vec![99, 0, 7], vec![0, 0, -6]]).unwrap(); + assert_eq!(restored.matrix(), expected.matrix()); + for solution in assignments(3) { + assert_eq!( + restored.evaluate(&solution).unwrap(), + expected.evaluate(&solution).unwrap() + ); + } + let floating: QUBO = + serde_json::from_str(r#"{"num_vars":3,"matrix":[[0.5,1,0],[0,0,0],[0,0,-2]]}"#).unwrap(); + assert_eq!( + serde_json::to_string(&floating).unwrap(), + r#"{"num_vars":3,"entries":[[0,0,0.5],[0,1,1.0],[2,2,-2.0]]}"# + ); +} + +#[test] +fn qubo_json_rejects_malformed_shapes() { + const SHAPE: &str = "problem construction failed: QUBO JSON must contain exactly one of \ + `entries` (sparse) or `matrix` (legacy dense)"; + assert_eq!(load_error::(r#"{"num_vars":3}"#), SHAPE); + assert_eq!( + load_error::(r#"{"num_vars":1,"entries":[],"matrix":[[0]]}"#), + SHAPE + ); + assert_eq!( + load_error::(r#"{"num_vars":3,"entries":[[0,3,1]]}"#), + "problem construction failed: QUBO entry index (0, 3) is outside 0..3" + ); + assert_eq!( + load_error::(r#"{"num_vars":3,"entries":[[3,0,1]]}"#), + "problem construction failed: QUBO entry index (3, 0) is outside 0..3" + ); + assert_eq!( + load_error::(r#"{"num_vars":3,"entries":[[1,2,4],[0,0,1],[1,2,5]]}"#), + "problem construction failed: QUBO entry (1, 2) is listed more than once" + ); + assert_eq!( + load_error::(r#"{"num_vars":3,"matrix":[[1,2,3],[0,4],[0,0,5]]}"#), + "problem construction failed: QUBO matrix row 1 has length 2, expected 3" + ); + assert_eq!( + load_error::(r#"{"num_vars":3,"matrix":[[1,2],[0,4]]}"#), + "problem construction failed: QUBO num_vars is 3, but the dense matrix has 2 rows" + ); + assert!(load_error::(r#"{"entries":[]}"#).starts_with("missing field `num_vars`")); + // The unreleased sprs CSR layout is not a supported shape. + assert!(load_error::( + r#"{"matrix":{"storage":"CSR","nrows":1,"ncols":1,"indptr":[0,1],"indices":[0],"data":[1]}}"# + ) + .starts_with("invalid type: map, expected a sequence")); + assert!(load_error::(r#"{"num_vars":1,"entries":[[0,0,0.5]]}"#) + .starts_with("invalid type: floating point `0.5`, expected i64")); +} + +#[test] +fn qubo_json_rejects_non_finite_coefficients() { + // JSON cannot spell a non-finite number, so the rejection is checked on the parsed mirror. + let sparse = QuboData { + num_vars: 3, + entries: Some(vec![(0, 0, 1.0), (1, 2, f64::INFINITY)]), + matrix: None, + }; + assert_eq!( + QUBO::try_from(sparse).unwrap_err().to_string(), + "non-finite floating-point construction value: QUBO coefficient must be finite at (1, 2)" + ); + let dense = QuboData { + num_vars: 3, + entries: None, + matrix: Some(vec![ + vec![0.0, 0.0, 0.0], + vec![0.0, 0.0, 0.0], + vec![0.0, 0.0, f64::NAN], + ]), + }; + assert_eq!( + QUBO::try_from(dense).unwrap_err().to_string(), + "non-finite floating-point construction value: QUBO coefficient must be finite at (2, 2)" + ); + assert!( + load_error::(r#"{"num_vars":1,"entries":[[0,0,1e999]]}"#) + .starts_with("number out of range") + ); +} + +#[test] +fn qubo_json_rejects_unallocatable_num_vars() { + assert_eq!( + QUBO::::from_entries(usize::MAX, vec![]) + .unwrap_err() + .to_string(), + "integer overflow during construction: counting QUBO rows" + ); + assert!(QUBO::::from_entries(usize::MAX - 1, vec![]) + .unwrap_err() + .to_string() + .starts_with("problem construction failed: allocating QUBO rows:")); } diff --git a/src/unit_tests/rules/closestvectorproblem_qubo.rs b/src/unit_tests/rules/closestvectorproblem_qubo.rs index e483678cf..a6d977552 100644 --- a/src/unit_tests/rules/closestvectorproblem_qubo.rs +++ b/src/unit_tests/rules/closestvectorproblem_qubo.rs @@ -186,7 +186,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["matrix"]["nrows"], 11); + assert_eq!(example.target.instance["num_vars"], 11); assert_eq!( example.solutions[0].source_config, serde_json::json!([1, 1]) diff --git a/src/unit_tests/rules/graphpartitioning_qubo.rs b/src/unit_tests/rules/graphpartitioning_qubo.rs index 1c6ac8fbc..34c5c13d8 100644 --- a/src/unit_tests/rules/graphpartitioning_qubo.rs +++ b/src/unit_tests/rules/graphpartitioning_qubo.rs @@ -77,7 +77,7 @@ fn test_graphpartitioning_to_qubo_canonical_example_spec() { assert_eq!(example.source.problem, "GraphPartitioning"); assert_eq!(example.target.problem, "QUBO"); - assert_eq!(example.target.instance["matrix"]["nrows"], 6); + assert_eq!(example.target.instance["num_vars"], 6); assert!(!example.solutions.is_empty()); } diff --git a/src/unit_tests/rules/knapsack_qubo.rs b/src/unit_tests/rules/knapsack_qubo.rs index 8d9e763b3..37dd12fa9 100644 --- a/src/unit_tests/rules/knapsack_qubo.rs +++ b/src/unit_tests/rules/knapsack_qubo.rs @@ -99,6 +99,6 @@ fn test_knapsack_to_qubo_canonical_example_spec() { assert_eq!(example.source.problem, "Knapsack"); assert_eq!(example.target.problem, "QUBO"); assert_eq!(example.source.instance["capacity"], 7); - assert_eq!(example.target.instance["matrix"]["nrows"], 7); + assert_eq!(example.target.instance["num_vars"], 7); assert!(!example.solutions.is_empty()); } diff --git a/src/unit_tests/rules/minimumdiscreteplanarinversekinematics_qubo.rs b/src/unit_tests/rules/minimumdiscreteplanarinversekinematics_qubo.rs index d8eac3741..70fc14e4d 100644 --- a/src/unit_tests/rules/minimumdiscreteplanarinversekinematics_qubo.rs +++ b/src/unit_tests/rules/minimumdiscreteplanarinversekinematics_qubo.rs @@ -135,7 +135,7 @@ fn test_minimumdiscreteplanarinversekinematics_to_qubo_canonical_example_spec() "MinimumDiscretePlanarInverseKinematics" ); assert_eq!(example.target.problem, "QUBO"); - assert_eq!(example.target.instance["matrix"]["nrows"], 4); + assert_eq!(example.target.instance["num_vars"], 4); assert_eq!( example.solutions[0].source_config, serde_json::json!([0, 1]) diff --git a/src/unit_tests/rules/paintshop_qubo.rs b/src/unit_tests/rules/paintshop_qubo.rs index cd97aaed0..ec52b6d39 100644 --- a/src/unit_tests/rules/paintshop_qubo.rs +++ b/src/unit_tests/rules/paintshop_qubo.rs @@ -120,6 +120,6 @@ fn test_paintshop_to_qubo_canonical_example_spec() { assert_eq!(example.source.problem, "PaintShop"); assert_eq!(example.target.problem, "QUBO"); assert_eq!(example.source.instance["num_cars"], 4); - assert_eq!(example.target.instance["matrix"]["nrows"], 4); + assert_eq!(example.target.instance["num_vars"], 4); assert!(!example.solutions.is_empty()); } diff --git a/tests/data/qubo_legacy_dense.json b/tests/data/qubo_legacy_dense.json new file mode 100644 index 000000000..1a44553ec --- /dev/null +++ b/tests/data/qubo_legacy_dense.json @@ -0,0 +1 @@ +{"num_vars":3,"matrix":[[3,-5,0],[99,0,7],[0,0,-6]]} From 0546e8868a84aef9bbc9fad52323ac00f195e97d Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Thu, 17 Sep 2026 23:51:42 +0800 Subject: [PATCH 35/42] Restore the checked integer transport into f64 ILP expressions Co-Authored-By: Claude Fable 5.1 --- src/models/algebraic/ilp.rs | 6 +++++- src/unit_tests/models/algebraic/ilp.rs | 30 ++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/src/models/algebraic/ilp.rs b/src/models/algebraic/ilp.rs index 709e2299f..a20f27743 100644 --- a/src/models/algebraic/ilp.rs +++ b/src/models/algebraic/ilp.rs @@ -78,7 +78,11 @@ impl ILPCoefficient for f64 { const NAME: &'static str = "f64"; fn from_integer(value: i64) -> Result { - Ok(value as f64) + crate::types::i64_to_exact_f64(value).map_err(|_| { + EvaluationError::InexactFloatConversion( + "transporting an integer variable into an f64 ILP expression".into(), + ) + }) } fn satisfies(lhs: Self, comparison: Comparison, rhs: Self) -> bool { diff --git a/src/unit_tests/models/algebraic/ilp.rs b/src/unit_tests/models/algebraic/ilp.rs index 01e00e9de..7cc5d34d3 100644 --- a/src/unit_tests/models/algebraic/ilp.rs +++ b/src/unit_tests/models/algebraic/ilp.rs @@ -61,6 +61,36 @@ fn float_constraints_use_float_arithmetic() { assert_eq!(ilp.evaluate_objective(&[1, 0]).unwrap(), 0.5); } +#[test] +fn float_ilp_rejects_integer_values_without_an_exact_f64_image() { + use crate::traits::EvaluationError; + use crate::types::MAX_EXACT_F64_INTEGER; + + let ilp = ILP::::new( + 1, + vec![LinearConstraint::le(vec![(0, 1.0)], 1.0e16)], + vec![(0, 1.0)], + ObjectiveSense::Maximize, + ) + .unwrap(); + + assert_eq!( + ilp.evaluate_objective(&[MAX_EXACT_F64_INTEGER]).unwrap(), + MAX_EXACT_F64_INTEGER as f64 + ); + assert!(ilp.is_feasible(&[MAX_EXACT_F64_INTEGER]).unwrap()); + assert!(matches!( + ilp.is_feasible(&[MAX_EXACT_F64_INTEGER + 1]), + Err(EvaluationError::InexactFloatConversion(_)) + )); + for value in [MAX_EXACT_F64_INTEGER + 1, -MAX_EXACT_F64_INTEGER - 1] { + assert!(matches!( + ilp.evaluate_objective(&[value]), + Err(EvaluationError::InexactFloatConversion(_)) + )); + } +} + #[test] fn float_ilp_rejects_non_finite_coefficients() { assert!(matches!( From b3758460d230bd592649c4ca8214018b98d23a0a Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Thu, 17 Sep 2026 23:53:01 +0800 Subject: [PATCH 36/42] Revert "Remove the unused Sum and And value wrappers" This reverts commit 675711d812309b0444b00b02d7da2e0badf98db4. --- .claude/CLAUDE.md | 4 +- .claude/skills/add-model/SKILL.md | 13 +- .claude/skills/fix-issue/SKILL.md | 2 +- .claude/skills/review-structural/SKILL.md | 4 +- docs/src/design.md | 4 +- docs/src/static/trait-hierarchy-dark.svg | 2 +- docs/src/static/trait-hierarchy.svg | 2 +- docs/src/static/trait-hierarchy.typ | 5 +- src/lib.rs | 7 +- src/types.rs | 49 +++++ src/unit_tests/registry/dispatch.rs | 3 +- src/unit_tests/rules/graph.rs | 252 +++++++++++----------- src/unit_tests/rules/traits.rs | 56 ++--- src/unit_tests/solvers/brute_force.rs | 25 ++- src/unit_tests/types.rs | 46 ++++ 15 files changed, 301 insertions(+), 173 deletions(-) diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 0b563b63d..fc5bda591 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -149,7 +149,7 @@ solves. Common aggregate wrappers live in `src/types.rs`: ```rust -Max, Min, Or, Extremum, ExtremumSense +Max, Min, Sum, Or, And, Extremum, ExtremumSense ``` `OptimizationValue` trait (in `src/types.rs`) enables generic Decision conversion: @@ -171,7 +171,7 @@ Max, Min, Or, Extremum, ExtremumSense - `ReductionResult` provides `target_problem()` and mandatory `recover_result(source, target_outcome)`. Recovery returns typed `Optimal`, `Feasible`, or `Infeasible` outcomes, including solution and evaluation. Each rule handles all statuses explicitly; no optional completion callback or separate value-only path exists. - `pred solve bundle.json` and `pred extract bundle.json --result target-result.json` use the same complete recovery. External results declare their status; the transport boundary validates target feasibility, while the external solver supplies the optimality claim. Insufficient witness quality is an error, never evidence of source infeasibility. - Decode only the reduction's defined mathematical mapping. Preserve reachable mathematical and representation errors; do not add fallback values or recovery branches for violations already excluded by the calling contract. Explicit mathematical alternatives and sentinels are allowed. -- CLI-facing dynamic formatting uses aggregate wrapper names directly (for example `Max(2)`, `Min(None)`, or `Or(true)`) +- CLI-facing dynamic formatting uses aggregate wrapper names directly (for example `Max(2)`, `Min(None)`, `Or(true)`, or `Sum(56)`) - Graph types: SimpleGraph, PlanarGraph, BipartiteGraph, UnitDiskGraph, KingsSubgraph, TriangularSubgraph - Weight types: `One` (unit weight marker), `i64`, `f64` — all implement `WeightElement` trait - `WeightElement` trait: `type Sum: NumericSize` + `fn to_sum(&self)` — converts weight to a summable numeric type diff --git a/.claude/skills/add-model/SKILL.md b/.claude/skills/add-model/SKILL.md index 926eb901b..4df33fdb6 100644 --- a/.claude/skills/add-model/SKILL.md +++ b/.claude/skills/add-model/SKILL.md @@ -17,7 +17,7 @@ Before any implementation, collect all required information. If called from `iss |---|------|-------------|---------| | 1 | **Problem name** | Struct name with optimization prefix | `MaximumClique`, `MinimumDominatingSet` | | 2 | **Mathematical definition** | Formal definition with objective/constraints | "Given graph G=(V,E), find max-weight subset S where all pairs in S are adjacent" | -| 3 | **Problem type** | Objective (`Max`/`Min`/`Extremum`) or witness (`Or`) | Objective (Maximize) | +| 3 | **Problem type** | Objective (`Max`/`Min`), witness (`bool`), or aggregate-only (`Sum`/`And`/custom `Aggregate`) | Objective (Maximize) | | 4 | **Type parameters** | Graph type `G`, weight type `W`, or other | `G: Graph`, `W: WeightElement` | | 5 | **Struct fields** | What the struct holds | `graph: G`, `weights: Vec` | | 6 | **Configuration space** | Mathematical solution representation and domain | One Boolean selection per vertex | @@ -26,7 +26,7 @@ Before any implementation, collect all required information. If called from `iss | 9 | **Best known exact algorithm** | Complexity with variable definitions | "O(1.1996^n) by Xiao & Nagamochi (2017), where n = \|V\|" | | 10 | **Solving strategy** | How it can be solved | "BruteForce works; ILP reduction available" | | 11 | **Category** | Which sub-module under `src/models/` | `graph`, `formula`, `set`, `algebraic`, `misc` | -| 12 | **Expected outcome from the issue** | Concrete outcome for the issue's example instance | Objective: one optimal solution + optimal value. Witness: one valid/satisfying solution + why it is valid | +| 12 | **Expected outcome from the issue** | Concrete outcome for the issue's example instance | Objective: one optimal solution + optimal value. Witness: one valid/satisfying solution + why it is valid. Aggregate-only: the final aggregate value and how it is derived | If any item is missing, ask the user to provide it. Do NOT proceed until the checklist is complete. @@ -66,7 +66,7 @@ Read these first to understand the patterns: - **Optimization problem:** `src/models/graph/maximum_independent_set.rs` - **Satisfaction problem:** `src/models/formula/sat.rs` - **Model tests:** `src/unit_tests/models/graph/maximum_independent_set.rs` -- **Trait definitions / aggregate types:** `src/traits.rs` (`Problem`), `src/types.rs` (`Aggregate`, `Max`, `Min`, `Or`, `Extremum`) +- **Trait definitions / aggregate types:** `src/traits.rs` (`Problem`), `src/types.rs` (`Aggregate`, `Max`, `Min`, `Sum`, `Or`, `And`, `Extremum`) - **Registry dispatch boundary:** `src/registry/mod.rs`, `src/registry/variant.rs` - **CLI and MCP construction:** discovered from the model's registry entry; no frontend model-name dispatch - **Canonical model examples:** `src/example_db/model_builders.rs` @@ -129,7 +129,7 @@ Key decisions: - **Schema metadata:** `ProblemSchemaEntry` must include the explicit structural `category` and reflect the construction interface through `display_name`, `aliases`, `dimensions`, and `fields` - **Objective problems:** use `type Value = Max<_>`, `Min<_>`, or `Extremum<_>` when the model should expose optimization-style witness helpers - **Witness problems:** use `type Value = Or` for existential feasibility problems -- **No value-only problems:** `Problem::Value` must implement `EvaluationValue` (`Max`, `Min`, `Extremum`, `Or`); global counts or statistics without a representative solution are not modeled as `Problem` +- **Aggregate-only problems:** use a value-only aggregate such as `Sum<_>`, `And`, or a custom `Aggregate` when witnesses are not meaningful - **Weight management:** use inherent methods (`weights()`, `set_weights()`, `is_weighted()`), NOT traits - **`dims()`:** returns the configuration space dimensions (e.g., `vec![2; n]` for binary variables) - **`evaluate()`:** must return `Result`. Invalid configurations remain the aggregate's invalid/false contribution; arithmetic overflow and non-finite computed values are errors. @@ -155,7 +155,7 @@ crate::declare_variants! { - A compiled `complexity_eval_fn` plus registry-backed load/serialize/solve dispatch metadata are auto-generated alongside the symbolic expression - See `src/models/graph/maximum_independent_set.rs` for the reference pattern -`declare_variants!` handles objective and witness models uniformly. Use manual `VariantEntry` wiring only for unusual dynamic-registration work, not for ordinary models. +`declare_variants!` now handles objective, witness-capable, and aggregate-only models uniformly. Use manual `VariantEntry` wiring only for unusual dynamic-registration work, not for ordinary models. ## Step 3: Register the model @@ -320,9 +320,10 @@ Structural and quality review is handled by the `review-pipeline` stage, not her | Omitting or inferring the model category | Set the required `ProblemSchemaEntry.category` explicitly to one of `Algebraic`, `Formula`, `Graph`, `Misc`, or `Set`; never parse `module_path!()`. | | Missing `#[path]` test link | Add `#[cfg(test)] #[path = "..."] mod tests;` at file bottom | | Wrong `dims()` | Must match the actual configuration space (e.g., `vec![2; n]` for binary) | -| Using the wrong aggregate wrapper | Objective models use `Max` / `Min` / `Extremum`, witness models use `Or` | +| Using the wrong aggregate wrapper | Objective models use `Max` / `Min` / `Extremum`, witness models use `bool`, aggregate-only models use a fold value like `Sum` / `And` | | Not registering in `mod.rs` | Must update both `/mod.rs` and `models/mod.rs` | | Forgetting `declare_variants!` | Required for variant complexity metadata and registry-backed load/serialize/solve dispatch | +| Wrong aggregate wrapper | Use `Max` / `Min` / `Extremum` for objective problems, `Or` for existential witness problems, and `Sum` / `And` (or a custom aggregate) for value-only folds | | Wrong `declare_variants!` syntax | Entries no longer use `opt` / `sat`; one entry per problem may be marked `default` | | Adding aliases in CLI code | Declare problem aliases in `ProblemSchemaEntry.aliases` and variant aliases in `declare_variants!` | | Adding a hand-written decision model | Use `Decision

` wrapper instead — see `decision_problem_meta!` + `register_decision_variant!` in `src/models/graph/minimum_vertex_cover.rs` for the pattern | diff --git a/.claude/skills/fix-issue/SKILL.md b/.claude/skills/fix-issue/SKILL.md index b8cfff097..fe1d1d777 100644 --- a/.claude/skills/fix-issue/SKILL.md +++ b/.claude/skills/fix-issue/SKILL.md @@ -196,7 +196,7 @@ Tag each issue as: | Incorrect mathematical claims | Domain expertise needed | | Incomplete reduction algorithm | Core technical content | | Incomplete or trivial example | Present **3 concrete example options** with pros/cons (use `AskUserQuestion` with previews showing vertex/edge counts, optimal values, and suboptimal cases). Prefer examples that match the model issue's example when a companion model exists. | -| Decision vs optimization framing | **Default to objective-style models** unless evidence points otherwise. In the current aggregate-value architecture, that usually means `type Value = Max<_>`, `Min<_>`, or `Extremum<_>` when the sense is runtime data. Check associated `[Rule]` issues (`gh issue list --search " in:title label:rule"`) to see how rules use this model — if rules only need the decision version (e.g., reducing to SAT with a bound), an objective model still works because the bound can be read from the optimal aggregate value. Use `Or` for inherently existential feasibility problems (SAT, KColoring) where there is no natural objective. A quantity that is a fold over all configurations with no representative witness (counting, global statistics) is not a `Problem` model. If switching to an objective model, add the appropriate `Minimum`/`Maximum` prefix per codebase conventions. | +| Decision vs optimization framing | **Default to objective-style models** unless evidence points otherwise. In the current aggregate-value architecture, that usually means `type Value = Max<_>`, `Min<_>`, or `Extremum<_>` when the sense is runtime data. Check associated `[Rule]` issues (`gh issue list --search " in:title label:rule"`) to see how rules use this model — if rules only need the decision version (e.g., reducing to SAT with a bound), an objective model still works because the bound can be read from the optimal aggregate value. Use `Or` for inherently existential feasibility problems (SAT, KColoring) where there is no natural objective. Use aggregate-only values such as `Sum<_>` or `And` only when the answer is genuinely a fold over all configurations and there is no representative witness. If switching to an objective model, add the appropriate `Minimum`/`Maximum` prefix per codebase conventions. | | Ambiguous overhead expressions | Requires understanding the reduction | --- diff --git a/.claude/skills/review-structural/SKILL.md b/.claude/skills/review-structural/SKILL.md index 0dfe94a3e..ed7a0365d 100644 --- a/.claude/skills/review-structural/SKILL.md +++ b/.claude/skills/review-structural/SKILL.md @@ -109,7 +109,7 @@ Report pass/fail. If tests fail, identify which tests. **Do NOT fix anything** ## Step 4: Semantic Review ### For Models: -1. **`evaluate()` correctness** — Does it check feasibility before computing the objective when the model has invalid configurations? Objective models should return `Max/Min/Extremum(None)` for infeasible configs, and witness problems should return `Or(false)`. +1. **`evaluate()` correctness** — Does it check feasibility before computing the objective when the model has invalid configurations? Objective models should return `Max/Min/Extremum(None)` for infeasible configs, witness problems should return `false`, and aggregate-only models should return the per-configuration contribution that matches the intended fold semantics. 2. **`dims()` correctness** — Does it return the actual configuration space? (e.g., `vec![2; n]` for binary) 3. **Size getter consistency** — Do inherent getter methods (e.g., `num_vertices()`, `num_edges()`) match names used in overhead expressions? 4. **Weight handling** — Are weights managed via inherent methods, not traits? @@ -131,7 +131,7 @@ Only if a linked issue was provided. |---|-------| | 1 | Problem name matches issue | | 2 | Mathematical definition matches | -| 3 | Problem framing (objective / witness) matches | +| 3 | Problem framing (objective / witness / aggregate-only) matches | | 4 | Type parameters match | | 5 | Configuration space matches | | 6 | Feasibility check matches | diff --git a/docs/src/design.md b/docs/src/design.md index 61c0f455f..d6d411c52 100644 --- a/docs/src/design.md +++ b/docs/src/design.md @@ -42,7 +42,7 @@ trait Problem: Clone { - **Objective problems** — typically use `Max`, `Min`, or `Extremum` as `Value`. - **Feasibility problems** — typically use `Or`. - **Solve contract** — a successful solve always returns the problem's `Solution`; a global count or statistic without a representative solution is not a `Problem` solve. -- **Common aggregate wrappers** — `Max`, `Min`, `Or`, `Extremum`, `ExtremumSense`. +- **Common aggregate wrappers** — `Max`, `Min`, `Sum`, `Or`, `And`, `Extremum`, `ExtremumSense`. ## Construction inputs @@ -198,7 +198,7 @@ its source-result relation, including thresholds and sentinel constructions. Guarantees must cover every qualifying witness, including tied optima. `SolutionAggregate` remains a brute-force solver capability for selecting from -an enumeration. Mathematical wrappers such as `Min`, `Max`, `Or`, and `Extremum` +an enumeration. Mathematical wrappers such as `Min`, `Max`, `Or`, and `Sum` remain model values. They do not require separate reduction traits or graph modes. Turing edges describe multiple adaptive queries and are retained only as theoretical graph relationships, not executable reductions. The library does not diff --git a/docs/src/static/trait-hierarchy-dark.svg b/docs/src/static/trait-hierarchy-dark.svg index 8393552e9..3e35702f4 100644 --- a/docs/src/static/trait-hierarchy-dark.svg +++ b/docs/src/static/trait-hierarchy-dark.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/docs/src/static/trait-hierarchy.svg b/docs/src/static/trait-hierarchy.svg index de23ebdc2..a1b82bfb0 100644 --- a/docs/src/static/trait-hierarchy.svg +++ b/docs/src/static/trait-hierarchy.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/docs/src/static/trait-hierarchy.typ b/docs/src/static/trait-hierarchy.typ index f1d9d6b94..a9ce6343d 100644 --- a/docs/src/static/trait-hierarchy.typ +++ b/docs/src/static/trait-hierarchy.typ @@ -66,8 +66,9 @@ node((0, 2), box(width: 48mm, align(left)[ #strong[Common Value Types]\ #text(size: 8pt, fill: secondary)[ - `Max | Min | Extremum | Or`\ - #text(style: "italic")[all implement `SolutionAggregate`] + `Max | Min | Extremum`\ + `Or | Sum | And`\ + #text(style: "italic")[only selecting values implement `SolutionAggregate`] ] ]), fill: type-fill, corner-radius: 6pt, inset: 10pt, name: ), diff --git a/src/lib.rs b/src/lib.rs index 8b1b2a1cd..e84718db2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -106,7 +106,9 @@ pub mod prelude { // Types pub use crate::error::{ProblemError, Result}; - pub use crate::types::{Extremum, ExtremumSense, Max, Min, One, Or, ProblemParameters}; + pub use crate::types::{ + And, Extremum, ExtremumSense, Max, Min, One, Or, ProblemParameters, Sum, + }; } // Re-export commonly used items at crate root @@ -120,7 +122,8 @@ pub use registry::{ComplexityClass, ProblemInfo}; pub use solvers::BruteForce; pub use traits::{EvaluationValue, Problem}; pub use types::{ - Extremum, ExtremumSense, Max, Min, NumericSize, One, Or, ProblemParameters, WeightElement, + And, Extremum, ExtremumSense, Max, Min, NumericSize, One, Or, ProblemParameters, Sum, + WeightElement, }; // Re-export proc macros for reduction registration and variant declaration diff --git a/src/types.rs b/src/types.rs index 1fba6f5b9..7b19d3e4e 100644 --- a/src/types.rs +++ b/src/types.rs @@ -284,6 +284,8 @@ impl std::fmt::Display for One { /// Failure while combining configuration values during a solve. #[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] pub enum AggregationError { + #[error("aggregate arithmetic overflow or non-finite result")] + ArithmeticOverflow, #[error("aggregate values are not comparable")] UnorderedComparison, #[error("cannot combine extrema with different optimization senses")] @@ -447,6 +449,29 @@ impl Optimiza } } +/// Additive fold value. +#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] +pub struct Sum(pub W); + +impl Aggregate for Sum { + fn identity() -> Self { + Sum(W::zero()) + } + + fn combine(self, other: Self) -> Result { + self.0 + .checked_add_value(other.0) + .map(Sum) + .map_err(|_| AggregationError::ArithmeticOverflow) + } +} + +impl fmt::Display for Sum { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Sum({})", self.0) + } +} + /// Disjunction aggregate for existential satisfaction. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] pub struct Or(pub bool); @@ -507,6 +532,30 @@ impl PartialEq for bool { } } +/// Conjunction aggregate for universal satisfaction. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct And(pub bool); + +impl Aggregate for And { + fn identity() -> Self { + And(true) + } + + fn combine(self, other: Self) -> Result { + Ok(And(self.0 && other.0)) + } + + fn is_absorbing(&self) -> bool { + !self.0 + } +} + +impl fmt::Display for And { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "And({})", self.0) + } +} + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum ExtremumSense { Maximize, diff --git a/src/unit_tests/registry/dispatch.rs b/src/unit_tests/registry/dispatch.rs index 8b665788e..9452c9cd6 100644 --- a/src/unit_tests/registry/dispatch.rs +++ b/src/unit_tests/registry/dispatch.rs @@ -5,7 +5,7 @@ use crate::registry::variant::find_variant_entry; use crate::registry::{load_dyn, serialize_any, DynProblem, LoadedDynProblem}; use crate::solvers::{brute_force_dimensions, solve, SolveOutcome, SolverRequest}; use crate::topology::SimpleGraph; -use crate::types::Max; +use crate::types::{Max, Sum}; use crate::Problem; use std::any::Any; use std::collections::BTreeMap; @@ -375,6 +375,7 @@ fn test_format_metric_uses_display() { assert_eq!(format_metric(&Max::(None)), "Max(None)"); assert_eq!(format_metric(&Min(Some(7))), "Min(7)"); assert_eq!(format_metric(&Or(true)), "Or(true)"); + assert_eq!(format_metric(&Sum(99u64)), "Sum(99)"); } #[test] diff --git a/src/unit_tests/rules/graph.rs b/src/unit_tests/rules/graph.rs index 8c6260f39..7d105b259 100644 --- a/src/unit_tests/rules/graph.rs +++ b/src/unit_tests/rules/graph.rs @@ -68,19 +68,19 @@ fn named_path(names: &[&str]) -> ReductionPath { } #[derive(Clone)] -struct OffsetChainSource; +struct AggregateChainSource; #[derive(Clone)] -struct OffsetChainMiddle; +struct AggregateChainMiddle; #[derive(Clone)] -struct OffsetChainTarget; +struct AggregateChainTarget; #[derive(Clone)] struct NaturalVariantProblem; -impl Problem for OffsetChainSource { - const NAME: &'static str = "OffsetChainSource"; +impl Problem for AggregateChainSource { + const NAME: &'static str = "AggregateChainSource"; type Solution = Vec; type Value = Min; @@ -103,7 +103,7 @@ impl Problem for OffsetChainSource { } } -impl crate::solvers::BruteForceProblem for OffsetChainSource { +impl crate::solvers::BruteForceProblem for AggregateChainSource { fn num_variables(&self) -> Result { Ok(1usize) } @@ -113,8 +113,8 @@ impl crate::solvers::BruteForceProblem for OffsetChainSource { } } -impl Problem for OffsetChainMiddle { - const NAME: &'static str = "OffsetChainMiddle"; +impl Problem for AggregateChainMiddle { + const NAME: &'static str = "AggregateChainMiddle"; type Solution = Vec; type Value = Min; @@ -137,7 +137,7 @@ impl Problem for OffsetChainMiddle { } } -impl crate::solvers::BruteForceProblem for OffsetChainMiddle { +impl crate::solvers::BruteForceProblem for AggregateChainMiddle { fn num_variables(&self) -> Result { Ok(1usize) } @@ -147,8 +147,8 @@ impl crate::solvers::BruteForceProblem for OffsetChainMiddle { } } -impl Problem for OffsetChainTarget { - const NAME: &'static str = "OffsetChainTarget"; +impl Problem for AggregateChainTarget { + const NAME: &'static str = "AggregateChainTarget"; type Solution = Vec; type Value = Min; @@ -171,7 +171,7 @@ impl Problem for OffsetChainTarget { } } -impl crate::solvers::BruteForceProblem for OffsetChainTarget { +impl crate::solvers::BruteForceProblem for AggregateChainTarget { fn num_variables(&self) -> Result { Ok(1usize) } @@ -215,13 +215,13 @@ impl crate::solvers::BruteForceProblem for NaturalVariantProblem { } } -struct SourceToMiddleOffsetResult { - target: OffsetChainMiddle, +struct SourceToMiddleAggregateResult { + target: AggregateChainMiddle, } -impl ReductionResult for SourceToMiddleOffsetResult { - type Source = OffsetChainSource; - type Target = OffsetChainMiddle; +impl ReductionResult for SourceToMiddleAggregateResult { + type Source = AggregateChainSource; + type Target = AggregateChainMiddle; fn target_problem(&self) -> &Self::Target { &self.target @@ -246,13 +246,13 @@ impl ReductionResult for SourceToMiddleOffsetResult { } } -struct MiddleToTargetOffsetResult { - target: OffsetChainTarget, +struct MiddleToTargetAggregateResult { + target: AggregateChainTarget, } -impl ReductionResult for MiddleToTargetOffsetResult { - type Source = OffsetChainMiddle; - type Target = OffsetChainTarget; +impl ReductionResult for MiddleToTargetAggregateResult { + type Source = AggregateChainMiddle; + type Target = AggregateChainTarget; fn target_problem(&self) -> &Self::Target { &self.target @@ -277,47 +277,47 @@ impl ReductionResult for MiddleToTargetOffsetResult { } } -fn reduce_source_to_middle_offset( +fn reduce_source_to_middle_aggregate( any: &dyn Any, ) -> Result { - any.downcast_ref::().ok_or( + any.downcast_ref::().ok_or( crate::rules::ReductionError::SourceTypeMismatch { - source_problem: OffsetChainSource::NAME, - target_problem: OffsetChainMiddle::NAME, - expected: std::any::type_name::(), + source_problem: AggregateChainSource::NAME, + target_problem: AggregateChainMiddle::NAME, + expected: std::any::type_name::(), }, )?; Ok(crate::rules::registry::ExecutedStep { - witness: std::rc::Rc::new(SourceToMiddleOffsetResult { - target: OffsetChainMiddle, + witness: std::rc::Rc::new(SourceToMiddleAggregateResult { + target: AggregateChainMiddle, }), }) } -fn reduce_middle_to_target_offset( +fn reduce_middle_to_target_aggregate( any: &dyn Any, ) -> Result { - any.downcast_ref::().ok_or( + any.downcast_ref::().ok_or( crate::rules::ReductionError::SourceTypeMismatch { - source_problem: OffsetChainMiddle::NAME, - target_problem: OffsetChainTarget::NAME, - expected: std::any::type_name::(), + source_problem: AggregateChainMiddle::NAME, + target_problem: AggregateChainTarget::NAME, + expected: std::any::type_name::(), }, )?; Ok(crate::rules::registry::ExecutedStep { - witness: std::rc::Rc::new(MiddleToTargetOffsetResult { - target: OffsetChainTarget, + witness: std::rc::Rc::new(MiddleToTargetAggregateResult { + target: AggregateChainTarget, }), }) } struct SourceToMiddleWitnessResult { - target: OffsetChainMiddle, + target: AggregateChainMiddle, } impl ReductionResult for SourceToMiddleWitnessResult { - type Source = OffsetChainSource; - type Target = OffsetChainMiddle; + type Source = AggregateChainSource; + type Target = AggregateChainMiddle; fn target_problem(&self) -> &Self::Target { &self.target @@ -358,16 +358,16 @@ impl SourceToMiddleWitnessResult { fn reduce_source_to_middle_witness( any: &dyn Any, ) -> Result { - any.downcast_ref::().ok_or( + any.downcast_ref::().ok_or( crate::rules::ReductionError::SourceTypeMismatch { - source_problem: OffsetChainSource::NAME, - target_problem: OffsetChainMiddle::NAME, - expected: std::any::type_name::(), + source_problem: AggregateChainSource::NAME, + target_problem: AggregateChainMiddle::NAME, + expected: std::any::type_name::(), }, )?; Ok(crate::rules::registry::ExecutedStep { witness: std::rc::Rc::new(SourceToMiddleWitnessResult { - target: OffsetChainMiddle, + target: AggregateChainMiddle, }), }) } @@ -376,8 +376,8 @@ fn fail_source_to_middle_witness( _any: &dyn Any, ) -> Result { Err(crate::rules::ReductionError::InvalidTarget { - source_problem: OffsetChainSource::NAME, - target_problem: OffsetChainMiddle::NAME, + source_problem: AggregateChainSource::NAME, + target_problem: AggregateChainMiddle::NAME, message: "synthetic target construction failure".to_string(), }) } @@ -392,12 +392,12 @@ fn reduce_counted_source_to_middle_witness( } struct MiddleToTargetWitnessResult { - target: OffsetChainTarget, + target: AggregateChainTarget, } impl ReductionResult for MiddleToTargetWitnessResult { - type Source = OffsetChainMiddle; - type Target = OffsetChainTarget; + type Source = AggregateChainMiddle; + type Target = AggregateChainTarget; fn target_problem(&self) -> &Self::Target { &self.target @@ -438,16 +438,16 @@ impl MiddleToTargetWitnessResult { fn reduce_middle_to_target_witness( any: &dyn Any, ) -> Result { - any.downcast_ref::().ok_or( + any.downcast_ref::().ok_or( crate::rules::ReductionError::SourceTypeMismatch { - source_problem: OffsetChainMiddle::NAME, - target_problem: OffsetChainTarget::NAME, - expected: std::any::type_name::(), + source_problem: AggregateChainMiddle::NAME, + target_problem: AggregateChainTarget::NAME, + expected: std::any::type_name::(), }, )?; Ok(crate::rules::registry::ExecutedStep { witness: std::rc::Rc::new(MiddleToTargetWitnessResult { - target: OffsetChainTarget, + target: AggregateChainTarget, }), }) } @@ -521,29 +521,29 @@ fn execute_paths_executes_a_shared_prefix_once() { }; let graph = ReductionGraph::from_test_edges( &[ - OffsetChainSource::NAME, - OffsetChainMiddle::NAME, - OffsetChainTarget::NAME, + AggregateChainSource::NAME, + AggregateChainMiddle::NAME, + AggregateChainTarget::NAME, ], &[ ( - OffsetChainSource::NAME, - OffsetChainMiddle::NAME, + AggregateChainSource::NAME, + AggregateChainMiddle::NAME, witness_edge(reduce_counted_source_to_middle_witness), ), ( - OffsetChainMiddle::NAME, - OffsetChainTarget::NAME, + AggregateChainMiddle::NAME, + AggregateChainTarget::NAME, witness_edge(reduce_middle_to_target_witness), ), ], ); let mut paths = vec![ - named_path(&[OffsetChainSource::NAME, OffsetChainMiddle::NAME]), + named_path(&[AggregateChainSource::NAME, AggregateChainMiddle::NAME]), named_path(&[ - OffsetChainSource::NAME, - OffsetChainMiddle::NAME, - OffsetChainTarget::NAME, + AggregateChainSource::NAME, + AggregateChainMiddle::NAME, + AggregateChainTarget::NAME, ]), ]; @@ -551,7 +551,7 @@ fn execute_paths_executes_a_shared_prefix_once() { paths.push(paths[1].clone()); let executed = graph - .execute_paths(&paths, &OffsetChainSource) + .execute_paths(&paths, &AggregateChainSource) .expect("both paths are executable"); assert_eq!(executed.len(), 4); @@ -559,8 +559,8 @@ fn execute_paths_executes_a_shared_prefix_once() { assert_eq!(execution.steps.len(), path.len()); assert_eq!( execution - .recover_result::( - &OffsetChainSource, + .recover_result::( + &AggregateChainSource, SolveOutcome::Optimal { solution: vec![1usize], evaluation: Min(Some(1)) @@ -759,24 +759,24 @@ fn test_find_direct_path() { } #[test] -fn test_reduction_chain_recovers_result_backwards() { +fn test_aggregate_reduction_chain_extracts_value_backwards() { let source_variant = BTreeMap::new(); let middle_variant = BTreeMap::new(); let target_variant = BTreeMap::new(); let nodes = vec![ VariantNode { - name: OffsetChainSource::NAME, + name: AggregateChainSource::NAME, variant: source_variant.clone(), complexity: "", }, VariantNode { - name: OffsetChainMiddle::NAME, + name: AggregateChainMiddle::NAME, variant: middle_variant.clone(), complexity: "", }, VariantNode { - name: OffsetChainTarget::NAME, + name: AggregateChainTarget::NAME, variant: target_variant.clone(), complexity: "", }, @@ -792,7 +792,7 @@ fn test_reduction_chain_recovers_result_backwards() { middle_idx, ReductionEdgeData { parameter_contract: empty_parameter_contract(), - reduce_fn: Some(reduce_source_to_middle_offset), + reduce_fn: Some(reduce_source_to_middle_aggregate), turing: false, }, ); @@ -801,7 +801,7 @@ fn test_reduction_chain_recovers_result_backwards() { target_idx, ReductionEdgeData { parameter_contract: empty_parameter_contract(), - reduce_fn: Some(reduce_middle_to_target_offset), + reduce_fn: Some(reduce_middle_to_target_aggregate), turing: false, }, ); @@ -810,43 +810,44 @@ fn test_reduction_chain_recovers_result_backwards() { graph, nodes, name_to_nodes: HashMap::from([ - (OffsetChainSource::NAME, vec![source_idx]), - (OffsetChainMiddle::NAME, vec![middle_idx]), - (OffsetChainTarget::NAME, vec![target_idx]), + (AggregateChainSource::NAME, vec![source_idx]), + (AggregateChainMiddle::NAME, vec![middle_idx]), + (AggregateChainTarget::NAME, vec![target_idx]), ]), default_variants: HashMap::new(), }; let path = ReductionPath { steps: vec![ ReductionStep { - name: OffsetChainSource::NAME.to_string(), + name: AggregateChainSource::NAME.to_string(), variant: source_variant, }, ReductionStep { - name: OffsetChainMiddle::NAME.to_string(), + name: AggregateChainMiddle::NAME.to_string(), variant: middle_variant, }, ReductionStep { - name: OffsetChainTarget::NAME.to_string(), + name: AggregateChainTarget::NAME.to_string(), variant: target_variant, }, ], }; let chain = reduction_graph - .reduce_along_path(&path, &OffsetChainSource as &dyn Any) - .expect("offset reduction should not fail") - .expect("expected offset reduction chain"); + .reduce_along_path(&path, &AggregateChainSource as &dyn Any) + .expect("aggregate reduction should not fail") + .expect("expected aggregate reduction chain"); assert_eq!( - crate::solvers::cartesian_dimensions(chain.target_problem::()).unwrap(), + crate::solvers::cartesian_dimensions(chain.target_problem::()) + .unwrap(), vec![1] ); assert_eq!( chain - .recover_result::( - &OffsetChainSource, - SolveOutcome::optimal(chain.target_problem::(), vec![7]) + .recover_result::( + &AggregateChainSource, + SolveOutcome::optimal(chain.target_problem::(), vec![7]) .unwrap() ) .unwrap(), @@ -862,9 +863,9 @@ fn default_path_search_rejects_turing_only_edge() { let source_variant = BTreeMap::new(); let target_variant = BTreeMap::new(); let graph = build_two_node_graph( - OffsetChainSource::NAME, + AggregateChainSource::NAME, source_variant.clone(), - OffsetChainMiddle::NAME, + AggregateChainMiddle::NAME, target_variant.clone(), ReductionEdgeData { parameter_contract: empty_parameter_contract(), @@ -875,18 +876,18 @@ fn default_path_search_rejects_turing_only_edge() { assert!(graph .find_paths_up_to( - OffsetChainSource::NAME, + AggregateChainSource::NAME, &source_variant, - OffsetChainMiddle::NAME, + AggregateChainMiddle::NAME, &target_variant, 1, ) .is_empty()); assert!(!graph .find_all_paths_mode( - OffsetChainSource::NAME, + AggregateChainSource::NAME, &source_variant, - OffsetChainMiddle::NAME, + AggregateChainMiddle::NAME, &target_variant, ReductionMode::Turing ) @@ -898,9 +899,9 @@ fn turing_path_search_rejects_witness_only_edge() { let source_variant = BTreeMap::new(); let target_variant = BTreeMap::new(); let graph = build_two_node_graph( - OffsetChainSource::NAME, + AggregateChainSource::NAME, source_variant.clone(), - OffsetChainMiddle::NAME, + AggregateChainMiddle::NAME, target_variant.clone(), ReductionEdgeData { parameter_contract: empty_parameter_contract(), @@ -912,18 +913,18 @@ fn turing_path_search_rejects_witness_only_edge() { assert!(graph .find_all_paths_mode( - OffsetChainSource::NAME, + AggregateChainSource::NAME, &source_variant, - OffsetChainMiddle::NAME, + AggregateChainMiddle::NAME, &target_variant, ReductionMode::Turing ) .is_empty()); assert!(!graph .find_all_paths_mode( - OffsetChainSource::NAME, + AggregateChainSource::NAME, &source_variant, - OffsetChainMiddle::NAME, + AggregateChainMiddle::NAME, &target_variant, ReductionMode::Witness ) @@ -971,24 +972,24 @@ fn witness_executor_does_not_imply_turing_capability() { fn reduce_result_along_path_rejects_single_step_path() { let source_variant = BTreeMap::new(); let graph = build_two_node_graph( - OffsetChainSource::NAME, + AggregateChainSource::NAME, source_variant.clone(), - OffsetChainMiddle::NAME, + AggregateChainMiddle::NAME, BTreeMap::new(), ReductionEdgeData { parameter_contract: empty_parameter_contract(), - reduce_fn: Some(reduce_source_to_middle_offset), + reduce_fn: Some(reduce_source_to_middle_aggregate), turing: false, }, ); let single_step_path = ReductionPath { steps: vec![ReductionStep { - name: OffsetChainSource::NAME.to_string(), + name: AggregateChainSource::NAME.to_string(), variant: source_variant, }], }; assert!(graph - .reduce_along_path(&single_step_path, &OffsetChainSource as &dyn Any) + .reduce_along_path(&single_step_path, &AggregateChainSource as &dyn Any) .expect("single-step path lookup should not fail") .is_none()); } @@ -998,9 +999,9 @@ fn reduce_result_returns_none_for_turing_only_edge() { let source_variant = BTreeMap::new(); let target_variant = BTreeMap::new(); let graph = build_two_node_graph( - OffsetChainSource::NAME, + AggregateChainSource::NAME, source_variant.clone(), - OffsetChainMiddle::NAME, + AggregateChainMiddle::NAME, target_variant.clone(), ReductionEdgeData { parameter_contract: empty_parameter_contract(), @@ -1012,17 +1013,17 @@ fn reduce_result_returns_none_for_turing_only_edge() { let path = ReductionPath { steps: vec![ ReductionStep { - name: OffsetChainSource::NAME.to_string(), + name: AggregateChainSource::NAME.to_string(), variant: source_variant, }, ReductionStep { - name: OffsetChainMiddle::NAME.to_string(), + name: AggregateChainMiddle::NAME.to_string(), variant: target_variant, }, ], }; assert!(graph - .reduce_along_path(&path, &OffsetChainSource as &dyn Any) + .reduce_along_path(&path, &AggregateChainSource as &dyn Any) .expect("Turing-only edge lookup should not fail") .is_none()); } @@ -1032,9 +1033,9 @@ fn reduce_along_path_preserves_edge_failure() { let source_variant = BTreeMap::new(); let target_variant = BTreeMap::new(); let graph = build_two_node_graph( - OffsetChainSource::NAME, + AggregateChainSource::NAME, source_variant.clone(), - OffsetChainMiddle::NAME, + AggregateChainMiddle::NAME, target_variant.clone(), ReductionEdgeData { parameter_contract: empty_parameter_contract(), @@ -1046,25 +1047,25 @@ fn reduce_along_path_preserves_edge_failure() { let path = ReductionPath { steps: vec![ ReductionStep { - name: OffsetChainSource::NAME.to_string(), + name: AggregateChainSource::NAME.to_string(), variant: source_variant, }, ReductionStep { - name: OffsetChainMiddle::NAME.to_string(), + name: AggregateChainMiddle::NAME.to_string(), variant: target_variant, }, ], }; - let error = match graph.reduce_along_path(&path, &OffsetChainSource as &dyn Any) { + let error = match graph.reduce_along_path(&path, &AggregateChainSource as &dyn Any) { Err(error) => error, Ok(_) => panic!("registered edge failure must be returned"), }; assert_eq!( error, crate::rules::ReductionError::InvalidTarget { - source_problem: OffsetChainSource::NAME, - target_problem: OffsetChainMiddle::NAME, + source_problem: AggregateChainSource::NAME, + target_problem: AggregateChainMiddle::NAME, message: "synthetic target construction failure".to_string(), } ); @@ -2094,11 +2095,11 @@ fn witness_and_value_mapping_share_one_executed_construction() { static CONSTRUCTIONS: AtomicUsize = AtomicUsize::new(0); let chain = crate::rules::ReductionChain::execute( - &OffsetChainSource, + &AggregateChainSource, &[|_| { CONSTRUCTIONS.fetch_add(1, Ordering::SeqCst); let result = Rc::new(SourceToMiddleWitnessResult { - target: OffsetChainMiddle, + target: AggregateChainMiddle, }); Ok(ExecutedStep { witness: result }) }], @@ -2108,17 +2109,20 @@ fn witness_and_value_mapping_share_one_executed_construction() { assert!(std::ptr::eq( step.witness .target_problem_any() - .downcast_ref::() + .downcast_ref::() .unwrap(), - chain.target_problem::(), + chain.target_problem::(), )); let witness = vec![7usize]; assert_eq!( chain - .recover_result::( - &OffsetChainSource, - SolveOutcome::optimal(chain.target_problem::(), witness.clone()) - .unwrap(), + .recover_result::( + &AggregateChainSource, + SolveOutcome::optimal( + chain.target_problem::(), + witness.clone() + ) + .unwrap(), ) .unwrap(), SolveOutcome::Optimal { diff --git a/src/unit_tests/rules/traits.rs b/src/unit_tests/rules/traits.rs index f9e304a2b..afe31fd6c 100644 --- a/src/unit_tests/rules/traits.rs +++ b/src/unit_tests/rules/traits.rs @@ -209,7 +209,7 @@ fn test_reduction() { } #[test] -fn decision_recovery_keeps_evaluation_errors_distinct_from_infeasible() { +fn aggregate_value_from_solution_keeps_evaluation_errors_distinct_from_false() { use crate::models::decision::Decision; use crate::models::graph::MinimumVertexCover; use crate::rules::ExtractionError; @@ -256,29 +256,29 @@ fn decision_recovery_keeps_evaluation_errors_distinct_from_infeasible() { } #[derive(Clone)] -struct OffsetSourceProblem; +struct AggregateSourceProblem; #[derive(Clone)] -struct OffsetTargetProblem; +struct AggregateTargetProblem; thread_local! { static TARGET_EVALUATIONS: std::cell::Cell = const { std::cell::Cell::new(0) }; } -impl OffsetSourceProblem { +impl AggregateSourceProblem { fn num_variables(&self) -> usize { 1 } } -impl OffsetTargetProblem { +impl AggregateTargetProblem { fn num_variables(&self) -> usize { 1 } } -impl Problem for OffsetSourceProblem { - const NAME: &'static str = "OffsetSource"; +impl Problem for AggregateSourceProblem { + const NAME: &'static str = "AggregateSource"; type Solution = Vec; type Value = Min; @@ -296,8 +296,8 @@ impl Problem for OffsetSourceProblem { } } -impl Problem for OffsetTargetProblem { - const NAME: &'static str = "OffsetTarget"; +impl Problem for AggregateTargetProblem { + const NAME: &'static str = "AggregateTarget"; type Solution = Vec; type Value = Min; @@ -316,14 +316,14 @@ impl Problem for OffsetTargetProblem { } } -struct TestOffsetReduction { - target: OffsetTargetProblem, +struct TestAggregateReduction { + target: AggregateTargetProblem, offset: u64, } -impl ReductionResult for TestOffsetReduction { - type Source = OffsetSourceProblem; - type Target = OffsetTargetProblem; +impl ReductionResult for TestAggregateReduction { + type Source = AggregateSourceProblem; + type Target = AggregateTargetProblem; fn target_problem(&self) -> &Self::Target { &self.target @@ -348,21 +348,21 @@ impl ReductionResult for TestOffsetReduction { } } -impl ReduceTo for OffsetSourceProblem { - type Result = TestOffsetReduction; +impl ReduceTo for AggregateSourceProblem { + type Result = TestAggregateReduction; fn reduce_to(&self) -> Result { - Ok(TestOffsetReduction { - target: OffsetTargetProblem, + Ok(TestAggregateReduction { + target: AggregateTargetProblem, offset: 3, }) } } #[test] -fn test_offset_reduction_recovers_shifted_result() { - let source = OffsetSourceProblem; - let result = >::reduce_to(&source) +fn test_aggregate_reduction_extracts_value() { + let source = AggregateSourceProblem; + let result = >::reduce_to(&source) .expect("reduction should succeed"); assert_eq!( @@ -380,16 +380,16 @@ fn test_offset_reduction_recovers_shifted_result() { } #[test] -fn test_dyn_reduction_result_recovers_shifted_result() { - let result = TestOffsetReduction { - target: OffsetTargetProblem, +fn test_dyn_aggregate_reduction_result_extracts_value() { + let result = TestAggregateReduction { + target: AggregateTargetProblem, offset: 2, }; let dyn_result: &dyn DynReductionResult = &result; assert!(dyn_result .target_problem_any() - .downcast_ref::() + .downcast_ref::() .is_some()); TARGET_EVALUATIONS.with(|count| count.set(0)); let (target, target_json) = dyn_result @@ -406,7 +406,7 @@ fn test_dyn_reduction_result_recovers_shifted_result() { ); assert_eq!(TARGET_EVALUATIONS.with(|count| count.get()), 1); let recovered = dyn_result - .recover_result_dyn(&OffsetSourceProblem, target) + .recover_result_dyn(&AggregateSourceProblem, target) .unwrap(); assert_eq!(TARGET_EVALUATIONS.with(|count| count.get()), 1); assert_eq!( @@ -420,8 +420,8 @@ fn test_dyn_reduction_result_recovers_shifted_result() { #[test] fn external_evaluation_is_optional_but_must_match_when_present() { - let result = TestOffsetReduction { - target: OffsetTargetProblem, + let result = TestAggregateReduction { + target: AggregateTargetProblem, offset: 2, }; for status in ["optimal", "feasible"] { diff --git a/src/unit_tests/solvers/brute_force.rs b/src/unit_tests/solvers/brute_force.rs index 32a7c5c41..8c653314c 100644 --- a/src/unit_tests/solvers/brute_force.rs +++ b/src/unit_tests/solvers/brute_force.rs @@ -1,6 +1,6 @@ use super::*; use crate::traits::Problem; -use crate::types::{AggregationError, Max, Min, Or}; +use crate::types::{AggregationError, Max, Min, Or, Sum}; use std::cell::Cell; use std::rc::Rc; @@ -442,6 +442,15 @@ fn test_solver_solve_stops_after_first_optimal_configuration() { assert_eq!(evaluations.get(), 2); } +#[test] +fn test_sum_fold_combines_values_without_problem_solving() { + let total = [Sum(1_u64), Sum(2), Sum(3)] + .into_iter() + .try_fold(Sum::identity(), Aggregate::combine) + .unwrap(); + assert_eq!(total, Sum(6)); +} + #[test] fn test_solver_find_all_witnesses() { let problem = SatProblem { @@ -456,6 +465,15 @@ fn test_solver_find_all_witnesses() { assert!(witnesses.contains(&vec![0, 1])); } +#[test] +fn test_sum_fold_uses_every_input_value() { + let total = [Sum(0_u64), Sum(2), Sum(1), Sum(3)] + .into_iter() + .try_fold(Sum::identity(), Aggregate::combine) + .unwrap(); + assert_eq!(total, Sum(6)); +} + #[test] fn test_solver_with_real_mis() { use crate::models::graph::MaximumIndependentSet; @@ -506,6 +524,11 @@ fn test_solve_with_witnesses_max() { assert_eq!(witnesses, vec![vec![1, 1, 1]]); } +#[test] +fn test_sum_fold_preserves_zero_identity() { + assert_eq!(Sum::::identity().combine(Sum(6)).unwrap(), Sum(6)); +} + #[test] fn solve_with_witnesses_enumerates_only_aggregate_and_witness_passes() { let evaluations = Rc::new(Cell::new(0)); diff --git a/src/unit_tests/types.rs b/src/unit_tests/types.rs index 243a77a21..de88e9934 100644 --- a/src/unit_tests/types.rs +++ b/src/unit_tests/types.rs @@ -36,6 +36,20 @@ fn test_max_and_min_report_unordered_comparisons() { ); } +#[test] +fn test_sum_identity_and_combine() { + assert_eq!(Sum::::identity(), Sum(0)); + assert_eq!(Sum(4_u64).combine(Sum(3_u64)).unwrap(), Sum(7)); +} + +#[test] +fn test_sum_combine_reports_overflow() { + assert_eq!( + Sum(u64::MAX).combine(Sum(1)), + Err(AggregationError::ArithmeticOverflow) + ); +} + #[test] fn test_weight_multiplication_reports_integer_overflow() { assert!(matches!( @@ -61,6 +75,27 @@ fn test_or_identity_and_combine() { assert!(Or(true).is_absorbing()); } +#[test] +fn test_and_identity_and_combine() { + assert_eq!(And::identity(), And(true)); + assert_eq!(And(true).combine(And(false)).unwrap(), And(false)); + assert_eq!(And(true).combine(And(true)).unwrap(), And(true)); + assert!(!And(true).is_absorbing()); + assert!(And(false).is_absorbing()); +} + +#[test] +fn test_sum_has_no_absorbing_value() { + assert!(!Sum(0_u64).is_absorbing()); + assert!(!Sum(u64::MAX).is_absorbing()); +} + +#[test] +fn test_and_absorbing_value_is_false() { + assert!(!And(true).is_absorbing()); + assert!(And(false).is_absorbing()); +} + #[test] fn test_max_helpers() { let size = Max(Some(42)); @@ -302,12 +337,23 @@ fn test_min_display() { assert_eq!(format!("{}", Min::(None)), "Min(None)"); } +#[test] +fn test_sum_display() { + assert_eq!(format!("{}", Sum(56_u64)), "Sum(56)"); +} + #[test] fn test_or_display() { assert_eq!(format!("{}", Or(true)), "Or(true)"); assert_eq!(format!("{}", Or(false)), "Or(false)"); } +#[test] +fn test_and_display() { + assert_eq!(format!("{}", And(true)), "And(true)"); + assert_eq!(format!("{}", And(false)), "And(false)"); +} + #[test] fn exact_i64_to_f64_accepts_range_endpoints() { assert_eq!( From 1b4d9ce4ac6817bf34700606430d93b6bc7e8714 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Fri, 18 Sep 2026 00:01:38 +0800 Subject: [PATCH 37/42] Require EvaluationValue only where candidate feasibility is asked Problem::Value returns to the base `Clone` bound so Sum- and And-valued problems evaluate and fold through the Aggregate contract again. EvaluationValue is now required by ReductionResult endpoints, by SolutionAggregate and OptimizationValue as a supertrait, by SolveOutcome constructors, and by registration through impl_dyn_problem!. Passing a fold-only problem to a solve or recovery API is a compile error. Co-Authored-By: Claude Fable 5.1 --- src/registry/dyn_problem.rs | 2 + src/rules/test_helpers.rs | 2 +- src/rules/traits.rs | 12 ++-- src/solvers/brute_force.rs | 4 +- src/solvers/outcome.rs | 41 ++++++++++++ src/traits.rs | 5 +- src/types.rs | 2 +- src/unit_tests/solvers/brute_force.rs | 93 ++++++++++++++++++++++++++- src/unit_tests/traits.rs | 18 ++++-- 9 files changed, 162 insertions(+), 17 deletions(-) diff --git a/src/registry/dyn_problem.rs b/src/registry/dyn_problem.rs index 1bdd77708..8459e2142 100644 --- a/src/registry/dyn_problem.rs +++ b/src/registry/dyn_problem.rs @@ -40,6 +40,8 @@ pub trait DynProblem: Any { /// Implement the existing dynamic transport boundary for a concrete problem type. /// /// Concrete value semantics determine feasibility; no solver capability is required. +/// Registration therefore requires `Problem::Value: EvaluationValue`; problems with +/// fold-only values such as `Sum` or `And` stay unregistered. #[macro_export] macro_rules! impl_dyn_problem { ($ty:ty) => { diff --git a/src/rules/test_helpers.rs b/src/rules/test_helpers.rs index 6f895a677..e152a3d9c 100644 --- a/src/rules/test_helpers.rs +++ b/src/rules/test_helpers.rs @@ -301,7 +301,7 @@ pub(crate) fn assert_suboptimal_feasible_target_is_insufficient( optimum: &::Solution, ) where R: ReductionResult, - ::Value: crate::traits::EvaluationValue + std::fmt::Debug + PartialEq, + ::Value: std::fmt::Debug + PartialEq, { let target = reduction.target_problem(); assert_ne!( diff --git a/src/rules/traits.rs b/src/rules/traits.rs index 74ab2e3f1..1402f1ce1 100644 --- a/src/rules/traits.rs +++ b/src/rules/traits.rs @@ -1,7 +1,7 @@ //! Core traits for problem reductions. use crate::solvers::{ProblemOutcome, SolveOutcome}; -use crate::traits::Problem; +use crate::traits::{EvaluationValue, Problem}; use std::any::Any; use std::marker::PhantomData; @@ -166,9 +166,9 @@ pub type ExtractionResult = std::result::Result; /// Stores the target and recovers complete source results using the executed mapping. pub trait ReductionResult { /// The source problem type. - type Source: Problem; + type Source: Problem; /// The target problem type. - type Target: Problem; + type Target: Problem; /// Get a reference to the target problem. fn target_problem(&self) -> &Self::Target; @@ -191,7 +191,7 @@ pub trait ReductionResult { /// Rules requiring optimum thresholds or rejecting feasible incumbents must instead /// interpret those outcomes in their own `recover_result` implementation. /// Source evaluation errors propagate; they never establish source infeasibility. -pub(super) fn recover_preserving_status( +pub(super) fn recover_preserving_status, S, V>( source: &P, target: SolveOutcome, map_solution: impl FnOnce(&S) -> ExtractionResult, @@ -277,8 +277,8 @@ impl VariantReductionResult { impl ReductionResult for VariantReductionResult where - S: Problem, - T: Problem, + S: Problem, + T: Problem, { type Source = S; type Target = T; diff --git a/src/solvers/brute_force.rs b/src/solvers/brute_force.rs index 7ddcdc7e2..1e8c413b1 100644 --- a/src/solvers/brute_force.rs +++ b/src/solvers/brute_force.rs @@ -3,7 +3,7 @@ use std::any::Any; use crate::solvers::SolveError; -use crate::traits::Problem; +use crate::traits::{EvaluationValue, Problem}; use crate::types::{Aggregate, Extremum, Max, Min, Or}; use serde::{de::DeserializeOwned, Serialize}; use std::fmt; @@ -12,7 +12,7 @@ use std::fmt; /// /// This is not required by model evaluation, reductions, or solvers that return /// their solutions directly. -pub trait SolutionAggregate: Aggregate { +pub trait SolutionAggregate: Aggregate + EvaluationValue { /// Whether a solution-level value contributes to the final aggregate value. fn contributes_to_solution(value: &Self, total: &Self) -> bool; } diff --git a/src/solvers/outcome.rs b/src/solvers/outcome.rs index 7bcb096c6..e57d88c3d 100644 --- a/src/solvers/outcome.rs +++ b/src/solvers/outcome.rs @@ -81,6 +81,47 @@ impl SolveOutcome { /// /// Returns an evaluation error if evaluation fails or the candidate violates /// the constraints. This checks feasibility, not optimality. + /// + /// The value type must be an [`EvaluationValue`]: + /// + /// ``` + /// # use problemreductions::traits::{EvaluationError, Problem}; + /// # use problemreductions::types::{Max, ProblemParameters, Sum}; + /// # use problemreductions::solvers::SolveOutcome; + /// # #[derive(Clone)] + /// # struct Constant(V); + /// # impl Problem for Constant { + /// # const NAME: &'static str = "Constant"; + /// # type Solution = (); + /// # type Value = V; + /// # fn parameter_names() -> &'static [&'static str] { &[] } + /// # fn parameters(&self) -> ProblemParameters { ProblemParameters::new(vec![]) } + /// # fn evaluate(&self, _: &()) -> Result { Ok(self.0.clone()) } + /// # fn variant() -> Vec<(&'static str, &'static str)> { vec![] } + /// # } + /// assert!(SolveOutcome::optimal(&Constant(Max(Some(1_u64))), ()).is_ok()); + /// ``` + /// + /// Fold-only values such as `Sum` and `And` carry no candidate feasibility, + /// so the same problem with a `Sum` value is rejected at compile time: + /// + /// ```compile_fail,E0277 + /// # use problemreductions::traits::{EvaluationError, Problem}; + /// # use problemreductions::types::{Max, ProblemParameters, Sum}; + /// # use problemreductions::solvers::SolveOutcome; + /// # #[derive(Clone)] + /// # struct Constant(V); + /// # impl Problem for Constant { + /// # const NAME: &'static str = "Constant"; + /// # type Solution = (); + /// # type Value = V; + /// # fn parameter_names() -> &'static [&'static str] { &[] } + /// # fn parameters(&self) -> ProblemParameters { ProblemParameters::new(vec![]) } + /// # fn evaluate(&self, _: &()) -> Result { Ok(self.0.clone()) } + /// # fn variant() -> Vec<(&'static str, &'static str)> { vec![] } + /// # } + /// assert!(SolveOutcome::optimal(&Constant(Sum(1_u64)), ()).is_ok()); + /// ``` pub fn optimal>( problem: &P, solution: S, diff --git a/src/traits.rs b/src/traits.rs index 60eaa1f72..a68fffe77 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -22,6 +22,9 @@ pub enum EvaluationError { /// /// Independent of aggregation and solver capabilities. An invalid value rejects /// this candidate; it does not establish that the problem has no feasible solution. +/// Required wherever a candidate is labeled feasible: solve outcomes, reduction +/// recovery, and registration. `Problem::Value` itself does not require it, so +/// fold-only values such as `Sum` and `And` remain usable for evaluation. pub trait EvaluationValue: Clone { fn is_valid(&self) -> bool; } @@ -37,7 +40,7 @@ pub trait Problem: Clone { /// Mathematical witness type for this problem. type Solution; /// The evaluation value type. - type Value: EvaluationValue; + type Value: Clone; /// Canonical parameter names for this problem model. fn parameter_names() -> &'static [&'static str]; /// Measure the complete canonical parameters of this concrete instance. diff --git a/src/types.rs b/src/types.rs index 7b19d3e4e..352e2972c 100644 --- a/src/types.rs +++ b/src/types.rs @@ -421,7 +421,7 @@ impl Min { } /// Trait for aggregate values that represent optimization objectives. -pub trait OptimizationValue: Aggregate { +pub trait OptimizationValue: Aggregate + crate::traits::EvaluationValue { /// The inner numeric type used for comparisons with decision bounds. type Inner: Clone + PartialOrd + fmt::Debug + Serialize + DeserializeOwned; diff --git a/src/unit_tests/solvers/brute_force.rs b/src/unit_tests/solvers/brute_force.rs index 8c653314c..09aa9700d 100644 --- a/src/unit_tests/solvers/brute_force.rs +++ b/src/unit_tests/solvers/brute_force.rs @@ -1,6 +1,6 @@ use super::*; use crate::traits::Problem; -use crate::types::{AggregationError, Max, Min, Or, Sum}; +use crate::types::{AggregationError, And, Max, Min, Or, Sum}; use std::cell::Cell; use std::rc::Rc; @@ -529,6 +529,97 @@ fn test_sum_fold_preserves_zero_identity() { assert_eq!(Sum::::identity().combine(Sum(6)).unwrap(), Sum(6)); } +/// Unregistered fold-only problem over three binary variables. `Sum` and `And` +/// are evaluation and fold values; they are not `EvaluationValue`s, so this +/// problem cannot enter the solve or recovery APIs. +#[derive(Clone)] +struct FoldProblem { + value: fn(&[usize]) -> V, + evaluations: Rc>, +} + +impl FoldProblem { + fn new(value: fn(&[usize]) -> V) -> Self { + Self { + value, + evaluations: Rc::new(Cell::new(0)), + } + } +} + +impl Problem for FoldProblem { + const NAME: &'static str = "FoldProblem"; + type Solution = Vec; + type Value = V; + + fn parameter_names() -> &'static [&'static str] { + &["num_variables"] + } + fn parameters(&self) -> crate::types::ProblemParameters { + crate::types::ProblemParameters::new(vec![("num_variables", 3)]) + } + + fn evaluate( + &self, + config: &Self::Solution, + ) -> Result { + self.evaluations.set(self.evaluations.get() + 1); + Ok((self.value)(config)) + } + + fn variant() -> Vec<(&'static str, &'static str)> { + vec![] + } +} + +impl crate::solvers::BruteForceProblem for FoldProblem { + fn num_variables(&self) -> Result { + Ok(3) + } + + fn dimension(&self, _variable: usize) -> Result { + Ok(2) + } +} + +#[test] +fn test_sum_valued_problem_folds_the_exact_count() { + // Count the assignments with at least two ones: 110, 101, 011, 111. + let problem = FoldProblem::new(|config| Sum(u64::from(config.iter().sum::() >= 2))); + + assert_eq!(problem.evaluate(&vec![1, 0, 1]).unwrap(), Sum(1)); + assert_eq!(problem.evaluate(&vec![1, 0, 0]).unwrap(), Sum(0)); + problem.evaluations.set(0); + + let total = BruteForce::new().solve_cartesian(&problem, |c| c).unwrap(); + assert_eq!(total, Sum(4)); + // A sum is never absorbing, so the fold visits every configuration. + assert_eq!(problem.evaluations.get(), 8); +} + +#[test] +fn test_and_valued_problem_folds_every_true_value() { + let problem = FoldProblem::new(|config| And(config.len() == 3)); + + let total = BruteForce::new().solve_cartesian(&problem, |c| c).unwrap(); + assert_eq!(total, And(true)); + assert_eq!(problem.evaluations.get(), 8); +} + +#[test] +fn test_and_valued_problem_stops_at_the_first_false_value() { + // Enumeration order is 000, 001, 010, ...; the third configuration fails. + let problem = FoldProblem::new(|config| And(config != [0, 1, 0])); + + assert_eq!(problem.evaluate(&vec![0, 1, 0]).unwrap(), And(false)); + problem.evaluations.set(0); + + let total = BruteForce::new().solve_cartesian(&problem, |c| c).unwrap(); + assert_eq!(total, And(false)); + // `And(false)` is absorbing, so the remaining five configurations are skipped. + assert_eq!(problem.evaluations.get(), 3); +} + #[test] fn solve_with_witnesses_enumerates_only_aggregate_and_witness_passes() { let evaluations = Rc::new(Cell::new(0)); diff --git a/src/unit_tests/traits.rs b/src/unit_tests/traits.rs index 1dfaeae53..14950fcf3 100644 --- a/src/unit_tests/traits.rs +++ b/src/unit_tests/traits.rs @@ -1,6 +1,6 @@ use crate::solvers::BruteForceProblem as _; use crate::traits::Problem; -use crate::types::{Max, Min, Or}; +use crate::types::{Max, Min, Or, Sum}; #[derive(Clone)] struct TestSatProblem { @@ -183,7 +183,7 @@ struct MultiDimProblem { impl Problem for MultiDimProblem { const NAME: &'static str = "MultiDim"; type Solution = Vec; - type Value = Min; + type Value = Sum; fn parameter_names() -> &'static [&'static str] { &["num_variables"] @@ -196,7 +196,7 @@ impl Problem for MultiDimProblem { &self, config: &Self::Solution, ) -> Result { - Ok(Min(Some(config.iter().map(|&c| c as i64).sum()))) + Ok(Sum(config.iter().map(|&c| c as i64).sum())) } fn variant() -> Vec<(&'static str, &'static str)> { @@ -225,8 +225,16 @@ fn test_multi_dim_problem() { vec![2, 3, 4] ); assert_eq!(p.num_variables().unwrap(), 3); - assert_eq!(p.evaluate(&vec![0, 0, 0]).unwrap(), Min(Some(0))); - assert_eq!(p.evaluate(&vec![1, 2, 3]).unwrap(), Min(Some(6))); + assert_eq!(p.evaluate(&vec![0, 0, 0]).unwrap(), Sum(0)); + assert_eq!(p.evaluate(&vec![1, 2, 3]).unwrap(), Sum(6)); + // Each coordinate value v in dimension d appears in 24 / d configurations: + // 12 * 1 + 8 * (1 + 2) + 6 * (1 + 2 + 3) = 72. + assert_eq!( + crate::solvers::BruteForce::new() + .solve_cartesian(&p, |config| config) + .unwrap(), + Sum(72) + ); } #[test] From c24119aacae551b0c3cc3e0c97bf753a9696dfee Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Fri, 18 Sep 2026 00:01:38 +0800 Subject: [PATCH 38/42] Document Sum and And as evaluation and fold values State where EvaluationValue is required, and stop advertising aggregate-only models in the skills: registration, solving, and reduction endpoints accept only Max, Min, Or, and Extremum values. Co-Authored-By: Claude Fable 5.1 --- .claude/CLAUDE.md | 7 ++++--- .claude/skills/add-model/SKILL.md | 11 +++++------ .claude/skills/fix-issue/SKILL.md | 2 +- .claude/skills/review-structural/SKILL.md | 4 ++-- docs/src/design.md | 13 +++++++------ 5 files changed, 19 insertions(+), 18 deletions(-) diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index fc5bda591..04fa55aa5 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -125,7 +125,7 @@ Problem (core trait — all problems must implement) │ ├── const NAME: &'static str // e.g., "MaximumIndependentSet" ├── type Solution // mathematical witness representation -├── type Value: EvaluationValue // per-solution value with is_valid() +├── type Value: Clone // per-solution evaluation value ├── fn parameter_names() // canonical problem-owned parameter schema ├── fn parameters(&self) -> ProblemParameters // concrete instance parameter values ├── fn evaluate(&self, solution) -> Result @@ -151,6 +151,7 @@ Common aggregate wrappers live in `src/types.rs`: ```rust Max, Min, Sum, Or, And, Extremum, ExtremumSense ``` +All six values implement the `Aggregate` fold. `Sum` and `And` are evaluation and fold values only; they do not implement `EvaluationValue`. `OptimizationValue` trait (in `src/types.rs`) enables generic Decision conversion: - `Min`: meets bound when value ≤ bound @@ -166,12 +167,12 @@ Max, Min, Sum, Or, And, Extremum, ExtremumSense - `BruteForce::solve()` returns `Result, SolveError>`; `None` means exhaustive search proved infeasibility - `BruteForce::find_all_witnesses()` is a reference-testing helper for collecting every optimal or satisfying solution - Each executed step constructs one result and shares it through `Rc`. Document the rule's domain, witness premise, source guarantee, and infeasibility interpretation; all tied qualifying optima must map correctly. -- `EvaluationValue` exposes candidate feasibility through `is_valid()`; `Min`, `Max`, `Or`, and `Extremum` implement it. `SolveOutcome::optimal()` and `feasible()` evaluate once and reject constraint-violating candidates with `EvaluationError::ConstraintViolation`. This does not prove problem infeasibility or optimality. +- `EvaluationValue` exposes candidate feasibility through `is_valid()`; `Min`, `Max`, `Or`, and `Extremum` implement it. `Problem::Value` does not require it; `SolveOutcome`, `ReductionResult` endpoints, `SolutionAggregate`, `OptimizationValue`, and `declare_variants!` registration do, so a `Sum`- or `And`-valued problem evaluates and folds but fails to compile in solve, recovery, and registration. `SolveOutcome::optimal()` and `feasible()` evaluate once and reject constraint-violating candidates with `EvaluationError::ConstraintViolation`. This does not prove problem infeasibility or optimality. - `SolutionAggregate` belongs to `solvers::BruteForce` witness selection. Models, pure reduction mappings, dynamic evaluation, and non-enumerative solving do not require it. See [executed lifecycle](../docs/src/design.md#executed-reduction-lifecycle). - `ReductionResult` provides `target_problem()` and mandatory `recover_result(source, target_outcome)`. Recovery returns typed `Optimal`, `Feasible`, or `Infeasible` outcomes, including solution and evaluation. Each rule handles all statuses explicitly; no optional completion callback or separate value-only path exists. - `pred solve bundle.json` and `pred extract bundle.json --result target-result.json` use the same complete recovery. External results declare their status; the transport boundary validates target feasibility, while the external solver supplies the optimality claim. Insufficient witness quality is an error, never evidence of source infeasibility. - Decode only the reduction's defined mathematical mapping. Preserve reachable mathematical and representation errors; do not add fallback values or recovery branches for violations already excluded by the calling contract. Explicit mathematical alternatives and sentinels are allowed. -- CLI-facing dynamic formatting uses aggregate wrapper names directly (for example `Max(2)`, `Min(None)`, `Or(true)`, or `Sum(56)`) +- CLI-facing dynamic formatting uses aggregate wrapper names directly (for example `Max(2)`, `Min(None)`, or `Or(true)`) - Graph types: SimpleGraph, PlanarGraph, BipartiteGraph, UnitDiskGraph, KingsSubgraph, TriangularSubgraph - Weight types: `One` (unit weight marker), `i64`, `f64` — all implement `WeightElement` trait - `WeightElement` trait: `type Sum: NumericSize` + `fn to_sum(&self)` — converts weight to a summable numeric type diff --git a/.claude/skills/add-model/SKILL.md b/.claude/skills/add-model/SKILL.md index 4df33fdb6..dc5b36658 100644 --- a/.claude/skills/add-model/SKILL.md +++ b/.claude/skills/add-model/SKILL.md @@ -17,7 +17,7 @@ Before any implementation, collect all required information. If called from `iss |---|------|-------------|---------| | 1 | **Problem name** | Struct name with optimization prefix | `MaximumClique`, `MinimumDominatingSet` | | 2 | **Mathematical definition** | Formal definition with objective/constraints | "Given graph G=(V,E), find max-weight subset S where all pairs in S are adjacent" | -| 3 | **Problem type** | Objective (`Max`/`Min`), witness (`bool`), or aggregate-only (`Sum`/`And`/custom `Aggregate`) | Objective (Maximize) | +| 3 | **Problem type** | Objective (`Max`/`Min`/`Extremum`) or witness (`Or`) | Objective (Maximize) | | 4 | **Type parameters** | Graph type `G`, weight type `W`, or other | `G: Graph`, `W: WeightElement` | | 5 | **Struct fields** | What the struct holds | `graph: G`, `weights: Vec` | | 6 | **Configuration space** | Mathematical solution representation and domain | One Boolean selection per vertex | @@ -26,7 +26,7 @@ Before any implementation, collect all required information. If called from `iss | 9 | **Best known exact algorithm** | Complexity with variable definitions | "O(1.1996^n) by Xiao & Nagamochi (2017), where n = \|V\|" | | 10 | **Solving strategy** | How it can be solved | "BruteForce works; ILP reduction available" | | 11 | **Category** | Which sub-module under `src/models/` | `graph`, `formula`, `set`, `algebraic`, `misc` | -| 12 | **Expected outcome from the issue** | Concrete outcome for the issue's example instance | Objective: one optimal solution + optimal value. Witness: one valid/satisfying solution + why it is valid. Aggregate-only: the final aggregate value and how it is derived | +| 12 | **Expected outcome from the issue** | Concrete outcome for the issue's example instance | Objective: one optimal solution + optimal value. Witness: one valid/satisfying solution + why it is valid | If any item is missing, ask the user to provide it. Do NOT proceed until the checklist is complete. @@ -129,7 +129,7 @@ Key decisions: - **Schema metadata:** `ProblemSchemaEntry` must include the explicit structural `category` and reflect the construction interface through `display_name`, `aliases`, `dimensions`, and `fields` - **Objective problems:** use `type Value = Max<_>`, `Min<_>`, or `Extremum<_>` when the model should expose optimization-style witness helpers - **Witness problems:** use `type Value = Or` for existential feasibility problems -- **Aggregate-only problems:** use a value-only aggregate such as `Sum<_>`, `And`, or a custom `Aggregate` when witnesses are not meaningful +- **Fold-only values:** `Sum<_>` and `And` are valid `Problem::Value` types for evaluation and `Aggregate` folding, but they do not implement `EvaluationValue`, so such a problem cannot be registered with `declare_variants!`, solved, or used as a reduction endpoint - **Weight management:** use inherent methods (`weights()`, `set_weights()`, `is_weighted()`), NOT traits - **`dims()`:** returns the configuration space dimensions (e.g., `vec![2; n]` for binary variables) - **`evaluate()`:** must return `Result`. Invalid configurations remain the aggregate's invalid/false contribution; arithmetic overflow and non-finite computed values are errors. @@ -155,7 +155,7 @@ crate::declare_variants! { - A compiled `complexity_eval_fn` plus registry-backed load/serialize/solve dispatch metadata are auto-generated alongside the symbolic expression - See `src/models/graph/maximum_independent_set.rs` for the reference pattern -`declare_variants!` now handles objective, witness-capable, and aggregate-only models uniformly. Use manual `VariantEntry` wiring only for unusual dynamic-registration work, not for ordinary models. +`declare_variants!` handles objective and witness models uniformly; it requires `Problem::Value: EvaluationValue`. Use manual `VariantEntry` wiring only for unusual dynamic-registration work, not for ordinary models. ## Step 3: Register the model @@ -320,10 +320,9 @@ Structural and quality review is handled by the `review-pipeline` stage, not her | Omitting or inferring the model category | Set the required `ProblemSchemaEntry.category` explicitly to one of `Algebraic`, `Formula`, `Graph`, `Misc`, or `Set`; never parse `module_path!()`. | | Missing `#[path]` test link | Add `#[cfg(test)] #[path = "..."] mod tests;` at file bottom | | Wrong `dims()` | Must match the actual configuration space (e.g., `vec![2; n]` for binary) | -| Using the wrong aggregate wrapper | Objective models use `Max` / `Min` / `Extremum`, witness models use `bool`, aggregate-only models use a fold value like `Sum` / `And` | +| Using the wrong aggregate wrapper | Objective models use `Max` / `Min` / `Extremum`, witness models use `Or` | | Not registering in `mod.rs` | Must update both `/mod.rs` and `models/mod.rs` | | Forgetting `declare_variants!` | Required for variant complexity metadata and registry-backed load/serialize/solve dispatch | -| Wrong aggregate wrapper | Use `Max` / `Min` / `Extremum` for objective problems, `Or` for existential witness problems, and `Sum` / `And` (or a custom aggregate) for value-only folds | | Wrong `declare_variants!` syntax | Entries no longer use `opt` / `sat`; one entry per problem may be marked `default` | | Adding aliases in CLI code | Declare problem aliases in `ProblemSchemaEntry.aliases` and variant aliases in `declare_variants!` | | Adding a hand-written decision model | Use `Decision

` wrapper instead — see `decision_problem_meta!` + `register_decision_variant!` in `src/models/graph/minimum_vertex_cover.rs` for the pattern | diff --git a/.claude/skills/fix-issue/SKILL.md b/.claude/skills/fix-issue/SKILL.md index fe1d1d777..f4bc84483 100644 --- a/.claude/skills/fix-issue/SKILL.md +++ b/.claude/skills/fix-issue/SKILL.md @@ -196,7 +196,7 @@ Tag each issue as: | Incorrect mathematical claims | Domain expertise needed | | Incomplete reduction algorithm | Core technical content | | Incomplete or trivial example | Present **3 concrete example options** with pros/cons (use `AskUserQuestion` with previews showing vertex/edge counts, optimal values, and suboptimal cases). Prefer examples that match the model issue's example when a companion model exists. | -| Decision vs optimization framing | **Default to objective-style models** unless evidence points otherwise. In the current aggregate-value architecture, that usually means `type Value = Max<_>`, `Min<_>`, or `Extremum<_>` when the sense is runtime data. Check associated `[Rule]` issues (`gh issue list --search " in:title label:rule"`) to see how rules use this model — if rules only need the decision version (e.g., reducing to SAT with a bound), an objective model still works because the bound can be read from the optimal aggregate value. Use `Or` for inherently existential feasibility problems (SAT, KColoring) where there is no natural objective. Use aggregate-only values such as `Sum<_>` or `And` only when the answer is genuinely a fold over all configurations and there is no representative witness. If switching to an objective model, add the appropriate `Minimum`/`Maximum` prefix per codebase conventions. | +| Decision vs optimization framing | **Default to objective-style models** unless evidence points otherwise. In the current aggregate-value architecture, that usually means `type Value = Max<_>`, `Min<_>`, or `Extremum<_>` when the sense is runtime data. Check associated `[Rule]` issues (`gh issue list --search " in:title label:rule"`) to see how rules use this model — if rules only need the decision version (e.g., reducing to SAT with a bound), an objective model still works because the bound can be read from the optimal aggregate value. Use `Or` for inherently existential feasibility problems (SAT, KColoring) where there is no natural objective. `Sum<_>` and `And` are fold-only values: a model using them evaluates and folds, but cannot be registered with `declare_variants!`, solved, or used as a reduction endpoint, because those paths require `EvaluationValue`. If switching to an objective model, add the appropriate `Minimum`/`Maximum` prefix per codebase conventions. | | Ambiguous overhead expressions | Requires understanding the reduction | --- diff --git a/.claude/skills/review-structural/SKILL.md b/.claude/skills/review-structural/SKILL.md index ed7a0365d..0dfe94a3e 100644 --- a/.claude/skills/review-structural/SKILL.md +++ b/.claude/skills/review-structural/SKILL.md @@ -109,7 +109,7 @@ Report pass/fail. If tests fail, identify which tests. **Do NOT fix anything** ## Step 4: Semantic Review ### For Models: -1. **`evaluate()` correctness** — Does it check feasibility before computing the objective when the model has invalid configurations? Objective models should return `Max/Min/Extremum(None)` for infeasible configs, witness problems should return `false`, and aggregate-only models should return the per-configuration contribution that matches the intended fold semantics. +1. **`evaluate()` correctness** — Does it check feasibility before computing the objective when the model has invalid configurations? Objective models should return `Max/Min/Extremum(None)` for infeasible configs, and witness problems should return `Or(false)`. 2. **`dims()` correctness** — Does it return the actual configuration space? (e.g., `vec![2; n]` for binary) 3. **Size getter consistency** — Do inherent getter methods (e.g., `num_vertices()`, `num_edges()`) match names used in overhead expressions? 4. **Weight handling** — Are weights managed via inherent methods, not traits? @@ -131,7 +131,7 @@ Only if a linked issue was provided. |---|-------| | 1 | Problem name matches issue | | 2 | Mathematical definition matches | -| 3 | Problem framing (objective / witness / aggregate-only) matches | +| 3 | Problem framing (objective / witness) matches | | 4 | Type parameters match | | 5 | Configuration space matches | | 6 | Feasibility check matches | diff --git a/docs/src/design.md b/docs/src/design.md index d6d411c52..dc3032ce7 100644 --- a/docs/src/design.md +++ b/docs/src/design.md @@ -27,7 +27,7 @@ Every problem implements `Problem`. The associated `Value` type is the per-confi trait Problem: Clone { const NAME: &'static str; // e.g., "MaximumIndependentSet" type Solution; // e.g., Vec, permutation, tuple - type Value: EvaluationValue; // e.g., Max, Or + type Value: Clone; // e.g., Max, Or fn parameter_names() -> &'static [&'static str]; fn parameters(&self) -> ProblemParameters; fn evaluate(&self, solution: &Self::Solution) -> Result; @@ -37,12 +37,12 @@ trait Problem: Clone { ``` - **`Problem`** — the base trait. Every problem declares a mathematical `Solution` type, evaluates that type directly, and reports its canonical instance parameters. For example, a 4-vertex MIS uses `Vec`; `evaluate(&[true, false, true, false])` returns `Ok(Max(Some(2)))` if vertices 0 and 2 form an independent set, or `Ok(Max(None))` if they share an edge. Inherent getters such as `num_vertices()` and `num_edges()` supply the named parameters used by reduction expressions. -- **`EvaluationValue`** — requires `Clone` and `is_valid()`, expressing whether one candidate satisfies the model constraints. `Min`, `Max`, `Or`, and `Extremum` implement it. Custom evaluation types implement this check without needing aggregation or solver capabilities. An invalid candidate does not establish that the problem is infeasible. +- **`EvaluationValue`** — requires `Clone` and `is_valid()`, expressing whether one candidate satisfies the model constraints. `Min`, `Max`, `Or`, and `Extremum` implement it. Custom evaluation types implement this check without needing aggregation or solver capabilities. An invalid candidate does not establish that the problem is infeasible. `Problem::Value` does not require it. It is required where a candidate is labeled feasible: `SolveOutcome::optimal` and `feasible`, the source and target of a `ReductionResult`, `SolutionAggregate`, `OptimizationValue`, and `declare_variants!` registration. - **`BruteForceProblem`** — the reference-solver capability for registered variants with a finite Cartesian coordinate space. Its fallible `num_variables()` and `dimension(variable)` methods describe coordinates without allocating their vector. These methods and the Cartesian iterator belong to the brute-force solver, not to the mathematical `Problem` contract. - **Objective problems** — typically use `Max`, `Min`, or `Extremum` as `Value`. - **Feasibility problems** — typically use `Or`. - **Solve contract** — a successful solve always returns the problem's `Solution`; a global count or statistic without a representative solution is not a `Problem` solve. -- **Common aggregate wrappers** — `Max`, `Min`, `Sum`, `Or`, `And`, `Extremum`, `ExtremumSense`. +- **Common aggregate wrappers** — `Max`, `Min`, `Sum`, `Or`, `And`, `Extremum`, `ExtremumSense`. All six values implement the `Aggregate` fold (`identity`, `combine`, `is_absorbing`). `Sum` and `And` are evaluation and fold values only: a problem may evaluate to them and fold them, but they do not implement `EvaluationValue`, so passing such a problem to a solve or recovery API, or registering it, is a compile error. ## Construction inputs @@ -198,9 +198,10 @@ its source-result relation, including thresholds and sentinel constructions. Guarantees must cover every qualifying witness, including tied optima. `SolutionAggregate` remains a brute-force solver capability for selecting from -an enumeration. Mathematical wrappers such as `Min`, `Max`, `Or`, and `Sum` -remain model values. They do not require separate reduction traits or graph -modes. Turing edges describe multiple adaptive queries and are retained only as +an enumeration. `Min`, `Max`, `Or`, `Extremum`, `Sum`, and `And` remain model +values; solving, recovery, and registration accept only the first four, which +implement `EvaluationValue`. They do not require separate reduction traits or +graph modes. Turing edges describe multiple adaptive queries and are retained only as theoretical graph relationships, not executable reductions. The library does not provide a Turing reduction solver. Default path search and execution exclude these edges. Exact recovery alone does not imply approximation or counting preservation. From d182327b0ee047af301be88b115d09479d794087 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Fri, 18 Sep 2026 00:03:10 +0800 Subject: [PATCH 39/42] Rename test fixtures that described the removed aggregate reduction path Co-Authored-By: Claude Fable 5.1 --- src/unit_tests/rules/graph.rs | 252 ++++++++++++++++----------------- src/unit_tests/rules/traits.rs | 56 ++++---- 2 files changed, 152 insertions(+), 156 deletions(-) diff --git a/src/unit_tests/rules/graph.rs b/src/unit_tests/rules/graph.rs index 7d105b259..8c6260f39 100644 --- a/src/unit_tests/rules/graph.rs +++ b/src/unit_tests/rules/graph.rs @@ -68,19 +68,19 @@ fn named_path(names: &[&str]) -> ReductionPath { } #[derive(Clone)] -struct AggregateChainSource; +struct OffsetChainSource; #[derive(Clone)] -struct AggregateChainMiddle; +struct OffsetChainMiddle; #[derive(Clone)] -struct AggregateChainTarget; +struct OffsetChainTarget; #[derive(Clone)] struct NaturalVariantProblem; -impl Problem for AggregateChainSource { - const NAME: &'static str = "AggregateChainSource"; +impl Problem for OffsetChainSource { + const NAME: &'static str = "OffsetChainSource"; type Solution = Vec; type Value = Min; @@ -103,7 +103,7 @@ impl Problem for AggregateChainSource { } } -impl crate::solvers::BruteForceProblem for AggregateChainSource { +impl crate::solvers::BruteForceProblem for OffsetChainSource { fn num_variables(&self) -> Result { Ok(1usize) } @@ -113,8 +113,8 @@ impl crate::solvers::BruteForceProblem for AggregateChainSource { } } -impl Problem for AggregateChainMiddle { - const NAME: &'static str = "AggregateChainMiddle"; +impl Problem for OffsetChainMiddle { + const NAME: &'static str = "OffsetChainMiddle"; type Solution = Vec; type Value = Min; @@ -137,7 +137,7 @@ impl Problem for AggregateChainMiddle { } } -impl crate::solvers::BruteForceProblem for AggregateChainMiddle { +impl crate::solvers::BruteForceProblem for OffsetChainMiddle { fn num_variables(&self) -> Result { Ok(1usize) } @@ -147,8 +147,8 @@ impl crate::solvers::BruteForceProblem for AggregateChainMiddle { } } -impl Problem for AggregateChainTarget { - const NAME: &'static str = "AggregateChainTarget"; +impl Problem for OffsetChainTarget { + const NAME: &'static str = "OffsetChainTarget"; type Solution = Vec; type Value = Min; @@ -171,7 +171,7 @@ impl Problem for AggregateChainTarget { } } -impl crate::solvers::BruteForceProblem for AggregateChainTarget { +impl crate::solvers::BruteForceProblem for OffsetChainTarget { fn num_variables(&self) -> Result { Ok(1usize) } @@ -215,13 +215,13 @@ impl crate::solvers::BruteForceProblem for NaturalVariantProblem { } } -struct SourceToMiddleAggregateResult { - target: AggregateChainMiddle, +struct SourceToMiddleOffsetResult { + target: OffsetChainMiddle, } -impl ReductionResult for SourceToMiddleAggregateResult { - type Source = AggregateChainSource; - type Target = AggregateChainMiddle; +impl ReductionResult for SourceToMiddleOffsetResult { + type Source = OffsetChainSource; + type Target = OffsetChainMiddle; fn target_problem(&self) -> &Self::Target { &self.target @@ -246,13 +246,13 @@ impl ReductionResult for SourceToMiddleAggregateResult { } } -struct MiddleToTargetAggregateResult { - target: AggregateChainTarget, +struct MiddleToTargetOffsetResult { + target: OffsetChainTarget, } -impl ReductionResult for MiddleToTargetAggregateResult { - type Source = AggregateChainMiddle; - type Target = AggregateChainTarget; +impl ReductionResult for MiddleToTargetOffsetResult { + type Source = OffsetChainMiddle; + type Target = OffsetChainTarget; fn target_problem(&self) -> &Self::Target { &self.target @@ -277,47 +277,47 @@ impl ReductionResult for MiddleToTargetAggregateResult { } } -fn reduce_source_to_middle_aggregate( +fn reduce_source_to_middle_offset( any: &dyn Any, ) -> Result { - any.downcast_ref::().ok_or( + any.downcast_ref::().ok_or( crate::rules::ReductionError::SourceTypeMismatch { - source_problem: AggregateChainSource::NAME, - target_problem: AggregateChainMiddle::NAME, - expected: std::any::type_name::(), + source_problem: OffsetChainSource::NAME, + target_problem: OffsetChainMiddle::NAME, + expected: std::any::type_name::(), }, )?; Ok(crate::rules::registry::ExecutedStep { - witness: std::rc::Rc::new(SourceToMiddleAggregateResult { - target: AggregateChainMiddle, + witness: std::rc::Rc::new(SourceToMiddleOffsetResult { + target: OffsetChainMiddle, }), }) } -fn reduce_middle_to_target_aggregate( +fn reduce_middle_to_target_offset( any: &dyn Any, ) -> Result { - any.downcast_ref::().ok_or( + any.downcast_ref::().ok_or( crate::rules::ReductionError::SourceTypeMismatch { - source_problem: AggregateChainMiddle::NAME, - target_problem: AggregateChainTarget::NAME, - expected: std::any::type_name::(), + source_problem: OffsetChainMiddle::NAME, + target_problem: OffsetChainTarget::NAME, + expected: std::any::type_name::(), }, )?; Ok(crate::rules::registry::ExecutedStep { - witness: std::rc::Rc::new(MiddleToTargetAggregateResult { - target: AggregateChainTarget, + witness: std::rc::Rc::new(MiddleToTargetOffsetResult { + target: OffsetChainTarget, }), }) } struct SourceToMiddleWitnessResult { - target: AggregateChainMiddle, + target: OffsetChainMiddle, } impl ReductionResult for SourceToMiddleWitnessResult { - type Source = AggregateChainSource; - type Target = AggregateChainMiddle; + type Source = OffsetChainSource; + type Target = OffsetChainMiddle; fn target_problem(&self) -> &Self::Target { &self.target @@ -358,16 +358,16 @@ impl SourceToMiddleWitnessResult { fn reduce_source_to_middle_witness( any: &dyn Any, ) -> Result { - any.downcast_ref::().ok_or( + any.downcast_ref::().ok_or( crate::rules::ReductionError::SourceTypeMismatch { - source_problem: AggregateChainSource::NAME, - target_problem: AggregateChainMiddle::NAME, - expected: std::any::type_name::(), + source_problem: OffsetChainSource::NAME, + target_problem: OffsetChainMiddle::NAME, + expected: std::any::type_name::(), }, )?; Ok(crate::rules::registry::ExecutedStep { witness: std::rc::Rc::new(SourceToMiddleWitnessResult { - target: AggregateChainMiddle, + target: OffsetChainMiddle, }), }) } @@ -376,8 +376,8 @@ fn fail_source_to_middle_witness( _any: &dyn Any, ) -> Result { Err(crate::rules::ReductionError::InvalidTarget { - source_problem: AggregateChainSource::NAME, - target_problem: AggregateChainMiddle::NAME, + source_problem: OffsetChainSource::NAME, + target_problem: OffsetChainMiddle::NAME, message: "synthetic target construction failure".to_string(), }) } @@ -392,12 +392,12 @@ fn reduce_counted_source_to_middle_witness( } struct MiddleToTargetWitnessResult { - target: AggregateChainTarget, + target: OffsetChainTarget, } impl ReductionResult for MiddleToTargetWitnessResult { - type Source = AggregateChainMiddle; - type Target = AggregateChainTarget; + type Source = OffsetChainMiddle; + type Target = OffsetChainTarget; fn target_problem(&self) -> &Self::Target { &self.target @@ -438,16 +438,16 @@ impl MiddleToTargetWitnessResult { fn reduce_middle_to_target_witness( any: &dyn Any, ) -> Result { - any.downcast_ref::().ok_or( + any.downcast_ref::().ok_or( crate::rules::ReductionError::SourceTypeMismatch { - source_problem: AggregateChainMiddle::NAME, - target_problem: AggregateChainTarget::NAME, - expected: std::any::type_name::(), + source_problem: OffsetChainMiddle::NAME, + target_problem: OffsetChainTarget::NAME, + expected: std::any::type_name::(), }, )?; Ok(crate::rules::registry::ExecutedStep { witness: std::rc::Rc::new(MiddleToTargetWitnessResult { - target: AggregateChainTarget, + target: OffsetChainTarget, }), }) } @@ -521,29 +521,29 @@ fn execute_paths_executes_a_shared_prefix_once() { }; let graph = ReductionGraph::from_test_edges( &[ - AggregateChainSource::NAME, - AggregateChainMiddle::NAME, - AggregateChainTarget::NAME, + OffsetChainSource::NAME, + OffsetChainMiddle::NAME, + OffsetChainTarget::NAME, ], &[ ( - AggregateChainSource::NAME, - AggregateChainMiddle::NAME, + OffsetChainSource::NAME, + OffsetChainMiddle::NAME, witness_edge(reduce_counted_source_to_middle_witness), ), ( - AggregateChainMiddle::NAME, - AggregateChainTarget::NAME, + OffsetChainMiddle::NAME, + OffsetChainTarget::NAME, witness_edge(reduce_middle_to_target_witness), ), ], ); let mut paths = vec![ - named_path(&[AggregateChainSource::NAME, AggregateChainMiddle::NAME]), + named_path(&[OffsetChainSource::NAME, OffsetChainMiddle::NAME]), named_path(&[ - AggregateChainSource::NAME, - AggregateChainMiddle::NAME, - AggregateChainTarget::NAME, + OffsetChainSource::NAME, + OffsetChainMiddle::NAME, + OffsetChainTarget::NAME, ]), ]; @@ -551,7 +551,7 @@ fn execute_paths_executes_a_shared_prefix_once() { paths.push(paths[1].clone()); let executed = graph - .execute_paths(&paths, &AggregateChainSource) + .execute_paths(&paths, &OffsetChainSource) .expect("both paths are executable"); assert_eq!(executed.len(), 4); @@ -559,8 +559,8 @@ fn execute_paths_executes_a_shared_prefix_once() { assert_eq!(execution.steps.len(), path.len()); assert_eq!( execution - .recover_result::( - &AggregateChainSource, + .recover_result::( + &OffsetChainSource, SolveOutcome::Optimal { solution: vec![1usize], evaluation: Min(Some(1)) @@ -759,24 +759,24 @@ fn test_find_direct_path() { } #[test] -fn test_aggregate_reduction_chain_extracts_value_backwards() { +fn test_reduction_chain_recovers_result_backwards() { let source_variant = BTreeMap::new(); let middle_variant = BTreeMap::new(); let target_variant = BTreeMap::new(); let nodes = vec![ VariantNode { - name: AggregateChainSource::NAME, + name: OffsetChainSource::NAME, variant: source_variant.clone(), complexity: "", }, VariantNode { - name: AggregateChainMiddle::NAME, + name: OffsetChainMiddle::NAME, variant: middle_variant.clone(), complexity: "", }, VariantNode { - name: AggregateChainTarget::NAME, + name: OffsetChainTarget::NAME, variant: target_variant.clone(), complexity: "", }, @@ -792,7 +792,7 @@ fn test_aggregate_reduction_chain_extracts_value_backwards() { middle_idx, ReductionEdgeData { parameter_contract: empty_parameter_contract(), - reduce_fn: Some(reduce_source_to_middle_aggregate), + reduce_fn: Some(reduce_source_to_middle_offset), turing: false, }, ); @@ -801,7 +801,7 @@ fn test_aggregate_reduction_chain_extracts_value_backwards() { target_idx, ReductionEdgeData { parameter_contract: empty_parameter_contract(), - reduce_fn: Some(reduce_middle_to_target_aggregate), + reduce_fn: Some(reduce_middle_to_target_offset), turing: false, }, ); @@ -810,44 +810,43 @@ fn test_aggregate_reduction_chain_extracts_value_backwards() { graph, nodes, name_to_nodes: HashMap::from([ - (AggregateChainSource::NAME, vec![source_idx]), - (AggregateChainMiddle::NAME, vec![middle_idx]), - (AggregateChainTarget::NAME, vec![target_idx]), + (OffsetChainSource::NAME, vec![source_idx]), + (OffsetChainMiddle::NAME, vec![middle_idx]), + (OffsetChainTarget::NAME, vec![target_idx]), ]), default_variants: HashMap::new(), }; let path = ReductionPath { steps: vec![ ReductionStep { - name: AggregateChainSource::NAME.to_string(), + name: OffsetChainSource::NAME.to_string(), variant: source_variant, }, ReductionStep { - name: AggregateChainMiddle::NAME.to_string(), + name: OffsetChainMiddle::NAME.to_string(), variant: middle_variant, }, ReductionStep { - name: AggregateChainTarget::NAME.to_string(), + name: OffsetChainTarget::NAME.to_string(), variant: target_variant, }, ], }; let chain = reduction_graph - .reduce_along_path(&path, &AggregateChainSource as &dyn Any) - .expect("aggregate reduction should not fail") - .expect("expected aggregate reduction chain"); + .reduce_along_path(&path, &OffsetChainSource as &dyn Any) + .expect("offset reduction should not fail") + .expect("expected offset reduction chain"); assert_eq!( - crate::solvers::cartesian_dimensions(chain.target_problem::()) - .unwrap(), + crate::solvers::cartesian_dimensions(chain.target_problem::()).unwrap(), vec![1] ); assert_eq!( chain - .recover_result::( - &AggregateChainSource, - SolveOutcome::optimal(chain.target_problem::(), vec![7]) + .recover_result::( + &OffsetChainSource, + SolveOutcome::optimal(chain.target_problem::(), vec![7]) .unwrap() ) .unwrap(), @@ -863,9 +862,9 @@ fn default_path_search_rejects_turing_only_edge() { let source_variant = BTreeMap::new(); let target_variant = BTreeMap::new(); let graph = build_two_node_graph( - AggregateChainSource::NAME, + OffsetChainSource::NAME, source_variant.clone(), - AggregateChainMiddle::NAME, + OffsetChainMiddle::NAME, target_variant.clone(), ReductionEdgeData { parameter_contract: empty_parameter_contract(), @@ -876,18 +875,18 @@ fn default_path_search_rejects_turing_only_edge() { assert!(graph .find_paths_up_to( - AggregateChainSource::NAME, + OffsetChainSource::NAME, &source_variant, - AggregateChainMiddle::NAME, + OffsetChainMiddle::NAME, &target_variant, 1, ) .is_empty()); assert!(!graph .find_all_paths_mode( - AggregateChainSource::NAME, + OffsetChainSource::NAME, &source_variant, - AggregateChainMiddle::NAME, + OffsetChainMiddle::NAME, &target_variant, ReductionMode::Turing ) @@ -899,9 +898,9 @@ fn turing_path_search_rejects_witness_only_edge() { let source_variant = BTreeMap::new(); let target_variant = BTreeMap::new(); let graph = build_two_node_graph( - AggregateChainSource::NAME, + OffsetChainSource::NAME, source_variant.clone(), - AggregateChainMiddle::NAME, + OffsetChainMiddle::NAME, target_variant.clone(), ReductionEdgeData { parameter_contract: empty_parameter_contract(), @@ -913,18 +912,18 @@ fn turing_path_search_rejects_witness_only_edge() { assert!(graph .find_all_paths_mode( - AggregateChainSource::NAME, + OffsetChainSource::NAME, &source_variant, - AggregateChainMiddle::NAME, + OffsetChainMiddle::NAME, &target_variant, ReductionMode::Turing ) .is_empty()); assert!(!graph .find_all_paths_mode( - AggregateChainSource::NAME, + OffsetChainSource::NAME, &source_variant, - AggregateChainMiddle::NAME, + OffsetChainMiddle::NAME, &target_variant, ReductionMode::Witness ) @@ -972,24 +971,24 @@ fn witness_executor_does_not_imply_turing_capability() { fn reduce_result_along_path_rejects_single_step_path() { let source_variant = BTreeMap::new(); let graph = build_two_node_graph( - AggregateChainSource::NAME, + OffsetChainSource::NAME, source_variant.clone(), - AggregateChainMiddle::NAME, + OffsetChainMiddle::NAME, BTreeMap::new(), ReductionEdgeData { parameter_contract: empty_parameter_contract(), - reduce_fn: Some(reduce_source_to_middle_aggregate), + reduce_fn: Some(reduce_source_to_middle_offset), turing: false, }, ); let single_step_path = ReductionPath { steps: vec![ReductionStep { - name: AggregateChainSource::NAME.to_string(), + name: OffsetChainSource::NAME.to_string(), variant: source_variant, }], }; assert!(graph - .reduce_along_path(&single_step_path, &AggregateChainSource as &dyn Any) + .reduce_along_path(&single_step_path, &OffsetChainSource as &dyn Any) .expect("single-step path lookup should not fail") .is_none()); } @@ -999,9 +998,9 @@ fn reduce_result_returns_none_for_turing_only_edge() { let source_variant = BTreeMap::new(); let target_variant = BTreeMap::new(); let graph = build_two_node_graph( - AggregateChainSource::NAME, + OffsetChainSource::NAME, source_variant.clone(), - AggregateChainMiddle::NAME, + OffsetChainMiddle::NAME, target_variant.clone(), ReductionEdgeData { parameter_contract: empty_parameter_contract(), @@ -1013,17 +1012,17 @@ fn reduce_result_returns_none_for_turing_only_edge() { let path = ReductionPath { steps: vec![ ReductionStep { - name: AggregateChainSource::NAME.to_string(), + name: OffsetChainSource::NAME.to_string(), variant: source_variant, }, ReductionStep { - name: AggregateChainMiddle::NAME.to_string(), + name: OffsetChainMiddle::NAME.to_string(), variant: target_variant, }, ], }; assert!(graph - .reduce_along_path(&path, &AggregateChainSource as &dyn Any) + .reduce_along_path(&path, &OffsetChainSource as &dyn Any) .expect("Turing-only edge lookup should not fail") .is_none()); } @@ -1033,9 +1032,9 @@ fn reduce_along_path_preserves_edge_failure() { let source_variant = BTreeMap::new(); let target_variant = BTreeMap::new(); let graph = build_two_node_graph( - AggregateChainSource::NAME, + OffsetChainSource::NAME, source_variant.clone(), - AggregateChainMiddle::NAME, + OffsetChainMiddle::NAME, target_variant.clone(), ReductionEdgeData { parameter_contract: empty_parameter_contract(), @@ -1047,25 +1046,25 @@ fn reduce_along_path_preserves_edge_failure() { let path = ReductionPath { steps: vec![ ReductionStep { - name: AggregateChainSource::NAME.to_string(), + name: OffsetChainSource::NAME.to_string(), variant: source_variant, }, ReductionStep { - name: AggregateChainMiddle::NAME.to_string(), + name: OffsetChainMiddle::NAME.to_string(), variant: target_variant, }, ], }; - let error = match graph.reduce_along_path(&path, &AggregateChainSource as &dyn Any) { + let error = match graph.reduce_along_path(&path, &OffsetChainSource as &dyn Any) { Err(error) => error, Ok(_) => panic!("registered edge failure must be returned"), }; assert_eq!( error, crate::rules::ReductionError::InvalidTarget { - source_problem: AggregateChainSource::NAME, - target_problem: AggregateChainMiddle::NAME, + source_problem: OffsetChainSource::NAME, + target_problem: OffsetChainMiddle::NAME, message: "synthetic target construction failure".to_string(), } ); @@ -2095,11 +2094,11 @@ fn witness_and_value_mapping_share_one_executed_construction() { static CONSTRUCTIONS: AtomicUsize = AtomicUsize::new(0); let chain = crate::rules::ReductionChain::execute( - &AggregateChainSource, + &OffsetChainSource, &[|_| { CONSTRUCTIONS.fetch_add(1, Ordering::SeqCst); let result = Rc::new(SourceToMiddleWitnessResult { - target: AggregateChainMiddle, + target: OffsetChainMiddle, }); Ok(ExecutedStep { witness: result }) }], @@ -2109,20 +2108,17 @@ fn witness_and_value_mapping_share_one_executed_construction() { assert!(std::ptr::eq( step.witness .target_problem_any() - .downcast_ref::() + .downcast_ref::() .unwrap(), - chain.target_problem::(), + chain.target_problem::(), )); let witness = vec![7usize]; assert_eq!( chain - .recover_result::( - &AggregateChainSource, - SolveOutcome::optimal( - chain.target_problem::(), - witness.clone() - ) - .unwrap(), + .recover_result::( + &OffsetChainSource, + SolveOutcome::optimal(chain.target_problem::(), witness.clone()) + .unwrap(), ) .unwrap(), SolveOutcome::Optimal { diff --git a/src/unit_tests/rules/traits.rs b/src/unit_tests/rules/traits.rs index afe31fd6c..f9e304a2b 100644 --- a/src/unit_tests/rules/traits.rs +++ b/src/unit_tests/rules/traits.rs @@ -209,7 +209,7 @@ fn test_reduction() { } #[test] -fn aggregate_value_from_solution_keeps_evaluation_errors_distinct_from_false() { +fn decision_recovery_keeps_evaluation_errors_distinct_from_infeasible() { use crate::models::decision::Decision; use crate::models::graph::MinimumVertexCover; use crate::rules::ExtractionError; @@ -256,29 +256,29 @@ fn aggregate_value_from_solution_keeps_evaluation_errors_distinct_from_false() { } #[derive(Clone)] -struct AggregateSourceProblem; +struct OffsetSourceProblem; #[derive(Clone)] -struct AggregateTargetProblem; +struct OffsetTargetProblem; thread_local! { static TARGET_EVALUATIONS: std::cell::Cell = const { std::cell::Cell::new(0) }; } -impl AggregateSourceProblem { +impl OffsetSourceProblem { fn num_variables(&self) -> usize { 1 } } -impl AggregateTargetProblem { +impl OffsetTargetProblem { fn num_variables(&self) -> usize { 1 } } -impl Problem for AggregateSourceProblem { - const NAME: &'static str = "AggregateSource"; +impl Problem for OffsetSourceProblem { + const NAME: &'static str = "OffsetSource"; type Solution = Vec; type Value = Min; @@ -296,8 +296,8 @@ impl Problem for AggregateSourceProblem { } } -impl Problem for AggregateTargetProblem { - const NAME: &'static str = "AggregateTarget"; +impl Problem for OffsetTargetProblem { + const NAME: &'static str = "OffsetTarget"; type Solution = Vec; type Value = Min; @@ -316,14 +316,14 @@ impl Problem for AggregateTargetProblem { } } -struct TestAggregateReduction { - target: AggregateTargetProblem, +struct TestOffsetReduction { + target: OffsetTargetProblem, offset: u64, } -impl ReductionResult for TestAggregateReduction { - type Source = AggregateSourceProblem; - type Target = AggregateTargetProblem; +impl ReductionResult for TestOffsetReduction { + type Source = OffsetSourceProblem; + type Target = OffsetTargetProblem; fn target_problem(&self) -> &Self::Target { &self.target @@ -348,21 +348,21 @@ impl ReductionResult for TestAggregateReduction { } } -impl ReduceTo for AggregateSourceProblem { - type Result = TestAggregateReduction; +impl ReduceTo for OffsetSourceProblem { + type Result = TestOffsetReduction; fn reduce_to(&self) -> Result { - Ok(TestAggregateReduction { - target: AggregateTargetProblem, + Ok(TestOffsetReduction { + target: OffsetTargetProblem, offset: 3, }) } } #[test] -fn test_aggregate_reduction_extracts_value() { - let source = AggregateSourceProblem; - let result = >::reduce_to(&source) +fn test_offset_reduction_recovers_shifted_result() { + let source = OffsetSourceProblem; + let result = >::reduce_to(&source) .expect("reduction should succeed"); assert_eq!( @@ -380,16 +380,16 @@ fn test_aggregate_reduction_extracts_value() { } #[test] -fn test_dyn_aggregate_reduction_result_extracts_value() { - let result = TestAggregateReduction { - target: AggregateTargetProblem, +fn test_dyn_reduction_result_recovers_shifted_result() { + let result = TestOffsetReduction { + target: OffsetTargetProblem, offset: 2, }; let dyn_result: &dyn DynReductionResult = &result; assert!(dyn_result .target_problem_any() - .downcast_ref::() + .downcast_ref::() .is_some()); TARGET_EVALUATIONS.with(|count| count.set(0)); let (target, target_json) = dyn_result @@ -406,7 +406,7 @@ fn test_dyn_aggregate_reduction_result_extracts_value() { ); assert_eq!(TARGET_EVALUATIONS.with(|count| count.get()), 1); let recovered = dyn_result - .recover_result_dyn(&AggregateSourceProblem, target) + .recover_result_dyn(&OffsetSourceProblem, target) .unwrap(); assert_eq!(TARGET_EVALUATIONS.with(|count| count.get()), 1); assert_eq!( @@ -420,8 +420,8 @@ fn test_dyn_aggregate_reduction_result_extracts_value() { #[test] fn external_evaluation_is_optional_but_must_match_when_present() { - let result = TestAggregateReduction { - target: AggregateTargetProblem, + let result = TestOffsetReduction { + target: OffsetTargetProblem, offset: 2, }; for status in ["optimal", "feasible"] { From 19b2547a75e7e23a51fbcfe011a58dc8b47c8ab5 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Fri, 18 Sep 2026 00:18:31 +0800 Subject: [PATCH 40/42] Name the failing edge on insufficient quality and note external claims in pred extract Type-erased recovery relocates a rule's InsufficientSolutionQuality to InsufficientSolutionQualityAt { source_problem, target_problem }, so chain and CLI errors name the hop that rejected the incumbent while direct typed recover_result calls keep returning the unit variant. is_insufficient_quality() matches both for callers behind a chain. pred extract prints a one-line stderr note when a source infeasible result rests on the external file's optimal or infeasible claim; stdout is unchanged. Co-Authored-By: Claude Fable 5.1 --- docs/src/design.md | 3 + problemreductions-cli/src/cli.rs | 3 +- problemreductions-cli/src/commands/extract.rs | 28 +++++- problemreductions-cli/tests/cli_tests.rs | 95 ++++++++++++++++++- src/rules/traits.rs | 21 ++++ src/unit_tests/example_db.rs | 2 +- src/unit_tests/rules/graph.rs | 36 +++++++ ...inimumvertexcover_minimumfeedbackarcset.rs | 7 +- 8 files changed, 187 insertions(+), 8 deletions(-) diff --git a/docs/src/design.md b/docs/src/design.md index dc3032ce7..ce6bd43ac 100644 --- a/docs/src/design.md +++ b/docs/src/design.md @@ -429,6 +429,9 @@ impl ReductionResult for ReductionISToVC { Construction returns `ReductionError`; recovery returns `ExtractionError`. Recovery must explicitly cover each result status. Required witness quality comes from the rule's proof, not from which caller happens to invoke it. +A rule reports `InsufficientSolutionQuality`; type-erased and chain recovery +relocate it to `InsufficientSolutionQualityAt { source_problem, target_problem }` +so the message names the failing hop. `is_insufficient_quality()` matches both. The adapter checks backend output against the target model. Recovery performs the mathematical reverse mapping and computes the source evaluation. Do not diff --git a/problemreductions-cli/src/cli.rs b/problemreductions-cli/src/cli.rs index a8dfb08de..7b075bb9f 100644 --- a/problemreductions-cli/src/cli.rs +++ b/problemreductions-cli/src/cli.rs @@ -231,7 +231,8 @@ Status: optimal the solver PROVED optimality; recovery may conclude the source is infeasible feasible a valid solution without an optimality proof (samplers, QAOA, annealers) infeasible the solver PROVED the target has no solution; takes no `solution` -Use feasible unless optimality is proven: a false optimal claim can yield a wrong source answer.")] +Use feasible unless optimality is proven: a false optimal claim can yield a wrong source answer. +When a source `infeasible` result rests on an optimal or infeasible claim, a note says so on stderr.")] Extract(ExtractArgs), /// Start MCP (Model Context Protocol) server for AI assistant integration #[cfg(feature = "mcp")] diff --git a/problemreductions-cli/src/commands/extract.rs b/problemreductions-cli/src/commands/extract.rs index 35cd02933..b9ff4c75e 100644 --- a/problemreductions-cli/src/commands/extract.rs +++ b/problemreductions-cli/src/commands/extract.rs @@ -1,7 +1,7 @@ use crate::dispatch::{read_input, BundleReplay, ReductionBundle}; use crate::output::OutputConfig; use anyhow::{Context, Result}; -use problemreductions::solvers::SolverExecution; +use problemreductions::solvers::{SolveOutcome, SolverExecution}; use serde::Deserialize; use std::path::Path; @@ -17,13 +17,28 @@ enum ResultEnvelope { Infeasible, } +impl ResultEnvelope { + /// The unverified external claim, if any, that a source `infeasible` result rests on. + fn infeasibility_note(&self) -> Option<&'static str> { + match self { + Self::Optimal { .. } => Some( + r#"note: source infeasibility rests on the external solver's optimality claim; pass "status":"feasible" unless optimality was proven"#, + ), + Self::Infeasible => Some( + "note: source infeasibility rests on the external solver's infeasibility claim; pred did not verify it", + ), + Self::Feasible { .. } => None, + } + } +} + /// Recover the source result from an external solver's explicit target result. pub fn extract(input: &Path, result_path: &Path, out: &OutputConfig) -> Result<()> { let bundle: ReductionBundle = serde_json::from_str(&read_input(input)?) .context("pred extract requires a reduction bundle produced by pred reduce")?; let target: serde_json::Value = serde_json::from_str(&read_input(result_path)?).context("Target result is not JSON")?; - ResultEnvelope::deserialize(&target).context(RESULT_SHAPE)?; + let claimed = ResultEnvelope::deserialize(&target).context(RESULT_SHAPE)?; let replay = BundleReplay::prepare(&bundle)?; let result = replay.recover_result(target, SolverExecution::External)?; out.emit( @@ -36,5 +51,12 @@ pub fn extract(input: &Path, result_path: &Path, out: &OutputConfig) -> Result<( text }, || Ok(result.to_json()), - ) + )?; + // stdout stays the machine-readable result; the caveat goes to stderr. + if matches!(result.source_outcome, SolveOutcome::Infeasible) { + if let Some(note) = claimed.infeasibility_note() { + out.info(note); + } + } + Ok(()) } diff --git a/problemreductions-cli/tests/cli_tests.rs b/problemreductions-cli/tests/cli_tests.rs index 2abbf2bce..a1f7d404e 100644 --- a/problemreductions-cli/tests/cli_tests.rs +++ b/problemreductions-cli/tests/cli_tests.rs @@ -10469,8 +10469,10 @@ fn test_extract_rejects_feasible_result_the_rule_cannot_use() { ); assert_eq!(output.status.code(), Some(1)); assert!(output.stdout.is_empty()); + // The error names the hop that rejected the incumbent, not the chain's endpoints. assert!(String::from_utf8_lossy(&output.stderr).contains( - "the target result does not establish the conditions required for source recovery" + "MaximumSetPacking -> QUBO: the target result does not establish the conditions \ + required for source recovery" )); // The same witness is usable once the solver claims optimality. @@ -10487,6 +10489,97 @@ fn test_extract_rejects_feasible_result_the_rule_cannot_use() { std::fs::remove_dir_all(directory).unwrap(); } +const EXTRACT_OPTIMALITY_NOTE: &str = + "note: source infeasibility rests on the external solver's optimality claim"; +const EXTRACT_INFEASIBILITY_NOTE: &str = + "note: source infeasibility rests on the external solver's infeasibility claim"; + +fn extract_test_partition_knapsack_bundle( + name: &str, + sizes: &str, +) -> (std::path::PathBuf, std::path::PathBuf) { + let directory = std::env::temp_dir().join(name); + std::fs::create_dir_all(&directory).unwrap(); + let problem_file = directory.join("source.json"); + let bundle_file = directory.join("bundle.json"); + let created = pred() + .args(["-o", problem_file.to_str().unwrap()]) + .args(["create", "Partition", "--sizes", sizes]) + .output() + .unwrap(); + assert!(created.status.success()); + let reduced = reduce_named_to_file( + &problem_file, + "Partition", + "Knapsack", + &["Partition", "Knapsack"], + &bundle_file, + ); + assert!( + reduced.status.success(), + "{}", + String::from_utf8_lossy(&reduced.stderr) + ); + (directory, bundle_file) +} + +#[test] +fn test_extract_notes_source_infeasibility_resting_on_an_external_claim() { + // Total 11 is odd: the Knapsack optimum 5 decodes to an unbalanced partition. + let (directory, bundle_file) = + extract_test_partition_knapsack_bundle("pred_extract_external_claim_note", "2,4,5"); + for (result, note) in [ + ( + r#"{"status":"optimal","solution":[false,false,true]}"#, + EXTRACT_OPTIMALITY_NOTE, + ), + (r#"{"status":"infeasible"}"#, EXTRACT_INFEASIBILITY_NOTE), + ] { + let output = extract_test_run(&directory, &bundle_file, result); + assert!(output.status.success()); + let stderr = String::from_utf8_lossy(&output.stderr); + assert_eq!(stderr.lines().count(), 1, "{stderr}"); + assert!(stderr.starts_with(note), "{stderr}"); + + // The note never reaches stdout: --quiet drops it and leaves the JSON byte-identical. + let quiet = pred() + .args(["--quiet", "--json", "extract"]) + .arg(&bundle_file) + .arg("--result") + .arg(directory.join("result.json")) + .output() + .unwrap(); + assert!(quiet.stderr.is_empty()); + assert_eq!(output.stdout, quiet.stdout); + let json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap(); + assert_eq!(json["status"], "infeasible"); + assert!(!String::from_utf8_lossy(&output.stdout).contains("note:")); + } + std::fs::remove_dir_all(directory).unwrap(); +} + +#[test] +fn test_extract_has_no_claim_note_when_a_source_solution_is_recovered() { + let (directory, bundle_file) = + extract_test_partition_knapsack_bundle("pred_extract_no_claim_note", "3,1,1,2,2,1"); + for status in ["feasible", "optimal"] { + let output = extract_test_run( + &directory, + &bundle_file, + &format!(r#"{{"status":"{status}","solution":[true,false,false,true,false,false]}}"#), + ); + assert!(output.status.success()); + assert!( + output.stderr.is_empty(), + "{}", + String::from_utf8_lossy(&output.stderr) + ); + let json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap(); + assert_eq!(json["status"], status); + } + std::fs::remove_dir_all(directory).unwrap(); +} + #[test] fn test_cvp_i64_create_and_solve() { use std::io::Write; diff --git a/src/rules/traits.rs b/src/rules/traits.rs index 1402f1ce1..a2d17e6c7 100644 --- a/src/rules/traits.rs +++ b/src/rules/traits.rs @@ -130,6 +130,15 @@ impl ReductionError { pub enum ExtractionError { #[error("the target result does not establish the conditions required for source recovery")] InsufficientSolutionQuality, + /// [`Self::InsufficientSolutionQuality`] located at the reduction that reported it. + #[error( + "{source_problem} -> {target_problem}: {}", + Self::InsufficientSolutionQuality + )] + InsufficientSolutionQualityAt { + source_problem: &'static str, + target_problem: &'static str, + }, #[error("{0}")] InvalidTargetSolution(String), #[error("{source_problem} -> {target_problem}: {message}")] @@ -147,6 +156,14 @@ impl ExtractionError { Self::InvalidTargetSolution(message.into()) } + /// Whether the target result was too weak for recovery, with or without edge context. + pub fn is_insufficient_quality(&self) -> bool { + matches!( + self, + Self::InsufficientSolutionQuality | Self::InsufficientSolutionQualityAt { .. } + ) + } + fn for_reduction(self) -> Self { match self { Self::InvalidTargetSolution(message) => Self::Reduction { @@ -154,6 +171,10 @@ impl ExtractionError { target_problem: T::NAME, message, }, + Self::InsufficientSolutionQuality => Self::InsufficientSolutionQualityAt { + source_problem: S::NAME, + target_problem: T::NAME, + }, error => error, } } diff --git a/src/unit_tests/example_db.rs b/src/unit_tests/example_db.rs index e8f85b52b..b6d791a32 100644 --- a/src/unit_tests/example_db.rs +++ b/src/unit_tests/example_db.rs @@ -744,7 +744,7 @@ fn rule_specs_solution_pairs_are_consistent() { assert!(valid, "Rule {label}: feasible recovery returned an invalid source witness"); assert_eq!(evaluation, actual); } - Err(crate::rules::ExtractionError::InsufficientSolutionQuality) => {} + Err(error) if error.is_insufficient_quality() => {} result => panic!("Rule {label}: feasible recovery returned an unjustified status: {result:?}"), } diff --git a/src/unit_tests/rules/graph.rs b/src/unit_tests/rules/graph.rs index 8c6260f39..ebbcb098c 100644 --- a/src/unit_tests/rules/graph.rs +++ b/src/unit_tests/rules/graph.rs @@ -2208,3 +2208,39 @@ fn composed_witness_agrees_across_direct_chain_path_and_json() { } ); } + +#[test] +fn insufficient_quality_names_the_failing_hop_of_a_chain() { + use crate::models::misc::Partition; + // Total 11 is odd, so no Knapsack candidate decodes to a balanced partition. + let source = Partition::new(vec![2, 4, 5]).unwrap(); + let path = ReductionPath { + steps: [ + (Partition::NAME, Partition::variant()), + (Knapsack::NAME, Knapsack::variant()), + (ILP::::NAME, ILP::::variant()), + ] + .into_iter() + .map(|(name, variant)| ReductionStep { + name: name.into(), + variant: ReductionGraph::variant_to_map(&variant), + }) + .collect(), + }; + let chain = ReductionGraph::new() + .reduce_along_path(&path, &source) + .unwrap() + .unwrap(); + // Knapsack -> ILP preserves the feasible incumbent; Partition -> Knapsack cannot use it. + let incumbent = + SolveOutcome::feasible(chain.target_problem::>(), vec![0, 0, 1]).unwrap(); + let error = chain + .recover_result::>(&source, incumbent) + .unwrap_err(); + assert!(error.is_insufficient_quality()); + assert_eq!( + error.to_string(), + "Partition -> Knapsack: the target result does not establish the conditions required \ + for source recovery" + ); +} diff --git a/src/unit_tests/rules/minimumvertexcover_minimumfeedbackarcset.rs b/src/unit_tests/rules/minimumvertexcover_minimumfeedbackarcset.rs index 391f3ac36..319e97060 100644 --- a/src/unit_tests/rules/minimumvertexcover_minimumfeedbackarcset.rs +++ b/src/unit_tests/rules/minimumvertexcover_minimumfeedbackarcset.rs @@ -198,13 +198,16 @@ fn feasible_feedback_arc_set_does_not_establish_a_vertex_cover() { reduction.recover_result(&source, target), Err(ExtractionError::InsufficientSolutionQuality), )); - // The external JSON boundary must report the same rule-level failure. + // The external JSON boundary must report the same rule-level failure, located at this edge. let target = reduction .target_result_from_json(serde_json::json!({"status": "feasible", "solution": candidate})) .unwrap() .0; assert!(matches!( reduction.recover_result_dyn(&source, target), - Err(ExtractionError::InsufficientSolutionQuality), + Err(ExtractionError::InsufficientSolutionQualityAt { + source_problem: "MinimumVertexCover", + target_problem: "MinimumFeedbackArcSet", + }), )); } From 41a24634b19b52ea9de2bb6758e82851309c38d9 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Fri, 18 Sep 2026 00:27:47 +0800 Subject: [PATCH 41/42] Share the two repeated recovery bodies between rules Add recover_by_source_evaluation for rules where a feasible source forces every tied target optimum to decode to a valid source solution, and route the nine rules that hand-wrote that body through it. Route the hand-written status-preserving matches through recover_preserving_status. Behaviour is unchanged; no rule test was modified. Co-Authored-By: Claude Fable 5.1 --- ...tcoverby3sets_algebraicequationsovergf2.rs | 11 +-- .../exactcoverby3sets_maximumsetpacking.rs | 32 +------ .../exactcoverby3sets_minimumaxiomset.rs | 32 +------ ...verby3sets_minimumfaultdetectiontestset.rs | 35 ++------ src/rules/exactcoverby3sets_subsetproduct.rs | 11 +-- ...niancircuit_bottlenecktravelingsalesman.rs | 34 +------- .../hamiltoniancircuit_travelingsalesman.rs | 34 +------- .../hamiltonianpath_isomorphicspanningtree.rs | 11 +-- src/rules/ilp_bool_ilp_i64.rs | 11 +-- src/rules/kcoloring_partitionintocliques.rs | 11 +-- src/rules/maxcut_minimummatrixcover.rs | 11 +-- .../maximumclique_maximumindependentset.rs | 11 +-- .../maximumindependentset_maximumclique.rs | 11 +-- ...maximumindependentset_maximumsetpacking.rs | 19 +---- .../maximummatching_maximumsetpacking.rs | 11 +-- ...nimumvertexcover_comparativecontainment.rs | 11 +-- ...mumvertexcover_minimumfeedbackvertexset.rs | 11 +-- .../minimumvertexcover_minimumhittingset.rs | 11 +-- .../minimumvertexcover_minimumsetcovering.rs | 11 +-- src/rules/partition_binpacking.rs | 35 ++------ .../partition_cosineproductintegration.rs | 11 +-- src/rules/partition_knapsack.rs | 34 +------- src/rules/partition_subsetsum.rs | 11 +-- src/rules/partition_sumofsquarespartition.rs | 32 +------ ...flength2_boundedcomponentspanningforest.rs | 11 +-- src/rules/satisfiability_nontautology.rs | 11 +-- src/rules/test_helpers.rs | 51 ++---------- ...mensionalmatching_minimumweightdecoding.rs | 32 +------ ...partition_resourceconstrainedscheduling.rs | 11 +-- src/rules/traits.rs | 35 ++++++++ src/unit_tests/rules/traits.rs | 83 +++++++++++++++++++ 31 files changed, 193 insertions(+), 493 deletions(-) diff --git a/src/rules/exactcoverby3sets_algebraicequationsovergf2.rs b/src/rules/exactcoverby3sets_algebraicequationsovergf2.rs index 7d4e9cf0d..d29fa0533 100644 --- a/src/rules/exactcoverby3sets_algebraicequationsovergf2.rs +++ b/src/rules/exactcoverby3sets_algebraicequationsovergf2.rs @@ -3,9 +3,8 @@ use crate::models::algebraic::AlgebraicEquationsOverGF2; use crate::models::set::ExactCoverBy3Sets; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; #[derive(Debug, Clone)] pub struct ReductionX3CToAlgebraicEquationsOverGF2 { @@ -25,13 +24,7 @@ impl ReductionResult for ReductionX3CToAlgebraicEquationsOverGF2 { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => Ok(SolveOutcome::optimal(source, solution)?), - SolveOutcome::Feasible { solution, .. } => { - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| Ok(solution.clone())) } } diff --git a/src/rules/exactcoverby3sets_maximumsetpacking.rs b/src/rules/exactcoverby3sets_maximumsetpacking.rs index aaee60a94..1d70476e2 100644 --- a/src/rules/exactcoverby3sets_maximumsetpacking.rs +++ b/src/rules/exactcoverby3sets_maximumsetpacking.rs @@ -7,10 +7,8 @@ use crate::models::set::{ExactCoverBy3Sets, MaximumSetPacking}; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_by_source_evaluation, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; -use crate::traits::Problem; use crate::types::One; /// Result of reducing ExactCoverBy3Sets to MaximumSetPacking. @@ -37,33 +35,7 @@ impl ReductionResult for ReductionXC3SToMaximumSetPacking { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - let evaluation = source.evaluate(&solution)?; - if evaluation.0 { - Ok(SolveOutcome::Optimal { - solution, - evaluation, - }) - } else { - Ok(SolveOutcome::Infeasible) - } - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - let evaluation = source.evaluate(&solution)?; - if evaluation.0 { - Ok(SolveOutcome::Feasible { - solution, - evaluation, - }) - } else { - Err(crate::rules::ExtractionError::InsufficientSolutionQuality) - } - } - } + recover_by_source_evaluation(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/exactcoverby3sets_minimumaxiomset.rs b/src/rules/exactcoverby3sets_minimumaxiomset.rs index 5b6be1107..acf7aef60 100644 --- a/src/rules/exactcoverby3sets_minimumaxiomset.rs +++ b/src/rules/exactcoverby3sets_minimumaxiomset.rs @@ -6,10 +6,8 @@ use crate::models::misc::MinimumAxiomSet; use crate::models::set::ExactCoverBy3Sets; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_by_source_evaluation, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; -use crate::traits::Problem; /// Result of reducing ExactCoverBy3Sets to MinimumAxiomSet. #[derive(Debug, Clone)] @@ -38,33 +36,7 @@ impl ReductionResult for ReductionXC3SToMinimumAxiomSet { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - let evaluation = source.evaluate(&solution)?; - if evaluation.0 { - Ok(SolveOutcome::Optimal { - solution, - evaluation, - }) - } else { - Ok(SolveOutcome::Infeasible) - } - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - let evaluation = source.evaluate(&solution)?; - if evaluation.0 { - Ok(SolveOutcome::Feasible { - solution, - evaluation, - }) - } else { - Err(crate::rules::ExtractionError::InsufficientSolutionQuality) - } - } - } + recover_by_source_evaluation(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/exactcoverby3sets_minimumfaultdetectiontestset.rs b/src/rules/exactcoverby3sets_minimumfaultdetectiontestset.rs index 704ceacb4..7a3c1d319 100644 --- a/src/rules/exactcoverby3sets_minimumfaultdetectiontestset.rs +++ b/src/rules/exactcoverby3sets_minimumfaultdetectiontestset.rs @@ -8,10 +8,8 @@ use crate::models::misc::MinimumFaultDetectionTestSet; use crate::models::set::ExactCoverBy3Sets; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_by_source_evaluation, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; -use crate::traits::Problem; /// Result of reducing ExactCoverBy3Sets to MinimumFaultDetectionTestSet. #[derive(Debug, Clone)] @@ -27,38 +25,15 @@ impl ReductionResult for ReductionXC3SToMinimumFaultDetectionTestSet { &self.target } + /// Each input-output pair covers exactly its subset's three element vertices, so an + /// exact cover is a test set of size `q`, and every optimum then selects `q` disjoint + /// triples; any other optimum proves NO. fn recover_result( &self, source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - let evaluation = source.evaluate(&solution)?; - if evaluation.0 { - Ok(SolveOutcome::Optimal { - solution, - evaluation, - }) - } else { - Ok(SolveOutcome::Infeasible) - } - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - let evaluation = source.evaluate(&solution)?; - if evaluation.0 { - Ok(SolveOutcome::Feasible { - solution, - evaluation, - }) - } else { - Err(crate::rules::ExtractionError::InsufficientSolutionQuality) - } - } - } + recover_by_source_evaluation(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/exactcoverby3sets_subsetproduct.rs b/src/rules/exactcoverby3sets_subsetproduct.rs index cf0dfa008..146c886df 100644 --- a/src/rules/exactcoverby3sets_subsetproduct.rs +++ b/src/rules/exactcoverby3sets_subsetproduct.rs @@ -9,9 +9,8 @@ use crate::models::formula::ksat::first_n_odd_primes; use crate::models::misc::SubsetProduct; use crate::models::set::ExactCoverBy3Sets; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use num_bigint::BigUint; use num_traits::One; @@ -33,13 +32,7 @@ impl ReductionResult for ReductionX3CToSubsetProduct { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => Ok(SolveOutcome::optimal(source, solution)?), - SolveOutcome::Feasible { solution, .. } => { - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| Ok(solution.clone())) } } diff --git a/src/rules/hamiltoniancircuit_bottlenecktravelingsalesman.rs b/src/rules/hamiltoniancircuit_bottlenecktravelingsalesman.rs index 926f5a242..182517ffc 100644 --- a/src/rules/hamiltoniancircuit_bottlenecktravelingsalesman.rs +++ b/src/rules/hamiltoniancircuit_bottlenecktravelingsalesman.rs @@ -6,11 +6,9 @@ use crate::models::graph::{BottleneckTravelingSalesman, HamiltonianCircuit}; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_by_source_evaluation, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; -use crate::traits::Problem; /// Result of reducing HamiltonianCircuit to BottleneckTravelingSalesman. #[derive(Debug, Clone)] @@ -26,38 +24,14 @@ impl ReductionResult for ReductionHamiltonianCircuitToBottleneckTravelingSalesma &self.target } + /// A Hamiltonian circuit is a tour of bottleneck 1, the minimum over weights 1 and 2, so + /// every optimal tour then uses only weight-1 (source) edges; any other optimum proves NO. fn recover_result( &self, source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - let evaluation = source.evaluate(&solution)?; - if evaluation.0 { - Ok(SolveOutcome::Optimal { - solution, - evaluation, - }) - } else { - Ok(SolveOutcome::Infeasible) - } - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - let evaluation = source.evaluate(&solution)?; - if evaluation.0 { - Ok(SolveOutcome::Feasible { - solution, - evaluation, - }) - } else { - Err(crate::rules::ExtractionError::InsufficientSolutionQuality) - } - } - } + recover_by_source_evaluation(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/hamiltoniancircuit_travelingsalesman.rs b/src/rules/hamiltoniancircuit_travelingsalesman.rs index 78e2f11e3..c3f4a3b24 100644 --- a/src/rules/hamiltoniancircuit_travelingsalesman.rs +++ b/src/rules/hamiltoniancircuit_travelingsalesman.rs @@ -6,11 +6,9 @@ use crate::models::graph::{HamiltonianCircuit, TravelingSalesman}; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_by_source_evaluation, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; -use crate::traits::Problem; /// Result of reducing HamiltonianCircuit to TravelingSalesman. #[derive(Debug, Clone)] @@ -26,38 +24,14 @@ impl ReductionResult for ReductionHamiltonianCircuitToTravelingSalesman { &self.target } + /// A Hamiltonian circuit is a tour of cost `n`, the minimum over weights 1 and 2, so + /// every optimal tour then uses only weight-1 (source) edges; any other optimum proves NO. fn recover_result( &self, source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - let evaluation = source.evaluate(&solution)?; - if evaluation.0 { - Ok(SolveOutcome::Optimal { - solution, - evaluation, - }) - } else { - Ok(SolveOutcome::Infeasible) - } - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - let evaluation = source.evaluate(&solution)?; - if evaluation.0 { - Ok(SolveOutcome::Feasible { - solution, - evaluation, - }) - } else { - Err(crate::rules::ExtractionError::InsufficientSolutionQuality) - } - } - } + recover_by_source_evaluation(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/hamiltonianpath_isomorphicspanningtree.rs b/src/rules/hamiltonianpath_isomorphicspanningtree.rs index fd05fb863..4b260768a 100644 --- a/src/rules/hamiltonianpath_isomorphicspanningtree.rs +++ b/src/rules/hamiltonianpath_isomorphicspanningtree.rs @@ -6,9 +6,8 @@ use crate::models::graph::{HamiltonianPath, IsomorphicSpanningTree}; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::topology::SimpleGraph; /// Result of reducing HamiltonianPath to IsomorphicSpanningTree. @@ -35,13 +34,7 @@ impl ReductionResult for ReductionHPToIST { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => Ok(SolveOutcome::optimal(source, solution)?), - SolveOutcome::Feasible { solution, .. } => { - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| Ok(solution.clone())) } } diff --git a/src/rules/ilp_bool_ilp_i64.rs b/src/rules/ilp_bool_ilp_i64.rs index 8bb5d289b..68d0016d9 100644 --- a/src/rules/ilp_bool_ilp_i64.rs +++ b/src/rules/ilp_bool_ilp_i64.rs @@ -6,9 +6,8 @@ use crate::models::algebraic::ILP; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; #[derive(Debug, Clone)] pub struct ReductionBinaryILPToIntILP { @@ -28,13 +27,7 @@ impl ReductionResult for ReductionBinaryILPToIntILP { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => Ok(SolveOutcome::optimal(source, solution)?), - SolveOutcome::Feasible { solution, .. } => { - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| Ok(solution.clone())) } } diff --git a/src/rules/kcoloring_partitionintocliques.rs b/src/rules/kcoloring_partitionintocliques.rs index 5b678a80b..9c553e9de 100644 --- a/src/rules/kcoloring_partitionintocliques.rs +++ b/src/rules/kcoloring_partitionintocliques.rs @@ -6,9 +6,8 @@ use crate::models::graph::{KColoring, PartitionIntoCliques}; use crate::reduction; use crate::rules::graph_helpers::complement_edges; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; use crate::variant::KN; @@ -32,13 +31,7 @@ impl ReductionResult for ReductionKColoringToPartitionIntoCliques { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => Ok(SolveOutcome::optimal(source, solution)?), - SolveOutcome::Feasible { solution, .. } => { - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| Ok(solution.clone())) } } diff --git a/src/rules/maxcut_minimummatrixcover.rs b/src/rules/maxcut_minimummatrixcover.rs index f354156ae..3090d45ed 100644 --- a/src/rules/maxcut_minimummatrixcover.rs +++ b/src/rules/maxcut_minimummatrixcover.rs @@ -23,9 +23,8 @@ use crate::models::algebraic::MinimumMatrixCover; use crate::models::graph::MaxCut; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; /// Result of reducing MaxCut to MinimumMatrixCover. @@ -55,13 +54,7 @@ impl ReductionResult for ReductionMaxCutToMMC { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => Ok(SolveOutcome::optimal(source, solution)?), - SolveOutcome::Feasible { solution, .. } => { - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| Ok(solution.clone())) } } diff --git a/src/rules/maximumclique_maximumindependentset.rs b/src/rules/maximumclique_maximumindependentset.rs index 43c1ab1d1..58957aae7 100644 --- a/src/rules/maximumclique_maximumindependentset.rs +++ b/src/rules/maximumclique_maximumindependentset.rs @@ -5,9 +5,8 @@ use crate::models::graph::{MaximumClique, MaximumIndependentSet}; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; use crate::types::{One, WeightElement}; @@ -35,13 +34,7 @@ where source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => Ok(SolveOutcome::optimal(source, solution)?), - SolveOutcome::Feasible { solution, .. } => { - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| Ok(solution.clone())) } } diff --git a/src/rules/maximumindependentset_maximumclique.rs b/src/rules/maximumindependentset_maximumclique.rs index c0327321d..86f1938a1 100644 --- a/src/rules/maximumindependentset_maximumclique.rs +++ b/src/rules/maximumindependentset_maximumclique.rs @@ -5,9 +5,8 @@ use crate::models::graph::{MaximumClique, MaximumIndependentSet}; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; use crate::types::{One, WeightElement}; @@ -35,13 +34,7 @@ where source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => Ok(SolveOutcome::optimal(source, solution)?), - SolveOutcome::Feasible { solution, .. } => { - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| Ok(solution.clone())) } } diff --git a/src/rules/maximumindependentset_maximumsetpacking.rs b/src/rules/maximumindependentset_maximumsetpacking.rs index 2de124b64..9fe7a3378 100644 --- a/src/rules/maximumindependentset_maximumsetpacking.rs +++ b/src/rules/maximumindependentset_maximumsetpacking.rs @@ -6,9 +6,8 @@ use crate::models::graph::MaximumIndependentSet; use crate::models::set::MaximumSetPacking; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; use crate::types::{One, WeightElement}; use std::collections::HashSet; @@ -36,13 +35,7 @@ where source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => Ok(SolveOutcome::optimal(source, solution)?), - SolveOutcome::Feasible { solution, .. } => { - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| Ok(solution.clone())) } } @@ -103,13 +96,7 @@ where source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => Ok(SolveOutcome::optimal(source, solution)?), - SolveOutcome::Feasible { solution, .. } => { - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| Ok(solution.clone())) } } diff --git a/src/rules/maximummatching_maximumsetpacking.rs b/src/rules/maximummatching_maximumsetpacking.rs index 34c37b5e3..433662acc 100644 --- a/src/rules/maximummatching_maximumsetpacking.rs +++ b/src/rules/maximummatching_maximumsetpacking.rs @@ -6,9 +6,8 @@ use crate::models::graph::MaximumMatching; use crate::models::set::MaximumSetPacking; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; use crate::types::WeightElement; @@ -37,13 +36,7 @@ where source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => Ok(SolveOutcome::optimal(source, solution)?), - SolveOutcome::Feasible { solution, .. } => { - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| Ok(solution.clone())) } } diff --git a/src/rules/minimumvertexcover_comparativecontainment.rs b/src/rules/minimumvertexcover_comparativecontainment.rs index f5bae21b1..38f3f767f 100644 --- a/src/rules/minimumvertexcover_comparativecontainment.rs +++ b/src/rules/minimumvertexcover_comparativecontainment.rs @@ -13,9 +13,8 @@ use crate::models::decision::Decision; use crate::models::graph::MinimumVertexCover; use crate::models::set::ComparativeContainment; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; /// Identity witness map for the signed-weight containment construction. @@ -37,13 +36,7 @@ impl ReductionResult for ReductionDecisionMVCToComparativeContainment { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => Ok(SolveOutcome::optimal(source, solution)?), - SolveOutcome::Feasible { solution, .. } => { - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| Ok(solution.clone())) } } diff --git a/src/rules/minimumvertexcover_minimumfeedbackvertexset.rs b/src/rules/minimumvertexcover_minimumfeedbackvertexset.rs index 26158e30b..88a2b373d 100644 --- a/src/rules/minimumvertexcover_minimumfeedbackvertexset.rs +++ b/src/rules/minimumvertexcover_minimumfeedbackvertexset.rs @@ -5,9 +5,8 @@ use crate::models::graph::{MinimumFeedbackVertexSet, MinimumVertexCover}; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::topology::{DirectedGraph, Graph, SimpleGraph}; use crate::types::WeightElement; @@ -33,13 +32,7 @@ where source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => Ok(SolveOutcome::optimal(source, solution)?), - SolveOutcome::Feasible { solution, .. } => { - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| Ok(solution.clone())) } } diff --git a/src/rules/minimumvertexcover_minimumhittingset.rs b/src/rules/minimumvertexcover_minimumhittingset.rs index 54b72d457..61e19ece6 100644 --- a/src/rules/minimumvertexcover_minimumhittingset.rs +++ b/src/rules/minimumvertexcover_minimumhittingset.rs @@ -6,9 +6,8 @@ use crate::models::graph::MinimumVertexCover; use crate::models::set::MinimumHittingSet; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; use crate::types::One; @@ -33,13 +32,7 @@ impl ReductionResult for ReductionVCToHS { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => Ok(SolveOutcome::optimal(source, solution)?), - SolveOutcome::Feasible { solution, .. } => { - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| Ok(solution.clone())) } } diff --git a/src/rules/minimumvertexcover_minimumsetcovering.rs b/src/rules/minimumvertexcover_minimumsetcovering.rs index c7dddd51b..6322f7bbe 100644 --- a/src/rules/minimumvertexcover_minimumsetcovering.rs +++ b/src/rules/minimumvertexcover_minimumsetcovering.rs @@ -6,9 +6,8 @@ use crate::models::graph::MinimumVertexCover; use crate::models::set::MinimumSetCovering; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; use crate::types::WeightElement; @@ -36,13 +35,7 @@ where source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => Ok(SolveOutcome::optimal(source, solution)?), - SolveOutcome::Feasible { solution, .. } => { - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| Ok(solution.clone())) } } diff --git a/src/rules/partition_binpacking.rs b/src/rules/partition_binpacking.rs index 217e38236..709705203 100644 --- a/src/rules/partition_binpacking.rs +++ b/src/rules/partition_binpacking.rs @@ -14,10 +14,8 @@ use crate::models::misc::{BinPacking, Partition}; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_by_source_evaluation, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; -use crate::traits::Problem; /// Result of reducing Partition to BinPacking. #[derive(Debug, Clone)] @@ -33,38 +31,15 @@ impl ReductionResult for ReductionPartitionToBinPacking { &self.target } + /// A balanced partition packs into two bins, and no packing uses fewer, so every + /// optimum then fills two bins of capacity `S/2` to exactly `S/2` each; any other + /// optimum proves NO. fn recover_result( &self, source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - let evaluation = source.evaluate(&solution)?; - if evaluation.0 { - Ok(SolveOutcome::Optimal { - solution, - evaluation, - }) - } else { - Ok(SolveOutcome::Infeasible) - } - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - let evaluation = source.evaluate(&solution)?; - if evaluation.0 { - Ok(SolveOutcome::Feasible { - solution, - evaluation, - }) - } else { - Err(crate::rules::ExtractionError::InsufficientSolutionQuality) - } - } - } + recover_by_source_evaluation(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/partition_cosineproductintegration.rs b/src/rules/partition_cosineproductintegration.rs index c1abec040..a22c8e5e6 100644 --- a/src/rules/partition_cosineproductintegration.rs +++ b/src/rules/partition_cosineproductintegration.rs @@ -12,9 +12,8 @@ use crate::models::misc::{CosineProductIntegration, Partition}; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; /// Result of reducing Partition to CosineProductIntegration. #[derive(Debug, Clone)] @@ -35,13 +34,7 @@ impl ReductionResult for ReductionPartitionToCPI { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => Ok(SolveOutcome::optimal(source, solution)?), - SolveOutcome::Feasible { solution, .. } => { - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| Ok(solution.clone())) } } diff --git a/src/rules/partition_knapsack.rs b/src/rules/partition_knapsack.rs index 96a4461b8..6f547fa93 100644 --- a/src/rules/partition_knapsack.rs +++ b/src/rules/partition_knapsack.rs @@ -2,10 +2,8 @@ use crate::models::misc::{Knapsack, Partition}; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_by_source_evaluation, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; -use crate::traits::Problem; /// Result of reducing Partition to Knapsack. #[derive(Debug, Clone)] @@ -21,38 +19,14 @@ impl ReductionResult for ReductionPartitionToKnapsack { &self.target } + /// A balanced partition fills the capacity `S/2`, so every Knapsack optimum then has + /// value `S/2` and is itself a balanced half; an optimum below `S/2` proves NO. fn recover_result( &self, source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - let evaluation = source.evaluate(&solution)?; - if evaluation.0 { - Ok(SolveOutcome::Optimal { - solution, - evaluation, - }) - } else { - Ok(SolveOutcome::Infeasible) - } - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - let evaluation = source.evaluate(&solution)?; - if evaluation.0 { - Ok(SolveOutcome::Feasible { - solution, - evaluation, - }) - } else { - Err(crate::rules::ExtractionError::InsufficientSolutionQuality) - } - } - } + recover_by_source_evaluation(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/partition_subsetsum.rs b/src/rules/partition_subsetsum.rs index f411ac7fe..58c502e99 100644 --- a/src/rules/partition_subsetsum.rs +++ b/src/rules/partition_subsetsum.rs @@ -6,9 +6,8 @@ use crate::models::misc::{Partition, SubsetSum}; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use num_bigint::{BigUint, ToBigUint}; /// Result of reducing Partition to SubsetSum. @@ -30,13 +29,7 @@ impl ReductionResult for ReductionPartitionToSubsetSum { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => Ok(SolveOutcome::optimal(source, solution)?), - SolveOutcome::Feasible { solution, .. } => { - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| Ok(solution.clone())) } } diff --git a/src/rules/partition_sumofsquarespartition.rs b/src/rules/partition_sumofsquarespartition.rs index ebd3c468b..8e5ba8b0f 100644 --- a/src/rules/partition_sumofsquarespartition.rs +++ b/src/rules/partition_sumofsquarespartition.rs @@ -20,10 +20,8 @@ use crate::models::misc::{Partition, SumOfSquaresPartition}; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_by_source_evaluation, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; -use crate::traits::Problem; /// Result of reducing Partition to SumOfSquaresPartition. #[derive(Debug, Clone)] @@ -51,33 +49,7 @@ impl ReductionResult for ReductionPartitionToSumOfSquaresPartition { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - let evaluation = source.evaluate(&solution)?; - if evaluation.0 { - Ok(SolveOutcome::Optimal { - solution, - evaluation, - }) - } else { - Ok(SolveOutcome::Infeasible) - } - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - let evaluation = source.evaluate(&solution)?; - if evaluation.0 { - Ok(SolveOutcome::Feasible { - solution, - evaluation, - }) - } else { - Err(crate::rules::ExtractionError::InsufficientSolutionQuality) - } - } - } + recover_by_source_evaluation(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/partitionintopathsoflength2_boundedcomponentspanningforest.rs b/src/rules/partitionintopathsoflength2_boundedcomponentspanningforest.rs index 1a1e74a6b..f37db7c67 100644 --- a/src/rules/partitionintopathsoflength2_boundedcomponentspanningforest.rs +++ b/src/rules/partitionintopathsoflength2_boundedcomponentspanningforest.rs @@ -12,9 +12,8 @@ use crate::models::graph::{BoundedComponentSpanningForest, PartitionIntoPathsOfLength2}; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; use crate::topology::{Graph, SimpleGraph}; /// Result of reducing PartitionIntoPathsOfLength2 to BoundedComponentSpanningForest. @@ -40,13 +39,7 @@ impl ReductionResult for ReductionPPL2ToBCSF { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => Ok(SolveOutcome::optimal(source, solution)?), - SolveOutcome::Feasible { solution, .. } => { - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| Ok(solution.clone())) } } diff --git a/src/rules/satisfiability_nontautology.rs b/src/rules/satisfiability_nontautology.rs index 1983c6da8..3f88fa79d 100644 --- a/src/rules/satisfiability_nontautology.rs +++ b/src/rules/satisfiability_nontautology.rs @@ -5,9 +5,8 @@ use crate::models::formula::{NonTautology, Satisfiability}; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; /// Result of reducing SAT to NonTautology. #[derive(Debug, Clone)] @@ -28,13 +27,7 @@ impl ReductionResult for ReductionSATToNonTautology { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => Ok(SolveOutcome::optimal(source, solution)?), - SolveOutcome::Feasible { solution, .. } => { - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| Ok(solution.clone())) } } diff --git a/src/rules/test_helpers.rs b/src/rules/test_helpers.rs index e152a3d9c..11f085180 100644 --- a/src/rules/test_helpers.rs +++ b/src/rules/test_helpers.rs @@ -325,8 +325,9 @@ mod tests { assert_satisfaction_round_trip_from_optimization_target, assert_satisfaction_round_trip_from_satisfaction_target, }; + use crate::rules::traits::recover_preserving_status; use crate::rules::ReductionResult; - use crate::solvers::{ProblemOutcome, SolveOutcome}; + use crate::solvers::ProblemOutcome; use crate::traits::Problem; use crate::types::{Max, Or}; @@ -464,17 +465,7 @@ mod tests { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } @@ -506,17 +497,7 @@ mod tests { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } @@ -548,17 +529,7 @@ mod tests { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } @@ -590,17 +561,7 @@ mod tests { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::optimal(source, solution)?) - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/threedimensionalmatching_minimumweightdecoding.rs b/src/rules/threedimensionalmatching_minimumweightdecoding.rs index f356fbf44..d661b2695 100644 --- a/src/rules/threedimensionalmatching_minimumweightdecoding.rs +++ b/src/rules/threedimensionalmatching_minimumweightdecoding.rs @@ -24,10 +24,8 @@ use crate::models::algebraic::MinimumWeightDecoding; use crate::models::set::ThreeDimensionalMatching; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_by_source_evaluation, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; -use crate::traits::Problem; /// Result of reducing ThreeDimensionalMatching to MinimumWeightDecoding. #[derive(Debug, Clone)] @@ -55,33 +53,7 @@ impl ReductionResult for ReductionThreeDimensionalMatchingToMinimumWeightDecodin source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => { - let solution = self.map_solution(&solution)?; - let evaluation = source.evaluate(&solution)?; - if evaluation.0 { - Ok(SolveOutcome::Optimal { - solution, - evaluation, - }) - } else { - Ok(SolveOutcome::Infeasible) - } - } - SolveOutcome::Feasible { solution, .. } => { - let solution = self.map_solution(&solution)?; - let evaluation = source.evaluate(&solution)?; - if evaluation.0 { - Ok(SolveOutcome::Feasible { - solution, - evaluation, - }) - } else { - Err(crate::rules::ExtractionError::InsufficientSolutionQuality) - } - } - } + recover_by_source_evaluation(source, target, |solution| self.map_solution(solution)) } } diff --git a/src/rules/threepartition_resourceconstrainedscheduling.rs b/src/rules/threepartition_resourceconstrainedscheduling.rs index e85d008aa..555ced049 100644 --- a/src/rules/threepartition_resourceconstrainedscheduling.rs +++ b/src/rules/threepartition_resourceconstrainedscheduling.rs @@ -20,9 +20,8 @@ use crate::models::misc::{ResourceConstrainedScheduling, ThreePartition}; use crate::reduction; -use crate::rules::traits::{ReduceTo, ReductionResult}; +use crate::rules::traits::{recover_preserving_status, ReduceTo, ReductionResult}; use crate::solvers::ProblemOutcome; -use crate::solvers::SolveOutcome; /// Result of reducing ThreePartition to ResourceConstrainedScheduling. #[derive(Debug, Clone)] @@ -45,13 +44,7 @@ impl ReductionResult for ReductionThreePartitionToRCS { source: &Self::Source, target: ProblemOutcome, ) -> crate::rules::ExtractionResult> { - match target { - SolveOutcome::Infeasible => Ok(SolveOutcome::Infeasible), - SolveOutcome::Optimal { solution, .. } => Ok(SolveOutcome::optimal(source, solution)?), - SolveOutcome::Feasible { solution, .. } => { - Ok(SolveOutcome::feasible(source, solution)?) - } - } + recover_preserving_status(source, target, |solution| Ok(solution.clone())) } } diff --git a/src/rules/traits.rs b/src/rules/traits.rs index a2d17e6c7..36908b129 100644 --- a/src/rules/traits.rs +++ b/src/rules/traits.rs @@ -228,6 +228,41 @@ pub(super) fn recover_preserving_status, S, V } } +/// Recover by evaluating the decoded target solution on the source. +/// +/// Use only when a feasible source forces EVERY tied target optimum to decode to a +/// valid source solution, so an optimal target whose decoding is source-invalid +/// proves source infeasibility. The caller must also establish that target +/// infeasibility implies source infeasibility. +/// A feasible incumbent carries no such proof: a valid decoding is a feasible source +/// solution, and an invalid one is `InsufficientSolutionQuality`. +/// Mapping and evaluation errors propagate; they never establish source infeasibility. +pub(super) fn recover_by_source_evaluation, S, V>( + source: &P, + target: SolveOutcome, + map_solution: impl FnOnce(&S) -> ExtractionResult, +) -> ExtractionResult> { + let (solution, optimal) = match &target { + SolveOutcome::Infeasible => return Ok(SolveOutcome::Infeasible), + SolveOutcome::Optimal { solution, .. } => (solution, true), + SolveOutcome::Feasible { solution, .. } => (solution, false), + }; + let solution = map_solution(solution)?; + let evaluation = source.evaluate(&solution)?; + match (evaluation.is_valid(), optimal) { + (true, true) => Ok(SolveOutcome::Optimal { + solution, + evaluation, + }), + (true, false) => Ok(SolveOutcome::Feasible { + solution, + evaluation, + }), + (false, true) => Ok(SolveOutcome::Infeasible), + (false, false) => Err(ExtractionError::InsufficientSolutionQuality), + } +} + /// Trait for problems that can be reduced to target type T. /// /// # Example diff --git a/src/unit_tests/rules/traits.rs b/src/unit_tests/rules/traits.rs index f9e304a2b..e82d3d9ff 100644 --- a/src/unit_tests/rules/traits.rs +++ b/src/unit_tests/rules/traits.rs @@ -72,6 +72,89 @@ fn recovery_propagates_mapping_and_evaluation_failures() { } } +#[test] +fn recovery_by_source_evaluation_separates_a_proved_no_from_a_weak_incumbent() { + use crate::models::misc::Partition; + use crate::rules::traits::recover_by_source_evaluation; + use crate::rules::ExtractionError; + use crate::traits::EvaluationError; + use crate::types::Or; + + let source = Partition::new(vec![1, 1]).unwrap(); + let decode = |solution: &Vec| Ok(solution.iter().map(|&value| value == 1).collect()); + let balanced = vec![true, false]; + assert_eq!( + recover_by_source_evaluation( + &source, + SolveOutcome::optimal(&TargetProblem, vec![1, 0]).unwrap(), + decode, + ) + .unwrap(), + SolveOutcome::Optimal { + solution: balanced.clone(), + evaluation: Or(true), + } + ); + assert_eq!( + recover_by_source_evaluation( + &source, + SolveOutcome::feasible(&TargetProblem, vec![1, 0]).unwrap(), + decode, + ) + .unwrap(), + SolveOutcome::Feasible { + solution: balanced, + evaluation: Or(true), + } + ); + // Only an optimum whose decoding is source-invalid proves NO. + assert_eq!( + recover_by_source_evaluation( + &source, + SolveOutcome::optimal(&TargetProblem, vec![1, 1]).unwrap(), + decode, + ) + .unwrap(), + SolveOutcome::Infeasible + ); + assert!(matches!( + recover_by_source_evaluation( + &source, + SolveOutcome::feasible(&TargetProblem, vec![1, 1]).unwrap(), + decode, + ), + Err(ExtractionError::InsufficientSolutionQuality) + )); + assert_eq!( + recover_by_source_evaluation( + &source, + crate::solvers::ProblemOutcome::::Infeasible, + |_| panic!("infeasibility has no witness to map"), + ) + .unwrap(), + SolveOutcome::Infeasible + ); + + // Mapping and evaluation failures are errors under either status, never NO. + for outcome in [ + SolveOutcome::optimal(&TargetProblem, vec![1, 1]).unwrap(), + SolveOutcome::feasible(&TargetProblem, vec![1, 1]).unwrap(), + ] { + assert!(matches!( + recover_by_source_evaluation(&source, outcome.clone(), |_| { + Err(ExtractionError::invalid("undecodable")) + }), + Err(ExtractionError::InvalidTargetSolution(_)) + )); + assert!(matches!( + recover_by_source_evaluation(&source, outcome, |_| Ok(vec![true])), + Err(ExtractionError::Evaluation( + EvaluationError::InvalidConfiguration(_) + )) + )); + } +} + #[derive(Clone)] struct SourceProblem; #[derive(Clone)] From 6f654162b5ef35579ca14e654fa31867cf69d8b7 Mon Sep 17 00:00:00 2001 From: GiggleLiu Date: Fri, 18 Sep 2026 00:28:57 +0800 Subject: [PATCH 42/42] Lint the whole workspace with all features in CI and make clippy The root-only command never linted the CLI and macros crates or the mcp and benchmarks features. Every feature is pure Rust, so the Clippy job needs no extra system packages. Co-Authored-By: Claude Fable 5.1 --- .claude/skills/add-model/SKILL.md | 2 +- .github/workflows/ci.yml | 4 ++-- Makefile | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.claude/skills/add-model/SKILL.md b/.claude/skills/add-model/SKILL.md index dc5b36658..698a487cb 100644 --- a/.claude/skills/add-model/SKILL.md +++ b/.claude/skills/add-model/SKILL.md @@ -298,7 +298,7 @@ make test clippy # Must pass If Step 4.7 applied, run ILP-enabled workspace verification instead: ```bash -cargo clippy --all-targets -- -D warnings +cargo clippy --workspace --all-targets --all-features -- -D warnings cargo test --features example-db --workspace --verbose ``` diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index aa3f64c0e..47399c5d9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,7 +38,7 @@ jobs: - name: Check formatting run: cargo fmt --all --check - # Lints. + # Lints every workspace crate with every feature (CLI `mcp`, `benchmarks`). clippy: name: Clippy runs-on: ubuntu-latest @@ -49,7 +49,7 @@ jobs: components: clippy - uses: Swatinem/rust-cache@v2 - name: Run clippy - run: cargo clippy --all-targets --features example-db -- -D warnings + run: cargo clippy --workspace --all-targets --all-features -- -D warnings # Build and exercise the HiGHS-backed CLI natively on Apple Silicon. macos-arm64: diff --git a/Makefile b/Makefile index 1a3634480..3a91d10e1 100644 --- a/Makefile +++ b/Makefile @@ -89,7 +89,7 @@ fmt-check: # Run clippy clippy: - cargo clippy --all-targets --features "$(TEST_FEATURES)" -- -D warnings + cargo clippy --workspace --all-targets --all-features -- -D warnings node_modules/elkjs/package.json: package.json package-lock.json npm ci